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:
@@ -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
|
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
|
supported Go, and must pass before it can be merged — unless it changes none of the
|
||||||
the files the build and its tests read, nor the workflow itself, in which case
|
files the build and its tests read, nor the workflow itself, in which case it's skipped.
|
||||||
it's skipped. That build runs vet, complexity, the format check, the Go tests and the
|
That build runs vet, complexity, the format check, the Go tests, the release tooling's
|
||||||
release tooling's tests; the rest of the gate is the changelog check, which CI runs
|
tests and the vulnerability filer's tests, which file an item into `todo.md` itself, so
|
||||||
against the PR base, and the package-repository checks below. Run `docker build .`
|
a change to the plan's shape can fail it; the rest of the gate is the changelog check,
|
||||||
locally before pushing. The lowest Go builds the `portable` stage, which skips the format
|
which CI runs against the PR base, and the package-repository checks below. Run `docker
|
||||||
check and the release tooling's tests:
|
build .` locally before pushing. The lowest Go builds the `portable` stage, which skips
|
||||||
|
the format check and both Python test suites:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
docker build . # latest
|
docker build . # latest
|
||||||
|
|||||||
+3
-1
@@ -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
|
`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
|
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.
|
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.
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
"""File each module's known vulnerabilities as one security item atop todo.md's next release.
|
"""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>
|
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
|
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
|
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
|
import sys
|
||||||
from dataclasses import dataclass, field
|
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+)`")
|
NEXT_ID = re.compile(r"`Next ID = (\d+)`")
|
||||||
|
DETAILS = "## Details\n\n"
|
||||||
SCORES = {"R": 1, "S": 1, "C": 1, "A": 8, "G": 8}
|
SCORES = {"R": 1, "S": 1, "C": 1, "A": 8, "G": 8}
|
||||||
GOAL = "1.1"
|
GOAL = "1.1"
|
||||||
|
|
||||||
@@ -100,20 +102,30 @@ def detail(module: Module) -> str:
|
|||||||
|
|
||||||
def file_items(modules: list[Module], todo: str) -> tuple[str, list[int]]:
|
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."""
|
"""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 = []
|
filed = []
|
||||||
for module in modules:
|
for module in modules:
|
||||||
if all(osv in todo for osv in module.osvs):
|
if all(osv in todo for osv in module.osvs):
|
||||||
continue
|
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))
|
next_id = int(NEXT_ID.search(todo).group(1))
|
||||||
line = tagline(module)
|
line = tagline(module)
|
||||||
scores = " | ".join(str(SCORES[k]) for k in "RSCAG")
|
scores = " | ".join(str(SCORES[k]) for k in "RSCAG")
|
||||||
row = f"| {next_id} | {first.group(2)} | security | **{line}** | {scores} | {GOAL} | 0.0 |\n"
|
row = f"| {next_id} | {release} | security | **{line}** | {scores} | {GOAL} | 0.0 |\n"
|
||||||
todo = todo[: first.start()] + row + todo[first.start() :]
|
text = f"### {next_id}. {line}\n\n{detail(module)}\n"
|
||||||
todo = todo.replace(f"`Next ID = {next_id}`", f"`Next ID = {next_id + 1}`", 1)
|
todo = todo[:rows_at] + row + todo[rows_at:]
|
||||||
todo = todo.replace("## Details\n\n", f"## Details\n\n### {next_id}. {line}\n\n{detail(module)}\n", 1)
|
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)
|
filed.append(next_id)
|
||||||
return todo, filed
|
return todo, filed
|
||||||
|
|
||||||
@@ -128,7 +140,11 @@ def main(argv: list[str]) -> int:
|
|||||||
count = sum(len(m.osvs) for m in modules)
|
count = sum(len(m.osvs) for m in modules)
|
||||||
known = f"{count} known {'vulnerability' if count == 1 else 'vulnerabilities'}"
|
known = f"{count} known {'vulnerability' if count == 1 else 'vulnerabilities'}"
|
||||||
with open(todo_path, encoding="utf-8") as f:
|
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:
|
if not filed:
|
||||||
print(f"{known}, already planned in todo.md.")
|
print(f"{known}, already planned in todo.md.")
|
||||||
return 1
|
return 1
|
||||||
|
|||||||
Reference in New Issue
Block a user