diff --git a/README.md b/README.md index 907fb28..4e0b47e 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/decisions.md b/docs/decisions.md index ae8a058..5d94965 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -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. diff --git a/vulnerability-filing/file_vulnerabilities.py b/vulnerability-filing/file_vulnerabilities.py index 37a2c22..dc479d9 100644 --- a/vulnerability-filing/file_vulnerabilities.py +++ b/vulnerability-filing/file_vulnerabilities.py @@ -2,7 +2,8 @@ """File each module's known vulnerabilities as one security item atop todo.md's next release. Usage: file_vulnerabilities.py -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\d+) \| (?P[^|]+?) \| [^|]*\| \*\*", 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