From ee6ffa85ca3d423162aca80347c78995e21f28ef Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Sun, 8 Nov 2026 03:51:30 +0800 Subject: [PATCH 1/2] feat(governance): align twin content and detect drift --- .github/workflows/governance-drift.yml | 21 +++++++++++ VERSION | 1 + pr-governance/README.md | 23 +++++++++--- .../__pycache__/secret_scan.cpython-313.pyc | Bin 15612 -> 0 bytes pr-governance/scripts/check-twin-drift.sh | 34 ++++++++++++++++++ pr-governance/scripts/main.cjs | 19 ++++++++++ pr-governance/scripts/test-main.cjs | 24 +++++++++++++ pr-governance/templates/risk-gate.yml | 5 +++ 8 files changed, 122 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/governance-drift.yml create mode 100644 VERSION delete mode 100644 pr-governance/scripts/__pycache__/secret_scan.cpython-313.pyc create mode 100755 pr-governance/scripts/check-twin-drift.sh diff --git a/.github/workflows/governance-drift.yml b/.github/workflows/governance-drift.yml new file mode 100644 index 0000000..f19c4c7 --- /dev/null +++ b/.github/workflows/governance-drift.yml @@ -0,0 +1,21 @@ +name: Governance drift + +on: + schedule: + - cron: '23 3 * * 1' + workflow_dispatch: + +permissions: + contents: read + +jobs: + drift: + name: drift + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Compare the twin repositories + run: sh pr-governance/scripts/check-twin-drift.sh diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..00750ed --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +3 diff --git a/pr-governance/README.md b/pr-governance/README.md index b21cae3..cfeec11 100644 --- a/pr-governance/README.md +++ b/pr-governance/README.md @@ -64,7 +64,10 @@ organization community-health files. `reviewer` and repository-specific `r2_paths`. Leave `owner` out: account identities belong in protected configuration, so the action reads `RISK_GATE_OWNER_ID` and `RISK_GATE_OWNER` from repository or organization - variables and replaces `owner` when they are set. + variables and replaces `owner` when they are set. A repository whose content + rules keep accounts out of public source may also leave `reviewer` out and + set `RISK_GATE_REVIEWER_APP_ID`, `RISK_GATE_REVIEWER_USER_ID` and + `RISK_GATE_REVIEWER_LOGIN` instead; a partial set fails closed. 2. Copy `templates/risk-gate.yml`, `templates/codex-nudge.yml`, `templates/gitleaks.yml` and `templates/weekly-secret-scan.yml` into `.github/workflows/`, replacing `__GOVERNANCE_REPO__` with the @@ -82,7 +85,8 @@ organization community-health files. The owner identity comes from the `RISK_GATE_OWNER_ID` and `RISK_GATE_OWNER` variables. A policy that still carries `owner` keeps working until the -variables are set, but no account identity is required in the file. +variables are set, but no account identity is required in the file. The +reviewer follows the same rule through the `RISK_GATE_REVIEWER_*` variables. ## Trust model @@ -120,6 +124,15 @@ a fresh review instead of silently reusing one. ## Private repositories GitHub Free does not provide branch protection or secret scanning for private -repositories, and hosted Actions may be billing-blocked. Use the local track: -the same policy file, a pre-push hook and `scripts/local` commands recording -the same receipt and scan results locally. See `docs/private-repositories.md`. +repositories, and hosted Actions may be billing-blocked. Private repositories +use a local enforcement track instead: Git hooks that reject staged credentials, +unconventional commit headers, direct pushes to a protected branch, and +non-fast-forward pushes, plus an audit over the tracked tree. + + +## Twin repositories + +`pr-governance/` is byte-identical in `Pigbibi/governance`, `GdeiAssistant/.github` +and `QuantStrategyLab/.github`; `VERSION` names the shared revision. Each +repository runs `scripts/check-twin-drift.sh` weekly and on demand. A change to +any shared file is applied to all three before the drift check can pass. diff --git a/pr-governance/scripts/__pycache__/secret_scan.cpython-313.pyc b/pr-governance/scripts/__pycache__/secret_scan.cpython-313.pyc deleted file mode 100644 index 856784bc80088c910147c01b7f549aae76333ddf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15612 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 Sr*if>*;EG?BTcn*{{IJ;&2 + status=1 + continue + fi + if ! tar -xzf "$archive" -C "$tmp/$slug" --strip-components=1; then + echo "cannot extract $repo" >&2 + status=1 + continue + fi + for path in pr-governance VERSION; do + if ! diff -ru -x '__pycache__' "$base/$path" "$tmp/$slug/$path"; then + echo "drift: $path differs from $repo" >&2 + status=1 + fi + done +done + +exit $status diff --git a/pr-governance/scripts/main.cjs b/pr-governance/scripts/main.cjs index 22c3e1b..f1ba8ce 100644 --- a/pr-governance/scripts/main.cjs +++ b/pr-governance/scripts/main.cjs @@ -19,6 +19,23 @@ function configuredOwner() { return { user_id: id, login }; } +// The reviewer is the same kind of account identity, so a repository that keeps +// accounts out of public source can configure it the same way. A partial set +// fails closed instead of falling back to a committed identity. +function configuredReviewer() { + const appId = process.env.GOVERNANCE_REVIEWER_APP_ID; + const userId = process.env.GOVERNANCE_REVIEWER_USER_ID; + const login = process.env.GOVERNANCE_REVIEWER_LOGIN; + if (!appId && !userId && !login) return null; + const app = Number(appId); + const user = Number(userId); + if (!Number.isSafeInteger(app) || app < 1 || !Number.isSafeInteger(user) || user < 1 + || typeof login !== 'string' || !login) { + throw new Error('Incomplete reviewer configuration'); + } + return { provider: process.env.GOVERNANCE_REVIEWER_PROVIDER || 'codex', app_id: app, user_id: user, login }; +} + async function defaultBranch(github, context) { const known = context.payload?.repository?.default_branch || context.payload?.pull_request?.base?.ref; @@ -45,6 +62,8 @@ async function loadPolicy(github, context, branch) { } const owner = configuredOwner(); if (owner) policy.owner = owner; + const reviewer = configuredReviewer(); + if (reviewer) policy.reviewer = reviewer; gate.validatePolicy(policy); // Without an owner nobody can ever satisfy owner-approved, so a missing // configuration must fail the check instead of silently downgrading it. diff --git a/pr-governance/scripts/test-main.cjs b/pr-governance/scripts/test-main.cjs index ef16177..f71db46 100644 --- a/pr-governance/scripts/test-main.cjs +++ b/pr-governance/scripts/test-main.cjs @@ -36,6 +36,9 @@ async function load(policy, environment) { } finally { delete process.env.GOVERNANCE_OWNER_ID; delete process.env.GOVERNANCE_OWNER_LOGIN; + delete process.env.GOVERNANCE_REVIEWER_APP_ID; + delete process.env.GOVERNANCE_REVIEWER_USER_ID; + delete process.env.GOVERNANCE_REVIEWER_LOGIN; Object.assign(process.env, saved); } } @@ -57,6 +60,27 @@ test('a policy without owner and without variables fails closed', async () => { await assert.rejects(load(POLICY, {})); }); +const OWNER_ENV = { GOVERNANCE_OWNER_ID: '42', GOVERNANCE_OWNER_LOGIN: 'owner' }; +const REVIEWER_ENV = { ...OWNER_ENV, GOVERNANCE_REVIEWER_APP_ID: '7', + GOVERNANCE_REVIEWER_USER_ID: '8', GOVERNANCE_REVIEWER_LOGIN: 'reviewer[bot]' }; + +test('reviewer configuration replaces a committed reviewer', async () => { + const { policy } = await load(POLICY, REVIEWER_ENV); + assert.deepEqual(policy.reviewer, { provider: 'codex', app_id: 7, user_id: 8, login: 'reviewer[bot]' }); +}); + +test('a policy without reviewer is valid when the variables are set', async () => { + const { reviewer, ...rest } = POLICY; + const { policy } = await load(rest, REVIEWER_ENV); + assert.deepEqual(policy.reviewer, { provider: 'codex', app_id: 7, user_id: 8, login: 'reviewer[bot]' }); +}); + +test('an incomplete reviewer configuration fails closed', async () => { + await assert.rejects(load(POLICY, { GOVERNANCE_REVIEWER_APP_ID: '7' }), /reviewer configuration/); + await assert.rejects(load(POLICY, { ...REVIEWER_ENV, GOVERNANCE_REVIEWER_USER_ID: '0' }), + /reviewer configuration/); +}); + test('an incomplete owner configuration fails closed', async () => { await assert.rejects(load(POLICY, { GOVERNANCE_OWNER_ID: '42' }), /owner configuration/); await assert.rejects(load(POLICY, diff --git a/pr-governance/templates/risk-gate.yml b/pr-governance/templates/risk-gate.yml index 1c68191..8a1a7af 100644 --- a/pr-governance/templates/risk-gate.yml +++ b/pr-governance/templates/risk-gate.yml @@ -88,6 +88,11 @@ jobs: env: GOVERNANCE_OWNER_ID: ${{ vars.RISK_GATE_OWNER_ID }} GOVERNANCE_OWNER_LOGIN: ${{ vars.RISK_GATE_OWNER }} + # Optional: a repository that keeps account identifiers out of public + # source configures the reviewer here instead of in policy.json. + GOVERNANCE_REVIEWER_APP_ID: ${{ vars.RISK_GATE_REVIEWER_APP_ID }} + GOVERNANCE_REVIEWER_USER_ID: ${{ vars.RISK_GATE_REVIEWER_USER_ID }} + GOVERNANCE_REVIEWER_LOGIN: ${{ vars.RISK_GATE_REVIEWER_LOGIN }} with: mode: gate record-label: 'false' From 3e15865aeef900d6b8a38fbf1b64de6a5b94441f Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Sun, 8 Nov 2026 03:52:05 +0800 Subject: [PATCH 2/2] docs(governance): state the static-site exemption --- pr-governance/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pr-governance/README.md b/pr-governance/README.md index cfeec11..025ed62 100644 --- a/pr-governance/README.md +++ b/pr-governance/README.md @@ -23,6 +23,10 @@ A pull request may merge when all of these hold: Branch protection, not this action, is the merge constraint. The action only publishes evidence; it cannot merge, label, or write repository contents. +The gate applies to repositories that build, deploy or run code. A repository +that only publishes a static site through a `pages` workflow keeps that +deployment workflow and is exempt from the gate. + ## Risk tiers | Tier | Typical paths | Requirement |