diff --git a/apps/studio/package.json b/apps/studio/package.json index c59a0c7b6..ea005ec66 100644 --- a/apps/studio/package.json +++ b/apps/studio/package.json @@ -41,8 +41,9 @@ "seed": "tsx prisma/seed.ts" }, "dependencies": { - "@aws-sdk/client-s3": "3.627.0", - "@aws-sdk/s3-request-presigner": "3.627.0", + "@aws-sdk/client-codebuild": "^3.624.0", + "@aws-sdk/client-s3": "3.626.0", + "@aws-sdk/s3-request-presigner": "3.626.0", "@chakra-ui/anatomy": "^2.2.2", "@chakra-ui/react": "^2.8.2", "@chakra-ui/styled-system": "^2.9.2", diff --git a/apps/studio/prisma/migrations/20240731163213_add_codebuild_id/migration.sql b/apps/studio/prisma/migrations/20240731163213_add_codebuild_id/migration.sql new file mode 100644 index 000000000..a5df33338 --- /dev/null +++ b/apps/studio/prisma/migrations/20240731163213_add_codebuild_id/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Site" ADD COLUMN "codeBuildId" TEXT; diff --git a/apps/studio/prisma/schema.prisma b/apps/studio/prisma/schema.prisma index e0248afd6..17dd6e42a 100644 --- a/apps/studio/prisma/schema.prisma +++ b/apps/studio/prisma/schema.prisma @@ -130,6 +130,7 @@ model Site { theme Json? navbar Navbar? footer Footer? + codeBuildId String? } model Navbar { 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 000000000..f7767d692 --- /dev/null +++ b/apps/studio/src/server/modules/aws/codebuild.service.ts @@ -0,0 +1,75 @@ +import type { StartBuildCommandOutput } from "@aws-sdk/client-codebuild" +import type pino from "pino" +import { + BatchGetBuildsCommand, + CodeBuildClient, + ListBuildsForProjectCommand, + StartBuildCommand, + StopBuildCommand, +} from "@aws-sdk/client-codebuild" + +const client = new CodeBuildClient({ region: "ap-southeast-1" }) + +export const stopRunningBuilds = async ( + logger: pino.Logger, + projectId: string, +): Promise => { + try { + // List builds for the given project + const listBuildsCommand = new ListBuildsForProjectCommand({ + projectName: projectId, + }) + const listBuildsResponse = await client.send(listBuildsCommand) + + const buildIds = listBuildsResponse.ids ?? [] + + if (buildIds.length === 0) { + logger.info({ projectId }, "No running builds found for the project") + return + } + + // Get details of the builds + const batchGetBuildsCommand = new BatchGetBuildsCommand({ ids: buildIds }) + const batchGetBuildsResponse = await client.send(batchGetBuildsCommand) + + // Stop running builds + for (const build of batchGetBuildsResponse.builds ?? []) { + if (build.buildStatus === "IN_PROGRESS") { + logger.info( + { buildId: build.id }, + "Stopping currently running CodeBuild", + ) + const stopBuildCommand = new StopBuildCommand({ id: build.id }) + await client.send(stopBuildCommand) + logger.info({ buildId: build.id }, "Build stopped successfully") + } + } + } catch (error) { + logger.error( + { projectId, error }, + "Unexpected error while stopping running builds", + ) + throw error + } +} + +export const startProjectById = async ( + logger: pino.Logger, + projectId: string, +): Promise => { + try { + // Stop any currently running builds + await stopRunningBuilds(logger, projectId) + + // Start a new build + const command = new StartBuildCommand({ projectName: projectId }) + const response = await client.send(command) + return response + } catch (error) { + logger.error( + { projectId, error }, + "Unexpected error when starting CodeBuild project run", + ) + 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 3ff467710..09fcac972 100644 --- a/apps/studio/src/server/modules/page/page.router.ts +++ b/apps/studio/src/server/modules/page/page.router.ts @@ -3,6 +3,7 @@ import { schema } from "@opengovsg/isomer-components" import { TRPCError } from "@trpc/server" import Ajv from "ajv" import isEqual from "lodash/isEqual" +import { z } from "zod" import { createPageSchema, @@ -15,6 +16,7 @@ import { } from "~/schemas/page" import { protectedProcedure, router } from "~/server/trpc" import { safeJsonParse } from "~/utils/safeJsonParse" +import { startProjectById, stopRunningBuilds } from "../aws/codebuild.service" import { db, ResourceType } from "../database" import { getFooter, @@ -24,7 +26,7 @@ import { updateBlobById, updatePageById, } from "../resource/resource.service" -import { getSiteConfig } from "../site/site.service" +import { getSiteConfig, getSiteNameAndCodeBuildId } from "../site/site.service" import { incrementVersion } from "../version/version.service" import { createDefaultPage } from "./page.service" @@ -254,8 +256,22 @@ export const pageRouter = router({ pageId, userId: ctx.user.id, }) - return addedVersionResult - /* TODO: Step 2: Use AWS SDK to start a CodeBuild */ + /* Step 2: Use AWS SDK to start a CodeBuild */ + const site = await getSiteNameAndCodeBuildId(siteId) + const codeBuildId = site.codeBuildId + if (!codeBuildId) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "No CodeBuild project ID found for site", + }) + } + + // stop any currently running builds for the site + await stopRunningBuilds(ctx.logger, codeBuildId) + + // initiate new build + await startProjectById(ctx.logger, 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 8328b3790..dcdc598f7 100644 --- a/apps/studio/src/server/modules/site/site.service.ts +++ b/apps/studio/src/server/modules/site/site.service.ts @@ -22,9 +22,16 @@ export const getSiteTheme = async (siteId: number) => { return theme } +export const getSiteNameAndCodeBuildId = async (siteId: number) => { + return await db + .selectFrom("Site") + .where("id", "=", siteId) + .select(["Site.codeBuildId", "Site.name"]) + .executeTakeFirstOrThrow() +} // Note: This overwrites the full site config -// TODO: Should triger immediate re-publish of site +// TODO: Should trigger immediate re-publish of site export const setSiteConfig = async ( siteId: number, config: IsomerSiteConfigProps, diff --git a/apps/studio/src/utils/trpc.ts b/apps/studio/src/utils/trpc.ts index 6dcc836d8..e1ea0f4b7 100644 --- a/apps/studio/src/utils/trpc.ts +++ b/apps/studio/src/utils/trpc.ts @@ -201,9 +201,7 @@ export const trpc = createTRPCNext< }, }, mutations: { - retry: (_, error) => { - return isErrorRetryableOnClient(error) - }, + retry: false, }, }, }, diff --git a/package-lock.json b/package-lock.json index 4b8aa480b..69b20daaa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,8 +22,9 @@ "name": "isomer-studio", "version": "0.0.1", "dependencies": { - "@aws-sdk/client-s3": "3.627.0", - "@aws-sdk/s3-request-presigner": "3.627.0", + "@aws-sdk/client-codebuild": "^3.624.0", + "@aws-sdk/client-s3": "3.626.0", + "@aws-sdk/s3-request-presigner": "3.626.0", "@chakra-ui/anatomy": "^2.2.2", "@chakra-ui/react": "^2.8.2", "@chakra-ui/styled-system": "^2.9.2", @@ -160,6 +161,127 @@ "webpack-glob-entries": "^1.0.1" } }, + "apps/studio/node_modules/@aws-sdk/client-s3": { + "version": "3.626.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.626.0.tgz", + "integrity": "sha512-+ul1NEdiAuq5L0lhxWb+FcQuw+1RKU4lNugdX/EF3Lr6Bpuo384K/4r9cRwOo/6PqRYMIengBMc9Q2HVAu8ZWg==", + "dependencies": { + "@aws-crypto/sha1-browser": "5.2.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.624.0", + "@aws-sdk/client-sts": "3.624.0", + "@aws-sdk/core": "3.624.0", + "@aws-sdk/credential-provider-node": "3.624.0", + "@aws-sdk/middleware-bucket-endpoint": "3.620.0", + "@aws-sdk/middleware-expect-continue": "3.620.0", + "@aws-sdk/middleware-flexible-checksums": "3.620.0", + "@aws-sdk/middleware-host-header": "3.620.0", + "@aws-sdk/middleware-location-constraint": "3.609.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.620.0", + "@aws-sdk/middleware-sdk-s3": "3.626.0", + "@aws-sdk/middleware-ssec": "3.609.0", + "@aws-sdk/middleware-user-agent": "3.620.0", + "@aws-sdk/region-config-resolver": "3.614.0", + "@aws-sdk/signature-v4-multi-region": "3.626.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.614.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.614.0", + "@aws-sdk/xml-builder": "3.609.0", + "@smithy/config-resolver": "^3.0.5", + "@smithy/core": "^2.3.2", + "@smithy/eventstream-serde-browser": "^3.0.5", + "@smithy/eventstream-serde-config-resolver": "^3.0.3", + "@smithy/eventstream-serde-node": "^3.0.4", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/hash-blob-browser": "^3.1.2", + "@smithy/hash-node": "^3.0.3", + "@smithy/hash-stream-node": "^3.1.2", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/md5-js": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.5", + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.14", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.1.12", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.14", + "@smithy/util-defaults-mode-node": "^3.0.14", + "@smithy/util-endpoints": "^2.0.5", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "@smithy/util-stream": "^3.1.3", + "@smithy/util-utf8": "^3.0.0", + "@smithy/util-waiter": "^3.1.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "apps/studio/node_modules/@aws-sdk/s3-request-presigner": { + "version": "3.626.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.626.0.tgz", + "integrity": "sha512-vG1HdYPymEMzAWTSfLVYTTdz25ZkH6WU26BCU5Wz3xrYZgCdabNZgPNyQg0c0ZVQDBWFD4LZ5ITP/3cWcm5o1A==", + "dependencies": { + "@aws-sdk/signature-v4-multi-region": "3.626.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-format-url": "3.609.0", + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.1.12", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "apps/studio/node_modules/@smithy/is-array-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-3.0.0.tgz", + "integrity": "sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "apps/studio/node_modules/@smithy/util-buffer-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-3.0.0.tgz", + "integrity": "sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA==", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "apps/studio/node_modules/@smithy/util-utf8": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-3.0.0.tgz", + "integrity": "sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA==", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, "apps/studio/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -439,46 +561,31 @@ "tslib": "^2.6.2" } }, - "node_modules/@aws-sdk/client-s3": { - "version": "3.627.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.627.0.tgz", - "integrity": "sha512-XTbtRLPVfq2lHo0SUP6HJb6HgBsKsJR54bhhVTwj5SZ4G26KOmx2iFOz9SgHie5apU7vWIhijb48LIhbLArgGg==", + "node_modules/@aws-sdk/client-codebuild": { + "version": "3.624.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-codebuild/-/client-codebuild-3.624.0.tgz", + "integrity": "sha512-PWRDndVlWp72MdYmXbxb3cmeKct/681pP0kCooPmZm3uTC/z7b5Q/aBP5jlJLo87PAFBY8m5NByt9s2JGFZ9GA==", "dependencies": { - "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/client-sso-oidc": "3.624.0", "@aws-sdk/client-sts": "3.624.0", "@aws-sdk/core": "3.624.0", "@aws-sdk/credential-provider-node": "3.624.0", - "@aws-sdk/middleware-bucket-endpoint": "3.620.0", - "@aws-sdk/middleware-expect-continue": "3.620.0", - "@aws-sdk/middleware-flexible-checksums": "3.620.0", "@aws-sdk/middleware-host-header": "3.620.0", - "@aws-sdk/middleware-location-constraint": "3.609.0", "@aws-sdk/middleware-logger": "3.609.0", "@aws-sdk/middleware-recursion-detection": "3.620.0", - "@aws-sdk/middleware-sdk-s3": "3.626.0", - "@aws-sdk/middleware-ssec": "3.609.0", "@aws-sdk/middleware-user-agent": "3.620.0", "@aws-sdk/region-config-resolver": "3.614.0", - "@aws-sdk/signature-v4-multi-region": "3.626.0", "@aws-sdk/types": "3.609.0", "@aws-sdk/util-endpoints": "3.614.0", "@aws-sdk/util-user-agent-browser": "3.609.0", "@aws-sdk/util-user-agent-node": "3.614.0", - "@aws-sdk/xml-builder": "3.609.0", "@smithy/config-resolver": "^3.0.5", "@smithy/core": "^2.3.2", - "@smithy/eventstream-serde-browser": "^3.0.5", - "@smithy/eventstream-serde-config-resolver": "^3.0.3", - "@smithy/eventstream-serde-node": "^3.0.4", "@smithy/fetch-http-handler": "^3.2.4", - "@smithy/hash-blob-browser": "^3.1.2", "@smithy/hash-node": "^3.0.3", - "@smithy/hash-stream-node": "^3.1.2", "@smithy/invalid-dependency": "^3.0.3", - "@smithy/md5-js": "^3.0.3", "@smithy/middleware-content-length": "^3.0.5", "@smithy/middleware-endpoint": "^3.1.0", "@smithy/middleware-retry": "^3.0.14", @@ -498,16 +605,14 @@ "@smithy/util-endpoints": "^2.0.5", "@smithy/util-middleware": "^3.0.3", "@smithy/util-retry": "^3.0.3", - "@smithy/util-stream": "^3.1.3", "@smithy/util-utf8": "^3.0.0", - "@smithy/util-waiter": "^3.1.2", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@smithy/is-array-buffer": { + "node_modules/@aws-sdk/client-codebuild/node_modules/@smithy/is-array-buffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-3.0.0.tgz", "integrity": "sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ==", @@ -518,7 +623,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-buffer-from": { + "node_modules/@aws-sdk/client-codebuild/node_modules/@smithy/util-buffer-from": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-3.0.0.tgz", "integrity": "sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA==", @@ -530,7 +635,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-utf8": { + "node_modules/@aws-sdk/client-codebuild/node_modules/@smithy/util-utf8": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-3.0.0.tgz", "integrity": "sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA==", @@ -1185,24 +1290,6 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/s3-request-presigner": { - "version": "3.627.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.627.0.tgz", - "integrity": "sha512-UA4EWIQITlBVFFMNC4AP5YUWWJ2Bmdzg7238Pm6W8xqpKOgHDPdbEKjhiEsSt1SG6B4Ogk4PnP0oSANtn3hjPQ==", - "dependencies": { - "@aws-sdk/signature-v4-multi-region": "3.626.0", - "@aws-sdk/types": "3.609.0", - "@aws-sdk/util-format-url": "3.609.0", - "@smithy/middleware-endpoint": "^3.1.0", - "@smithy/protocol-http": "^4.1.0", - "@smithy/smithy-client": "^3.1.12", - "@smithy/types": "^3.3.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, "node_modules/@aws-sdk/signature-v4-multi-region": { "version": "3.626.0", "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.626.0.tgz", @@ -36772,7 +36859,11 @@ } }, "tooling/build": { - "version": "0.0.0" + "version": "0.0.0", + "devDependencies": { + "@isomer/eslint-config": "*", + "@isomer/prettier-config": "*" + } }, "tooling/eslint": { "name": "@isomer/eslint-config",