Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@ SESSION_SECRET="super-duper-s3cret"
HONEYPOT_SECRET="super-duper-s3cret"
INTERNAL_COMMAND_TOKEN="some-made-up-token"
SENTRY_DSN="your-dsn"
# SENTRY_ORG and SENTRY_PROJECT are BUILD-TIME only — @sentry/vite-plugin reads
# them while the image is being built (vite.config.ts) and never again, so the
# deploy workflow passes them to `flyctl deploy --build-secret` and
# other/Dockerfile mounts them into the build stage. Setting them here has no
# effect on a deployed app.
#
# Left empty rather than filled with a placeholder on purpose: a local build
# skips release management unless SENTRY_AUTH_TOKEN is also set, and once it is,
# getSentryPlugin() treats an empty value as missing and refuses to build.
# A placeholder like "your-org-slug" would pass that check and send an
# authenticated request to an org that does not exist, which the plugin logs and
# then swallows — a green build and no release, which is the bug this guards.
SENTRY_ORG=""
SENTRY_PROJECT=""

NODEMAILER_HOST="your-host"
NODEMAILER_USER="your-email"
Expand Down
38 changes: 35 additions & 3 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,42 @@ jobs:
- name: 🎈 Setup Fly
uses: superfly/flyctl-actions/setup-flyctl@1.5

# `run: |` rather than a folded plain scalar, so Prettier cannot reflow
# this command and wrap a flag across a line boundary.
#
# SENTRY_ORG and SENTRY_PROJECT are build-time only: @sentry/vite-plugin
# reads them while the Docker image is being built, and never again. They
# used to be Fly *runtime* secrets instead, which put them in the running
# container where the build cannot see them — the plugin's `org` guard then
# fired, and it silently skipped creating the release and uploading source
# maps while still injecting debug IDs, so the build stayed green. See #16.
#
# Every name passed here must also be mounted and exported in the build
# stage of other/Dockerfile. tests/sentry-release.test.ts fails if the two
# drift apart.
- name: 🚀 Deploy Production
if: ${{ github.ref == 'refs/heads/main' }}
run:
flyctl deploy --remote-only --build-arg COMMIT_SHA=${{ github.sha }}
--build-secret SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
run: |
# An unset secret is the empty string, not an unset variable, so a
# deleted SENTRY_AUTH_TOKEN would arrive as "" and turn
# `getSentryPlugin()`'s "off" branch on — a green deploy with no
# release, no debug IDs and no evidence anything was skipped. Check
# here, where the name of the missing secret is still obvious.
for name in SENTRY_AUTH_TOKEN SENTRY_ORG SENTRY_PROJECT; do
if [ -z "${!name}" ]; then
echo "::error::$name is unset or empty in this repository's secrets"
exit 1
fi
done
flyctl deploy --remote-only \
--build-arg COMMIT_SHA=${{ github.sha }} \
--build-secret "SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN" \
--build-secret "SENTRY_ORG=$SENTRY_ORG" \
--build-secret "SENTRY_PROJECT=$SENTRY_PROJECT"
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
# Passed through the environment rather than interpolated into the
# script with ${{ }}, so the values are never part of the command text.
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,7 @@ node_modules

# generated files
/app/components/ui/icons
.react-router/
.react-router/
# Read by @sentry/vite-plugin at build time and merged into process.env.
# Holds a Sentry auth token, so it must never be committed or baked into an image.
.env.sentry-build-plugin
22 changes: 19 additions & 3 deletions other/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,25 @@ COPY --from=deps /myapp/node_modules /myapp/node_modules

ADD . .

# Mount the secret and set it as an environment variable and run the build
RUN --mount=type=secret,id=SENTRY_AUTH_TOKEN \
export SENTRY_AUTH_TOKEN=$(cat /run/secrets/SENTRY_AUTH_TOKEN) && \
# Mount the secrets and run the build.
#
# `required=true` is what makes a missing secret fail the build. It looks like the
# `&&` chain below would do it instead, but it would not: `export X="$(cat
# /run/secrets/X)"` exits 0 even when the `cat` fails, because a failed command
# substitution does not propagate through `export`. Without `required=true` an
# absent secret silently becomes an empty string, and an empty string is exactly
# what `getSentryPlugin()` in vite.config.ts treats as missing — so the build
# would still fail, several layers later and with a less obvious message.
#
# The `cat`s are needed because a secret mount sets no environment variable on
# its own; only the `env=` mount form does, and that needs a Dockerfile syntax
# pin this file does not currently carry.
RUN --mount=type=secret,id=SENTRY_AUTH_TOKEN,required=true \
--mount=type=secret,id=SENTRY_ORG,required=true \
--mount=type=secret,id=SENTRY_PROJECT,required=true \
export SENTRY_AUTH_TOKEN="$(cat /run/secrets/SENTRY_AUTH_TOKEN)" && \
export SENTRY_ORG="$(cat /run/secrets/SENTRY_ORG)" && \
export SENTRY_PROJECT="$(cat /run/secrets/SENTRY_PROJECT)" && \
npm run build

# Finally, build the production image with minimal footprint
Expand Down
3 changes: 3 additions & 0 deletions other/Dockerfile.dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@
/.cache
/public/build
/build

# Read by @sentry/vite-plugin at build time. Holds a Sentry auth token.
.env.sentry-build-plugin
Loading
Loading