Skip to content

Commit a1b71c3

Browse files
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-<env> 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) <noreply@anthropic.com>
1 parent 4d9b37f commit a1b71c3

5 files changed

Lines changed: 235 additions & 19 deletions

File tree

.github/workflows/_build-cloud.yml

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
# Internal reusable workflow for building a non-OSS ("cloud") Docker image and
2+
# pushing it to Amazon ECR.
3+
#
4+
# Unlike _build.yml (which builds the public, multi-platform OSS image for GHCR),
5+
# this workflow bakes environment-specific configuration into the image — Sentry
6+
# DSNs, the Sentry environment name — and uploads source maps to Sentry. Those
7+
# values come from the GitHub Environment named by `environment`, so a new
8+
# deployment environment is a new Environment plus a small caller workflow.
9+
#
10+
# Each environment publishes to its own ECR repository, `sourcebot-<environment>`,
11+
# owned by CicdStack in the sourcebot-demo-infra repo.
12+
#
13+
# Single-platform (linux/amd64): the EKS `general-purpose` NodePool that runs
14+
# Sourcebot pins `kubernetes.io/arch: amd64`. Building one platform lets us push
15+
# tags directly, skipping the push-by-digest + manifest-merge dance _build.yml
16+
# needs. It also avoids ECR lifecycle rules for untagged images silently deleting
17+
# a manifest list's per-platform children.
18+
19+
name: Build Cloud Image
20+
21+
on:
22+
workflow_call:
23+
inputs:
24+
environment:
25+
description: "GitHub Environment supplying the Sentry vars/secrets. Also scopes the OIDC subject used to assume the ECR push role."
26+
required: true
27+
type: string
28+
git_ref:
29+
description: "Git ref to checkout and build"
30+
required: true
31+
type: string
32+
docker_tags:
33+
description: "Docker tags configuration for docker/metadata-action"
34+
required: true
35+
type: string
36+
aws_region:
37+
description: "Region the ECR repository lives in"
38+
required: false
39+
type: string
40+
default: us-west-1
41+
42+
jobs:
43+
build:
44+
runs-on: ubuntu-latest
45+
# Gates the job on the Environment's protection rules, and — because the
46+
# OIDC subject for a job with an environment is
47+
# `repo:<org>/<repo>:environment:<name>` — is what the ECR push role's trust
48+
# policy matches on. Also what makes `vars`/`secrets` below resolve.
49+
environment: ${{ inputs.environment }}
50+
permissions:
51+
contents: read
52+
# Required to request the OIDC token that assumes the AWS role.
53+
id-token: write
54+
55+
steps:
56+
- name: Checkout repository
57+
uses: actions/checkout@v4
58+
with:
59+
ref: ${{ inputs.git_ref }}
60+
submodules: "true"
61+
fetch-depth: 0
62+
63+
# The exact commit built. `github.sha` is the SHA of the ref the workflow
64+
# was *dispatched* on, which is not necessarily `git_ref`.
65+
- name: Resolve build commit SHA
66+
id: commit
67+
run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
68+
69+
# Fail fast, and loudly. The Dockerfile only uploads source maps when every
70+
# Sentry input is non-empty, so a missing var or secret would otherwise
71+
# produce a perfectly green build of an image with no Sentry wiring. Values
72+
# are never printed — only whether each resolved to something.
73+
- name: Validate environment configuration
74+
env:
75+
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
76+
AWS_ECR_ROLE_ARN: ${{ vars.AWS_ECR_ROLE_ARN }}
77+
NEXT_PUBLIC_SENTRY_ENVIRONMENT: ${{ vars.NEXT_PUBLIC_SENTRY_ENVIRONMENT }}
78+
NEXT_PUBLIC_SENTRY_WEBAPP_DSN: ${{ vars.NEXT_PUBLIC_SENTRY_WEBAPP_DSN }}
79+
NEXT_PUBLIC_SENTRY_BACKEND_DSN: ${{ vars.NEXT_PUBLIC_SENTRY_BACKEND_DSN }}
80+
SENTRY_ORG: ${{ vars.SENTRY_ORG }}
81+
SENTRY_WEBAPP_PROJECT: ${{ vars.SENTRY_WEBAPP_PROJECT }}
82+
SENTRY_BACKEND_PROJECT: ${{ vars.SENTRY_BACKEND_PROJECT }}
83+
run: |
84+
missing=0
85+
for name in SENTRY_AUTH_TOKEN AWS_ECR_ROLE_ARN \
86+
NEXT_PUBLIC_SENTRY_ENVIRONMENT \
87+
NEXT_PUBLIC_SENTRY_WEBAPP_DSN \
88+
NEXT_PUBLIC_SENTRY_BACKEND_DSN \
89+
SENTRY_ORG SENTRY_WEBAPP_PROJECT SENTRY_BACKEND_PROJECT; do
90+
if [ -z "${!name}" ]; then
91+
echo "::error::${name} is not set on the '${{ inputs.environment }}' environment (or the repository)."
92+
missing=1
93+
else
94+
echo "ok: ${name}"
95+
fi
96+
done
97+
if [ "$missing" -ne 0 ]; then
98+
echo "::error::Refusing to build: the image would ship without Sentry wiring."
99+
exit 1
100+
fi
101+
102+
- name: Configure AWS credentials
103+
uses: aws-actions/configure-aws-credentials@v4
104+
with:
105+
role-to-assume: ${{ vars.AWS_ECR_ROLE_ARN }}
106+
aws-region: ${{ inputs.aws_region }}
107+
108+
- name: Login to Amazon ECR
109+
id: ecr
110+
uses: aws-actions/amazon-ecr-login@v2
111+
112+
# Each environment publishes to its own registry (see CicdStack), so a
113+
# staging build can never overwrite a prod tag.
114+
- name: Extract Docker metadata
115+
id: meta
116+
uses: docker/metadata-action@v5
117+
with:
118+
images: ${{ steps.ecr.outputs.registry }}/sourcebot-${{ inputs.environment }}
119+
tags: ${{ inputs.docker_tags }}
120+
121+
- name: Set up Docker Buildx
122+
uses: docker/setup-buildx-action@v3
123+
124+
- name: Build and push Docker image
125+
uses: docker/build-push-action@v7
126+
with:
127+
context: .
128+
platforms: linux/amd64
129+
push: true
130+
tags: ${{ steps.meta.outputs.tags }}
131+
labels: ${{ steps.meta.outputs.labels }}
132+
# SENTRY_RELEASE is the commit SHA rather than SOURCEBOT_VERSION so that
133+
# every prod build gets a distinct release (prod tracks `main`, where the
134+
# version only moves on a tagged release). packages/backend/src/instrument.ts
135+
# reports NEXT_PUBLIC_BUILD_COMMIT_SHA as its release to match; the webapp
136+
# gets SENTRY_RELEASE injected into its bundle by withSentryConfig.
137+
build-args: |
138+
NEXT_PUBLIC_BUILD_COMMIT_SHA=${{ steps.commit.outputs.sha }}
139+
NEXT_PUBLIC_SENTRY_ENVIRONMENT=${{ vars.NEXT_PUBLIC_SENTRY_ENVIRONMENT }}
140+
NEXT_PUBLIC_SENTRY_WEBAPP_DSN=${{ vars.NEXT_PUBLIC_SENTRY_WEBAPP_DSN }}
141+
NEXT_PUBLIC_SENTRY_BACKEND_DSN=${{ vars.NEXT_PUBLIC_SENTRY_BACKEND_DSN }}
142+
NEXT_PUBLIC_LANGFUSE_PUBLIC_KEY=${{ vars.NEXT_PUBLIC_LANGFUSE_PUBLIC_KEY }}
143+
NEXT_PUBLIC_LANGFUSE_BASE_URL=${{ vars.NEXT_PUBLIC_LANGFUSE_BASE_URL }}
144+
SENTRY_ORG=${{ vars.SENTRY_ORG }}
145+
SENTRY_WEBAPP_PROJECT=${{ vars.SENTRY_WEBAPP_PROJECT }}
146+
SENTRY_BACKEND_PROJECT=${{ vars.SENTRY_BACKEND_PROJECT }}
147+
SENTRY_RELEASE=${{ steps.commit.outputs.sha }}
148+
# Passed as a secret, not a build-arg: build args are recorded in layer
149+
# metadata that `mode=max` exports to the cache. @see: Dockerfile
150+
secrets: |
151+
sentry_auth_token=${{ secrets.SENTRY_AUTH_TOKEN }}
152+
# Cache scope is per-environment, and distinct from the OSS build's
153+
# (which is keyed on platform alone). Sharing a scope would let a build
154+
# that never sees SENTRY_AUTH_TOKEN restore layers from one that did.
155+
cache-from: type=gha,scope=cloud-${{ inputs.environment }}-amd64
156+
cache-to: type=gha,mode=max,scope=cloud-${{ inputs.environment }}-amd64
157+
158+
- name: Summarize
159+
env:
160+
TAGS: ${{ steps.meta.outputs.tags }}
161+
run: |
162+
{
163+
echo "### Pushed to ECR"
164+
echo
165+
echo "| | |"
166+
echo "|---|---|"
167+
echo "| Environment | \`${{ inputs.environment }}\` |"
168+
echo "| Commit | \`${{ steps.commit.outputs.sha }}\` |"
169+
echo "| Sentry release | \`${{ steps.commit.outputs.sha }}\` |"
170+
echo
171+
echo '```'
172+
echo "$TAGS"
173+
echo '```'
174+
} >> "$GITHUB_STEP_SUMMARY"
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Builds the production image (app.sourcebot.dev) and pushes it to Amazon ECR.
2+
#
3+
# This is *not* the OSS image: it has the prod Sentry DSNs baked in and uploads
4+
# source maps to Sentry. See _build-cloud.yml.
5+
#
6+
# Publishes to the `sourcebot-prod` ECR repository:
7+
#
8+
# push to main -> :main, :sha-<commit-sha>
9+
# push v*.*.* tag -> :v<x.y.z>, :latest
10+
#
11+
# The `sha-` prefix is load-bearing: CicdStack's lifecycle rule expires old commit
12+
# images by matching that prefix, which is what keeps `main`, `latest` and `v*`
13+
# from ever being expired by count.
14+
#
15+
# Prod deploys from the `:main` tag. The OSS image (ghcr.io/sourcebot-dev/sourcebot)
16+
# is still built independently by release-dev.yml / release-prod.yml.
17+
#
18+
# Note: the Prisma migration backstop is not repeated here — release-dev.yml runs
19+
# `check-prisma-migrations` on the same commit for every push to main.
20+
21+
name: Release Sourcebot (Cloud - Production)
22+
23+
# The called workflow's permissions are capped by the caller's, and `id-token` is
24+
# never granted by default — without requesting it here, the OIDC token request
25+
# that assumes the ECR push role fails.
26+
permissions:
27+
contents: read
28+
id-token: write
29+
30+
on:
31+
push:
32+
branches: ["main"]
33+
tags: ["v*.*.*"]
34+
workflow_dispatch:
35+
36+
concurrency:
37+
group: release-cloud-prod-${{ github.ref }}
38+
cancel-in-progress: false
39+
40+
jobs:
41+
build:
42+
uses: ./.github/workflows/_build-cloud.yml
43+
with:
44+
environment: prod
45+
git_ref: ${{ github.ref }}
46+
docker_tags: |
47+
type=raw,value=main,enable=${{ github.ref == 'refs/heads/main' }}
48+
type=sha,format=long,enable=${{ github.ref == 'refs/heads/main' }}
49+
type=semver,pattern=v{{version}},enable=${{ startsWith(github.ref, 'refs/tags/v') }}
50+
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
51+
secrets: inherit

