diff --git a/.github/workflows/_build-roboshield.yml b/.github/workflows/_build-roboshield.yml new file mode 100644 index 0000000000..074bab50ff --- /dev/null +++ b/.github/workflows/_build-roboshield.yml @@ -0,0 +1,70 @@ +name: Reusable Build | RoboShield + +on: + workflow_call: + inputs: + tag: + required: true + type: string + description: "Image tag to build or push" + push: + required: true + type: boolean + description: "Whether to push the built image to the registry" + base_tag: + required: true + type: string + description: "Published base image tag to build from" + app_url: + required: true + type: string + description: "NEXT_PUBLIC_APP_URL baked into the client bundle (public, not a secret)" + sentry_environment: + required: true + type: string + description: "Sentry environment baked into the app" + set: + required: false + type: string + default: "" + description: "Additional bake --set overrides" + secrets: + DOCKER_HUB_USERNAME: + required: false + DOCKER_HUB_ACCESS_TOKEN: + required: false + DATABASE_URL: + required: false + PAYLOAD_SECRET: + required: false + SENTRY_AUTH_TOKEN: + required: false + SENTRY_ORG: + required: false + SENTRY_PROJECT: + required: false + +jobs: + build: + permissions: + actions: read + contents: read + uses: ./.github/workflows/_bake-and-push.yml + with: + target: roboshield + target_os: '["ubuntu-24.04-arm"]' + base_tag: ${{ inputs.base_tag }} + tag: ${{ inputs.tag }} + push: ${{ inputs.push }} + set: | + roboshield.args.NEXT_PUBLIC_APP_URL=${{ inputs.app_url }} + roboshield.args.SENTRY_ENVIRONMENT=${{ inputs.sentry_environment }} + ${{ inputs.set }} + secrets: + DOCKER_HUB_USERNAME: ${{ secrets.DOCKER_HUB_USERNAME }} + DOCKER_HUB_ACCESS_TOKEN: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} + DATABASE_URL: ${{ secrets.DATABASE_URL }} + PAYLOAD_SECRET: ${{ secrets.PAYLOAD_SECRET }} + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + SENTRY_ORG: ${{ secrets.SENTRY_ORG }} + SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index 2fc4b44453..4cbd931fc2 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -5,9 +5,11 @@ on: types: [opened, synchronize, reopened, ready_for_review] paths: - "apps/pesayetu/**" + - "apps/roboshield/**" - "apps/techlabblog/**" - "apps/trustlab/**" - "docker/apps/pesayetu/**" + - "docker/apps/roboshield/**" - "docker/apps/techlabblog/**" - "docker/apps/trustlab/**" - "docker-bake.hcl" @@ -21,10 +23,12 @@ on: - "scripts/pr-build-targets.test.mjs" - ".github/workflows/_bake-and-push.yml" - ".github/workflows/_build-pesayetu.yml" + - ".github/workflows/_build-roboshield.yml" - ".github/workflows/_build-techlabblog.yml" - ".github/workflows/_build-trustlab.yml" - ".github/workflows/pesayetu.yml" - ".github/workflows/pr-build.yml" + - ".github/workflows/roboshield.yml" - ".github/workflows/techlabblog.yml" - ".github/workflows/trustlab.yml" @@ -120,6 +124,31 @@ jobs: WORDPRESS_APPLICATION_PASSWORD: pr-build-unused WORDPRESS_PREVIEW_SECRET: pr-build-unused + build-roboshield: + name: Build roboshield image + needs: affected + if: ${{ contains(fromJSON(needs.affected.outputs.targets), 'roboshield') }} + permissions: + actions: read + contents: read + uses: ./.github/workflows/_build-roboshield.yml + with: + base_tag: ${{ vars.UI_BASE_TAG }} + tag: pr-${{ github.event.pull_request.number }}-${{ github.sha }} + push: false + app_url: "http://localhost:3000" + sentry_environment: ci + secrets: + # Payload's mongooseAdapter is not queried during static generation, so + # PR builds don't need a reachable database — a well-formed but unused + # connection string is enough to satisfy the build. The remaining + # secrets are not required to compile either. + DATABASE_URL: "mongodb://pr-build-unused/pr-build-unused" + PAYLOAD_SECRET: pr-build-unused + SENTRY_AUTH_TOKEN: pr-build-unused + SENTRY_ORG: pr-build-unused + SENTRY_PROJECT: pr-build-unused + build-techlabblog: name: Build techlabblog image needs: affected diff --git a/.github/workflows/roboshield-deploy-dev.yml b/.github/workflows/roboshield-deploy-dev.yml index 52f5d0b079..911c7fb7d2 100644 --- a/.github/workflows/roboshield-deploy-dev.yml +++ b/.github/workflows/roboshield-deploy-dev.yml @@ -70,7 +70,7 @@ jobs: platforms: linux/arm64 push: true secrets: | - "mongo_url=${{ secrets.ROBOSHIELD_MONGO_URL }}" + "database_url=${{ secrets.ROBOSHIELD_MONGO_URL }}" "payload_secret=${{ secrets.ROBOSHIELD_PAYLOAD_SECRET }}" "sentry_auth_token=${{ secrets.SENTRY_AUTH_TOKEN }}" "sentry_org=${{ secrets.SENTRY_ORG }}" diff --git a/.github/workflows/roboshield-deploy-prod.yml b/.github/workflows/roboshield-deploy-prod.yml index 84f8d78f07..dfe749b6e5 100644 --- a/.github/workflows/roboshield-deploy-prod.yml +++ b/.github/workflows/roboshield-deploy-prod.yml @@ -73,7 +73,7 @@ jobs: uses: docker/build-push-action@v6 with: build-args: | - MONGO_URL=${{ secrets.ROBOSHIELD_MONGO_URL }} + DATABASE_URL=${{ secrets.ROBOSHIELD_MONGO_URL }} NEXT_PUBLIC_APP_URL=${{ env.NEXT_PUBLIC_APP_URL }} NEXT_PUBLIC_SENTRY_DSN=${{ secrets.ROBOSHIELD_SENTRY_DSN }} SENTRY_ENVIRONMENT=${{ env.SENTRY_ENVIRONMENT }} @@ -83,7 +83,7 @@ jobs: platforms: linux/arm64 push: true secrets: | - "mongo_url=${{ secrets.ROBOSHIELD_MONGO_URL }}" + "database_url=${{ secrets.ROBOSHIELD_MONGO_URL }}" "payload_secret=${{ secrets.ROBOSHIELD_PAYLOAD_SECRET }}" "sentry_auth_token=${{ secrets.SENTRY_AUTH_TOKEN }}" "sentry_org=${{ secrets.SENTRY_ORG }}" diff --git a/.github/workflows/roboshield.yml b/.github/workflows/roboshield.yml new file mode 100644 index 0000000000..c7d58af4d9 --- /dev/null +++ b/.github/workflows/roboshield.yml @@ -0,0 +1,127 @@ +name: RoboShield + +on: + push: + branches: + - main + paths: + - "apps/roboshield/**" + - "docker/apps/roboshield/**" + - "docker-bake.hcl" + - ".github/workflows/_bake-and-push.yml" + - ".github/workflows/_build-roboshield.yml" + - ".github/workflows/roboshield.yml" + +permissions: + actions: read + contents: read + +concurrency: + group: "${{ github.workflow }} @ ${{ github.ref }}" + cancel-in-progress: true + +jobs: + # Checks whether apps/roboshield/package.json has a version bump. + # The prod deploy is gated on this: every push triggers a dev build+deploy, + # only a version bump triggers a prod build+deploy. + version-check: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-24.04] + permissions: + contents: read + outputs: + changed: ${{ steps.check.outputs.changed }} + version: ${{ steps.check.outputs.version }} + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - uses: actions/setup-node@v6 + with: + node-version-file: "package.json" + + - name: Check if version is bumped + id: check + uses: EndBug/version-check@v3 + with: + diff-search: true + file-name: apps/roboshield/package.json + + # Builds the DEV image and pushes it to DockerHub on every push. roboshield + # bakes its canonical URL into the client bundle (NEXT_PUBLIC_APP_URL), and + # DEV/PROD use different Dokku apps with different URLs, so — unlike + # techlabblog/trustlab which build once and promote — DEV and PROD each get + # their own build, matching the pre-bake two-workflow setup this replaces. + # + # Required GitHub Secrets: + # ROBOSHIELD_MONGO_URL, ROBOSHIELD_PAYLOAD_SECRET, + # ROBOSHIELD_SENTRY_PROJECT, SENTRY_AUTH_TOKEN, SENTRY_ORG, UI_BASE_TAG (var) + build-dev: + permissions: + actions: read + contents: read + uses: ./.github/workflows/_build-roboshield.yml + with: + base_tag: ${{ vars.UI_BASE_TAG }} + tag: ${{ github.sha }} + push: true + app_url: "https://roboshield-ui.dev.codeforafrica.org" + sentry_environment: development + secrets: + DOCKER_HUB_USERNAME: ${{ secrets.DOCKER_HUB_USERNAME }} + DOCKER_HUB_ACCESS_TOKEN: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} + DATABASE_URL: ${{ secrets.ROBOSHIELD_MONGO_URL }} + PAYLOAD_SECRET: ${{ secrets.ROBOSHIELD_PAYLOAD_SECRET }} + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + SENTRY_ORG: ${{ secrets.SENTRY_ORG }} + SENTRY_PROJECT: ${{ secrets.ROBOSHIELD_SENTRY_PROJECT }} + + deploy-dev: + needs: build-dev + permissions: {} + uses: ./.github/workflows/push-to-dokku.yml + with: + git_remote_url: "ssh://azureuser@ui-1.dev.codeforafrica.org/roboshield-ui" + deploy_docker_image: "codeforafrica/roboshield:${{ github.sha }}" + secrets: + SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} + + # Builds the PROD image only when the package.json version is bumped, using + # the PROD canonical URL and the semver tag. + build-prod: + needs: version-check + if: needs.version-check.outputs.changed == 'true' + permissions: + actions: read + contents: read + uses: ./.github/workflows/_build-roboshield.yml + with: + base_tag: ${{ vars.UI_BASE_TAG }} + tag: ${{ needs.version-check.outputs.version }} + push: true + app_url: "https://roboshield.trustlab.africa" + sentry_environment: production + set: | + roboshield.tags+=codeforafrica/roboshield:latest + secrets: + DOCKER_HUB_USERNAME: ${{ secrets.DOCKER_HUB_USERNAME }} + DOCKER_HUB_ACCESS_TOKEN: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} + DATABASE_URL: ${{ secrets.ROBOSHIELD_MONGO_URL }} + PAYLOAD_SECRET: ${{ secrets.ROBOSHIELD_PAYLOAD_SECRET }} + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + SENTRY_ORG: ${{ secrets.SENTRY_ORG }} + SENTRY_PROJECT: ${{ secrets.ROBOSHIELD_SENTRY_PROJECT }} + + deploy-prod: + needs: [version-check, build-prod] + if: needs.version-check.outputs.changed == 'true' + permissions: {} + uses: ./.github/workflows/push-to-dokku.yml + with: + git_remote_url: "ssh://dokku@ui-2.prod.codeforafrica.org/roboshield-ui" + deploy_docker_image: "codeforafrica/roboshield:${{ needs.version-check.outputs.version }}" + secrets: + SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} diff --git a/Dockerfile b/Dockerfile index b9da4e209f..854536dd9f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -704,7 +704,7 @@ COPY apps/roboshield ./apps/roboshield/ # When building Next.js app, Next.js needs to connect to local Payload ENV PAYLOAD_PUBLIC_APP_URL=http://localhost:3000 -RUN --mount=type=secret,id=mongo_url,env=MONGO_URL \ +RUN --mount=type=secret,id=database_url,env=DATABASE_URL \ --mount=type=secret,id=payload_secret,env=PAYLOAD_SECRET \ --mount=type=secret,id=sentry_auth_token,env=SENTRY_AUTH_TOKEN \ --mount=type=secret,id=sentry_org,env=SENTRY_ORG \ diff --git a/Makefile b/Makefile index b3700b779d..82c8e6ef58 100644 --- a/Makefile +++ b/Makefile @@ -31,7 +31,7 @@ pesayetu: ./scripts/bake-up.sh pesayetu roboshield: - ./scripts/dc.sh roboshield + ./scripts/bake-up.sh roboshield techlabblog: ./scripts/bake-up.sh techlabblog diff --git a/apps/roboshield/.env.template b/apps/roboshield/.env.template index beadf0f6ad..3e9070a6be 100644 --- a/apps/roboshield/.env.template +++ b/apps/roboshield/.env.template @@ -10,7 +10,7 @@ NEXT_PUBLIC_APP_URL # Defaults to 3000. PORT # https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/ -NEXT_PUBLIC_SENTRY_DSN= +SENTRY_DSN= # Defaults to local SENTRY_ENVIRONMENT=local|development|staging|production # Defaults to code-for-africa @@ -19,7 +19,7 @@ SENTRY_ORG= SENTRY_PROJECT= # Payload -MONGO_URL= +DATABASE_URL= PAYLOAD_SECRET= PAYLOAD_PUBLIC_APP_URL= diff --git a/apps/roboshield/instrumentation-client.ts b/apps/roboshield/instrumentation-client.ts index 6800c33aa1..e9370f55cc 100644 --- a/apps/roboshield/instrumentation-client.ts +++ b/apps/roboshield/instrumentation-client.ts @@ -4,10 +4,10 @@ import * as Sentry from "@sentry/nextjs"; -const SENTRY_DSN = process.env.NEXT_PUBLIC_SENTRY_DSN; +import site from "@/roboshield/utils/site"; Sentry.init({ - dsn: SENTRY_DSN, + dsn: site.sentryDsn, // Add optional integrations for additional features integrations: [ diff --git a/apps/roboshield/migrations/20250422_064614_migration.ts b/apps/roboshield/migrations/20250422_064614_migration.ts index b14ce4fa3f..b9c797a894 100644 --- a/apps/roboshield/migrations/20250422_064614_migration.ts +++ b/apps/roboshield/migrations/20250422_064614_migration.ts @@ -1,7 +1,7 @@ import { MigrateDownArgs, MigrateUpArgs } from "@payloadcms/db-mongodb"; import mongoose from "mongoose"; -const mongoURL = process.env.MONGO_URL || ""; +const mongoURL = process.env.DATABASE_URL || ""; const { ObjectId } = mongoose.Types; export async function up({ payload }: MigrateUpArgs): Promise { diff --git a/apps/roboshield/package.json b/apps/roboshield/package.json index 649591b544..354c8b0376 100644 --- a/apps/roboshield/package.json +++ b/apps/roboshield/package.json @@ -64,6 +64,7 @@ "tsconfig-paths": "catalog:" }, "devDependencies": { + "@babel/preset-react": "catalog:", "@svgr/webpack": "catalog:", "@types/express": "catalog:", "@types/node": "catalog:", diff --git a/apps/roboshield/payload.config.ts b/apps/roboshield/payload.config.ts index 115716a189..4e40696969 100644 --- a/apps/roboshield/payload.config.ts +++ b/apps/roboshield/payload.config.ts @@ -54,7 +54,7 @@ export default buildConfig({ ], }), db: mongooseAdapter({ - url: process.env.MONGO_URL ?? false, + url: process.env.DATABASE_URL ?? false, migrationDir: process.env.MIGRATIONS_DIR, }), collections: [Media, Pages, Users] as CollectionConfig[], diff --git a/apps/roboshield/sentry.edge.config.ts b/apps/roboshield/sentry.edge.config.ts index efc59b6adb..586fba488f 100644 --- a/apps/roboshield/sentry.edge.config.ts +++ b/apps/roboshield/sentry.edge.config.ts @@ -5,8 +5,10 @@ import * as Sentry from "@sentry/nextjs"; +import site from "@/roboshield/utils/site"; + Sentry.init({ - dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, + dsn: site.sentryDsn, // Adjust this value in production, or use tracesSampler for greater control tracesSampleRate: 1, diff --git a/apps/roboshield/sentry.server.config.ts b/apps/roboshield/sentry.server.config.ts index 4ecf38688a..5964d0d6d3 100644 --- a/apps/roboshield/sentry.server.config.ts +++ b/apps/roboshield/sentry.server.config.ts @@ -4,8 +4,10 @@ import * as Sentry from "@sentry/nextjs"; +import site from "@/roboshield/utils/site"; + Sentry.init({ - dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, + dsn: site.sentryDsn, // Adjust this value in production, or use tracesSampler for greater control tracesSampleRate: 1, diff --git a/apps/roboshield/src/pages/_document.tsx b/apps/roboshield/src/pages/_document.tsx index a30ef2f18a..02a99058b1 100644 --- a/apps/roboshield/src/pages/_document.tsx +++ b/apps/roboshield/src/pages/_document.tsx @@ -17,6 +17,10 @@ import theme from "@/roboshield/theme"; export default function MyDocument( props: DocumentProps & DocumentHeadTagsProps, ) { + const runtimeConfig = JSON.stringify({ + SENTRY_DSN: process.env.SENTRY_DSN, + }); + return ( @@ -43,6 +47,11 @@ export default function MyDocument( +