Repository navigation
Fork governance: one PR comment, three lanes, every repository #1332
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
c49d27d
fix(security): close every open code-scanning alert
hyperpolymath 58c3237
feat(governance): fork governance as a system, on every PR, in every …
hyperpolymath 23be14a
fix(governance): onboarding the lockfile, name the baseline gate's fi…
hyperpolymath eb0acae
fix(scan): mktemp for the alert-feed stderr, not a predictable /tmp path
hyperpolymath 0f4f334
Merge branch 'main' into arena/9a30eb21-standards
hyperpolymath File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| { | ||
| "_comment": [ | ||
| "Accepted-risk record for GitHub code-scanning alerts that CANNOT be fixed in", | ||
| "this repository's source. Consumed by scripts/dismiss-code-scanning-alerts.sh,", | ||
| "which refuses to dismiss anything not listed here. Keep every entry specific:", | ||
| "an entry with no path or line matches every alert with that rule id.", | ||
| "", | ||
| "Two classes live here, both documented in", | ||
| "docs/audits/2026-10-24-code-scanning-triage.adoc:", | ||
| " 1. TokenPermissionsID — a true finding about a write permission that is", | ||
| " structurally required (SARIF upload / same-repo push + PR). There is no", | ||
| " code shape that avoids it, so the choice is dismiss-with-reason or leave", | ||
| " the alert open forever. The workflows already declare least privilege:", | ||
| " read at workflow level, the one write at job level.", | ||
| " 2. SASTID — scorecard's SAST probe samples merged commits; commits that", | ||
| " predate code scanning keep it open even though CodeQL now reports a", | ||
| " successful github-advanced-security check run on every pull request." | ||
| ], | ||
| "dismissals": [ | ||
| { | ||
| "alert_number": null, | ||
| "comment": "security-events: write is the ONLY permission that can upload SARIF to code scanning; it cannot be expressed as read. The reusable this job calls declares the same permission, and GitHub rejects a run at startup when the caller grants less than the callee requests (startup_failure, zero jobs) - so downgrading this to read does not reduce privilege, it silently disables code scanning. See standards#451 and the in-file comment.", | ||
| "line": 25, | ||
| "path": ".github/workflows/hypatia-scan.yml", | ||
| "reason": "won't fix", | ||
| "rule_id": "TokenPermissionsID" | ||
| }, | ||
| { | ||
| "alert_number": null, | ||
| "comment": "Same structural requirement as the caller above: the scan job uploads its own SARIF, so security-events: write is the minimum this reusable can declare. Workflow-level actions/contents stay read.", | ||
| "line": 44, | ||
| "path": ".github/workflows/hypatia-scan-reusable.yml", | ||
| "reason": "won't fix", | ||
| "rule_id": "TokenPermissionsID" | ||
| }, | ||
| { | ||
| "alert_number": null, | ||
| "comment": "security-events: write is required for the CodeQL SARIF upload that branch protection's code-scanning rule depends on. A caller of a reusable workflow must grant at least what the callee declares (startup_failure otherwise), so there is no lower-privilege spelling of this workflow. actions/contents remain read.", | ||
| "line": 40, | ||
| "path": ".github/workflows/codeql.yml", | ||
| "reason": "won't fix", | ||
| "rule_id": "TokenPermissionsID" | ||
| }, | ||
| { | ||
| "alert_number": null, | ||
| "comment": "Job-scoped contents: write, needed to push propagated git hooks into target repositories; the workflow level is contents: read. pull-requests and actions were removed as dead grants. Scorecard reports every write permission without evaluating whether the job performs the matching operation.", | ||
| "line": 193, | ||
| "path": ".github/workflows/propagate-hooks.yml", | ||
| "reason": "won't fix", | ||
| "rule_id": "TokenPermissionsID" | ||
| }, | ||
| { | ||
| "alert_number": null, | ||
| "comment": "Job-scoped contents: write + pull-requests: write, needed to push the machine-written measurement branch and open/update its single recording PR. The workflow level is contents: read. Nothing else in the repository writes with this job's token.", | ||
| "line": 43, | ||
| "path": ".github/workflows/debt-measure.yml", | ||
| "reason": "won't fix", | ||
| "rule_id": "TokenPermissionsID" | ||
| }, | ||
| { | ||
| "alert_number": null, | ||
| "comment": "Job-scoped contents: write + pull-requests: write, needed by the commit-back and pr-back modes that push a regenerated CHANGELOG.md and open its PR. The reusable never grants this at workflow level, and check-only mode writes nothing. Callers that only ever pass mode=check-only can drop it on their side.", | ||
| "line": 111, | ||
| "path": ".github/workflows/changelog-reusable.yml", | ||
| "reason": "won't fix", | ||
| "rule_id": "TokenPermissionsID" | ||
| }, | ||
| { | ||
| "alert_number": null, | ||
| "comment": "False positive as stated: this repository runs CodeQL on every push and pull request and the github-advanced-security 'CodeQL' check run reports success on current pull-request heads. Scorecard's SAST probe scores its sample of merged commits, so commits merged before code scanning was enabled keep the aggregate finding open; no commit can be retroactively given a check run. Dismissing this does not disable the SAST check - it is a threshold finding, not a control.", | ||
| "line": null, | ||
| "path": null, | ||
| "reason": "false positive", | ||
| "rule_id": "SASTID" | ||
| }, | ||
| { | ||
| "alert_number": null, | ||
| "comment": "Same structural requirement as codeql.yml and hypatia-scan.yml: this job is the caller of hypatia-scan-reusable.yml, which uploads SARIF and therefore declares security-events: write. A caller may not grant less than the callee declares - the run then dies at startup with zero jobs - so there is no lower-privilege spelling of this workflow. Workflow level stays contents: read.", | ||
| "line": 81, | ||
| "path": ".github/workflows/fork-governance.yml", | ||
| "reason": "won't fix", | ||
| "rule_id": "TokenPermissionsID" | ||
| } | ||
| ] | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| // SPDX-License-Identifier: CC-BY-SA-4.0 | ||
| // SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk> | ||
| // GENERATED FILE — copied verbatim into every estate repository by | ||
| // .github/workflows/propagate-hooks.yml. Do not edit the copies; edit this. | ||
| = Fork governance — this repository reports its conformance on every pull request | ||
| :toc: none | ||
| :revdate: 2026-10-24 | ||
|
|
||
| Every pull request in this repository carries one comment titled *Fork | ||
| governance*. It is regenerated in place on every push, and it reports three | ||
| lanes, in this order: | ||
|
|
||
| [cols="1,4",options="header"] | ||
| |=== | ||
| | Lane | What it tells you | ||
| | DEED syntax | whether the repository's `.deed` self-description still parses | ||
| | k9-deed conformance | whether its committed K9 contracts pass layers L0–L2 | ||
| | Hypatia verdict grid | what the scanners ran and what each of them reported | ||
| |=== | ||
|
|
||
| The comment opens with a summary table, then the k9-deed grid, then the | ||
| Hypatia grid. A row that is *orange* means the evidence is missing, not that | ||
| the check passed. Nothing in the comment is a security certification. | ||
|
|
||
| == Where the normative documentation lives | ||
|
|
||
| All of it, in AsciiDoc, in one place: | ||
|
|
||
| https://github.com/hyperpolymath/standards/tree/main/3-practice/fork-governance | ||
|
|
||
| * `README.adoc` — what the system is and how to read a row | ||
| * `SPEC.adoc` — the normative comment contract (markers, order, states) | ||
| * `LANES.adoc` — each lane's implementation, artifacts and permissions | ||
| * `ADOPTION.adoc` — how to turn it on in another repository | ||
| * `DISCOVERY.adoc` — this chain, and what keeps it from rotting | ||
|
|
||
| == If the comment is missing | ||
|
|
||
| The comment is produced by `.github/workflows/fork-governance.yml` in this | ||
| repository. If a pull request has no such comment, that workflow did not run | ||
| on it — which is a defect in this repository, not an absence of findings. Check | ||
| the Actions tab for a run named *Fork governance*. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,191 @@ | ||
| # SPDX-License-Identifier: MPL-2.0 | ||
| # SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk> | ||
| # | ||
| # fork-governance-reusable.yml — post the Fork governance summary on a pull | ||
| # request, with the lanes in a fixed order: | ||
| # | ||
| # 1. summary DEED syntax · k9-deed conformance · Hypatia verdict grid | ||
| # 2. k9-deed conformance grid | ||
| # 3. Hypatia verdict grid | ||
| # | ||
| # This workflow POSTS; it does not run the lanes. A reusable cannot read the | ||
| # caller's `needs` graph, so the caller passes its lane results and verdicts in | ||
| # as inputs. That is deliberate: the posting step must not be able to invent a | ||
| # lane outcome, and a lane that never ran has no result to pass — it renders | ||
| # orange, which is the truth. | ||
| # | ||
| # WHY THE LANES DO NOT POST THEMSELVES. `k9-conformance.yml` and | ||
| # `hypatia-scan-reusable.yml` render grids and publish artifacts. They ask for | ||
| # no write permission, because a reusable may not request more than its caller | ||
| # grants and most callers grant only reads. This workflow is the single place | ||
| # that is allowed to write to a pull request, so there is exactly one comment, | ||
| # one marker, and one update loop per repository. | ||
| # | ||
| # Documented in 3-practice/fork-governance/ (SPEC.adoc, LANES.adoc, | ||
| # ADOPTION.adoc). Read those before changing the comment shape: the markers are | ||
| # a contract with every downstream reader. | ||
|
|
||
| name: Fork governance (reusable) | ||
|
|
||
| on: | ||
| workflow_call: | ||
| inputs: | ||
| pr-number: | ||
| description: "Pull request number to comment on. Pass github.event.pull_request.number." | ||
| type: string | ||
| required: true | ||
| deed-result: | ||
| description: "needs.<deed job>.result" | ||
| type: string | ||
| required: false | ||
| default: "" | ||
| k9-result: | ||
| description: "needs.<k9 job>.result" | ||
| type: string | ||
| required: false | ||
| default: "" | ||
| k9-verdict: | ||
| description: "needs.<k9 job>.outputs.verdict — green, yellow, orange, red or grey." | ||
| type: string | ||
| required: false | ||
| default: "" | ||
| k9-artifact: | ||
| description: "Artifact name carrying k9-grid/report.md. Empty to skip the download." | ||
| type: string | ||
| required: false | ||
| default: "k9-grid" | ||
| hypatia-result: | ||
| description: "needs.<hypatia job>.result" | ||
| type: string | ||
| required: false | ||
| default: "" | ||
| hypatia-verdict: | ||
| description: "needs.<hypatia job>.outputs.verdict — green, yellow, orange, red, purple or grey." | ||
| type: string | ||
| required: false | ||
| default: "" | ||
| hypatia-artifact: | ||
| description: "Artifact name carrying verdict-grid/report.md. Empty to skip the download." | ||
| type: string | ||
| required: false | ||
| default: "verdict-grid" | ||
| grid-author: | ||
| description: "The account whose previous comment is read as the baseline. Only this account's comment is trusted." | ||
| type: string | ||
| required: false | ||
| default: "github-actions[bot]" | ||
| canon-ref: | ||
| description: "Ref of hyperpolymath/standards holding the renderer. Pin it in production." | ||
| type: string | ||
| required: false | ||
| default: "main" | ||
|
|
||
| permissions: {} | ||
|
|
||
| jobs: | ||
| post: | ||
| name: Post the fork governance summary | ||
| # Always: a lane that failed is exactly when the summary matters most. | ||
| if: ${{ always() && inputs.pr-number != '' }} | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 5 | ||
| permissions: | ||
| contents: read | ||
| actions: read | ||
| pull-requests: write | ||
| steps: | ||
| # In this repository the renderer is already checked out below the | ||
| # workspace; anywhere else it comes from the canon at a pinned ref. | ||
| - name: Checkout the caller | ||
| if: ${{ github.repository == 'hyperpolymath/standards' }} | ||
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||
| with: | ||
| persist-credentials: false | ||
| sparse-checkout: scripts/fork-governance-render.sh | ||
| sparse-checkout-cone-mode: false | ||
|
|
||
| - name: Checkout the canon | ||
| if: ${{ github.repository != 'hyperpolymath/standards' }} | ||
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||
| with: | ||
| repository: hyperpolymath/standards | ||
| ref: ${{ inputs.canon-ref }} | ||
| path: .fork-governance-canon | ||
| persist-credentials: false | ||
| sparse-checkout: scripts/fork-governance-render.sh | ||
| sparse-checkout-cone-mode: false | ||
|
|
||
| # `continue-on-error` on both downloads, and never a condition on the | ||
| # posting step: an artifact that did not upload is a fact to report | ||
| # (orange row), not a reason to post nothing. A summary that fails to | ||
| # appear when things went wrong is worse than no summary at all. | ||
| - name: Download the k9-deed grid | ||
| if: ${{ inputs.k9-artifact != '' }} | ||
| continue-on-error: true | ||
| uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 | ||
| with: | ||
| name: ${{ inputs.k9-artifact }} | ||
| path: k9-grid | ||
|
|
||
| - name: Download the Hypatia verdict grid | ||
| if: ${{ inputs.hypatia-artifact != '' }} | ||
| continue-on-error: true | ||
| uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 | ||
| with: | ||
| name: ${{ inputs.hypatia-artifact }} | ||
| path: verdict-grid | ||
|
|
||
| - name: Render the combined verdict | ||
| id: render | ||
| env: | ||
| DEED_RESULT: ${{ inputs.deed-result }} | ||
| K9_RESULT: ${{ inputs.k9-result }} | ||
| K9_VERDICT: ${{ inputs.k9-verdict }} | ||
| HYPATIA_RESULT: ${{ inputs.hypatia-result }} | ||
| HYPATIA_VERDICT: ${{ inputs.hypatia-verdict }} | ||
| run: | | ||
| set -euo pipefail | ||
| case "${{ github.repository }}" in | ||
| hyperpolymath/standards) RENDERER=scripts/fork-governance-render.sh ;; | ||
| *) RENDERER=.fork-governance-canon/scripts/fork-governance-render.sh ;; | ||
| esac | ||
| [ -f "$RENDERER" ] || { echo "::error::renderer not found at $RENDERER"; exit 1; } | ||
| args=(--out "$RUNNER_TEMP/fork-governance" | ||
| --deed-result "$DEED_RESULT" | ||
| --k9-result "$K9_RESULT" --k9-verdict "$K9_VERDICT" | ||
| --hypatia-result "$HYPATIA_RESULT" --hypatia-verdict "$HYPATIA_VERDICT" | ||
| --sha "$GITHUB_SHA" --run "$GITHUB_RUN_ID" --repo "$GITHUB_REPOSITORY") | ||
| [ -s k9-grid/report.md ] && args+=(--k9-report k9-grid/report.md) | ||
| [ -s verdict-grid/report.md ] && args+=(--hypatia-report verdict-grid/report.md) | ||
| bash "$RENDERER" "${args[@]}" >/dev/null | ||
| { | ||
| echo "## Fork governance" | ||
| echo | ||
| cat "$RUNNER_TEMP/fork-governance/body.md" | ||
| } >> "$GITHUB_STEP_SUMMARY" | ||
|
|
||
| - name: Publish the comment | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| PR_NUMBER: ${{ inputs.pr-number }} | ||
| GRID_AUTHOR: ${{ inputs.grid-author }} | ||
| run: | | ||
| set -euo pipefail | ||
| # One comment per pull request, updated in place. The marker is read | ||
| # before the body is written, so a rerun of the same commit cannot | ||
| # produce two comments and a reader cannot see two verdicts at once. | ||
| comment_id=$(gh api --paginate --slurp "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" \ | ||
| | jq -r --arg a "$GRID_AUTHOR" \ | ||
| '[.[][] | select((.user.login // "") == $a and ((.body // "") | contains("<!-- fork-governance-verdict:")))] | ||
| | last | .id // ""') | ||
| jq -n --rawfile body "$RUNNER_TEMP/fork-governance/body.md" '{body: $body}' \ | ||
| > "$RUNNER_TEMP/fork-governance/comment.json" | ||
| if [ -n "$comment_id" ]; then | ||
| gh api -X PATCH "repos/$GITHUB_REPOSITORY/issues/comments/$comment_id" \ | ||
| --input "$RUNNER_TEMP/fork-governance/comment.json" >/dev/null | ||
| echo "updated the fork governance comment on #$PR_NUMBER ($comment_id)" | ||
| else | ||
| gh api -X POST "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" \ | ||
| --input "$RUNNER_TEMP/fork-governance/comment.json" >/dev/null | ||
| echo "posted the fork governance comment on #$PR_NUMBER" | ||
| fi |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do not claim that propagation activates governance.
If a target repository has not adopted the governance workflows, propagation still copies this pointer to its root. The statement that every pull request carries a comment is then false. Describe the comment as available after adoption, and direct readers to
ADOPTION.adoc.