fix(ci): keep deployment secrets out of PR-triggered runs #421

Merged
Claude-Run merged 1 commit from fix/BUNYIP-425-pr-ci-secret-scope into main 2026-07-31 18:11:42 +02:00
Member

The E2E workflow triggered on pull_request, and its job-level env: block resolved ten secrets (staging Stripe key, account password, TOTP seed, mail-sink credentials with an embedded app password, tenant and client ids) into the environment of every step. On that event the runner uses the workflow file from the PR head, so three separate paths executed unreviewed, attacker-authored content with those secrets in scope: the workflow file itself, npm lifecycle scripts pulled in by npm ci, and the Playwright specs. Blast radius was anyone who can push a branch.

Split by whether a run may hold credentials. e2e.yml keeps the full env: block and now triggers only on push to main and workflow_dispatch, both of which run the definition from main. A PR-time full suite is a maintainer-triggered dispatch, taken after reading the diff. The new e2e-pr.yml is the pull_request gate: it declares only E2E_STAGING_BASE_URL and OIDC_ISSUER_STAGING (neither authenticates anything), installs the lockfile, installs Chromium and runs the reachability probe. It does not run the specs. Every npm ci under .forgejo/workflows/ now passes --ignore-scripts; the only install script in the lockfile is fsevents, which is darwin-only and optional.

scripts/check-workflow-secrets.sh enforces all three properties so the split cannot regress silently: no pull_request trigger on e2e.yml, no non-allowlisted secret in e2e-pr.yml, no npm ci without --ignore-scripts. It runs in the Check workflow and in just check, and the Check push trigger now watches all of .forgejo/workflows/** rather than only its own file.

Follow-on removals: the BUNYIP-303 hub_live output existed so the suite step could skip when a PR-run soft probe caught the hub down. The PR gate no longer runs the suite, so health-check.mjs no longer writes it and the suite's skip branch is gone. The soft hub probe (BUNYIP-301) moves to the PR gate, where it still stops a staging outage from deadlocking outage-fix PRs.

Two operator actions this commit cannot perform: rotate E2E_STAGING_STRIPE_SECRET_KEY, E2E_STAGING_PASSWORD and E2E_STAGING_TOTP_SECRET (reachable from PR-controlled code for the life of the workflow), and update branch protection on main to require e2e-pr instead of e2e, which no longer reports a PR status.

#BUNYIP-425

The E2E workflow triggered on `pull_request`, and its job-level `env:` block resolved ten secrets (staging Stripe key, account password, TOTP seed, mail-sink credentials with an embedded app password, tenant and client ids) into the environment of every step. On that event the runner uses the workflow file from the PR head, so three separate paths executed unreviewed, attacker-authored content with those secrets in scope: the workflow file itself, npm lifecycle scripts pulled in by `npm ci`, and the Playwright specs. Blast radius was anyone who can push a branch. Split by whether a run may hold credentials. `e2e.yml` keeps the full `env:` block and now triggers only on push to `main` and `workflow_dispatch`, both of which run the definition from `main`. A PR-time full suite is a maintainer-triggered dispatch, taken after reading the diff. The new `e2e-pr.yml` is the pull_request gate: it declares only `E2E_STAGING_BASE_URL` and `OIDC_ISSUER_STAGING` (neither authenticates anything), installs the lockfile, installs Chromium and runs the reachability probe. It does not run the specs. Every `npm ci` under `.forgejo/workflows/` now passes `--ignore-scripts`; the only install script in the lockfile is fsevents, which is darwin-only and optional. `scripts/check-workflow-secrets.sh` enforces all three properties so the split cannot regress silently: no `pull_request` trigger on `e2e.yml`, no non-allowlisted secret in `e2e-pr.yml`, no `npm ci` without `--ignore-scripts`. It runs in the Check workflow and in `just check`, and the Check push trigger now watches all of `.forgejo/workflows/**` rather than only its own file. Follow-on removals: the BUNYIP-303 `hub_live` output existed so the suite step could skip when a PR-run soft probe caught the hub down. The PR gate no longer runs the suite, so health-check.mjs no longer writes it and the suite's skip branch is gone. The soft hub probe (BUNYIP-301) moves to the PR gate, where it still stops a staging outage from deadlocking outage-fix PRs. Two operator actions this commit cannot perform: rotate `E2E_STAGING_STRIPE_SECRET_KEY`, `E2E_STAGING_PASSWORD` and `E2E_STAGING_TOTP_SECRET` (reachable from PR-controlled code for the life of the workflow), and update branch protection on `main` to require `e2e-pr` instead of `e2e`, which no longer reports a PR status. #BUNYIP-425
fix(ci): keep deployment secrets out of PR-triggered runs
All checks were successful
E2E PR gate / Install + reachability (no deployment secrets) (pull_request) Successful in 13s
Check / fmt + clippy + build + tests (pull_request) Successful in 4m4s
Create release / Create release from merged PR (pull_request) Has been skipped
7dc2fed4e4
The E2E workflow triggered on `pull_request`, and its job-level `env:` block resolved ten secrets (staging Stripe key, account password, TOTP seed, mail-sink credentials with an embedded app password, tenant and client ids) into the environment of every step. On that event the runner uses the workflow file from the PR head, so three separate paths executed unreviewed, attacker-authored content with those secrets in scope: the workflow file itself, npm lifecycle scripts pulled in by `npm ci`, and the Playwright specs. Blast radius was anyone who can push a branch.

Split by whether a run may hold credentials. `e2e.yml` keeps the full `env:` block and now triggers only on push to `main` and `workflow_dispatch`, both of which run the definition from `main`. A PR-time full suite is a maintainer-triggered dispatch, taken after reading the diff. The new `e2e-pr.yml` is the pull_request gate: it declares only `E2E_STAGING_BASE_URL` and `OIDC_ISSUER_STAGING` (neither authenticates anything), installs the lockfile, installs Chromium and runs the reachability probe. It does not run the specs. Every `npm ci` under `.forgejo/workflows/` now passes `--ignore-scripts`; the only install script in the lockfile is fsevents, which is darwin-only and optional.

`scripts/check-workflow-secrets.sh` enforces all three properties so the split cannot regress silently: no `pull_request` trigger on `e2e.yml`, no non-allowlisted secret in `e2e-pr.yml`, no `npm ci` without `--ignore-scripts`. It runs in the Check workflow and in `just check`, and the Check push trigger now watches all of `.forgejo/workflows/**` rather than only its own file.

Follow-on removals: the BUNYIP-303 `hub_live` output existed so the suite step could skip when a PR-run soft probe caught the hub down. The PR gate no longer runs the suite, so health-check.mjs no longer writes it and the suite's skip branch is gone. The soft hub probe (BUNYIP-301) moves to the PR gate, where it still stops a staging outage from deadlocking outage-fix PRs.

Two operator actions this commit cannot perform: rotate `E2E_STAGING_STRIPE_SECRET_KEY`, `E2E_STAGING_PASSWORD` and `E2E_STAGING_TOTP_SECRET` (reachable from PR-controlled code for the life of the workflow), and update branch protection on `main` to require `e2e-pr` instead of `e2e`, which no longer reports a PR status.

#BUNYIP-425
Claude-Run deleted branch fix/BUNYIP-425-pr-ci-secret-scope 2026-07-31 18:11:42 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/bunyip!421
No description provided.