Make great documentation goal 1.2, and plan the README's reorganisation and the nested-template gaps #33
@@ -30,7 +30,7 @@ jobs:
|
|||||||
else
|
else
|
||||||
base='${{ github.event.before }}'
|
base='${{ github.event.before }}'
|
||||||
fi
|
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"
|
echo "code=false" >> "$GITHUB_OUTPUT"
|
||||||
else
|
else
|
||||||
echo "code=true" >> "$GITHUB_OUTPUT"
|
echo "code=true" >> "$GITHUB_OUTPUT"
|
||||||
@@ -82,7 +82,7 @@ jobs:
|
|||||||
run: docker build --no-cache-filter vulnerabilities --target vulnerability-report --output "$RUNNER_TEMP/vulnerabilities" .
|
run: docker build --no-cache-filter vulnerabilities --target vulnerability-report --output "$RUNNER_TEMP/vulnerabilities" .
|
||||||
- name: File each one in todo.md
|
- name: File each one in todo.md
|
||||||
id: file
|
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
|
- name: Propose the filed items
|
||||||
if: failure() && steps.file.outcome == 'failure' && github.ref == 'refs/heads/main'
|
if: failure() && steps.file.outcome == 'failure' && github.ref == 'refs/heads/main'
|
||||||
env:
|
env:
|
||||||
@@ -97,7 +97,7 @@ jobs:
|
|||||||
git commit -qam "File the known vulnerabilities govulncheck reports"
|
git commit -qam "File the known vulnerabilities govulncheck reports"
|
||||||
git push -qf "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" vulnerabilities
|
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" \
|
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.
|
# A pull request the workflow's token opens starts no workflow.
|
||||||
gh workflow run test.yml --ref vulnerabilities
|
gh workflow run test.yml --ref vulnerabilities
|
||||||
|
|
||||||
|
|||||||
+6
-1
@@ -21,9 +21,13 @@ COPY --from=vulnerabilities /report.json /
|
|||||||
|
|
||||||
FROM python:3.14.8-slim AS release-tooling
|
FROM python:3.14.8-slim AS release-tooling
|
||||||
COPY release-tooling/ /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.
|
# The filer's test files an item into the plan itself.
|
||||||
COPY todo.md /todo.md
|
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
|
FROM goreleaser/goreleaser:v2.18.3 AS packaging
|
||||||
WORKDIR /check
|
WORKDIR /check
|
||||||
@@ -33,6 +37,7 @@ RUN git init -q && git remote add origin https://github.com/larvit/fejkdata.git
|
|||||||
FROM portable
|
FROM portable
|
||||||
# BuildKit skips a stage nothing copies from.
|
# BuildKit skips a stage nothing copies from.
|
||||||
COPY --from=release-tooling /tested /tmp/release-tooling-tested
|
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
|
COPY --from=packaging /tested /tmp/packaging-tested
|
||||||
RUN unformatted="$(gofmt -l .)"; test -z "$unformatted" || \
|
RUN unformatted="$(gofmt -l .)"; test -z "$unformatted" || \
|
||||||
{ echo "unformatted files:"; echo "$unformatted"; exit 1; }
|
{ echo "unformatted files:"; echo "$unformatted"; exit 1; }
|
||||||
|
|||||||
@@ -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/ 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
|
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
|
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)
|
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
|
## Development
|
||||||
|
|||||||
+3
-3
@@ -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
|
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
|
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.
|
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
|
`vulnerability-filing/file_vulnerabilities.py` writes that item in `todo.md`'s shape with a
|
||||||
by `todo.md`'s formula, because CI cannot reach `rank_todo.py`; a test files one into the
|
Score of 0.0, since CI cannot reach `rank_todo.py`, which scores it on the next commit to the
|
||||||
repository's own `todo.md`. When the next release has no item left and waits to be cut,
|
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
|
`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.
|
||||||
|
|||||||
+10
-37
@@ -4,25 +4,24 @@
|
|||||||
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, 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 the item's ID, and the release of the plan's top row, the next
|
||||||
todo.md's `Next ID = N` gives its ID, the release of the plan's top row (the next release)
|
release, gives its release. Its Score is 0.0 until rank_todo.py, which CI cannot run, scores
|
||||||
gives its release, and its Score is todo.md's `Score = …` formula over the scores below and
|
it; a security item ranks first in its release whatever it scores. The test that files into
|
||||||
the weight todo.md's goal table gives goal 1.1. The test that files into the repository's
|
the repository's own todo.md fails the gate when the plan's shape drifts from what this
|
||||||
own todo.md fails the gate when the plan's shape drifts from what this script reads.
|
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
|
Scores, as todo.md's table spells them: R (risk), S (size) and C (difficulty) are 1, since
|
||||||
known vulnerability in the binaries works against README goal 1.1: installing fejkdata the
|
the fix is a version bump; A (value to the audience) and G (value to the goal) are 8, the goal
|
||||||
way one installs other tools, which image scanners then flag.
|
being README goal 1.1, installing fejkdata the way one installs other tools, which a flagged
|
||||||
|
binary works against.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import ast
|
|
||||||
import json
|
import json
|
||||||
import re
|
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"^\| (\d+) \| ([^|]+?) \| [^|]*\| \*\*", re.M)
|
||||||
WEIGHT = re.compile(r"^\| ([\d.]+) \| ([\d.]+) \|$", re.M)
|
|
||||||
NEXT_ID = re.compile(r"`Next ID = (\d+)`")
|
NEXT_ID = re.compile(r"`Next ID = (\d+)`")
|
||||||
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"
|
||||||
@@ -81,32 +80,6 @@ def version_key(version: str):
|
|||||||
return [int(part) for part in re.findall(r"\d+", version)]
|
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:
|
def tagline(module: Module) -> str:
|
||||||
if module.unfixed:
|
if module.unfixed:
|
||||||
return f"Ship no release while fejkdata carries {module.carried}, some of whose known vulnerabilities no version fixes yet."
|
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))
|
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} | {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[: first.start()] + row + todo[first.start() :]
|
||||||
todo = todo.replace(f"`Next ID = {next_id}`", f"`Next ID = {next_id + 1}`", 1)
|
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)
|
todo = todo.replace("## Details\n\n", f"## Details\n\n### {next_id}. {line}\n\n{detail(module)}\n", 1)
|
||||||
+2
-2
@@ -77,7 +77,7 @@ class FileItems(unittest.TestCase):
|
|||||||
todo, filed = file(TODO, *STDLIB)
|
todo, filed = file(TODO, *STDLIB)
|
||||||
self.assertEqual(filed, [7])
|
self.assertEqual(filed, [7])
|
||||||
self.assertIn("`Next ID = 8`", todo)
|
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(rows(todo)[1].startswith("| 5 |"))
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
todo.split("## Details\n\n", 1)[1].startswith(
|
todo.split("## Details\n\n", 1)[1].startswith(
|
||||||
@@ -133,7 +133,7 @@ class FileItems(unittest.TestCase):
|
|||||||
row = rows(todo)[0]
|
row = rows(todo)[0]
|
||||||
self.assertEqual(row.count("|"), header.count("|"))
|
self.assertEqual(row.count("|"), header.count("|"))
|
||||||
self.assertEqual(row.split(" | ")[1], rows(plan)[0].split(" | ")[1])
|
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)
|
self.assertIn(f"`Next ID = {filed[0] + 1}`", todo)
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user