ci: guard release provenance + manifest; drop rolling 'latest' tag - #7
Merged
Conversation
A separate release on a tag literally named `latest` had diverged from GitHub's own "Latest release" pointer: it accumulated assets from four build SHAs and shipped a stale, mps2-less manifest.json. So `gh release download latest` (and anything resolving the tag) silently got stale, multi-SHA, manifest-less assets, while `releases/latest/download/...` got the current build. Add validate_release.py (typed, doctested, mypy --strict, unit-tested): before publishing, assert every shipped asset carries this build's SHA, every primary image has a manifest entry, every entry's artifact and launch-command file exists, every .signed.bin belongs to a fixture, and there are no stray or duplicate artifacts. Wire it into the release job (it runs before the gh-release upload, so a bad release never publishes) and into the check-script gate (ruff + mypy --strict + pytest). The rolling `latest` tag/release is retired (deleted); the only "latest" is now GitHub's newest-release pointer plus the self-consistent per-SHA releases consumers can pin. Closes #4 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR hardens the release pipeline by adding a CI guard that validates release self-consistency (provenance-by-SHA and manifest↔assets agreement) and by removing reliance on a rolling tag literally named latest, aligning behavior with GitHub’s “Latest release” pointer.
Changes:
- Add a typed, doctested, unit-tested release validator script that enforces provenance + manifest invariants before publishing.
- Run the validator in the release job prior to uploading assets; extend the existing script quality gate (ruff/mypy/pytest) to include it.
- Update documentation to explain self-contained releases and per-SHA pinning (no rolling
latesttag).
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| README.md | Documents release self-containment, GitHub “latest” pointer usage, and per-SHA pinning. |
| .github/workflows/build.yaml | Adds validator checks in CI gate and runs validation before publishing a release. |
| .github/scripts/validate_release.py | New release validation CLI + core checks for SHA provenance and manifest↔asset consistency. |
| .github/scripts/test_validate_release.py | New unit + end-to-end tests defining validator behavior. |
| .github/scripts/pyproject.toml | Extends ruff/isort + mypy file targets to include the new validator and tests. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+269
to
+291
| def main(argv: Sequence[str] | None = None) -> int: | ||
| args = parse_args(argv) | ||
| entries = parse_entries(json.loads(args.manifest.read_text())) | ||
| asset_names = list_assets(args.assets_dir) | ||
| problems = validate(asset_names, entries, args.git_sha) | ||
| if problems: | ||
| print(f"Release validation FAILED ({len(problems)} problem(s)):", file=sys.stderr) | ||
| for problem in problems: | ||
| print(f" - {problem}", file=sys.stderr) | ||
| return 1 | ||
| print( | ||
| f"Release validation OK: {len(entries)} manifest entries, " | ||
| f"{sum(is_asset(name) for name in asset_names)} assets, all SHA {short_sha(args.git_sha)}." | ||
| ) | ||
| return 0 | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| try: | ||
| sys.exit(main()) | ||
| except ValidationError as err: | ||
| print(f"error: {err}", file=sys.stderr) | ||
| sys.exit(1) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #4.
Problem
A separate release on a tag literally named
latesthad diverged from GitHub's own "Latest release" pointer: it accumulated assets from four build SHAs (9ccdd4d2,a2497820,cbc3cd94,f5968188) and shipped a stale, mps2-lessmanifest.json. Sogh release download latestsilently got stale, multi-SHA, manifest-less assets, whilereleases/latest/download/...got the current build.Fix
.github/scripts/validate_release.py(typed, doctested,mypy --strict, unit-tested): before publishing, assert every shipped asset carries this build's SHA, every primary image has a manifest entry, every entry'sartifactand everyrun/qemu_cmd-referenced file exists, every.signed.binbelongs to a fixture, and there are no stray or duplicate artifacts..github/workflows/build.yaml: runs the guard in the release job before thegh-releaseupload (a bad release never publishes), and lints/type-checks/tests it in the check-script gate.latesttag/release (deleted). The only "latest" is now GitHub's newest-release pointer + the self-consistent per-SHA releases consumers can pin.README.md: documents release self-containment + per-SHA pinning.Verification
2ad0d6bfrelease (0 problems); REJECTS the stalelatest(54 problems — 27 foreign-SHA + orphan images/payloads).ruff format --check,mypy --strict, pytest (72, incl. doctests).