Give the vulnerability filer a folder of its own, and leave its item's score to rank_todo.py

This commit is contained in:
2026-10-14 13:31:21 +02:00
committed by Lillem4n
parent 15965e7a77
commit 5a6928750c
6 changed files with 26 additions and 47 deletions
+3 -3
View File
@@ -30,7 +30,7 @@ jobs:
else
base='${{ github.event.before }}'
fi
if git diff --quiet "$base"...HEAD -- '*.go' go.mod go.sum Dockerfile .dockerignore .github data release-tooling testdata README.md todo.md; then
if git diff --quiet "$base"...HEAD -- '*.go' go.mod go.sum Dockerfile .dockerignore .github data release-tooling testdata vulnerability-filing README.md todo.md; then
echo "code=false" >> "$GITHUB_OUTPUT"
else
echo "code=true" >> "$GITHUB_OUTPUT"
@@ -82,7 +82,7 @@ jobs:
run: docker build --no-cache-filter vulnerabilities --target vulnerability-report --output "$RUNNER_TEMP/vulnerabilities" .
- name: File each one in todo.md
id: file
run: python3 release-tooling/file_vulnerabilities.py "$RUNNER_TEMP/vulnerabilities/report.json" todo.md
run: python3 vulnerability-filing/file_vulnerabilities.py "$RUNNER_TEMP/vulnerabilities/report.json" todo.md
- name: Propose the filed items
if: failure() && steps.file.outcome == 'failure' && github.ref == 'refs/heads/main'
env:
@@ -97,7 +97,7 @@ jobs:
git commit -qam "File the known vulnerabilities govulncheck reports"
git push -qf "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" vulnerabilities
gh pr create --head vulnerabilities --title "File the known vulnerabilities govulncheck reports" \
--body "govulncheck reports known vulnerabilities in the Go version or a required module, and no release is cut until they are gone. This files them atop the next release in todo.md; each item names the bump that fixes it."
--body "govulncheck reports known vulnerabilities in the Go version or a required module, and no release is cut until they are gone. This files them atop the next release in todo.md, scored 0.0 until rank_todo.py scores them; each item names the bump that fixes it."
# A pull request the workflow's token opens starts no workflow.
gh workflow run test.yml --ref vulnerabilities
+6 -1
View File
@@ -21,9 +21,13 @@ COPY --from=vulnerabilities /report.json /
FROM python:3.14.8-slim AS release-tooling
COPY release-tooling/ /release-tooling/
RUN python3 -m unittest discover -s /release-tooling -p '*_test.py' && touch /tested
FROM python:3.14.8-slim AS vulnerability-filing
COPY vulnerability-filing/ /vulnerability-filing/
# The filer's test files an item into the plan itself.
COPY todo.md /todo.md
RUN python3 -m unittest discover -s /release-tooling -p '*_test.py' && touch /tested
RUN python3 -m unittest discover -s /vulnerability-filing -p '*_test.py' && touch /tested
FROM goreleaser/goreleaser:v2.18.3 AS packaging
WORKDIR /check
@@ -33,6 +37,7 @@ RUN git init -q && git remote add origin https://github.com/larvit/fejkdata.git
FROM portable
# BuildKit skips a stage nothing copies from.
COPY --from=release-tooling /tested /tmp/release-tooling-tested
COPY --from=vulnerability-filing /tested /tmp/vulnerability-filing-tested
COPY --from=packaging /tested /tmp/packaging-tested
RUN unformatted="$(gofmt -l .)"; test -z "$unformatted" || \
{ echo "unformatted files:"; echo "$unformatted"; exit 1; }
+2 -1
View File
@@ -1170,8 +1170,9 @@ internal/rows/ a table's rows: the TSV, the options proved over it, the links b
data/ the shipped modules, each a Go package embedding its categories (JSON, and a TSV per table) and its manifest; and Modules, which lists them
data-import/ the scripts that rebuild each sourced table (see DATA-LICENSES.md), and the modules they share
docs/ the decision log, the register research behind the shipped data, the survey of other libraries, panels' scores and rulings on how readable the code is, and what each chunk cost an agent to read
release-tooling/ the release CI publishes from the changelog heading, and the binaries, packages, image and package repositories built for it; and the filing of known vulnerabilities
release-tooling/ the release CI publishes from the changelog heading, and the binaries, packages, image and package repositories built for it
testdata/ the pinned shipped shape (see Versioning)
vulnerability-filing/ the script CI runs to file each known vulnerability govulncheck reports as a `todo.md` item
```
## Development
+3 -3
View File
@@ -1001,9 +1001,9 @@ protection does not require. So a vulnerability published in Go, or an outage of
vuln.go.dev, holds the release and leaves unrelated pull requests mergeable. Each
vulnerability is filed as a `security` item atop the next release in `todo.md`, by a pull
request CI opens, so the plan names it before anyone reads a red run.
`release-tooling/file_vulnerabilities.py` writes that item in `todo.md`'s shape and scores it
by `todo.md`'s formula, because CI cannot reach `rank_todo.py`; a test files one into the
repository's own `todo.md`. When the next release has no item left and waits to be cut,
`vulnerability-filing/file_vulnerabilities.py` writes that item in `todo.md`'s shape with a
Score of 0.0, since CI cannot reach `rank_todo.py`, which scores it on the next commit to the
plan; a test files one into the repository's own `todo.md`. When the next release has no item left and waits to be cut,
`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.
@@ -4,25 +4,24 @@
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.
The item is written the way ~/.claude/bin/rank_todo.py writes one, which CI cannot run:
todo.md's `Next ID = N` gives its ID, the release of the plan's top row (the next release)
gives its release, and its Score is todo.md's `Score = …` formula over the scores below and
the weight todo.md's goal table gives goal 1.1. The test that files into the repository's
own todo.md fails the gate when the plan's shape drifts from what this script reads.
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
it; a security item ranks first in its release whatever it scores. The test that files into
the repository's own todo.md fails the gate when the plan's shape drifts from what this
script reads.
A security item scores R, S and C 1, since the fix is a version bump, and A and G 8, since a
known vulnerability in the binaries works against README goal 1.1: installing fejkdata the
way one installs other tools, which image scanners then flag.
Scores, as todo.md's table spells them: R (risk), S (size) and C (difficulty) are 1, since
the fix is a version bump; A (value to the audience) and G (value to the goal) are 8, the goal
being README goal 1.1, installing fejkdata the way one installs other tools, which a flagged
binary works against.
"""
import ast
import json
import re
import sys
from dataclasses import dataclass, field
ROW = re.compile(r"^\| (\d+) \| ([^|]+?) \| [^|]*\| \*\*", re.M)
WEIGHT = re.compile(r"^\| ([\d.]+) \| ([\d.]+) \|$", re.M)
NEXT_ID = re.compile(r"`Next ID = (\d+)`")
SCORES = {"R": 1, "S": 1, "C": 1, "A": 8, "G": 8}
GOAL = "1.1"
@@ -81,32 +80,6 @@ def version_key(version: str):
return [int(part) for part in re.findall(r"\d+", version)]
def evaluate(node, values):
"""The value of todo.md's score formula: numbers, letters, unary minus and + - * /."""
match node:
case ast.Constant(value=value):
return value
case ast.Name(id=name):
return values[name]
case ast.UnaryOp(op=ast.USub(), operand=operand):
return -evaluate(operand, values)
case ast.BinOp(left=left, op=ast.Add(), right=right):
return evaluate(left, values) + evaluate(right, values)
case ast.BinOp(left=left, op=ast.Sub(), right=right):
return evaluate(left, values) - evaluate(right, values)
case ast.BinOp(left=left, op=ast.Mult(), right=right):
return evaluate(left, values) * evaluate(right, values)
case ast.BinOp(left=left, op=ast.Div(), right=right):
return evaluate(left, values) / evaluate(right, values)
raise ValueError(f"todo.md's formula holds {ast.dump(node)}, which this script cannot evaluate")
def score(todo: str) -> str:
formula = re.search(r"`Score = (.+?)`", todo).group(1)
values = dict(SCORES, W=float(dict(WEIGHT.findall(todo))[GOAL]))
return f"{evaluate(ast.parse(formula, mode='eval').body, values):.1f}"
def tagline(module: Module) -> str:
if module.unfixed:
return f"Ship no release while fejkdata carries {module.carried}, some of whose known vulnerabilities no version fixes yet."
@@ -137,7 +110,7 @@ def file_items(modules: list[Module], todo: str) -> tuple[str, list[int]]:
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} | {score(todo)} |\n"
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)
@@ -77,7 +77,7 @@ class FileItems(unittest.TestCase):
todo, filed = file(TODO, *STDLIB)
self.assertEqual(filed, [7])
self.assertIn("`Next ID = 8`", todo)
self.assertEqual(rows(todo)[0], f"| 7 | 0.2.0 | security | **{GO_TAGLINE}** | 1 | 1 | 1 | 8 | 8 | 1.1 | 29.1 |")
self.assertEqual(rows(todo)[0], f"| 7 | 0.2.0 | security | **{GO_TAGLINE}** | 1 | 1 | 1 | 8 | 8 | 1.1 | 0.0 |")
self.assertTrue(rows(todo)[1].startswith("| 5 |"))
self.assertTrue(
todo.split("## Details\n\n", 1)[1].startswith(
@@ -133,7 +133,7 @@ class FileItems(unittest.TestCase):
row = rows(todo)[0]
self.assertEqual(row.count("|"), header.count("|"))
self.assertEqual(row.split(" | ")[1], rows(plan)[0].split(" | ")[1])
self.assertRegex(row, rf"^\| {filed[0]} \| .* \| \d+\.\d \|$")
self.assertRegex(row, rf"^\| {filed[0]} \| .* \| 0\.0 \|$")
self.assertIn(f"`Next ID = {filed[0] + 1}`", todo)