Dockerfile

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -61,19 +61,12 @@ ENV NEXT_PUBLIC_LANGFUSE_BASE_URL=$NEXT_PUBLIC_LANGFUSE_BASE_URL
6161
ARG NEXT_PUBLIC_BUILD_COMMIT_SHA
6262
ENV NEXT_PUBLIC_BUILD_COMMIT_SHA=$NEXT_PUBLIC_BUILD_COMMIT_SHA
6363

64-
# To upload source maps to Sentry, we need to set the following build-time args.
65-
# It's important that we don't set these for oss builds, otherwise the Sentry
66-
# auth token will be exposed.
67-
# @see : next.config.mjs
6864
ARG SENTRY_ORG
6965
ENV SENTRY_ORG=$SENTRY_ORG
7066
ARG SENTRY_WEBAPP_PROJECT
7167
ENV SENTRY_WEBAPP_PROJECT=$SENTRY_WEBAPP_PROJECT
7268
ARG SENTRY_RELEASE
7369
ENV SENTRY_RELEASE=$SENTRY_RELEASE
74-
# SMUAT = Source Map Upload Auth Token
75-
ARG SENTRY_SMUAT
76-
ENV SENTRY_SMUAT=$SENTRY_SMUAT
7770
# -----------
7871

7972
RUN apk add --no-cache libc6-compat
@@ -92,7 +85,9 @@ COPY --from=shared-libs-builder /app/packages/queryLanguage ./packages/queryLang
9285
RUN yarn workspace @sourcebot/web install
9386

