Skip to content

Commit 67da15e

Browse files
committed
Verify release archive contents
1 parent 567ae27 commit 67da15e

9 files changed

Lines changed: 139 additions & 5 deletions

File tree

.github/workflows/release.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,21 @@ jobs:
9191
- name: Archive release example artifacts
9292
run: tar -czf dist/a7-example-artifacts-release.tar.gz -C build release
9393

94+
- name: Verify release archive contents
95+
run: |
96+
uv run python scripts/verify_archive_contents.py dist/a7-docs-site.tar.gz \
97+
--require dist/llms.txt \
98+
--require dist/llms-full.txt \
99+
--require dist/docs/index.md \
100+
--require dist/docs/agent-usage.md \
101+
--require dist/docs/release.md \
102+
--require dist/docs/status.md
103+
uv run python scripts/verify_archive_contents.py dist/a7-example-artifacts-release.tar.gz \
104+
--require release/zig/src/001_hello.zig \
105+
--require release/zig/bin/001_hello \
106+
--require release/c/src/001_hello.c \
107+
--require release/c/bin/001_hello
108+
94109
- name: Generate release checksums
95110
run: |
96111
sdist="$(ls dist/a7_py-*.tar.gz)"

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4545
recorded artifact hashes and sizes before upload.
4646
- Expanded curl.md-friendly docs with a full-context aggregate, route aliases,
4747
first-class page metadata, and visible Markdown links from the docs app.
48+
- Added release archive content verification so docs/native artifact tarballs
49+
must contain required curl.md entry points and example outputs before upload.
4850

4951
## [0.3.0] - 2026-05-07
5052

COMPLETION_AUDIT.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ Deliverables implied by the active objective:
3434
| Release artifacts | Hosted CI run `25526234447`; `scripts/build_examples.py --profile release --backend both --clean` | Passing |
3535
| Python package build | Hosted CI run `25526234447`; local clean `rm -rf dist && uv build` | Passing |
3636
| Local package hygiene | `README.md`, `RELEASE.md`, `site/public/docs/release.md` now require `rm -rf dist` before `uv build` | Covered |
37-
| Release checksums | `scripts/generate_release_manifest.py`; `scripts/verify_release_manifest.py`; `test/test_release_tooling.py`; release workflow validates required paths and re-checks hashes before upload | Covered |
37+
| Release checksums and archive contents | `scripts/generate_release_manifest.py`; `scripts/verify_release_manifest.py`; `scripts/verify_archive_contents.py`; `test/test_release_tooling.py`; release workflow validates required paths, required archive members, and re-checks hashes before upload | Covered |
3838
| Docs style/build | Hosted CI run `25526234447`; local `scripts/check_docs_style.py`; local `site npm run check` | Passing |
3939
| Docs deploy | Hosted Deploy Docs run `25526234454` | Passing |
4040
| curl.md/agent documentation | `site/public/llms.txt`, `site/public/llms-full.txt`, `site/public/docs/*.md`, sitemap and robots entries | Implemented |

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ Generate a checksum manifest for release artifacts:
118118
```bash
119119
uv run python scripts/generate_release_manifest.py dist --output dist/SHA256SUMS
120120
uv run python scripts/verify_release_manifest.py dist/SHA256SUMS
121+
uv run python scripts/verify_archive_contents.py dist/a7-docs-site.tar.gz --require dist/llms.txt --require dist/llms-full.txt
121122
```
122123

123124
The installed CLI entrypoint is `a7`:

RELEASE.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,12 +90,13 @@ artifact archives, run:
9090
```bash
9191
uv run python scripts/generate_release_manifest.py dist --output dist/SHA256SUMS
9292
uv run python scripts/verify_release_manifest.py dist/SHA256SUMS
93+
uv run python scripts/verify_archive_contents.py dist/a7-docs-site.tar.gz --require dist/llms.txt --require dist/llms-full.txt
9394
```
9495

9596
The tag release workflow generates `dist/SHA256SUMS` after all release archives
9697
are built, verifies that the manifest contains the package, docs, and native
97-
artifact archives, re-checks the hashes and sizes on disk, then attaches it to
98-
the draft GitHub release.
98+
artifact archives, verifies required archive members, re-checks the hashes and
99+
sizes on disk, then attaches it to the draft GitHub release.
99100

100101
## Tagging
101102

RELEASE_READINESS_REVIEW.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,9 @@ not factually provable from local tests alone.
145145
draft GitHub release artifacts and verifies that expected package, docs, and
146146
native artifact archive paths are present before upload, then re-checks the
147147
recorded hashes and sizes on disk.
148+
- Release tooling now verifies required members inside the docs and native
149+
example archives before checksum generation, including `llms.txt`,
150+
`llms-full.txt`, public Markdown docs, and `001_hello` Zig/C outputs.
148151

149152
## Residual Risks
150153

