diff --git a/.github/workflows/_build-cloud.yml b/.github/workflows/_build-cloud.yml new file mode 100644 index 000000000..d9a44d173 --- /dev/null +++ b/.github/workflows/_build-cloud.yml @@ -0,0 +1,157 @@ +# Internal reusable workflow for building a non-OSS ("cloud") Docker image and +# pushing it to Amazon ECR. +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 + 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 + # 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 + run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + + - 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 }} + 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 '${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: Check Prisma migrations + uses: ./.github/actions/check-prisma-migrations + + - 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: + ENVIRONMENT: ${{ inputs.environment }} + COMMIT_SHA: ${{ steps.commit.outputs.sha }} + TAGS: ${{ steps.meta.outputs.tags }} + run: | + { + echo "### Pushed to ECR" + echo + echo "| | |" + echo "|---|---|" + echo "| Environment | \`${ENVIRONMENT}\` |" + echo "| Commit | \`${COMMIT_SHA}\` |" + echo "| Sentry release | \`${COMMIT_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..e613e22f6 --- /dev/null +++ b/.github/workflows/release-cloud-prod.yml @@ -0,0 +1,28 @@ +name: Release Sourcebot (Cloud - Production) + +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..953fd480f 100644 --- a/packages/web/next.config.mjs +++ b/packages/web/next.config.mjs @@ -146,8 +146,8 @@ 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, - release: process.env.SENTRY_RELEASE, + authToken: process.env.SENTRY_AUTH_TOKEN, + release: { name: process.env.SENTRY_RELEASE }, // Only print logs for uploading source maps in CI silent: !process.env.CI, diff --git a/packages/web/sentry.client.config.ts b/packages/web/src/instrumentation-client.ts similarity index 58% rename from packages/web/sentry.client.config.ts rename to packages/web/src/instrumentation-client.ts index f22073e65..07dc43073 100644 --- a/packages/web/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/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.edge.config.ts b/packages/web/src/sentry.edge.config.ts similarity index 96% rename from packages/web/sentry.edge.config.ts rename to packages/web/src/sentry.edge.config.ts index dbc3e5c54..77866181f 100644 --- a/packages/web/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/sentry.server.config.ts b/packages/web/src/sentry.server.config.ts similarity index 96% rename from packages/web/sentry.server.config.ts rename to packages/web/src/sentry.server.config.ts index c9416bc64..c51b7246c 100644 --- a/packages/web/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, });