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
15 changes: 9 additions & 6 deletions .github/workflows/remi-proof.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
# - otherwise: pending ("needs a remi pass at <sha>") until a maintainer, or a Claude session working for one,
# records remi's verdict on that exact commit (CONTRIBUTING.md, layer 4, "Proof on remi"). Statuses belong to a commit, so
# every push starts over.
# A verdict someone recorded by hand is never overwritten here, whatever the state.
# A recorded verdict (remi-verdict.yml's, or one a person posted) is never overwritten here, whatever the state.
#
# remi is never reached from here, and nothing here can start a run there: remi only runs what a maintainer starts.
#
Expand Down Expand Up @@ -60,8 +60,9 @@ jobs:
# the entry point that runs them
HARNESS = re.compile(r"(tools/|adapters/|boxes/|wend$)")
# the workflows that define the required checks run from the PR (gate.yml) or become the next judge
# (this file): nothing in CI judges a change to them either. Dependabot's own action bumps are exempt.
CHECKS = re.compile(r"\.github/workflows/(gate|remi-proof)\.yml$")
# (this file, and remi-verdict.yml, which records verdicts): nothing in CI judges a change to them either.
# Dependabot's own action bumps are exempt.
CHECKS = re.compile(r"\.github/workflows/(gate|remi-proof|remi-verdict)\.yml$")
# package and unit names (a prefix of the name, any case)
BOOTNET = re.compile(
r"(kernel|linux-(image|headers|modules|generic|server|virtual|signed)|linux-firmware|grub|shim|efibootmgr"
Expand Down Expand Up @@ -165,10 +166,12 @@ jobs:
print(f"#{PR} head {sha}")

def stand_by_hand_verdict():
"""A verdict recorded by hand stands; only this workflow's own success/pending is ours to replace."""
"""A recorded verdict stands: one a person posted, or one remi-verdict.yml posted for them (it's
github-actions[bot]'s too, and says so). Only this workflow's own success/pending is ours to replace."""
mine = [s for s in api(f"repos/{REPO}/commits/{sha}/statuses?per_page=100") if s["context"] == CONTEXT]
if mine and (mine[0].get("creator") or {}).get("login") != "github-actions[bot]":
print(f"already recorded by hand: {mine[0]['creator']['login']} {mine[0]['state']}")
if mine and ((mine[0].get("creator") or {}).get("login") != "github-actions[bot]"
or (mine[0].get("description") or "").startswith("remi verdict:")):
print(f"verdict already recorded: {mine[0]['state']}: {mine[0].get('description')}")
sys.exit(0)

stand_by_hand_verdict()
Expand Down
71 changes: 71 additions & 0 deletions .github/workflows/remi-verdict.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Records remi's verdict on a PR's exact head as its `remi-proof` status (CONTRIBUTING.md, layer 4, "Proof on remi").
#
# Run by hand, after a maintainer-run proof on remi: Actions, remi-verdict, Run workflow (or the dispatch API). Only
# people with write access can start it, the same people who could post the status themselves; it exists because the
# API proxy Claude sessions use can dispatch a workflow but can't write a commit status. It never touches remi.
#
# It refuses a commit that isn't the PR's current head, so a pass on an old push can't green a newer one. Its statuses
# say "remi verdict:", which is how remi-proof.yml tells them from its own (both are github-actions[bot]'s) and leaves
# them standing.
name: remi-verdict

on:
workflow_dispatch:
Comment thread
TusanHomichi marked this conversation as resolved.
inputs:
pr: { description: "PR number", required: true }
sha: { description: "the full commit remi ran (run.json head.commit)", required: true }
state: { description: "remi's verdict", required: true, type: choice, options: [success, failure] }
summary: { description: "what ran and how it ended, e.g. trusty-to-resolute on ubuntu/trusty-web, BIOS, passed", required: true }
link: { description: "the write-up: a PR comment or thread (optional)", required: false }

permissions:
contents: read
pull-requests: read
statuses: write

jobs:
record:
runs-on: ubuntu-24.04
steps:
- env:
GH_TOKEN: "${{ github.token }}"
REPO: "${{ github.repository }}"
ACTOR: "${{ github.actor }}"
PR: "${{ inputs.pr }}"
SHA: "${{ inputs.sha }}"
STATE: "${{ inputs.state }}"
SUMMARY: "${{ inputs.summary }}"
LINK: "${{ inputs.link }}"
shell: python3 -I {0}
run: |
import json, os, re, sys, urllib.request

REPO, PR, SHA, STATE = os.environ["REPO"], os.environ["PR"], os.environ["SHA"], os.environ["STATE"]

def api(path, body=None):
req = urllib.request.Request("https://api.github.com/" + path, method="POST" if body else "GET",
data=json.dumps(body).encode() if body else None)
req.add_header("Authorization", "Bearer " + os.environ["GH_TOKEN"])
req.add_header("Accept", "application/vnd.github+json")
req.add_header("X-GitHub-Api-Version", "2022-11-28")
with urllib.request.urlopen(req, timeout=60) as r:
return json.loads(r.read())

if not re.fullmatch(r"[0-9]+", PR) or not re.fullmatch(r"[0-9a-f]{40}", SHA) or STATE not in ("success", "failure"):
sys.exit("need a PR number, a full 40-character commit and success or failure")
pr = api(f"repos/{REPO}/pulls/{PR}")
if pr["state"] != "open":
sys.exit(f"#{PR} is not open")
if pr["head"]["sha"] != SHA:
sys.exit(f"#{PR}'s head is {pr['head']['sha']}, not {SHA}: remi has to prove the current head")

link = os.environ["LINK"].strip()
if link and not re.fullmatch(r"https://[^\s]+", link):
sys.exit("the link must be an https URL")
desc = f"remi verdict: {os.environ['SUMMARY'].strip()}"
desc = desc if len(desc) <= 140 else desc[:139] + "…"
body = {"state": STATE, "context": "remi-proof", "description": desc}
if link:
body["target_url"] = link
api(f"repos/{REPO}/statuses/{SHA}", body)
print(f"#{PR} {SHA[:7]}: remi-proof {STATE}, recorded for {os.environ['ACTOR']}: {desc}")
34 changes: 19 additions & 15 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ PRs pass at once: the gate's replays are their proof. A PR stays pending until s
verdict on its exact head commit when it:

- touches `tools/`, `adapters/`, `boxes/` or `wend`, or the workflows behind the required checks (`gate.yml`,
`remi-proof.yml`; Dependabot's own action bumps excepted);
`remi-proof.yml`, `remi-verdict.yml`; Dependabot's own action bumps excepted);
- adds, changes, removes, pins or unpins a fix a chroot can't judge (before or after the change), because it works
on what a chroot never boots: a `net.*` or `fs.layout` op, or one that names a kernel, bootloader, initramfs,
storage, init, network or firewall package or unit in any argument (`install`'s packages, `rpm.transit`'s
Expand All @@ -86,28 +86,32 @@ verdict on its exact head commit when it:

Statuses belong to a commit, so every push is judged again from scratch.

To record a verdict, check that the run's `run.json` `head` is the PR's current head and not dirty, then:
To record a verdict, check that the run's `run.json` `head` is the PR's current head and not dirty, then run the
`remi-verdict` workflow (Actions, remi-verdict, Run workflow). It needs write access, refuses a commit that isn't the
PR's head, and posts the status as "remi verdict: <summary>":

```
pr=123
sha=$(gh pr view $pr -R FieldmouseWorks/wend --json headRefOid -q .headRefOid) # must equal run.json head.commit
state=success # or failure
desc="remi: trusty-to-resolute on ubuntu/trusty-web, BIOS, passed" # 140 characters at most
gh api -X POST repos/FieldmouseWorks/wend/statuses/$sha -f context=remi-proof -f state=$state \
-f description="$desc" -f target_url="<link to the write-up: the PR comment or thread>"
gh workflow run remi-verdict.yml -R FieldmouseWorks/wend -f pr=123 -f sha=<run.json head.commit> -f state=success \
-f summary="trusty-to-resolute on ubuntu/trusty-web, BIOS, passed" -f link="<the write-up: PR comment or thread>"
```

A Claude session whose `gh` isn't signed in but whose proxy authenticates GitHub API calls sends the same with curl:
A Claude session whose `gh` isn't signed in but whose proxy authenticates GitHub API calls sends the same with curl
(the proxy refuses a direct commit status write, but not this):

```
curl -fsS -X POST -H "Content-Type: application/json" https://api.github.com/repos/FieldmouseWorks/wend/statuses/$sha \
-d "$(jq -n --arg s "$state" --arg d "$desc" --arg u "<link>" '{context:"remi-proof",state:$s,description:$d,target_url:$u}')"
curl -fsS -X POST -H "Content-Type: application/json" \
https://api.github.com/repos/FieldmouseWorks/wend/actions/workflows/remi-verdict.yml/dispatches \
-d "$(jq -n --arg pr 123 --arg sha <full sha> --arg s success --arg m "<summary>" --arg l "<link>" \
'{ref:"main",inputs:{pr:$pr,sha:$sha,state:$s,summary:$m,link:$l}}')"
```

A verdict recorded this way stands: the workflow never overwrites a status a person or a session set. Record it with
a person's own credentials, never from an Actions job's `GITHUB_TOKEN`: the workflow tells its own statuses apart by
that token's account. To re-judge an
open PR that was opened before the workflow existed, run it by hand: Actions, remi-proof, Run workflow, PR number.
Then check the run in Actions (it fails, posting nothing, on a stale or wrong commit) and the PR's `remi-proof` status.
Someone with a token that can write statuses may also post the status directly (`gh api -X POST
repos/FieldmouseWorks/wend/statuses/<sha> -f context=remi-proof -f state=success -f description=...`). A recorded
verdict stands: remi-proof.yml never overwrites a status a person posted or one whose description starts with
"remi verdict:". Never post one from any other workflow: the classifier treats the rest of github-actions[bot]'s
statuses as its own. To re-judge an open PR that was opened before the workflow existed, run it by hand: Actions,
remi-proof, Run workflow, PR number.

### 5. Claude review (automatic, minutes)

Expand Down