diff --git a/apps/studio/src/server/modules/aws/codebuild.service.ts b/apps/studio/src/server/modules/aws/codebuild.service.ts new file mode 100644 index 0000000000..bc62bd27c4 --- /dev/null +++ b/apps/studio/src/server/modules/aws/codebuild.service.ts @@ -0,0 +1,110 @@ +import { + BatchGetProjectsCommand, + CodeBuildClient, + CreateProjectCommand, + StartBuildCommand, +} from "@aws-sdk/client-codebuild" +import { ServiceException } from "@aws-sdk/smithy-client" + +const client = new CodeBuildClient({ region: "ap-southeast-1" }) + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const getProjectById = async (projectId: string): Promise => { + const command = new BatchGetProjectsCommand({ names: [projectId] }) + + try { + const response = await client.send(command) + console.log("AWS response", JSON.stringify(response)) + if (response.projects && response.projects.length > 0) { + return response.projects[0] + } else { + throw new Error(`Project with ID ${projectId} not found`) + } + } catch (error) { + if (error instanceof ServiceException) { + console.error("Service error:", error) + } else { + console.error("Unexpected error:", error) + } + throw error + } +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const startProjectById = async (projectId: string): Promise => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const project = await getProjectById(projectId) + + if (!project) { + throw new Error(`Project with ID ${projectId} not found`) + } + + const command = new StartBuildCommand({ projectName: projectId }) + + try { + const response = await client.send(command) + return response + } catch (error) { + if (error instanceof ServiceException) { + console.error("Service error:", error) + } else { + console.error("Unexpected error:", error) + } + throw error + } +} + +export const createCodeBuildProject = async ({ + projectId, + siteId, +}: { + projectId: string + siteId: number +}): Promise => { + /* TODO: Things to add +1. VPC config +2. Default service role +3. Move buildspec out +4. Setup logs +*/ + const command = new CreateProjectCommand({ + name: projectId, + source: { + type: "NO_SOURCE", + buildspec: + 'version: 0.2\n\nphases:\n build:\n commands:\n - echo "hello"\n', + }, + artifacts: { + type: "NO_ARTIFACTS", + }, + environment: { + type: "LINUX_CONTAINER", + image: "aws/codebuild/amazonlinux2-x86_64-standard:5.0", + imagePullCredentialsType: "CODEBUILD", + computeType: "BUILD_GENERAL1_SMALL", + environmentVariables: [{ name: "SITE_ID", value: siteId.toString() }], + }, + serviceRole: "", + logsConfig: { + cloudWatchLogs: { + status: "DISABLED", + }, + s3Logs: { + status: "DISABLED", + }, + }, + }) + + try { + const response = await client.send(command) + console.log("AWS Create project res: ", JSON.stringify(response)) + return response + } catch (error) { + if (error instanceof ServiceException) { + console.error("Service error:", error) + } else { + console.error("Unexpected error:", error) + } + throw error + } +} diff --git a/apps/studio/src/server/modules/page/page.router.ts b/apps/studio/src/server/modules/page/page.router.ts index d9ff1dfd00..2a37fc8822 100644 --- a/apps/studio/src/server/modules/page/page.router.ts +++ b/apps/studio/src/server/modules/page/page.router.ts @@ -11,13 +11,21 @@ import { } from "~/schemas/page" import { protectedProcedure, router } from "~/server/trpc" import { safeJsonParse } from "~/utils/safeJsonParse" +import { + createCodeBuildProject, + startProjectById, +} from "../aws/codebuild.service" import { db, ResourceType } from "../database" import { getFooter, getFullPageById, getNavBar, } from "../resource/resource.service" -import { getSiteConfig } from "../site/site.service" +import { + getSiteConfig, + getSiteNameAndCodeBuildId, + setSiteCodeBuildId, +} from "../site/site.service" import { addNewVersion, createDefaultPage } from "./page.service" const ajv = new Ajv({ allErrors: true, strict: false, logger: false }) @@ -143,12 +151,43 @@ export const pageRouter = router({ ), publishPage: protectedProcedure .input(publishPageSchema) - .mutation(async ({ input: { siteId, pageId } }) => { + .mutation(async ({ ctx, input: { siteId, pageId } }) => { /* Step 1: Update DB table to latest state */ // Create a new version const addedVersionResult = await addNewVersion(siteId, pageId) - return addedVersionResult /* TODO: Step 2: Use AWS SDK to start a CodeBuild */ + const site = await getSiteNameAndCodeBuildId(siteId) + let codeBuildId = site.codeBuildId + if (!codeBuildId) { + // create a codebuild project + const projectName = site.shortName || site.name + const formattedProjectName = projectName + .toLowerCase() + .split(" ") + .join("-") + const projectId = `${formattedProjectName}-${siteId}` + + try { + await createCodeBuildProject({ + projectId, + siteId, + }) + } catch (e) { + ctx.logger.error("CodeBuild project creation failure", { + userId: ctx.user.id, + siteId, + }) + } + + // update site to the newly created codebuild project id + await setSiteCodeBuildId(siteId, projectId) + codeBuildId = projectId + } + + // initiate new build + await startProjectById(codeBuildId) + + return addedVersionResult }), }) diff --git a/apps/studio/src/server/modules/site/site.service.ts b/apps/studio/src/server/modules/site/site.service.ts index 027521ca0d..bda8601a15 100644 --- a/apps/studio/src/server/modules/site/site.service.ts +++ b/apps/studio/src/server/modules/site/site.service.ts @@ -12,6 +12,22 @@ export const getSiteConfig = async (siteId: number) => { return config } +export const getSiteNameAndCodeBuildId = async (siteId: number) => { + return await db + .selectFrom("Site") + .where("id", "=", siteId) + .select(["Site.codeBuildId", "Site.name", "Site.shortName"]) + .executeTakeFirstOrThrow() +} + +export const setSiteCodeBuildId = async (siteId: number, projectId: string) => { + return await db + .updateTable("Site") + .set({ codeBuildId: projectId }) + .where("id", "=", siteId) + .executeTakeFirstOrThrow() +} + // Note: This overwrites the full site config // TODO: Should triger immediate re-publish of site export const setSiteConfig = async (