Skip to content
93 changes: 93 additions & 0 deletions .github/workflows/governance-enforce.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
name: governance-enforce

# A_BLOCK gate: no-secrets-in-git / secrets-from-doppler / no-hardcoded-paths, enforced on the
# PR diff via the @wave-av/governance package (the org fan-out channel). Diff-scoped: blocks NEW
# violations without failing on legacy debt. Isolated install bypasses any min-release-age policy.
# The org ruleset `governance-a-block-enforce` requires this job's `enforce` check.
#
# VENDORED 2026-08-05 (claude-workstation#1624, E4 T4.9a). This repo was never in that ruleset's
# include list, because the list is 112 hand-maintained names and every one of them matches
# `wave-*`. A naming convention had silently become a security boundary: the repos that publish
# our npm packages — cli, sdk, adk, mcp-server, workflow-sdk — were the ones running with no
# A_BLOCK secrets scan at all.
#
# HARDENED 2026-08-05 (claude-workstation#1747), before any of the fan-out merged. The copy first
# vendored here could report PASS having examined nothing. Five fixes, each marked at its site
# below. A gate may not return a passing value for input it did not examine.
#
# DO NOT add this repo to `governance-a-block-enforce` until this check is observed green here.
# A required status check that never reports is a permanent deadlock, not a stricter gate.

on:
pull_request:
push:
branches: [main, master]

permissions:
contents: read
packages: read

# FIX 4 — a cancelled push run's commits were scanned by NOBODY. Every push run diffs only its
# own before..HEAD range, so cancelling run N when run N+1 starts leaves N's commits permanently
# unexamined. PR runs are safe to supersede: each one re-diffs the whole branch against its base.
concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.ref || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
enforce:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "22"
- name: fetch governance enforcer (isolated install)
# FIX 1 — token scoped to THIS STEP. At job level it was also in scope for the step that
# executes the downloaded package, and for anything else the job ever grows.
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
mkdir -p "$RUNNER_TEMP/gov" && cd "$RUNNER_TEMP/gov"
trap 'rm -f "$RUNNER_TEMP/gov/.npmrc"' EXIT
printf '@wave-av:registry=https://npm.pkg.github.com\n//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}\n' > .npmrc
# FIX 2 — --ignore-scripts. npm runs preinstall/install/postinstall by default, so this
# step would execute dependency-authored code with the registry token in its environment.
# FIX 3 — exact pin, and 0.4.6 specifically. `^0.4.4` resolved to 0.4.4, whose file lister
# is `catch { return []; }` — ANY git error became zero files and rendered as
# `OK[enforce]: 0 changed file(s) scanned`. 0.4.6 fails closed on a git error instead.
# A caret is also a standing authorization for whatever is published next; a bump is now
# a visible commit in this file.
npm install @wave-av/governance@0.4.6 --no-save --no-audit --no-fund --ignore-scripts
- name: A_BLOCK enforce (secrets + hardcoded paths on the diff)
env:
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
PUSH_BEFORE_SHA: ${{ github.event.before }}
run: |
set -euo pipefail
ENFORCE="$RUNNER_TEMP/gov/node_modules/@wave-av/governance/bin/enforce.mjs"
BASE="${PR_BASE_SHA:-}"
[ -n "$BASE" ] || BASE="${PUSH_BEFORE_SHA:-}"
# A base can be PRESENT and still unusable: a force-push leaves `github.event.before`
# pointing at a commit this checkout no longer contains.
if [ -z "$BASE" ] || [ "$BASE" = "0000000000000000000000000000000000000000" ] \
|| ! git cat-file -e "$BASE^{commit}" 2>/dev/null; then
BASE="$(git rev-parse --verify --quiet 'HEAD~1' || true)"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High workflows/governance-enforce.yml:78