scripts/verify_archive_contents.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#!/usr/bin/env python3
2+
"""Verify that a release archive contains required member paths."""
3+
4+
from __future__ import annotations
5+
6+
import argparse
7+
import sys
8+
import tarfile
9+
from pathlib import Path
10+
11+
12+
def normalize_member(path: str) -> str:
13+
return path.strip().lstrip("./").rstrip("/")
14+
15+
16+
def archive_members(path: Path) -> set[str]:
17+
with tarfile.open(path, "r:*") as archive:
18+
return {normalize_member(member.name) for member in archive.getmembers()}
19+
20+
21+
def main() -> int:
22+
parser = argparse.ArgumentParser(description="Verify required files inside a tar archive")
23+
parser.add_argument("archive", help="Archive to inspect")
24+
parser.add_argument(
25+
"--require",
26+
action="append",
27+
default=[],
28+
help="Required archive member path. May be supplied multiple times.",
29+
)
30+
args = parser.parse_args()
31+
32+
archive_path = Path(args.archive)
33+
if not archive_path.is_file():
34+
print(f"archive does not exist: {archive_path}", file=sys.stderr)
35+
return 2
36+
if not args.require:
37+
print("at least one --require path is needed", file=sys.stderr)
38+
return 2
39+
40+
try:
41+
members = archive_members(archive_path)
42+
except (tarfile.TarError, OSError) as exc:
43+
print(f"could not read archive {archive_path}: {exc}", file=sys.stderr)
44+
return 2
45+
46+
required = [normalize_member(item) for item in args.require]
47+
missing = [item for item in required if item not in members]
48+
if missing:
49+
print(f"archive content verification failed for {archive_path}:", file=sys.stderr)
50+
for item in missing:
51+
print(f"- missing: {item}", file=sys.stderr)
52+
return 1
53+
54+
print(f"archive content verified: {archive_path} ({len(required)} required paths)")
55+
return 0
56+
57+
58+
if __name__ == "__main__":
59+
raise SystemExit(main())

site/public/docs/release.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,12 @@ Generate checksums before uploading local artifacts:
2222
```bash
2323
uv run python scripts/generate_release_manifest.py dist --output dist/SHA256SUMS
2424
uv run python scripts/verify_release_manifest.py dist/SHA256SUMS
25+
uv run python scripts/verify_archive_contents.py dist/a7-docs-site.tar.gz --require dist/llms.txt --require dist/llms-full.txt
2526
```
2627

2728
The tag workflow also verifies that `SHA256SUMS` contains the expected package,
28-
docs, and native artifact archives before upload, then re-checks the hashes and
29-
sizes on disk.
29+
docs, and native artifact archives before upload. It also checks required
30+
archive members, then re-checks the hashes and sizes on disk.
3031

3132
## Debug and Release Artifacts
3233

test/test_release_tooling.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import shutil
66
import subprocess
77
import sys
8+
import tarfile
89
from hashlib import sha256
910
from pathlib import Path
1011

@@ -221,3 +222,54 @@ def test_verify_release_manifest_detects_tampering(tmp_path: Path) -> None:
221222

222223
assert tampered.returncode == 1
223224
assert "release manifest verification failed" in tampered.stderr
225+
226+
227+
def test_verify_archive_contents_requires_expected_members(tmp_path: Path) -> None:
228+
archive_path = tmp_path / "docs.tar.gz"
229+
docs_dir = tmp_path / "dist"
230+
docs_dir.mkdir()
231+
(docs_dir / "llms.txt").write_text("index\n", encoding="utf-8")
232+
nested = docs_dir / "docs"
233+
nested.mkdir()
234+
(nested / "index.md").write_text("# Docs\n", encoding="utf-8")
235+
236+
with tarfile.open(archive_path, "w:gz") as archive:
237+
archive.add(docs_dir / "llms.txt", arcname="dist/llms.txt")
238+
archive.add(nested / "index.md", arcname="dist/docs/index.md")
239+
240+
ok_result = subprocess.run(
241+
[
242+
sys.executable,
243+
"scripts/verify_archive_contents.py",
244+
str(archive_path),
245+
"--require",
246+
"dist/llms.txt",
247+
"--require",
248+
"./dist/docs/index.md",
249+
],
250+
cwd=ROOT,
251+
text=True,
252+
capture_output=True,
253+
timeout=10,
254+
)
255+
256+
assert ok_result.returncode == 0, ok_result.stderr or ok_result.stdout
257+
assert "archive content verified" in ok_result.stdout
258+
259+
missing_result = subprocess.run(
260+
[
261+
sys.executable,
262+
"scripts/verify_archive_contents.py",
263+
str(archive_path),
264+
"--require",
265+
"dist/llms-full.txt",
266+
],
267+
cwd=ROOT,
268+
text=True,
269+
capture_output=True,
270+
timeout=10,
271+
)
272+
273+
assert missing_result.returncode == 1
274+
assert "archive content verification failed" in missing_result.stderr
275+
assert "dist/llms-full.txt" in missing_result.stderr

0 commit comments

Comments
 (0)