Skip to content

fix(sentry): give the build the org and project it needs to create a release (#16) - #20

Merged
arpitdalal merged 3 commits into
mainfrom
issue-16-sentry-build-secrets
Sep 29, 2026
Merged

arpitdalal merged 3 commits into
mainfrom
issue-16-sentry-build-secrets

Conversation

@arpitdalal

Copy link
Copy Markdown
Owner

Closes #16.

@sentry/vite-plugin only creates a Sentry release and uploads source maps when org, project, authToken and a release name are all present at build time. It reports a missing one with logger.warn and returns — after it has already injected the debug IDs during the bundle phase. So a build with nothing configured and a build that worked perfectly produce indistinguishable artifacts: both have debug IDs in the bundle, and both build green.

SENTRY_ORG and SENTRY_PROJECT were Fly runtime secrets, which the Docker build cannot see. The plugin's org guard therefore fired on every deploy since 2024-04-16: no release was ever created, and no source map was ever uploaded.

Verified on the live app before the change:

$ flyctl ssh console --app arpitdalal-dev-256c
root@9080e30da734d8:/myapp# find build -name '*.map' | wc -l
0

Zero source maps, client and server. That is the part the original issue understates: filesToDeleteAfterUpload is honoured in the finally of the plugin's writeBundle hook whether or not the upload succeeded, so the maps were deleted anyway. Symbolication has been broken on both halves, with nothing left to debug it with.

What changed

deploy.yml passes SENTRY_ORG/SENTRY_PROJECT as --build-secret, from a step env: rather than ${{ }} interpolation; rejects an empty secret before calling flyctl
other/Dockerfile mounts and exports all three, each required=true
vite.config.ts refuses to build when the plugin is enabled but anything it needs is missing or empty; restores the plugin's documented throw-on-error; drops the setCommits that skipped finalizeRelease
.gitignore, other/Dockerfile.dockerignore .env.sentry-build-plugin (holds an auth token; the plugin merges it into process.env)
tests/sentry-release.test.ts new, 19 tests

Five ways a release could still silently not happen, and what closes each

  1. The plugin swallows its own errors. Documented default is to throw and stop the build; shipped 11.0.0 logs and continues. Measured on this repo against a stub returning 500 for everything: EXIT=0, two Couldn't finish all operations errors, and find build -name '*.map' went from 40 files to 0. errorHandler: (error) => { throw error } restores the contract.
  2. setCommits: { auto: true } could never work, and made it worse. The base image has no git (node:22.23.3-bookworm-slim → git: not found), so --auto has no repository to read. Worse, naming auto explicitly opts out of the shouldNotThrowOnFailure: true the plugin's own default carries, so the failure rethrew and skipped finalizeRelease — a release created and left in new forever, on every deploy. Omitting the option restores the resilient default. Commit association still does not happen; it needs a Sentry GitHub integration or a sentry-cli releases set-commits run from the deploy job, which has a checkout. Not in scope here, noted so it is not mistaken for working.
  3. The && chain did not fail the build. export X="$(cat /run/secrets/X)" && echo exits 0 even when the cat fails — a failed command substitution does not propagate through export. A missing secret became an empty string. required=true is what actually enforces it.
  4. An unset GitHub secret is "", not unset. So a deleted SENTRY_AUTH_TOKEN took the "release management is off" branch: green deploy, no release, no debug IDs, nothing in the log. The deploy step now checks first.
  5. .env.sentry-build-plugin was in neither ignore file. The plugin merges it into process.env at construction, it holds an auth token, and ADD . . copies it into both image stages. It can also set SENTRY_URL, which nothing in the config passes, redirecting the release and upload to another host with the token attached.

Why a test, when the issue said none was practical

The issue's position was that the wiring only manifests inside a remote Docker build. True of the values — getSentryPlugin() now refuses to build when one is absent, and a test here could not add to that, because by then the guard and the plugin are reading the same empty variable and agreeing with each other.

But the issue also said the half worth guarding was the removal, "because it is the part that can silently break production." That half is mechanically testable: tests/sentry-release.test.ts asserts the three build-only credentials are never read by app/, server/ or index.ts, and that SENTRY_DSN still is. Verified non-vacuous — adding one process.env.SENTRY_ORG to server/index.ts fails the test, and the scan covers 76 files.

The wiring itself is tested by set equality, not a hardcoded list: the names come from matching process.env.… in vite.config.ts, so adding one to getSentryEnv() demands the workflow flag, the mount and the export for it. Verified by mutation — removing the --build-secret line fails 2 tests, the --mount 1, the export 1, the step env: entry 1.

Not in this PR

  • flyctl secrets unset of the three build-only secrets from the Fly runtime environment. Deliberately not done here: it destroys the only record of the org/project slugs and restarts the app, so it runs immediately after this deploys and is verified green. The values are already in GitHub Actions secrets, captured from the live container before the unset.
  • The server has no release yet. COMMIT_SHA still does not cross into the runtime image, so server events remain unattributed after this merges. That is Server-side Sentry events carry no release — COMMIT_SHA is dropped at the end of the Docker build stage #15, and it has to land for server symbolication to work — this PR makes the release exist and the maps upload, Server-side Sentry events carry no release — COMMIT_SHA is dropped at the end of the Docker build stage #15 makes the server tag events with it.
  • Commit association, per finding 2 above.
  • SENTRY_AUTH_TOKEN rotation. It is a legacy 64-hex token rather than an org-scoped sntrys_ one, and it has been readable in the production process environment since 2024-04-16. An sntrys_ token would also remove the need for SENTRY_ORG entirely. Separate change, deliberately not folded in here.

Verified

npm run validate clean · npm run test:run → 14 files, 199 tests · npm run build ok.

Note for whoever verifies by hand: a release appearing in Sentry under the full SHA is not sufficient evidence, and neither is the absence of a warning. The original evidence in #15 was _sentryDebugIds in the bundle, which proves only that the plugin ran. Check that the release is finalized and has associated commits; and read the build log for Successfully uploaded source maps, not for the absence of an error.

…release

@Sentry/vite-plugin only creates a release and uploads source maps when
org, project, authToken and a release name are all present at build time.
It reports a missing one with logger.warn and returns — after it has
already injected the debug IDs during the bundle phase. So a build with
nothing configured and a build that worked perfectly produce
indistinguishable artifacts: both have debug IDs in the bundle, and both
build green.

SENTRY_ORG and SENTRY_PROJECT were Fly *runtime* secrets, which the
Docker build cannot see. The plugin's org guard therefore fired on every
deploy since 2024-04-16: no release was ever created, and no source map
was ever uploaded. Worse, filesToDeleteAfterUpload is honoured in the
finally of writeBundle rather than only after a successful upload, so the
maps were deleted anyway — leaving symbolication broken on the client
too, with nothing left to debug it with. See #16.

Passes both as --build-secret, mounts and exports them in the build
stage alongside the token, and turns the remaining silent skips into a
build failure: the plugin has exactly one chance to upload, so a red
build is far cheaper than a release that silently does not exist.

tests/sentry-release.test.ts links the three files that have to agree —
the workflow that supplies the values, the Dockerfile that mounts them,
and the config that reads them. Nothing in the type system did, so
dropping one flag was a small edit that disabled the whole pipeline
without failing a check.
…t text

Interpolating ${{ secrets.* }} puts the value into the rendered command.
The step already passes FLY_API_TOKEN through `env:`, so this follows the
file's own convention and keeps the secrets out of the script body.

Adds a test for the resulting indirection: a --build-secret whose shell
variable has no matching `env:` entry would forward the literal string
$SENTRY_ORG, and the build would fail far from the cause.
The first pass made the *wiring* correct, but a review of the installed
plugin turned up five ways a build could still end up green with no
release, no source maps, and no evidence either was attempted.

- errorHandler: the plugin's documented default is to throw and stop the
  build; the shipped 11.0.0 implementation logs and continues. Restoring
  the documented behaviour is what makes the guard in this file mean
  anything, because `filesToDeleteAfterUpload` is honoured in the `finally`
  of its writeBundle hook whether or not the upload succeeded. A swallowed
  failure deleted every source map from the image and still exited zero.

- setCommits: dropped. Naming `auto` explicitly opts out of the
  `shouldNotThrowOnFailure` the plugin's own default carries, so a failure
  rethrew and skipped `finalizeRelease`, leaving the release stuck in `new`
  on every deploy. It could not have succeeded anyway — the base image has
  no `git`, so there is no repository for `--auto` to read.

- required=true on the secret mounts. The `&&` chain does not do this:
  `export X="$(cat /run/secrets/X)"` exits 0 even when the cat fails, so a
  missing secret arrived as an empty string rather than stopping the build.

- The deploy step now rejects an empty secret before calling flyctl. An
  unset GitHub secret is the empty string, not an unset variable, so a
  deleted SENTRY_AUTH_TOKEN took the "release management is off" branch:
  green build, no release, no debug IDs, nothing in the log.

- .env.sentry-build-plugin is now gitignored and dockerignored. The plugin
  merges it into process.env, and it holds an auth token; neither ignore
  file covered it, and `ADD . .` copies it into both image stages.

Also treats an empty value as absent rather than merely undefined, tests
getSentryPlugin() on both branches, and guards the build/runtime boundary:
the three build-only credentials are asserted to be unread by the running
app, which is what makes removing them from the Fly runtime environment
safe. That removal is the outstanding operational step of #16.
@coderabbitai

coderabbitai Bot commented Sep 29, 2026

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 1d673890-e4ad-461a-8b42-2165b1f8d39e

📥 Commits

Reviewing files that changed from the base of the PR and between 0927588 and 6c9a9a7.

📒 Files selected for processing (7)
  • .env.example
  • .github/workflows/deploy.yml
  • .gitignore
  • other/Dockerfile
  • other/Dockerfile.dockerignore
  • tests/sentry-release.test.ts
  • vite.config.ts
 _____________________
< Git gud or git out. >
 ---------------------
  \
   \   (\__/)
       (•ㅅ•)
       /   づ
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@arpitdalal
arpitdalal merged commit 6e751d2 into main Sep 29, 2026
5 of 6 checks passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6c9a9a756d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread vite.config.ts
Comment on lines +63 to +66
const missing = Object.entries(env)
.filter(([, value]) => !value)
.map(([name]) => name)
if (missing.length > 0) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate after loading the Sentry build environment

When SENTRY_AUTH_TOKEN and COMMIT_SHA are exported but SENTRY_ORG or SENTRY_PROJECT are stored in the supported .env.sentry-build-plugin file, this check throws before sentryVitePlugin() is constructed; construction is what loads that file into process.env. Production-like local or CI builds using the plugin's environment file therefore fail despite supplying valid credentials. Load the file before validating, or validate the plugin-normalized environment instead.

Useful? React with 👍 / 👎.

Comment on lines +269 to +274
const loadUnder = async (env: Record<string, string | undefined>) => {
vi.resetModules()
vi.unstubAllEnvs()
for (const [name, value] of Object.entries(env)) {
vi.stubEnv(name, value ?? '')
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clear inherited Sentry variables before each test import

When a developer runs the suite with any Sentry credentials exported in the shell, vi.unstubAllEnvs() restores those real values and this helper only replaces names present in env. For example, the no-token case supplies only NODE_ENV, so an inherited SENTRY_AUTH_TOKEN can make the config import reject for missing values or make getSentryPlugin() non-null, causing an environment-dependent failure. Stub all four Sentry build inputs to empty before applying each case's overrides.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant