Skip to content

Commit e454f13

Browse files
committed
fix(release): verify publication evidence reliably
1 parent 7f45fd8 commit e454f13

8 files changed

Lines changed: 71 additions & 12 deletions

File tree

.github/workflows/release.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,10 @@ jobs:
167167
with:
168168
name: dist
169169
path: dist/
170+
- uses: actions/download-artifact@v8
171+
with:
172+
name: release-evidence
173+
path: release-evidence/
170174
- name: Verify GitHub release
171175
id: github-release
172176
env:

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ consolidated into the next published release.
1010

1111
## [Unreleased]
1212

13+
## [0.23.3] - 2026-07-18
14+
15+
### Fixed
16+
- Post-publication verification now downloads the immutable release seal before
17+
creating its receipt and retries bounded PyPI propagation failures.
18+
1319
## [0.23.2] - 2026-07-18
1420

1521
### Added
@@ -1010,7 +1016,8 @@ See git history for per-commit details on intermediate versions.
10101016

10111017
---
10121018

1013-
[Unreleased]: https://github.com/layer1labs/specsmith/compare/v0.23.2...HEAD
1019+
[Unreleased]: https://github.com/layer1labs/specsmith/compare/v0.23.3...HEAD
1020+
[0.23.3]: https://github.com/layer1labs/specsmith/compare/v0.23.2...v0.23.3
10141021
[0.23.2]: https://github.com/layer1labs/specsmith/compare/v0.22.5...v0.23.2
10151022
[0.22.5]: https://github.com/layer1labs/specsmith/compare/v0.22.4...v0.22.5
10161023
[0.22.4]: https://github.com/layer1labs/specsmith/compare/v0.22.3...v0.22.4

docs/SPECSMITH.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ description: |
99
through governance, traceability, and automated compliance checking.
1010
1111
type: python
12-
version: 0.23.2
12+
version: 0.23.3
1313
# Governance schema/tool version that last generated or migrated this project.
1414
# This is intentionally distinct from the package/project release version above.
15-
spec_version: 0.23.2
15+
spec_version: 0.23.3
1616
license: MIT
1717
author: Layer1 Labs
1818
url: https://github.com/layer1labs/specsmith

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "specsmith"
7-
version = "0.23.2"
7+
version = "0.23.3"
88
description = "AEE governance toolkit for AI-assisted development — session preflight gates, multi-agent dispatch, requirements↔test traceability, ESDB persistence, MCP server, and skills for Warp, Cursor, Claude Code, Copilot, Windsurf, Aider, and Zoo Code."
99
readme = "README.md"
1010
license = "MIT"