When a force-push makes github.event.before unreachable in the checkout, the fallback at line 80 narrows the scan to HEAD~1, so only the final commit's diff is examined. Any earlier commits introduced by that force-push are skipped entirely, and a secret or hardcoded path pushed in one of those earlier commits reaches main without this gate examining it. Instead of silently narrowing the diff to HEAD~1, the fallback should fail closed or compute a reachable merge-base.

          if [ -z "$BASE" ] || [ "$BASE" = "0000000000000000000000000000000000000000" ] \
             || ! git cat-file -e "$BASE^{commit}" 2>/dev/null; then
-            BASE="$(git rev-parse --verify --quiet 'HEAD~1' || true)"
+            BASE="$(git merge-base --octopus HEAD 2>/dev/null || true)"
          fi
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @.github/workflows/governance-enforce.yml around lines 78-80:

When a force-push makes `github.event.before` unreachable in the checkout, the fallback at line 80 narrows the scan to `HEAD~1`, so only the final commit's diff is examined. Any earlier commits introduced by that force-push are skipped entirely, and a secret or hardcoded path pushed in one of those earlier commits reaches `main` without this gate examining it. Instead of silently narrowing the diff to `HEAD~1`, the fallback should fail closed or compute a reachable merge-base.

Evidence trail:
.github/workflows/governance-enforce.yml:74-93 @ 67d046c0; `git blame REVIEWED_COMMIT -L 74,91 -- .github/workflows/governance-enforce.yml`

fi
# FIX 5 — fail CLOSED. This previously fell back to `BASE=HEAD`, and `--changed HEAD`
# diffs HEAD against itself: an empty diff, zero files scanned, job green. With no
# resolvable base, diff against the EMPTY TREE so every tracked file reads as added and
# the whole repo is scanned. (`--all` also exists in 0.4.6 and would do most of this, but
# it is documented as NOT covering the diff-scoped over-grant detectors. Routing through
# the diff path with an empty base keeps every detector in play.)
if [ -z "$BASE" ]; then
BASE="$(git hash-object -t tree /dev/null)"
echo "::notice::no diff base resolved (root commit or unreachable before-sha) — scanning the whole tree against the empty tree"
fi
Comment on lines +96 to +105

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Empty-tree fallback turns the diff-scoped gate into a full-repo scan that will trip on legacy debt

The header states the gate is diff-scoped precisely so it "blocks NEW violations without failing on legacy debt" (lines 4-5). The new fallback makes every tracked file read as added, so any pre-existing hardcoded path or secret-shaped string anywhere in the repo will fail the job. On pull_request this cannot happen (base.sha is always present and fetched), but on a force-pushed branch's push to main, or a root commit, the job would fail for reasons unrelated to the pushed change. That is the intended "loud" behavior, but on push to main it produces a red default-branch build that no one can fix without whitelisting legacy debt — consider whether the fallback should warn-and-scan-full instead of blocking on non-PR events.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

echo "diffing against $BASE"
exec node "$ENFORCE" --changed "$BASE"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Empty-tree fallback depends on the enforcer accepting a tree-ish base

The fail-closed path substitutes the empty tree object (git hash-object -t tree /dev/null) as the --changed argument. This only produces a whole-tree scan if @wave-av/governance@0.4.6's enforcer runs a two-dot diff (git diff <base> HEAD) or git diff --name-only <base>. If it uses three-dot (<base>...HEAD) or resolves the argument via rev-parse <base>^{commit}, the empty tree hash is not a commit and the command will error — which, given 0.4.6 is described as failing closed on git errors, would turn every unresolvable-base run (root commit, unreachable force-push before-sha) into a hard red job rather than a full scan. Worth confirming against the package's bin/enforce.mjs behavior once observed in a real run.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified against the 0.4.6 source: changedArgs uses the two-argument git diff <base> HEAD form, which accepts trees by explicit design (its doc comment cites this exact empty-tree CI fallback), and running the enforcer with the empty-tree base against this repo scans all tracked files and exits 0. Both enforce.mjs and the spawned enforce-ramp.mjs share that code path, and neither resolves the base via ^{commit} or three-dot merge-base.

Loading