9487
ENV NEXT_TELEMETRY_DISABLED=1
95-
RUN yarn workspace @sourcebot/web build
88+
89+
RUN --mount=type=secret,id=sentry_auth_token,env=SENTRY_AUTH_TOKEN \
90+
yarn workspace @sourcebot/web build
9691
ENV SKIP_ENV_VALIDATION=0
9792
# ------------------------------
9893

@@ -101,16 +96,10 @@ FROM node-alpine AS backend-builder
10196
ENV SKIP_ENV_VALIDATION=1
10297
# -----------
10398

104-
# To upload source maps to Sentry, we need to set the following build-time args.
105-
# It's important that we don't set these for oss builds, otherwise the Sentry
106-
# auth token will be exposed.
10799
ARG SENTRY_ORG
108100
ENV SENTRY_ORG=$SENTRY_ORG
109101
ARG SENTRY_BACKEND_PROJECT
110102
ENV SENTRY_BACKEND_PROJECT=$SENTRY_BACKEND_PROJECT
111-
# SMUAT = Source Map Upload Auth Token
112-
ARG SENTRY_SMUAT
113-
ENV SENTRY_SMUAT=$SENTRY_SMUAT
114103
ARG SENTRY_RELEASE
115104
ENV SENTRY_RELEASE=$SENTRY_RELEASE
116105
# -----------
@@ -129,11 +118,10 @@ COPY --from=shared-libs-builder /app/packages/queryLanguage ./packages/queryLang
129118
RUN yarn workspace @sourcebot/backend install
130119
RUN yarn workspace @sourcebot/backend build
131120

