Skip to content
Merged
Show file tree
Hide file tree
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
177 changes: 177 additions & 0 deletions .github/workflows/pr-metadata.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
name: PR metadata gates

on:
push:
branches: [main]
pull_request:
# Description edits and retargets re-run only the metadata gates, using the current PR.
types: [opened, synchronize, reopened, edited]
workflow_dispatch:

permissions: {}
cache-mode: none

# Queue edits without replacing a pending run; API reads make freshness independent of queue order.
# github/docs@336b7f546d94, data/reusables/actions/actions-group-concurrency.md.
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
queue: max

jobs:
verdict-review-gate:
Comment thread
seathatflowsinourveins marked this conversation as resolved.
# Required check (docs/decisions/2026-09-22-github-automation-closure.md, "verdict-review-gate
# (2026-09-23)"): with one maintainer and zero required approvals, a PR that changes a
# layer-verdict row merges only when the row is consistent with the sealed cross-family lane
# returns its wave registers (the returns are self-attested; see the record's residual). It has
# no path filter so it reports on every pull request and can be required.
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: read
pull-requests: read
steps:
- name: Harden the runner (audit-only network egress)
uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1
with:
egress-policy: audit
- name: Check out repository with full history
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
fetch-depth: 0
# Full history: the gate reads the base commit's ledger and wave registry with git show.
- name: Require the current PR base to match this event
if: github.event_name == 'pull_request'
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const {data: pullRequest} = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number
});
const eventBase = context.payload.pull_request.base;
if (pullRequest.base.ref !== eventBase.ref) {
core.setFailed('verdict-review-gate: the current PR base differs from the event base; failing closed (push a commit or close and reopen the pull request to rebuild its merge commit).');
}
- name: Require sealed cross-family review for changed verdict rows
shell: bash
env:
EVENT_NAME: ${{ github.event_name }}
# The pull request's base branch (GITHUB_BASE_REF): the gate judges only pull requests into main.
PR_BASE_REF: ${{ github.base_ref }}
# Cross-check only: the base is the checked-out merge commit's first parent.
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
# The checked-out HEAD must be the merge commit whose second parent is this PR head.
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
# The pushed range's previous commit on main.
PUSH_BEFORE_SHA: ${{ github.event.before }}
run: |
set -euo pipefail
case "$EVENT_NAME" in
pull_request)
# A pull request into another branch is not judged against main, and retargeting it to
# main later re-runs this job (the edited type): fail closed rather than pass a base the
# gate does not protect.
if [ "${PR_BASE_REF:-}" != "main" ]; then
echo "::error::verdict-review-gate: the pull request's base branch is '${PR_BASE_REF:-}', not main; the gate judges only pull requests into main (retarget it to main); failing closed"
exit 1
fi
# actions/checkout checks out the PR merge commit; its first parent is the base-branch
# commit the PR is merged into, which is what the gate must compare with. Assert that
# premise: HEAD has exactly two parents and the second is the payload's PR head, or
# HEAD^1 could be the PR's own previous commit and the gate would judge only the last one.
second_parent="$(git rev-parse --verify --quiet 'HEAD^2^{commit}' || true)"
if [ -z "${PR_HEAD_SHA:-}" ] || [ "$second_parent" != "$PR_HEAD_SHA" ] \
|| git rev-parse --verify --quiet 'HEAD^3^{commit}' >/dev/null; then
echo "::error::verdict-review-gate: HEAD is not the merge commit of the PR head '${PR_HEAD_SHA:-}' (second parent '$second_parent'); failing closed"
exit 1
fi
first_parent="$(git rev-parse --verify --quiet 'HEAD^1^{commit}' || true)"
payload="${PR_BASE_SHA:-}"
if [ -n "$first_parent" ] && [ -n "$payload" ]; then
if ! git merge-base --is-ancestor "$payload" "$first_parent"; then
echo "::error::verdict-review-gate: the payload base $payload is not an ancestor of the merge commit's first parent $first_parent"
exit 1
fi
base="$first_parent"
elif [ -n "$first_parent" ]; then
base="$first_parent"
elif [ -n "$payload" ]; then
base="$payload"
else
echo "::error::verdict-review-gate: neither the merge commit's first parent nor the payload base is available"
exit 1
fi
# The base must be a commit on main itself (full history fetches origin/main), not a
# branch that merely contains main's tip, such as a merge commit left built against an
# earlier base: a retargeted PR is otherwise judged against the branch it left.
if ! git merge-base --is-ancestor "$base" "refs/remotes/origin/$PR_BASE_REF"; then
echo "::error::verdict-review-gate: the base $base is not a commit on origin/$PR_BASE_REF; failing closed (after a retarget the edited run checks out the merge commit built on the old base: close and reopen the pull request, or push a commit, to rebuild it on $PR_BASE_REF)"
exit 1
fi
;;
push)
base="${PUSH_BEFORE_SHA:-}"
# A branch-creating push has an all-zero before: there is no base to judge against.
if [ -z "$base" ] || [ -z "${base//0/}" ]; then
echo "::error::verdict-review-gate: the push has no previous commit (before is '$base'); failing closed"
exit 1
fi
;;
*)
# workflow_dispatch has no base in its payload: compare with the parent.
base="$(git rev-parse --verify 'HEAD^1^{commit}')"
;;
esac
# The rules come from the base: run the base commit's gate (and the code it imports) against
# this checkout, so a pull request cannot weaken the gate that judges it. Only the pull
# request that adds the gate (git diff reports it as A against the base) finds none at its
# base and runs its own copy; any other base without the gate fails closed.
gate="scripts/verdict_review_gate.py"
if git cat-file -e "$base:$gate" 2>/dev/null; then
git worktree add --quiet --detach "$RUNNER_TEMP/gate-base" "$base"
gate="$RUNNER_TEMP/gate-base/$gate"
else
added="$(git diff --no-renames --name-status -z "$base...HEAD" -- "$gate" | tr '\0' '\n')"
if [ "$added" != $'A\n'"$gate" ]; then
echo "::error::verdict-review-gate: the base $base has no $gate and this change does not add it; failing closed"
exit 1
fi
echo "verdict-review-gate: this change adds the gate; running this checkout's copy (bootstrap)"
fi
python3 "$gate" --root "$GITHUB_WORKSPACE" --base "$base" | tee -a "$GITHUB_STEP_SUMMARY"

