From e806486341bfa4f0b62ebc06fb59a7f43e19e402 Mon Sep 17 00:00:00 2001 From: ArchonVII Date: Wed, 20 May 2026 14:06:03 -0500 Subject: [PATCH 1/4] fix(pr-policy): grant checks:read and contents:read permissions (C2) When `permissions:` is declared on a job, every unlisted scope becomes `none`. The evidence-check step calls `checks.listForRef` (needs `checks: read`) and `git.getCommit` (needs `contents: read`), both of which would 403 against any consumer repo with the previous `pull-requests: read` only block. Refs ArchonVII/github-workflows#10 ArchonVII/github-workflows#12 --- .github/workflows/pr-policy.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/pr-policy.yml b/.github/workflows/pr-policy.yml index e84ec7a..c6310e4 100644 --- a/.github/workflows/pr-policy.yml +++ b/.github/workflows/pr-policy.yml @@ -69,7 +69,18 @@ jobs: policy: runs-on: ubuntu-latest permissions: + # `pull-requests: read` — read PR body, files, draft state. + # `checks: read` — required by the evidence parser's `location: ci` + # handling, which calls `checks.listForRef` for the PR head SHA. + # `contents: read` — required to fetch the head commit via + # `git.getCommit` for the parser's freshness/now comparison, and + # for `actions/checkout` of this workflow's repo for the parser + # script. When `permissions:` is declared, anything unlisted + # becomes `none` — so both must be explicit. Source: PR #19 + # adversarial review (2026-05-20), finding C2. pull-requests: read + checks: read + contents: read steps: # Check out THIS reusable workflow's repo (ArchonVII/github-workflows) # into a sibling path so the evidence-check step can dynamic-import From 2b15f46313ddb149585484f193aa7aad9cdfd519 Mon Sep 17 00:00:00 2001 From: ArchonVII Date: Wed, 20 May 2026 14:06:35 -0500 Subject: [PATCH 2/4] fix(pr-policy): wrap parser import in try/catch (M2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dynamic `import()` of `scripts/parse-evidence.mjs` previously had no error handling — a missing parser (wrong `workflow-library-ref`, skipped checkout) would crash the step even though Phase 1 is warning-only. Degrade cleanly to a job-summary warning and return. Even when `enforce-evidence` is true we still `core.warning` rather than `core.setFailed` because a missing parser is an ecosystem config problem, not a PR-author problem. Refs ArchonVII/github-workflows#10 ArchonVII/github-workflows#12 --- .github/workflows/pr-policy.yml | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pr-policy.yml b/.github/workflows/pr-policy.yml index c6310e4..adf0632 100644 --- a/.github/workflows/pr-policy.yml +++ b/.github/workflows/pr-policy.yml @@ -178,6 +178,7 @@ jobs: DOC_EXT_LIST: ${{ inputs.doc-only-extensions }} DOC_PREFIXES: ${{ inputs.doc-only-path-prefixes }} ENFORCE_EVIDENCE: ${{ inputs.enforce-evidence }} + WORKFLOW_LIBRARY_REF: ${{ inputs.workflow-library-ref }} with: script: | const pr = context.payload.pull_request; @@ -240,13 +241,34 @@ jobs: const headCommitTime = headCommit.data.committer.date; // Dynamic-import the pure parser so this step has no module wiring. + // Wrap the import in try/catch so a missing parser file (e.g. + // checkout skipped, or workflow-library-ref points at a pre-parser + // ref) degrades to a job-summary warning instead of an unhandled + // step failure. Phase 1 is warning-only — even when + // enforce-evidence is true, a missing parser is an ecosystem + // config problem (wrong ref/permissions) not a PR-author + // problem, so we still emit core.warning, never + // core.setFailed. Source: PR #19 adversarial review + // (2026-05-20), finding M2. const path = require('path'); const url = require('url'); const parserPath = path.resolve( process.env.GITHUB_WORKSPACE, '__github-workflows__/scripts/parse-evidence.mjs', ); - const { parseEvidence } = await import(url.pathToFileURL(parserPath).href); + const workflowRef = process.env.WORKFLOW_LIBRARY_REF || '(unset)'; + let parseEvidence; + try { + ({ parseEvidence } = await import(url.pathToFileURL(parserPath).href)); + } catch (err) { + const msg = `Could not load evidence parser at ${parserPath} (workflow-library-ref=${workflowRef}): ${err && err.message ? err.message : err}. Evidence check skipped.`; + core.warning(msg); + await summary + .addHeading('Evidence check (skipped)', 3) + .addRaw(msg) + .write(); + return; + } const result = parseEvidence(pr.body || '', { headCommitTime, From d4d3b99921081023fa8bccc0489ed543adcad6f2 Mon Sep 17 00:00:00 2001 From: ArchonVII Date: Wed, 20 May 2026 14:06:50 -0500 Subject: [PATCH 3/4] perf(pr-policy): skip parser checkout on draft PRs (M1) Draft PRs early-return in the evidence-check step, so cloning github-workflows just to dynamic-import a parser that never runs is pure waste (~5-10s per draft run). Gate the checkout on `github.event.pull_request.draft == false`. Doc-only PRs still pay the checkout cost because doc-only detection requires an API call we can't make at `if:`-evaluation time. Refs ArchonVII/github-workflows#10 ArchonVII/github-workflows#12 --- .github/workflows/pr-policy.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/pr-policy.yml b/.github/workflows/pr-policy.yml index adf0632..22bc8a0 100644 --- a/.github/workflows/pr-policy.yml +++ b/.github/workflows/pr-policy.yml @@ -94,7 +94,20 @@ jobs: # match. Without an explicit ref, actions/checkout would default to # the repository's default branch (main), creating "workflow from v1, # parser from main" drift. + # Skip the parser checkout for draft PRs — the evidence-check step + # early-returns for drafts (see the `pr.draft` block below) so the + # parser is never invoked, and the ~5-10s clone is pure waste. + # + # We do NOT also skip for doc-only PRs at this stage: doc-only + # detection requires `pulls.listFiles` (an API call) which can't + # be evaluated at `if:`-expression time without a preceding step. + # The doc-only short-circuit still fires inside the evidence step; + # we just pay the checkout cost. Future refactor: an explicit + # "classify PR" step that exports outputs for both subsequent + # steps to gate on. Source: PR #19 adversarial review + # (2026-05-20), finding M1. - name: Check out github-workflows for parser script + if: github.event.pull_request.draft == false uses: actions/checkout@v4 with: repository: ArchonVII/github-workflows From b11d17b234b048911dd98f1faf846c14dc2234ce Mon Sep 17 00:00:00 2001 From: ArchonVII Date: Wed, 20 May 2026 14:07:03 -0500 Subject: [PATCH 4/4] docs(examples): document checks:read requirement (C2) Consumer-side `permissions:` block must include `checks: read` for the reusable workflow's F2/F10 evidence parser to call `checks.listForRef`. A reusable workflow cannot grant itself more than the caller grants, so this scope must be present in the caller too. Refs ArchonVII/github-workflows#10 ArchonVII/github-workflows#12 --- examples/pr-policy.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/examples/pr-policy.yml b/examples/pr-policy.yml index 11cbc15..d9d2cbf 100644 --- a/examples/pr-policy.yml +++ b/examples/pr-policy.yml @@ -9,9 +9,21 @@ on: pull_request: types: [opened, edited, reopened, synchronize] +# `permissions:` here scopes the GITHUB_TOKEN passed into the reusable +# workflow. The reusable workflow itself sets a stricter job-level block, +# but a reusable workflow can never grant itself MORE than the caller +# grants — so every scope the reusable workflow needs must also be +# present here: +# - pull-requests: read → read PR body / files / draft state +# - contents: read → checkout + `git.getCommit` for head commit time +# - checks: read → required by the F2/F10 evidence parser to look +# up CI check-runs for `location: ci` evidence +# rows via `checks.listForRef`. Source: PR #19 +# adversarial review (2026-05-20), finding C2. permissions: pull-requests: read contents: read + checks: read jobs: policy: