Skip to content
Open
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
107 changes: 107 additions & 0 deletions .github/workflows/governance-enforce.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
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:
# A required check that never reports on an event the repo actually uses is a permanent
# deadlock, not a stricter gate. None of these repos runs a merge queue today; declaring
# `merge_group` costs nothing until one does, and closes that hole in advance.
merge_group:
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
Comment on lines +53 to +70

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Pull requests from forks will always fail the new governance check

The new check downloads a private organization package using the automatic per-run token (npm install @wave-av/governance@0.4.6 at .github/workflows/governance-enforce.yml:70), which forked pull requests do not have permission to read, so every outside contribution fails this check with an install error instead of a real result.
Impact: External contributors following the documented fork-and-PR flow get a permanently red, unfixable check on their pull requests.

Why the token cannot read the package on fork PRs

For pull_request events triggered from a fork, GitHub issues a GITHUB_TOKEN scoped to the fork repository with read-only contents permission; it is not granted read access to private packages owned by wave-av. The .npmrc written at .github/workflows/governance-enforce.yml:62 authenticates to npm.pkg.github.com with that token, so npm install returns 401/404 and, with set -euo pipefail, the step fails. CONTRIBUTING.md documents fork-based contributions as the expected flow, and README/repo guard indicate this is a public repo, so this path is reachable.

Mitigations would be to skip or soft-fail the job when github.event.pull_request.head.repo.fork is true (while keeping it enforcing for same-repo PRs and pushes), or to make the package publicly readable / vendor the enforcer.

Prompt for agents
The governance-enforce workflow installs the private @wave-av/governance package from GitHub Packages using secrets.GITHUB_TOKEN. On pull_request events originating from forks (the flow documented in CONTRIBUTING.md for this public repo), that token is scoped to the fork and cannot read org-private packages, so the install step fails and the check is permanently red for external contributors. Consider gating the job on github.event.pull_request.head.repo.full_name == github.repository (running it only for same-repo PRs, pushes and merge_group), or moving the enforcement to a pull_request_target/scheduled job, or publishing the enforcer where fork PRs can fetch it. Note the workflow header explicitly warns against adding this repo to the required-check ruleset until the check is observed green, so the fork behaviour should be decided before that happens.
Open in Devin Review

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

- name: A_BLOCK enforce (secrets + hardcoded paths on the diff)
env:
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
MERGE_BASE_SHA: ${{ github.event.merge_group.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="${MERGE_BASE_SHA:-}"
[ -n "$BASE" ] || BASE="${PUSH_BEFORE_SHA:-}"
Comment on lines +73 to +81

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔍 PR base sha may drag in unrelated base-branch commits into the scanned diff

github.event.pull_request.base.sha is the base commit at the time the PR event fired, and the checkout for a pull_request event is the merge commit. If the enforcer diffs two-dot (base..HEAD), any commits merged into main since the PR was opened appear in the diff, so unrelated legacy debt from other people's commits can fail this PR's gate. The workflow header explicitly claims "blocks NEW violations without failing on legacy debt", which only holds if the enforcer uses a merge-base (three-dot) diff. Note this expectation conflicts with ANALYSIS-0001's requirement that a bare tree object be acceptable — the two fallback paths want opposite diff semantics, so both should be confirmed against 0.4.6's implementation.

Open in Devin Review

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

# FIX 5 — fail CLOSED, and do not settle for a PARTIAL range either.
#
# This previously fell back to `BASE=HEAD`, and `--changed HEAD` diffs HEAD against
# itself: an empty diff, zero files scanned, job green.
#
# `HEAD~1` is the obvious replacement and is ALSO wrong — it scans exactly one commit,
# so a five-commit push whose base is indeterminate would examine the last one and
# report a confident pass on the other four. A narrowed scan reported as a full pass is
# the same defect in a quieter costume. (This is wave-av/wave-rig's reasoning, already
# correct on its main; the fan-out copied the broken shape from elsewhere.)
#
# A base can also be PRESENT and still unusable: a force-push leaves
# `github.event.before` pointing at a commit this checkout no longer contains.
#
# So: no resolvable base of any kind → diff against the EMPTY TREE, which makes every
# tracked file read as added and scans the whole repo. Loud, never partial, never empty.
# (`--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" ] || [ "$BASE" = "0000000000000000000000000000000000000000" ] \

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, the fallback to HEAD~1 only scans the latest commit, so any other commits introduced by the force-push are never examined while the gate can pass green. The HEAD~1 fallback narrows the diff to one commit instead of scanning from the empty tree like the documented fail-closed path. Remove the HEAD~1 fallback so an unreachable push base falls through to the empty-tree scan, or fail the job.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @.github/workflows/governance-enforce.yml around line 78:

When a force-push makes `github.event.before` unreachable, the fallback to `HEAD~1` only scans the latest commit, so any other commits introduced by the force-push are never examined while the gate can pass green. The `HEAD~1` fallback narrows the diff to one commit instead of scanning from the empty tree like the documented fail-closed path. Remove the `HEAD~1` fallback so an unreachable push base falls through to the empty-tree scan, or fail the job.

Evidence trail:
.github/workflows/governance-enforce.yml:74-93 at cc0e626f
.git diff MERGE_BASE REVIEWED_COMMIT -- .github/workflows/governance-enforce.yml

|| ! git cat-file -e "$BASE^{commit}" 2>/dev/null; then
BASE="$(git hash-object -t tree /dev/null)"
echo "::warning::indeterminate diff base (root commit, branch creation, or unreachable before-sha) — scanning the full tree against the empty-tree object so no commit is skipped"
fi
echo "diffing against $BASE"
exec node "$ENFORCE" --changed "$BASE"
Loading