132-
# Upload source maps to Sentry if we have the necessary build-time args.
133-
RUN if [ -n "$SENTRY_SMUAT" ] && [ -n "$SENTRY_ORG" ] && [ -n "$SENTRY_BACKEND_PROJECT" ] && [ -n "$SENTRY_RELEASE" ]; then \
121+
RUN --mount=type=secret,id=sentry_auth_token,env=SENTRY_AUTH_TOKEN \
122+
if [ -n "$SENTRY_AUTH_TOKEN" ] && [ -n "$SENTRY_ORG" ] && [ -n "$SENTRY_BACKEND_PROJECT" ] && [ -n "$SENTRY_RELEASE" ]; then \
134123
apk add --no-cache curl; \
135124
curl -sL https://sentry.io/get-cli/ | sh; \
136-
sentry-cli login --auth-token $SENTRY_SMUAT; \
137125
sentry-cli sourcemaps inject --org $SENTRY_ORG --project $SENTRY_BACKEND_PROJECT --release $SENTRY_RELEASE ./packages/backend/dist; \
138126
sentry-cli sourcemaps upload --org $SENTRY_ORG --project $SENTRY_BACKEND_PROJECT --release $SENTRY_RELEASE ./packages/backend/dist; \
139127
fi

packages/backend/src/instrument.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ const logger = createLogger('instrument');
77
if (!!env.NEXT_PUBLIC_SENTRY_BACKEND_DSN && !!env.NEXT_PUBLIC_SENTRY_ENVIRONMENT) {
88
Sentry.init({
99
dsn: env.NEXT_PUBLIC_SENTRY_BACKEND_DSN,
10-
release: SOURCEBOT_VERSION,
10+
// Must match the release our source maps are uploaded under, which the
11+
// Dockerfile sets from SENTRY_RELEASE (the build's commit SHA). Falls back
12+
// to the version for builds that don't pass a commit SHA.
13+
release: env.NEXT_PUBLIC_BUILD_COMMIT_SHA ?? SOURCEBOT_VERSION,
1114
environment: env.NEXT_PUBLIC_SENTRY_ENVIRONMENT,
1215
});
1316
} else {

packages/web/next.config.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ export default withSentryConfig(nextConfig, {
146146
// For all available options, see:
147147
org: process.env.SENTRY_ORG,
148148
project: process.env.SENTRY_WEBAPP_PROJECT,
149-
authToken: process.env.SENTRY_SMUAT,
149+
authToken: process.env.SENTRY_AUTH_TOKEN,
150150
release: process.env.SENTRY_RELEASE,
151151

152152
// Only print logs for uploading source maps in CI

0 commit comments

Comments
 (0)