Check the plan before filing into it, keep filed items in order, name the filer's tests in the gate, and record the merge over a worse ruling

This commit is contained in:
2026-10-14 13:39:42 +02:00
committed by Lillem4n
parent 16afcff09c
commit 7739c09163
3 changed files with 37 additions and 18 deletions
+8 -7
View File
@@ -1208,13 +1208,14 @@ docker compose run --rm --user root --entrypoint chown test -R "$(id -u):$(id -g
```
Every pull request runs `docker build .` against both the latest and the lowest
supported Go, and must pass before it can be merged — unless it changes none of
the files the build and its tests read, nor the workflow itself, in which case
it's skipped. That build runs vet, complexity, the format check, the Go tests and the
release tooling's tests; the rest of the gate is the changelog check, which CI runs
against the PR base, and the package-repository checks below. Run `docker build .`
locally before pushing. The lowest Go builds the `portable` stage, which skips the format
check and the release tooling's tests:
supported Go, and must pass before it can be merged — unless it changes none of the
files the build and its tests read, nor the workflow itself, in which case it's skipped.
That build runs vet, complexity, the format check, the Go tests, the release tooling's
tests and the vulnerability filer's tests, which file an item into `todo.md` itself, so
a change to the plan's shape can fail it; the rest of the gate is the changelog check,
which CI runs against the PR base, and the package-repository checks below. Run `docker
build .` locally before pushing. The lowest Go builds the `portable` stage, which skips
the format check and both Python test suites:
```sh
docker build . # latest
+3 -1
View File
@@ -1007,4 +1007,6 @@ plan; a test files one into the repository's own `todo.md`. When the next releas
`todo.md` names no version for it, so the item lands in the release after; the maintainer
moves it. Where no Go version fixes a vulnerability yet, govulncheck has no ignore list: the
maintainer drops the job from the release's `needs` until one does, and records why here.
Valid while the binaries are built from the gate's Go version.
The paired ruling against f90b4d6 found the project reads worse for it: the gate now reads
`todo.md`, and a release waits on vuln.go.dev. The maintainer merged it anyway, as the cost
of filing into the plan. Valid while the binaries are built from the gate's Go version.
+26 -10
View File
@@ -2,7 +2,8 @@
"""File each module's known vulnerabilities as one security item atop todo.md's next release.
Usage: file_vulnerabilities.py <govulncheck -format json report> <todo.md>
Exits 1 while any vulnerability is known, filed now or before, and 0 on a clean report.
Exits 1 while any vulnerability is known, filed now or before, 0 on a clean report, and 2 on
a todo.md it cannot read.
todo.md's `Next ID = N` gives the item's ID, and the release of the plan's top row, the next
release, gives its release. Its Score is 0.0 until rank_todo.py, which CI cannot run, scores
@@ -21,8 +22,9 @@ import re
import sys
from dataclasses import dataclass, field
ROW = re.compile(r"^\| (\d+) \| ([^|]+?) \| [^|]*\| \*\*", re.M)
ROW = re.compile(r"^\| (?P<id>\d+) \| (?P<release>[^|]+?) \| [^|]*\| \*\*", re.M)
NEXT_ID = re.compile(r"`Next ID = (\d+)`")
DETAILS = "## Details\n\n"
SCORES = {"R": 1, "S": 1, "C": 1, "A": 8, "G": 8}
GOAL = "1.1"
@@ -100,20 +102,30 @@ def detail(module: Module) -> str:
def file_items(modules: list[Module], todo: str) -> tuple[str, list[int]]:
"""Return todo.md with an item for each module whose vulnerabilities it does not yet name, and the items' IDs."""
first = ROW.search(todo)
if first is None:
raise ValueError("todo.md has no item to take the next release from")
if NEXT_ID.search(todo) is None:
raise ValueError("todo.md has no `Next ID = N` line to number the item by")
if DETAILS not in todo:
raise ValueError(f"todo.md has no {DETAILS.strip()!r} heading to put the item's detail under")
release, rows_at = first.group("release"), first.start()
details_at = todo.index(DETAILS) + len(DETAILS)
filed = []
for module in modules:
if all(osv in todo for osv in module.osvs):
continue
first = ROW.search(todo)
if first is None:
raise ValueError("todo.md has no item to take the next release from")
next_id = int(NEXT_ID.search(todo).group(1))
line = tagline(module)
scores = " | ".join(str(SCORES[k]) for k in "RSCAG")
row = f"| {next_id} | {first.group(2)} | security | **{line}** | {scores} | {GOAL} | 0.0 |\n"
todo = todo[: first.start()] + row + todo[first.start() :]
todo = todo.replace(f"`Next ID = {next_id}`", f"`Next ID = {next_id + 1}`", 1)
todo = todo.replace("## Details\n\n", f"## Details\n\n### {next_id}. {line}\n\n{detail(module)}\n", 1)
row = f"| {next_id} | {release} | security | **{line}** | {scores} | {GOAL} | 0.0 |\n"
text = f"### {next_id}. {line}\n\n{detail(module)}\n"
todo = todo[:rows_at] + row + todo[rows_at:]
rows_at += len(row)
details_at += len(row)
todo = todo[:details_at] + text + todo[details_at:]
details_at += len(text)
todo = NEXT_ID.sub(f"`Next ID = {next_id + 1}`", todo, count=1)
filed.append(next_id)
return todo, filed
@@ -128,7 +140,11 @@ def main(argv: list[str]) -> int:
count = sum(len(m.osvs) for m in modules)
known = f"{count} known {'vulnerability' if count == 1 else 'vulnerabilities'}"
with open(todo_path, encoding="utf-8") as f:
todo, filed = file_items(modules, f.read())
try:
todo, filed = file_items(modules, f.read())
except ValueError as err:
print(err, file=sys.stderr)
return 2
if not filed:
print(f"{known}, already planned in todo.md.")
return 1