scripts/verify_publication.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import argparse
66
import hashlib
77
import json
8+
import time
9+
import urllib.error
810
import urllib.request
911
from pathlib import Path
1012
from typing import Any
@@ -41,6 +43,28 @@ def verify_pypi_files(dist_dir: Path, payload: dict[str, Any]) -> list[dict[str,
4143
return verified
4244

4345

46+
def fetch_pypi_payload(
47+
version: str, *, attempts: int = 6, delay_seconds: float = 10
48+
) -> dict[str, Any]:
49+
"""Fetch a release after allowing bounded time for PyPI propagation."""
50+
if attempts < 1:
51+
raise ValueError("attempts must be at least 1")
52+
url = f"https://pypi.org/pypi/specsmith/{version}/json"
53+
for attempt in range(1, attempts + 1):
54+
try:
55+
with urllib.request.urlopen(url, timeout=30) as response:
56+
return json.load(response)
57+
except urllib.error.HTTPError as error:
58+
retryable = error.code == 404 or 500 <= error.code < 600
59+
if not retryable or attempt == attempts:
60+
raise
61+
except urllib.error.URLError:
62+
if attempt == attempts:
63+
raise
64+
time.sleep(delay_seconds)
65+
raise AssertionError("unreachable")
66+
67+
4468
def main() -> int:
4569
parser = argparse.ArgumentParser(description=__doc__)
4670
parser.add_argument("--seal", type=Path, required=True)
@@ -52,15 +76,12 @@ def main() -> int:
5276
parser.add_argument("--output", type=Path, required=True)
5377
args = parser.parse_args()
5478

79+
seal = json.loads(args.seal.read_text(encoding="utf-8"))
5580
version = args.version.removeprefix("v")
56-
with urllib.request.urlopen(
57-
f"https://pypi.org/pypi/specsmith/{version}/json", timeout=30
58-
) as response:
59-
payload = json.load(response)
81+
payload = fetch_pypi_payload(version)
6082
if payload.get("info", {}).get("version") != version:
6183
raise ValueError("PyPI version response does not match the release")
6284

63-
seal = json.loads(args.seal.read_text(encoding="utf-8"))
6485
publication = {
6586
"version": version,
6687
"tag": args.tag,

src/specsmith/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
try:
99
__version__: str = _pkg_version("specsmith")
1010
except PackageNotFoundError: # running from source without install
11-
__version__ = "0.23.2" # fallback: keep in sync with pyproject.toml
11+
__version__ = "0.23.3" # fallback: keep in sync with pyproject.toml
1212

1313
# Governance/schema version — independent from the package version.
1414
# Bump this when the scaffold config schema or governance rules change.
15-
GOVERNANCE_VERSION: str = "0.23.2"
15+
GOVERNANCE_VERSION: str = "0.23.3"

tests/test_release_evidence.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
import json
12
from copy import deepcopy
3+
from io import BytesIO
24
from pathlib import Path
5+
from urllib.error import HTTPError
36

47
import pytest
58

69
from scripts.release_evidence import create_receipt, create_seal, digest
7-
from scripts.verify_publication import verify_pypi_files
10+
from scripts.verify_publication import fetch_pypi_payload, verify_pypi_files
811

912

1013
def test_receipt_links_immutable_seal() -> None:
@@ -38,3 +41,25 @@ def test_pypi_verification_requires_matching_artifact_digest(tmp_path: Path) ->
3841
}
3942
with pytest.raises(ValueError, match="digest mismatch"):
4043
verify_pypi_files(tmp_path, payload)
44+
45+
46+
def test_pypi_fetch_retries_release_propagation(monkeypatch: pytest.MonkeyPatch) -> None:
47+
responses: list[Exception | BytesIO] = [
48+
HTTPError("https://example.invalid", 404, "Not Found", {}, None),
49+
BytesIO(json.dumps({"info": {"version": "1.2.3"}}).encode()),
50+
]
51+
52+
def fake_urlopen(url: str, timeout: int) -> BytesIO:
53+
response = responses.pop(0)
54+
if isinstance(response, Exception):
55+
raise response
56+
return response
57+
58+
sleeps: list[float] = []
59+
monkeypatch.setattr("scripts.verify_publication.urllib.request.urlopen", fake_urlopen)
60+
monkeypatch.setattr("scripts.verify_publication.time.sleep", sleeps.append)
61+
62+
payload = fetch_pypi_payload("1.2.3", attempts=2, delay_seconds=0)
63+
64+
assert payload["info"]["version"] == "1.2.3"
65+
assert sleeps == [0]

tests/test_release_workflows.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ def test_tag_workflow_is_non_mutating_and_rejects_duplicates() -> None:
4242
assert "publication-receipt.json" in text
4343
assert "name: release-evidence" in text
4444
assert "--seal release-evidence/release-seal.json" in text
45+
verify_block = text.split("verify-publication:", 1)[1].split("cleanup-dev-releases:", 1)[0]
46+
assert "name: release-evidence" in verify_block
4547

4648

4749
def test_canonical_runbook_has_fixed_point_and_immutable_recovery() -> None:

0 commit comments

Comments
 (0)