From 9e15fd1aee4aa0961ddfaf7c293ea29f4a71c09d Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Thu, 9 Jul 2026 10:36:12 -0700 Subject: [PATCH 1/6] wip --- packages/web/src/instrumentation.ts | 4 ++-- packages/web/{ => src}/sentry.client.config.ts | 0 packages/web/{ => src}/sentry.edge.config.ts | 0 packages/web/{ => src}/sentry.server.config.ts | 0 4 files changed, 2 insertions(+), 2 deletions(-) rename packages/web/{ => src}/sentry.client.config.ts (100%) rename packages/web/{ => src}/sentry.edge.config.ts (100%) rename packages/web/{ => src}/sentry.server.config.ts (100%) diff --git a/packages/web/src/instrumentation.ts b/packages/web/src/instrumentation.ts index 7421c4824..6e135fd21 100644 --- a/packages/web/src/instrumentation.ts +++ b/packages/web/src/instrumentation.ts @@ -19,11 +19,11 @@ export async function register() { } if (process.env.NEXT_RUNTIME === 'nodejs') { - await import('../sentry.server.config'); + await import('./sentry.server.config'); } if (process.env.NEXT_RUNTIME === 'edge') { - await import('../sentry.edge.config'); + await import('./sentry.edge.config'); } if (process.env.NEXT_RUNTIME === 'nodejs') { diff --git a/packages/web/sentry.client.config.ts b/packages/web/src/sentry.client.config.ts similarity index 100% rename from packages/web/sentry.client.config.ts rename to packages/web/src/sentry.client.config.ts diff --git a/packages/web/sentry.edge.config.ts b/packages/web/src/sentry.edge.config.ts similarity index 100% rename from packages/web/sentry.edge.config.ts rename to packages/web/src/sentry.edge.config.ts diff --git a/packages/web/sentry.server.config.ts b/packages/web/src/sentry.server.config.ts similarity index 100% rename from packages/web/sentry.server.config.ts rename to packages/web/src/sentry.server.config.ts From a1b71c3eaa7ebd37f5bddaca17ceab6ffa728519 Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Thu, 9 Jul 2026 18:34:28 -0700 Subject: [PATCH 2/6] ci: build prod image with Sentry and publish to ECR Adds a reusable cloud image build workflow, parameterized on a GitHub Environment, that bakes the environment's Sentry DSNs into the image, uploads source maps, and pushes linux/amd64 to sourcebot- in ECR via OIDC. Wires it up for prod on pushes to main and v*.*.* tags. Passes the Sentry auth token as a BuildKit secret rather than a build arg, since cache-to: mode=max exports intermediate stage layers (and their metadata) to the repo-scoped Actions cache. Drops sentry-cli login, which writes the token back to a .sentryclirc in the layer and is redundant now that the token is exposed under the name sentry-cli reads natively. Reports the build commit SHA as the backend's Sentry release so events match the release its source maps are uploaded under. Previously the backend reported SOURCEBOT_VERSION, which only coincided with SENTRY_RELEASE on tagged builds. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/_build-cloud.yml | 174 +++++++++++++++++++++++ .github/workflows/release-cloud-prod.yml | 51 +++++++ Dockerfile | 22 +-- packages/backend/src/instrument.ts | 5 +- packages/web/next.config.mjs | 2 +- 5 files changed, 235 insertions(+), 19 deletions(-) create mode 100644 .github/workflows/_build-cloud.yml create mode 100644 .github/workflows/release-cloud-prod.yml diff --git a/.github/workflows/_build-cloud.yml b/.github/workflows/_build-cloud.yml new file mode 100644 index 000000000..9d9f777d0 --- /dev/null +++ b/.github/workflows/_build-cloud.yml @@ -0,0 +1,174 @@ +# Internal reusable workflow for building a non-OSS ("cloud") Docker image and +# pushing it to Amazon ECR. +# +# Unlike _build.yml (which builds the public, multi-platform OSS image for GHCR), +# this workflow bakes environment-specific configuration into the image — Sentry +# DSNs, the Sentry environment name — and uploads source maps to Sentry. Those +# values come from the GitHub Environment named by `environment`, so a new +# deployment environment is a new Environment plus a small caller workflow. +# +# Each environment publishes to its own ECR repository, `sourcebot-`, +# owned by CicdStack in the sourcebot-demo-infra repo. +# +# Single-platform (linux/amd64): the EKS `general-purpose` NodePool that runs +# Sourcebot pins `kubernetes.io/arch: amd64`. Building one platform lets us push +# tags directly, skipping the push-by-digest + manifest-merge dance _build.yml +# needs. It also avoids ECR lifecycle rules for untagged images silently deleting +# a manifest list's per-platform children. + +name: Build Cloud Image + +on: + workflow_call: + inputs: + environment: + description: "GitHub Environment supplying the Sentry vars/secrets. Also scopes the OIDC subject used to assume the ECR push role." + required: true + type: string + git_ref: + description: "Git ref to checkout and build" + required: true + type: string + docker_tags: + description: "Docker tags configuration for docker/metadata-action" + required: true + type: string + aws_region: + description: "Region the ECR repository lives in" + required: false + type: string + default: us-west-1 + +jobs: + build: + runs-on: ubuntu-latest + # Gates the job on the Environment's protection rules, and — because the + # OIDC subject for a job with an environment is + # `repo:/:environment:` — is what the ECR push role's trust + # policy matches on. Also what makes `vars`/`secrets` below resolve. + environment: ${{ inputs.environment }} + permissions: + contents: read + # Required to request the OIDC token that assumes the AWS role. + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ inputs.git_ref }} + submodules: "true" + fetch-depth: 0 + + # The exact commit built. `github.sha` is the SHA of the ref the workflow + # was *dispatched* on, which is not necessarily `git_ref`. + - name: Resolve build commit SHA + id: commit + run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + + # Fail fast, and loudly. The Dockerfile only uploads source maps when every + # Sentry input is non-empty, so a missing var or secret would otherwise + # produce a perfectly green build of an image with no Sentry wiring. Values + # are never printed — only whether each resolved to something. + - name: Validate environment configuration + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + AWS_ECR_ROLE_ARN: ${{ vars.AWS_ECR_ROLE_ARN }} + NEXT_PUBLIC_SENTRY_ENVIRONMENT: ${{ vars.NEXT_PUBLIC_SENTRY_ENVIRONMENT }} + NEXT_PUBLIC_SENTRY_WEBAPP_DSN: ${{ vars.NEXT_PUBLIC_SENTRY_WEBAPP_DSN }} + NEXT_PUBLIC_SENTRY_BACKEND_DSN: ${{ vars.NEXT_PUBLIC_SENTRY_BACKEND_DSN }} + SENTRY_ORG: ${{ vars.SENTRY_ORG }} + SENTRY_WEBAPP_PROJECT: ${{ vars.SENTRY_WEBAPP_PROJECT }} + SENTRY_BACKEND_PROJECT: ${{ vars.SENTRY_BACKEND_PROJECT }} + run: | + missing=0 + for name in SENTRY_AUTH_TOKEN AWS_ECR_ROLE_ARN \ + NEXT_PUBLIC_SENTRY_ENVIRONMENT \ + NEXT_PUBLIC_SENTRY_WEBAPP_DSN \ + NEXT_PUBLIC_SENTRY_BACKEND_DSN \ + SENTRY_ORG SENTRY_WEBAPP_PROJECT SENTRY_BACKEND_PROJECT; do + if [ -z "${!name}" ]; then + echo "::error::${name} is not set on the '${{ inputs.environment }}' environment (or the repository)." + missing=1 + else + echo "ok: ${name}" + fi + done + if [ "$missing" -ne 0 ]; then + echo "::error::Refusing to build: the image would ship without Sentry wiring." + exit 1 + fi + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ vars.AWS_ECR_ROLE_ARN }} + aws-region: ${{ inputs.aws_region }} + + - name: Login to Amazon ECR + id: ecr + uses: aws-actions/amazon-ecr-login@v2 + + # Each environment publishes to its own registry (see CicdStack), so a + # staging build can never overwrite a prod tag. + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ steps.ecr.outputs.registry }}/sourcebot-${{ inputs.environment }} + tags: ${{ inputs.docker_tags }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and push Docker image + uses: docker/build-push-action@v7 + with: + context: . + platforms: linux/amd64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + # SENTRY_RELEASE is the commit SHA rather than SOURCEBOT_VERSION so that + # every prod build gets a distinct release (prod tracks `main`, where the + # version only moves on a tagged release). packages/backend/src/instrument.ts + # reports NEXT_PUBLIC_BUILD_COMMIT_SHA as its release to match; the webapp + # gets SENTRY_RELEASE injected into its bundle by withSentryConfig. + build-args: | + NEXT_PUBLIC_BUILD_COMMIT_SHA=${{ steps.commit.outputs.sha }} + NEXT_PUBLIC_SENTRY_ENVIRONMENT=${{ vars.NEXT_PUBLIC_SENTRY_ENVIRONMENT }} + NEXT_PUBLIC_SENTRY_WEBAPP_DSN=${{ vars.NEXT_PUBLIC_SENTRY_WEBAPP_DSN }} + NEXT_PUBLIC_SENTRY_BACKEND_DSN=${{ vars.NEXT_PUBLIC_SENTRY_BACKEND_DSN }} + NEXT_PUBLIC_LANGFUSE_PUBLIC_KEY=${{ vars.NEXT_PUBLIC_LANGFUSE_PUBLIC_KEY }} + NEXT_PUBLIC_LANGFUSE_BASE_URL=${{ vars.NEXT_PUBLIC_LANGFUSE_BASE_URL }} + SENTRY_ORG=${{ vars.SENTRY_ORG }} + SENTRY_WEBAPP_PROJECT=${{ vars.SENTRY_WEBAPP_PROJECT }} + SENTRY_BACKEND_PROJECT=${{ vars.SENTRY_BACKEND_PROJECT }} + SENTRY_RELEASE=${{ steps.commit.outputs.sha }} + # Passed as a secret, not a build-arg: build args are recorded in layer + # metadata that `mode=max` exports to the cache. @see: Dockerfile + secrets: | + sentry_auth_token=${{ secrets.SENTRY_AUTH_TOKEN }} + # Cache scope is per-environment, and distinct from the OSS build's + # (which is keyed on platform alone). Sharing a scope would let a build + # that never sees SENTRY_AUTH_TOKEN restore layers from one that did. + cache-from: type=gha,scope=cloud-${{ inputs.environment }}-amd64 + cache-to: type=gha,mode=max,scope=cloud-${{ inputs.environment }}-amd64 + + - name: Summarize + env: + TAGS: ${{ steps.meta.outputs.tags }} + run: | + { + echo "### Pushed to ECR" + echo + echo "| | |" + echo "|---|---|" + echo "| Environment | \`${{ inputs.environment }}\` |" + echo "| Commit | \`${{ steps.commit.outputs.sha }}\` |" + echo "| Sentry release | \`${{ steps.commit.outputs.sha }}\` |" + echo + echo '```' + echo "$TAGS" + echo '```' + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/release-cloud-prod.yml b/.github/workflows/release-cloud-prod.yml new file mode 100644 index 000000000..248cfae69 --- /dev/null +++ b/.github/workflows/release-cloud-prod.yml @@ -0,0 +1,51 @@ +# Builds the production image (app.sourcebot.dev) and pushes it to Amazon ECR. +# +# This is *not* the OSS image: it has the prod Sentry DSNs baked in and uploads +# source maps to Sentry. See _build-cloud.yml. +# +# Publishes to the `sourcebot-prod` ECR repository: +# +# push to main -> :main, :sha- +# push v*.*.* tag -> :v, :latest +# +# The `sha-` prefix is load-bearing: CicdStack's lifecycle rule expires old commit +# images by matching that prefix, which is what keeps `main`, `latest` and `v*` +# from ever being expired by count. +# +# Prod deploys from the `:main` tag. The OSS image (ghcr.io/sourcebot-dev/sourcebot) +# is still built independently by release-dev.yml / release-prod.yml. +# +# Note: the Prisma migration backstop is not repeated here — release-dev.yml runs +# `check-prisma-migrations` on the same commit for every push to main. + +name: Release Sourcebot (Cloud - Production) + +# The called workflow's permissions are capped by the caller's, and `id-token` is +# never granted by default — without requesting it here, the OIDC token request +# that assumes the ECR push role fails. +permissions: + contents: read + id-token: write + +on: + push: + branches: ["main"] + tags: ["v*.*.*"] + workflow_dispatch: + +concurrency: + group: release-cloud-prod-${{ github.ref }} + cancel-in-progress: false + +jobs: + build: + uses: ./.github/workflows/_build-cloud.yml + with: + environment: prod + git_ref: ${{ github.ref }} + docker_tags: | + type=raw,value=main,enable=${{ github.ref == 'refs/heads/main' }} + type=sha,format=long,enable=${{ github.ref == 'refs/heads/main' }} + type=semver,pattern=v{{version}},enable=${{ startsWith(github.ref, 'refs/tags/v') }} + type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }} + secrets: inherit diff --git a/Dockerfile b/Dockerfile index 6c6b2439f..4a1b016c7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -61,19 +61,12 @@ ENV NEXT_PUBLIC_LANGFUSE_BASE_URL=$NEXT_PUBLIC_LANGFUSE_BASE_URL ARG NEXT_PUBLIC_BUILD_COMMIT_SHA ENV NEXT_PUBLIC_BUILD_COMMIT_SHA=$NEXT_PUBLIC_BUILD_COMMIT_SHA -# To upload source maps to Sentry, we need to set the following build-time args. -# It's important that we don't set these for oss builds, otherwise the Sentry -# auth token will be exposed. -# @see : next.config.mjs ARG SENTRY_ORG ENV SENTRY_ORG=$SENTRY_ORG ARG SENTRY_WEBAPP_PROJECT ENV SENTRY_WEBAPP_PROJECT=$SENTRY_WEBAPP_PROJECT ARG SENTRY_RELEASE ENV SENTRY_RELEASE=$SENTRY_RELEASE -# SMUAT = Source Map Upload Auth Token -ARG SENTRY_SMUAT -ENV SENTRY_SMUAT=$SENTRY_SMUAT # ----------- RUN apk add --no-cache libc6-compat @@ -92,7 +85,9 @@ COPY --from=shared-libs-builder /app/packages/queryLanguage ./packages/queryLang RUN yarn workspace @sourcebot/web install ENV NEXT_TELEMETRY_DISABLED=1 -RUN yarn workspace @sourcebot/web build + +RUN --mount=type=secret,id=sentry_auth_token,env=SENTRY_AUTH_TOKEN \ + yarn workspace @sourcebot/web build ENV SKIP_ENV_VALIDATION=0 # ------------------------------ @@ -101,16 +96,10 @@ FROM node-alpine AS backend-builder ENV SKIP_ENV_VALIDATION=1 # ----------- -# To upload source maps to Sentry, we need to set the following build-time args. -# It's important that we don't set these for oss builds, otherwise the Sentry -# auth token will be exposed. ARG SENTRY_ORG ENV SENTRY_ORG=$SENTRY_ORG ARG SENTRY_BACKEND_PROJECT ENV SENTRY_BACKEND_PROJECT=$SENTRY_BACKEND_PROJECT -# SMUAT = Source Map Upload Auth Token -ARG SENTRY_SMUAT -ENV SENTRY_SMUAT=$SENTRY_SMUAT ARG SENTRY_RELEASE ENV SENTRY_RELEASE=$SENTRY_RELEASE # ----------- @@ -129,11 +118,10 @@ COPY --from=shared-libs-builder /app/packages/queryLanguage ./packages/queryLang RUN yarn workspace @sourcebot/backend install RUN yarn workspace @sourcebot/backend build -# Upload source maps to Sentry if we have the necessary build-time args. -RUN if [ -n "$SENTRY_SMUAT" ] && [ -n "$SENTRY_ORG" ] && [ -n "$SENTRY_BACKEND_PROJECT" ] && [ -n "$SENTRY_RELEASE" ]; then \ +RUN --mount=type=secret,id=sentry_auth_token,env=SENTRY_AUTH_TOKEN \ + if [ -n "$SENTRY_AUTH_TOKEN" ] && [ -n "$SENTRY_ORG" ] && [ -n "$SENTRY_BACKEND_PROJECT" ] && [ -n "$SENTRY_RELEASE" ]; then \ apk add --no-cache curl; \ curl -sL https://sentry.io/get-cli/ | sh; \ - sentry-cli login --auth-token $SENTRY_SMUAT; \ sentry-cli sourcemaps inject --org $SENTRY_ORG --project $SENTRY_BACKEND_PROJECT --release $SENTRY_RELEASE ./packages/backend/dist; \ sentry-cli sourcemaps upload --org $SENTRY_ORG --project $SENTRY_BACKEND_PROJECT --release $SENTRY_RELEASE ./packages/backend/dist; \ fi diff --git a/packages/backend/src/instrument.ts b/packages/backend/src/instrument.ts index ab37f297e..87866b378 100644 --- a/packages/backend/src/instrument.ts +++ b/packages/backend/src/instrument.ts @@ -7,7 +7,10 @@ const logger = createLogger('instrument'); if (!!env.NEXT_PUBLIC_SENTRY_BACKEND_DSN && !!env.NEXT_PUBLIC_SENTRY_ENVIRONMENT) { Sentry.init({ dsn: env.NEXT_PUBLIC_SENTRY_BACKEND_DSN, - release: SOURCEBOT_VERSION, + // Must match the release our source maps are uploaded under, which the + // Dockerfile sets from SENTRY_RELEASE (the build's commit SHA). Falls back + // to the version for builds that don't pass a commit SHA. + release: env.NEXT_PUBLIC_BUILD_COMMIT_SHA ?? SOURCEBOT_VERSION, environment: env.NEXT_PUBLIC_SENTRY_ENVIRONMENT, }); } else { diff --git a/packages/web/next.config.mjs b/packages/web/next.config.mjs index f22ccf3c5..6bfbb0cf8 100644 --- a/packages/web/next.config.mjs +++ b/packages/web/next.config.mjs @@ -146,7 +146,7 @@ export default withSentryConfig(nextConfig, { // For all available options, see: org: process.env.SENTRY_ORG, project: process.env.SENTRY_WEBAPP_PROJECT, - authToken: process.env.SENTRY_SMUAT, + authToken: process.env.SENTRY_AUTH_TOKEN, release: process.env.SENTRY_RELEASE, // Only print logs for uploading source maps in CI From efade0f1b54db1e70fa19eefface22393eaaf17a Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Thu, 9 Jul 2026 19:04:55 -0700 Subject: [PATCH 3/6] fix(web): enable Sentry tracing and restore client-side Sentry Renames sentry.client.config.ts to instrumentation-client.ts, which Next.js loads itself. The old file was only wired in by @sentry/nextjs' webpack plugin, which never runs: next build defaults to Turbopack in Next 16, and next.config.mjs defines a turbopack block, so Next's "webpack config with no turbopack config" guard passes silently. Client errors, replays and spans were therefore never reported in production. Exports onRouterTransitionStart so App Router client-side navigations are instrumented as spans; Next only reads that export from instrumentation-client.ts. Sets tracesSampleRate on client, server and edge. Tracing is off unless this option is present at all, since hasSpansEnabled() checks for non-nullish. Sampling every trace for now to get a read on span volume before tuning the rate down. Co-Authored-By: Claude Opus 4.8 (1M context) --- ...ntry.client.config.ts => instrumentation-client.ts} | 10 ++++++++++ packages/web/src/sentry.edge.config.ts | 2 ++ packages/web/src/sentry.server.config.ts | 2 ++ 3 files changed, 14 insertions(+) rename packages/web/src/{sentry.client.config.ts => instrumentation-client.ts} (58%) diff --git a/packages/web/src/sentry.client.config.ts b/packages/web/src/instrumentation-client.ts similarity index 58% rename from packages/web/src/sentry.client.config.ts rename to packages/web/src/instrumentation-client.ts index f22073e65..07dc43073 100644 --- a/packages/web/src/sentry.client.config.ts +++ b/packages/web/src/instrumentation-client.ts @@ -1,6 +1,10 @@ // This file configures the initialization of Sentry on the client. // The config you add here will be used whenever a users loads a page in their browser. // https://docs.sentry.io/platforms/javascript/guides/nextjs/ +// +// Must be named `instrumentation-client.ts`. Next.js loads this file itself, whereas +// `sentry.client.config.ts` is only picked up by @sentry/nextjs' webpack plugin, +// which never runs since `next build` defaults to Turbopack. import * as Sentry from "@sentry/nextjs"; @@ -9,9 +13,15 @@ if (!!process.env.NEXT_PUBLIC_SENTRY_WEBAPP_DSN && !!process.env.NEXT_PUBLIC_SEN dsn: process.env.NEXT_PUBLIC_SENTRY_WEBAPP_DSN, environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT, + tracesSampleRate: 1.0, + // Setting this option to true will print useful information to the console while you're setting up Sentry. debug: false, }); } else { console.debug("[client] Sentry was not initialized"); } + +// Instruments App Router client-side navigations as spans. Next.js only reads this +// export from `instrumentation-client.ts`. A no-op when Sentry is uninitialized. +export const onRouterTransitionStart = Sentry.captureRouterTransitionStart; diff --git a/packages/web/src/sentry.edge.config.ts b/packages/web/src/sentry.edge.config.ts index dbc3e5c54..77866181f 100644 --- a/packages/web/src/sentry.edge.config.ts +++ b/packages/web/src/sentry.edge.config.ts @@ -10,6 +10,8 @@ if (!!process.env.NEXT_PUBLIC_SENTRY_WEBAPP_DSN && !!process.env.NEXT_PUBLIC_SEN dsn: process.env.NEXT_PUBLIC_SENTRY_WEBAPP_DSN, environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT, + tracesSampleRate: 1.0, + // Setting this option to true will print useful information to the console while you're setting up Sentry. debug: false, }); diff --git a/packages/web/src/sentry.server.config.ts b/packages/web/src/sentry.server.config.ts index c9416bc64..c51b7246c 100644 --- a/packages/web/src/sentry.server.config.ts +++ b/packages/web/src/sentry.server.config.ts @@ -12,6 +12,8 @@ if (!!process.env.NEXT_PUBLIC_SENTRY_WEBAPP_DSN && !!process.env.NEXT_PUBLIC_SEN dsn: process.env.NEXT_PUBLIC_SENTRY_WEBAPP_DSN, environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT, + tracesSampleRate: 1.0, + // Setting this option to true will print useful information to the console while you're setting up Sentry. debug: false, }); From ca52435e27f00aea4f4c9be7bbf51d75453fe16a Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Thu, 9 Jul 2026 19:29:31 -0700 Subject: [PATCH 4/6] s --- .github/workflows/_build-cloud.yml | 26 ------------------------ .github/workflows/release-cloud-prod.yml | 23 --------------------- packages/web/next.config.mjs | 2 +- 3 files changed, 1 insertion(+), 50 deletions(-) diff --git a/.github/workflows/_build-cloud.yml b/.github/workflows/_build-cloud.yml index 9d9f777d0..7c6ab892f 100644 --- a/.github/workflows/_build-cloud.yml +++ b/.github/workflows/_build-cloud.yml @@ -1,21 +1,5 @@ # Internal reusable workflow for building a non-OSS ("cloud") Docker image and # pushing it to Amazon ECR. -# -# Unlike _build.yml (which builds the public, multi-platform OSS image for GHCR), -# this workflow bakes environment-specific configuration into the image — Sentry -# DSNs, the Sentry environment name — and uploads source maps to Sentry. Those -# values come from the GitHub Environment named by `environment`, so a new -# deployment environment is a new Environment plus a small caller workflow. -# -# Each environment publishes to its own ECR repository, `sourcebot-`, -# owned by CicdStack in the sourcebot-demo-infra repo. -# -# Single-platform (linux/amd64): the EKS `general-purpose` NodePool that runs -# Sourcebot pins `kubernetes.io/arch: amd64`. Building one platform lets us push -# tags directly, skipping the push-by-digest + manifest-merge dance _build.yml -# needs. It also avoids ECR lifecycle rules for untagged images silently deleting -# a manifest list's per-platform children. - name: Build Cloud Image on: @@ -42,10 +26,6 @@ on: jobs: build: runs-on: ubuntu-latest - # Gates the job on the Environment's protection rules, and — because the - # OIDC subject for a job with an environment is - # `repo:/:environment:` — is what the ECR push role's trust - # policy matches on. Also what makes `vars`/`secrets` below resolve. environment: ${{ inputs.environment }} permissions: contents: read @@ -60,16 +40,10 @@ jobs: submodules: "true" fetch-depth: 0 - # The exact commit built. `github.sha` is the SHA of the ref the workflow - # was *dispatched* on, which is not necessarily `git_ref`. - name: Resolve build commit SHA id: commit run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" - # Fail fast, and loudly. The Dockerfile only uploads source maps when every - # Sentry input is non-empty, so a missing var or secret would otherwise - # produce a perfectly green build of an image with no Sentry wiring. Values - # are never printed — only whether each resolved to something. - name: Validate environment configuration env: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} diff --git a/.github/workflows/release-cloud-prod.yml b/.github/workflows/release-cloud-prod.yml index 248cfae69..e613e22f6 100644 --- a/.github/workflows/release-cloud-prod.yml +++ b/.github/workflows/release-cloud-prod.yml @@ -1,28 +1,5 @@ -# Builds the production image (app.sourcebot.dev) and pushes it to Amazon ECR. -# -# This is *not* the OSS image: it has the prod Sentry DSNs baked in and uploads -# source maps to Sentry. See _build-cloud.yml. -# -# Publishes to the `sourcebot-prod` ECR repository: -# -# push to main -> :main, :sha- -# push v*.*.* tag -> :v, :latest -# -# The `sha-` prefix is load-bearing: CicdStack's lifecycle rule expires old commit -# images by matching that prefix, which is what keeps `main`, `latest` and `v*` -# from ever being expired by count. -# -# Prod deploys from the `:main` tag. The OSS image (ghcr.io/sourcebot-dev/sourcebot) -# is still built independently by release-dev.yml / release-prod.yml. -# -# Note: the Prisma migration backstop is not repeated here — release-dev.yml runs -# `check-prisma-migrations` on the same commit for every push to main. - name: Release Sourcebot (Cloud - Production) -# The called workflow's permissions are capped by the caller's, and `id-token` is -# never granted by default — without requesting it here, the OIDC token request -# that assumes the ECR push role fails. permissions: contents: read id-token: write diff --git a/packages/web/next.config.mjs b/packages/web/next.config.mjs index 6bfbb0cf8..953fd480f 100644 --- a/packages/web/next.config.mjs +++ b/packages/web/next.config.mjs @@ -147,7 +147,7 @@ export default withSentryConfig(nextConfig, { org: process.env.SENTRY_ORG, project: process.env.SENTRY_WEBAPP_PROJECT, authToken: process.env.SENTRY_AUTH_TOKEN, - release: process.env.SENTRY_RELEASE, + release: { name: process.env.SENTRY_RELEASE }, // Only print logs for uploading source maps in CI silent: !process.env.CI, From 974369860fd417b07cf18c132429e4f72a856852 Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Thu, 9 Jul 2026 19:36:20 -0700 Subject: [PATCH 5/6] ci: harden the cloud build workflow Sets persist-credentials: false on checkout. Nothing after it talks to the remote (only git rev-parse HEAD, which is local), so there is no reason to leave GITHUB_TOKEN in the workspace's .git/config. Binds inputs.environment and the resolved commit SHA to step-level env vars rather than interpolating them into run: scripts, where GitHub substitutes them into the shell source before bash parses it. This workflow is reusable and its environment input is caller-supplied. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/_build-cloud.yml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/_build-cloud.yml b/.github/workflows/_build-cloud.yml index 7c6ab892f..1fa275d67 100644 --- a/.github/workflows/_build-cloud.yml +++ b/.github/workflows/_build-cloud.yml @@ -39,6 +39,9 @@ jobs: ref: ${{ inputs.git_ref }} submodules: "true" fetch-depth: 0 + # Nothing after checkout talks to the remote, so don't leave the token + # behind in the workspace's .git/config. + persist-credentials: false - name: Resolve build commit SHA id: commit @@ -46,6 +49,7 @@ jobs: - name: Validate environment configuration env: + ENVIRONMENT: ${{ inputs.environment }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} AWS_ECR_ROLE_ARN: ${{ vars.AWS_ECR_ROLE_ARN }} NEXT_PUBLIC_SENTRY_ENVIRONMENT: ${{ vars.NEXT_PUBLIC_SENTRY_ENVIRONMENT }} @@ -62,7 +66,7 @@ jobs: NEXT_PUBLIC_SENTRY_BACKEND_DSN \ SENTRY_ORG SENTRY_WEBAPP_PROJECT SENTRY_BACKEND_PROJECT; do if [ -z "${!name}" ]; then - echo "::error::${name} is not set on the '${{ inputs.environment }}' environment (or the repository)." + echo "::error::${name} is not set on the '${ENVIRONMENT}' environment (or the repository)." missing=1 else echo "ok: ${name}" @@ -131,6 +135,8 @@ jobs: - name: Summarize env: + ENVIRONMENT: ${{ inputs.environment }} + COMMIT_SHA: ${{ steps.commit.outputs.sha }} TAGS: ${{ steps.meta.outputs.tags }} run: | { @@ -138,9 +144,9 @@ jobs: echo echo "| | |" echo "|---|---|" - echo "| Environment | \`${{ inputs.environment }}\` |" - echo "| Commit | \`${{ steps.commit.outputs.sha }}\` |" - echo "| Sentry release | \`${{ steps.commit.outputs.sha }}\` |" + echo "| Environment | \`${ENVIRONMENT}\` |" + echo "| Commit | \`${COMMIT_SHA}\` |" + echo "| Sentry release | \`${COMMIT_SHA}\` |" echo echo '```' echo "$TAGS" From ec420973a1844a83d7cb4de76e711f8431f86a06 Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Thu, 9 Jul 2026 19:55:26 -0700 Subject: [PATCH 6/6] ci: run the Prisma drift check on cloud builds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _build.yml runs this check, but that is a separate workflow run triggered by the same push to main — it cannot gate the cloud build. Without it here, a commit whose migrations drift from schema.prisma reddens the OSS build while still publishing an image to ECR, which is what prod actually runs. Placed before the AWS credentials step so a drifted commit never assumes the ECR push role. base-ref is omitted, so the drift check runs unconditionally, the PR-only ordering check is skipped, and the action never reaches for the git remote (compatible with persist-credentials: false). Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/_build-cloud.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/_build-cloud.yml b/.github/workflows/_build-cloud.yml index 1fa275d67..d9a44d173 100644 --- a/.github/workflows/_build-cloud.yml +++ b/.github/workflows/_build-cloud.yml @@ -77,6 +77,9 @@ jobs: exit 1 fi + - name: Check Prisma migrations + uses: ./.github/actions/check-prisma-migrations + - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v4 with: