Skip to content

perf(release-management): sibling scripts for the deterministic steps - #1512

Merged
potiuk merged 5 commits into
perf/release-verify-toolfrom
perf/release-sibling-scripts
Oct 4, 2026
Merged

potiuk merged 5 commits into
perf/release-verify-toolfrom
perf/release-sibling-scripts

Conversation

@potiuk

@potiuk potiuk commented Oct 4, 2026

Copy link
Copy Markdown
Member

Third of three stacked PRs (#1345); based on the release-verify PR below it.

Summary

  • Seven stdlib sibling scripts take over steps the model computed by hand:

    • vote-tally: binding resolution and the pass rule;
    • audit-report: the record renderer and schema check;
    • archive-sweep: retention;
    • keys-sync: the key-strength and URL check;
    • prepare: the previous tag, the Category-X scan and the next dev version.

    Classifying each vote, retention summaries, and every command the release manager runs stay with the model.

  • Rules settled by the maintainer:

    • an already-expired key blocks keys-sync (expiring within 90 days stays advisory);
    • only the configured version file counts as Python-style;
    • when someone votes more than once, only their latest vote counts. "Latest" is thread order, never the sender-written Date, and superseded votes are listed.
  • From reviews of the scripts:

    • revoked, invalid or disabled keys are refused;
    • vote identities are namespaced, so a bare handle never collides with a roster member;
    • a pre-release in the release area never decides retention;
    • every planning-issue value in the audit record is escaped, and only GitHub-login-shaped handles are accepted.
  • Tests now run: each script has a unittest suite. Nothing ran the plugins/*/skills/*/tests suites before; a new skill-script-tests pre-commit hook now runs all of them.

Test plan

  • skill-script-tests hook: all eight skill test directories pass
  • Workspace ruff, ruff format and mypy over the scripts
  • vote-tally, audit-report, archive-sweep, keys-sync and prepare evals. Everything passes except keys-sync step-0 case-3, which fails on main too.

🤖 Generated with Claude Code

@potiuk
potiuk added this pull request to stack #1513 October 4, 2026 23:06
@github-actions github-actions Bot added the substrate:framework-dev Tool substrate: build / validate / eval the framework itself label Oct 4, 2026
potiuk added 5 commits October 5, 2026 01:34
Seven stdlib scripts take over steps the model computed by hand:
vote-tally's binding resolution and pass rule (reply text never
reaches the count), audit-report's record renderer and schema check,
archive-sweep's retention, keys-sync's key-strength and URL check,
and prepare's previous tag, Category-X scan and next dev version.
Classifying each vote, retention summaries, and every command the
release manager runs stay with the model.

Rules settled by the maintainer: an already-expired key blocks
keys-sync (expiring within 90 days stays advisory; secp256k1 refused);
only the configured version file counts as Python-style for the next
dev version.

The affected eval cases embed the scripts' real output. The
audit-report injection case now carries the injection source, so the
judge, which sees only the output, can verify it.

Tests: every script has a stdlib unittest suite under the skill's
tests/ directory. The vote-counting and key-check suites cover the
pass-rule boundaries, the veto override, GitHub handles never being
binding, reply text never reaching the output, the strength floor and
accepted curves, expired keys, fingerprint normalisation, and each
script's command-line errors. Nothing ran the plugins/*/skills/*/tests
suites before; a new skill-script-tests pre-commit hook
(tools/dev/run-skill-script-tests.sh) now runs all of them.

Generated-by: Claude Opus 5
…he scripts

- keys-sync check_key.py read only strength and expiry, so a revoked,
  invalid or disabled key passed. gpg's validity field now blocks it.
- vote-tally tally.py counted every vote, so one member voting twice,
  or writing from two addresses, was counted twice. Votes are grouped
  by person (roster Apache ID, else address); any repeat halts the
  tally, even under --force-close, until the RM says which vote
  stands.
- archive-sweep retention.py let a pre-release in the release area
  count as a train's latest, which kept the RC and proposed archiving
  the real latest release. Pre-releases are now listed for the RM and
  never decide retention.
- audit-report render_record.py rendered planning-issue values into
  the record unescaped, so a line break or `|` could break the tables
  or add sections, and a link field could carry any target. Values are
  confined to their cell or code span, and link fields accept only a
  plain https:// URL.

Each fix has tests; the four skills describe the new output.

Generated-by: Claude Opus 5
A follow-up security review found the earlier sanitisation incomplete:
it only flattened line breaks and escaped `|`, so planning-issue text
could still render as links, images (a tracking pixel), HTML tags,
headings or @-mentions in the audit record.

Every free-text value (product name, version, redaction reasons,
injection sources, the planning-issue reference) now has each markdown
and HTML character backslash-escaped. binding_voters accepts only
GitHub-login-shaped handles, so a team mention such as
@apache/committers is refused.

Generated-by: Claude Opus 5
The maintainer decided that a changed vote should settle itself:
when one person votes more than once, or a member writes from two
addresses, only their latest vote counts (newest date, else the
later one in the thread). The earlier votes are listed in
superseded_votes for the tally to name. A clear later vote replaces
an earlier ambiguous one; an ambiguous latest vote still halts.

This replaces the earlier halt on any repeated vote.

Also lints the sibling scripts to the workspace rules: the plugins
workspace member runs ruff, ruff format and mypy over the skills'
scripts and tests too.

Generated-by: Claude Opus 5
…keep identities apart

From a security review of the latest-vote rule:

- "Latest" was the newest Date, but the sender writes their own Date
  header, so a message with a forged future date always won. The
  latest vote is now the later one in thread order, the order the list
  archive received the votes. A vote whose date runs backwards against
  that order is reported in date_order_mismatches for the RM.
- A non-roster sender written as a bare "alice" shared an identity
  with the roster member whose Apache ID is "alice", so a non-binding
  vote could replace a binding one. Identities are namespaced
  (roster:<id>, address:<addr>).

Generated-by: Claude Opus 5
@potiuk
potiuk force-pushed the perf/release-sibling-scripts branch from 2aabf5e to a869eaa Compare October 4, 2026 23:38
@potiuk
potiuk merged commit 3ee2b93 into main Oct 4, 2026
19 checks passed
@potiuk
potiuk deleted the perf/release-sibling-scripts branch October 4, 2026 23:44
@potiuk potiuk added capability:resolve Close-out: invalidate, dedupe, CVE allocate, announcement capability:stats Read-only dashboards, metrics, governance evidence capability:triage Sweep + classify + propose disposition family:ci .github workflows, prek, validators family:docs Docs, MISSION.md, READMEs family:release-management release-* skills family:tools tools/* labels Oct 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

capability:resolve Close-out: invalidate, dedupe, CVE allocate, announcement capability:stats Read-only dashboards, metrics, governance evidence capability:triage Sweep + classify + propose disposition family:ci .github workflows, prek, validators family:docs Docs, MISSION.md, READMEs family:release-management release-* skills family:tools tools/* substrate:framework-dev Tool substrate: build / validate / eval the framework itself

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant