Skip to content
Merged
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
135 changes: 135 additions & 0 deletions .github/workflows/secrets-codegen-check.yml
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
)
Copy link
Copy Markdown

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_paths array only covers the _envs/ subdirectories, but the repository has per-app encrypted payloads under packages/gen/env/src/runtime/generated-payloads/api/, web/, docs/, stackpanel-go/ and companion data files under packages/gen/env/data/dev/, prod/, staging/. These are real encrypted runtime payloads loaded via loadGeneratedPayload() in registry.ts — not the "typed env wrappers under src/<app>/" that get cosmetic churn (those live at src/exports/). A SOPS source rotation affecting per-app secrets like POSTGRES_URL would leave per-app payloads stale while this check passes silently — the same bug class the workflow was built to prevent.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 9020ad6. Configure here.


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
Loading