diff --git a/.github/workflows/pr-policy.yml b/.github/workflows/pr-policy.yml index e84ec7a..22bc8a0 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 @@ -83,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 @@ -167,6 +191,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; @@ -229,13 +254,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, 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: