-
Notifications
You must be signed in to change notification settings - Fork 1
ci: detect drift between source SOPS YAMLs and embedded @gen/env payloads #18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,135 @@ | ||
| name: Secrets codegen drift check | ||
|
|
||
| # Verifies that the SOPS-encrypted runtime payloads embedded in @gen/env are | ||
| # in sync with the source-of-truth SOPS YAMLs under .stack/secrets/vars/. | ||
| # | ||
| # Why this exists (see beads stackpanel-04d for full context): | ||
| # The runtime alchemy deploy reads the embedded payload at | ||
| # packages/gen/env/src/runtime/generated-payloads/_envs/<env>.ts | ||
| # and the encrypted JSON at | ||
| # packages/gen/env/data/_envs/<env>.sops.json | ||
| # NOT the source SOPS YAML directly. Those embedded files only get | ||
| # regenerated by `stackpanel codegen build env`, which runs as part of | ||
| # the devshell shell-hook. If a contributor edits a SOPS source (via | ||
| # `sops`, `chore: rekey`, `himitsu set`, etc.) and commits without | ||
| # re-entering the devshell, the embedded payload silently keeps shipping | ||
| # the old plaintext. This was the bug behind PR #15/#17: a Cloudflare | ||
| # API token rotation merged to main but the embedded payload still | ||
| # carried the under-scoped previous token, so every deploy 401'd. | ||
| # | ||
| # This workflow re-runs codegen in CI and fails if it produced any change | ||
| # under the embedded-payload tree. The remediation is printed inline so | ||
| # contributors don't need to dig through docs. | ||
| on: | ||
| pull_request: | ||
| types: [opened, reopened, synchronize] | ||
| paths: | ||
| # Source SOPS YAMLs (everything in .stack/secrets/ except generated/cached state) | ||
| - ".stack/secrets/vars/**" | ||
| - ".stack/secrets/apps/**" | ||
| - ".stack/secrets/**.yaml" | ||
| # Nix-side env declarations (apps.<app>.env / stackpanel.envs.<scope>) | ||
| - ".stack/config.nix" | ||
| - ".stack/data/**.nix" | ||
| - "nix/stackpanel/db/schemas/secrets**" | ||
| - "nix/stackpanel/lib/codegen/**" | ||
| - "nix/stackpanel/modules/env-codegen/**" | ||
| # The codegen implementation itself | ||
| - "apps/stackpanel-go/internal/codegen/**" | ||
| # The output tree (catches manual edits / accidental rollbacks) | ||
| - "packages/gen/env/data/**" | ||
| - "packages/gen/env/src/runtime/generated-payloads/**" | ||
| # The workflow file | ||
| - ".github/workflows/secrets-codegen-check.yml" | ||
| push: | ||
| branches: [main] | ||
| paths: | ||
| - ".stack/secrets/vars/**" | ||
| - ".stack/secrets/apps/**" | ||
| - ".stack/secrets/**.yaml" | ||
| - ".stack/config.nix" | ||
| - ".stack/data/**.nix" | ||
| - "nix/stackpanel/db/schemas/secrets**" | ||
| - "nix/stackpanel/lib/codegen/**" | ||
| - "nix/stackpanel/modules/env-codegen/**" | ||
| - "apps/stackpanel-go/internal/codegen/**" | ||
| - "packages/gen/env/data/**" | ||
| - "packages/gen/env/src/runtime/generated-payloads/**" | ||
| - ".github/workflows/secrets-codegen-check.yml" | ||
| workflow_dispatch: | ||
|
|
||
| concurrency: | ||
| group: secrets-codegen-check-${{ github.ref }} | ||
| cancel-in-progress: true | ||
|
|
||
| jobs: | ||
| verify: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| - name: Install Nix | ||
| uses: DeterminateSystems/nix-installer-action@main | ||
| with: | ||
| extra-conf: | | ||
| accept-flake-config = true | ||
| extra-substituters = https://devenv.cachix.org https://darkmatter.cachix.org | ||
| extra-trusted-public-keys = devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw= darkmatter.cachix.org-1:7R5qAiOVHxDpFy7yguECfC1JqVDgMdckGc+CDKk2pWA= | ||
|
|
||
| - name: Setup Cachix | ||
| uses: cachix/cachix-action@v16 | ||
| with: | ||
| name: darkmatter | ||
| authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} | ||
| extraPullNames: devenv | ||
|
|
||
| - name: Run codegen | ||
| env: | ||
| # Same key the deploy workflows use — it has decrypt access for every | ||
| # SOPS file under .stack/secrets/vars/ and packages/gen/env/data/. | ||
| # The codegen needs to *decrypt* the source YAMLs (to resolve env vars) | ||
| # and then re-*encrypt* the resolved plaintext back into the embedded | ||
| # payload, both of which require this key. | ||
| SOPS_AGE_KEY: ${{ secrets.SECRETS_AGE_KEY_DEV }} | ||
| run: | | ||
| set -euo pipefail | ||
| # Devshell entry runs the shell-hook which: | ||
| # 1. writes .stack/gen/codegen/env-manifest.json (the codegen input) | ||
| # 2. invokes `stackpanel codegen build` itself | ||
| # We re-invoke `stackpanel codegen build` explicitly afterwards as a | ||
| # belt-and-braces guarantee that the build was run with the same SOPS | ||
| # key the embedded payloads were encrypted with. | ||
| nix develop --impure --command bash -lc ' | ||
| set -euo pipefail | ||
| stackpanel codegen build | ||
| ' | ||
|
|
||
| - name: Verify no drift | ||
| run: | | ||
| set -euo pipefail | ||
| # Limit the diff to the files that actually matter for runtime drift. | ||
| # We deliberately do NOT diff `packages/gen/env/src/<app>/...` because | ||
| # those are the typed env wrappers — codegen rewrites them on every | ||
| # devshell entry (timestamp/comment churn) and that churn is | ||
| # cosmetically noisy without affecting deploy behaviour. See | ||
| # stackpanel-04d for the rationale: the ONLY drift class that broke | ||
| # production was the embedded encrypted payload + its TS wrapper. | ||
| target_paths=( | ||
| packages/gen/env/data/_envs | ||
| packages/gen/env/src/runtime/generated-payloads/_envs | ||
| ) | ||
|
|
||
| if git diff --quiet -- "${target_paths[@]}"; then | ||
| echo "OK: embedded SOPS payloads are in sync with source schemas." | ||
| exit 0 | ||
| fi | ||
|
|
||
| echo "::error title=Embedded SOPS payloads are stale::Run \`nix develop --impure --command stackpanel codegen build\` and commit the resulting changes under packages/gen/env/." | ||
| echo | ||
| echo "===== drift detected in =====" >&2 | ||
| git diff --name-only -- "${target_paths[@]}" >&2 | ||
| echo | ||
| echo "===== diff (truncated to 200 lines) =====" >&2 | ||
| git diff -- "${target_paths[@]}" | head -200 >&2 | ||
|
|
||
| exit 1 | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Drift check misses per-app encrypted runtime payloads
High Severity
The
target_pathsarray only covers the_envs/subdirectories, but the repository has per-app encrypted payloads underpackages/gen/env/src/runtime/generated-payloads/api/,web/,docs/,stackpanel-go/and companion data files underpackages/gen/env/data/dev/,prod/,staging/. These are real encrypted runtime payloads loaded vialoadGeneratedPayload()inregistry.ts— not the "typed env wrappers undersrc/<app>/" that get cosmetic churn (those live atsrc/exports/). A SOPS source rotation affecting per-app secrets likePOSTGRES_URLwould leave per-app payloads stale while this check passes silently — the same bug class the workflow was built to prevent.Reviewed by Cursor Bugbot for commit 9020ad6. Configure here.