sota-sources:
# Top rule (AGENTS.md): every change names the maintained repository or published reference it
# installs or follows. This job fails a PR whose description lacks a non-empty "SOTA sources" section
# (## or ###). It reads the current body through the official actions/github-script
# (github.com/actions/github-script, v9.0.0; docs.github.com/en/rest/pulls/pulls#get-a-pull-request).
# A retrieval error fails the action; no body text reaches a shell.
if: github.event_name == 'pull_request'
runs-on: ubuntu-24.04
timeout-minutes: 2
permissions:
pull-requests: read
steps:
- name: Harden the runner (audit-only network egress)
uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1
with:
egress-policy: audit
- name: Require a non-empty "SOTA sources" section in the PR description
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const {data: pullRequest} = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number
});
const body = pullRequest.body || '';
const match = body.match(/^#{2,3}[ \t]+SOTA sources[ \t]*$([\s\S]*?)(?=^#{2,3}[ \t]|(?![\s\S]))/m);
const content = match ? match[1].replace(/<!--[\s\S]*?-->/g, '').trim() : '';
if (!content) {
core.setFailed('The PR description needs a non-empty "SOTA sources" section: the repository, pin and file, or the published reference, behind each change (AGENTS.md top rule).');
} else {
core.info(`SOTA sources section present (${content.length} characters).`);
}
24 changes: 16 additions & 8 deletions .github/workflows/sota-sources-gate.yml
Original file line number Diff line number Diff line change
@@ -1,30 +1,33 @@
name: SOTA sources gate

# The reusable form of validate.yml's required sota-sources job, for other repositories: the new-repository scaffold's
# The reusable form of pr-metadata.yml's required sota-sources job, for other repositories: the new-repository scaffold's
# .github/workflows/sota-sources.yml (adoption/scaffold/, written by tools/adoption/scaffold_repo.py) calls this file at
# a pinned commit of this repository's main. From its `if:` line on, the job below is validate.yml's job byte for byte
# (tests/test_sota_sources_gate.py); change both together. validate.yml keeps its own copy because .github/
# a pinned commit of this repository's main. From its `if:` line on, the job below is pr-metadata.yml's job byte for byte
# (tests/test_sota_sources_gate.py); change both together. pr-metadata.yml keeps its own copy because .github/
# main-ruleset.json requires that job's check here. A called workflow runs in its caller's context, so
# github.event_name and the pull_request payload are the caller's, and it can only reduce the caller's token
# github.event_name and the PR number are the caller's, and it can only reduce the caller's token
# permissions (docs.github.com/en/actions/reference/workflows-and-actions/reusing-workflow-configurations, "github
# context" and "Supported keywords for jobs that call a reusable workflow").

on:
workflow_call:

# No token scope and no cache access (docs/decisions/2026-10-04-ci-least-privilege.md): the job reads only the caller's
# pull_request payload. No concurrency group here: in a called workflow ${{ github.workflow }} is the caller's name.
# No default token scope and no cache access (docs/decisions/2026-10-04-ci-least-privilege.md). The job grants only
# pull-requests: read to fetch the current body; callers must grant it too and bump their commit pin to adopt this
# change. No concurrency group here: in a called workflow ${{ github.workflow }} is the caller's name.
permissions: {}
cache-mode: none

jobs:
sota-sources:
# Top rule: every change names the maintained repository or published reference it installs or follows. This job
# fails a pull request whose description lacks a non-empty "SOTA sources" section (## or ###), reading the body
# from the caller's pull_request payload through actions/github-script; no body text reaches a shell.
# from the current PR through actions/github-script; a retrieval error fails the action and no body text reaches a shell.
if: github.event_name == 'pull_request'
runs-on: ubuntu-24.04
timeout-minutes: 2
permissions:
pull-requests: read
steps:
- name: Harden the runner (audit-only network egress)
uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1
Expand All @@ -34,7 +37,12 @@ jobs:
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const body = (context.payload.pull_request && context.payload.pull_request.body) || '';
const {data: pullRequest} = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number
});
const body = pullRequest.body || '';
const match = body.match(/^#{2,3}[ \t]+SOTA sources[ \t]*$([\s\S]*?)(?=^#{2,3}[ \t]|(?![\s\S]))/m);
const content = match ? match[1].replace(/<!--[\s\S]*?-->/g, '').trim() : '';
if (!content) {
Expand Down
Loading
Loading