Skip to content

Commit ddb945e

Browse files
committed
fix: require complete release asset inventory
1 parent b562443 commit ddb945e

3 files changed

Lines changed: 81 additions & 5 deletions

File tree

.github/workflows/release.yml

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,17 @@ jobs:
2424
- run: python scripts/security_scan.py .
2525
- run: python -m build
2626
- run: python scripts/generate_supply_chain_docs.py
27+
- name: Stage flat source assets
28+
run: |
29+
mkdir -p build/release-assets
30+
cp dist/multi_api_test_executor-*.whl build/release-assets/
31+
cp dist/multi_api_test_executor-*.tar.gz build/release-assets/
32+
cp build/release/sbom.cdx.json build/release-assets/
33+
cp build/release/THIRD_PARTY_LICENSES.txt build/release-assets/
2734
- uses: actions/upload-artifact@v4
2835
with:
2936
name: release-source-assets
30-
path: |
31-
dist/multi_api_test_executor-*.whl
32-
dist/multi_api_test_executor-*.tar.gz
33-
build/release/sbom.cdx.json
34-
build/release/THIRD_PARTY_LICENSES.txt
37+
path: build/release-assets/*
3538

3639
native-bundles:
3740
strategy:
@@ -73,6 +76,7 @@ jobs:
7376
- uses: actions/checkout@v4
7477
- uses: actions/download-artifact@v4
7578
with: {path: artifacts, merge-multiple: true}
79+
- run: python scripts/validate_release_assets.py artifacts --tag ${{ github.ref_name }}
7680
- name: Write aggregate checksums
7781
run: |
7882
python - <<'PY'

scripts/validate_release_assets.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
from __future__ import annotations
2+
3+
import argparse
4+
import json
5+
import re
6+
from pathlib import Path
7+
8+
VERSION = re.compile(r"^v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.]+)?)$")
9+
10+
11+
def expected_asset_names(tag: str) -> set[str]:
12+
match = VERSION.fullmatch(tag)
13+
if not match:
14+
raise ValueError(f"unsupported release tag: {tag}")
15+
version = match.group(1)
16+
package_version = version.split("-", 1)[0]
17+
return {
18+
f"multi-api-test-executor-{version}-windows-x64.zip",
19+
f"multi-api-test-executor-{version}-linux-x64.tar.gz",
20+
f"multi-api-test-executor-{version}-macos-x64.tar.gz",
21+
f"multi_api_test_executor-{package_version}-py3-none-any.whl",
22+
f"multi_api_test_executor-{package_version}.tar.gz",
23+
"sbom.cdx.json",
24+
"THIRD_PARTY_LICENSES.txt",
25+
}
26+
27+
28+
def validate_release_assets(directory: Path, tag: str) -> set[str]:
29+
expected = expected_asset_names(tag)
30+
actual = {path.name for path in directory.iterdir() if path.is_file()}
31+
missing = sorted(expected - actual)
32+
unexpected = sorted(actual - expected)
33+
empty = sorted(name for name in expected & actual if not (directory / name).stat().st_size)
34+
if missing or unexpected or empty:
35+
raise ValueError(
36+
f"release inventory mismatch: missing={missing}, unexpected={unexpected}, empty={empty}"
37+
)
38+
return actual
39+
40+
41+
def main() -> None:
42+
parser = argparse.ArgumentParser()
43+
parser.add_argument("directory", type=Path)
44+
parser.add_argument("--tag", required=True)
45+
args = parser.parse_args()
46+
assets = validate_release_assets(args.directory, args.tag)
47+
print(json.dumps({"tag": args.tag, "assets": sorted(assets)}))
48+
49+
50+
if __name__ == "__main__":
51+
main()

tests/unit/test_release_packaging.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,5 +47,26 @@ def test_release_workflow_publishes_native_assets_and_supply_chain_files() -> No
4747
"SHA256SUMS.txt",
4848
"docs/release-notes-v0.1.0.md",
4949
"prerelease: ${{ contains(github.ref_name, '-') }}",
50+
"python scripts/validate_release_assets.py",
51+
"path: build/release-assets/*",
5052
]:
5153
assert phrase in workflow
54+
55+
56+
def test_release_asset_validator_requires_complete_exact_inventory(tmp_path: Path) -> None:
57+
script = ROOT / "scripts/validate_release_assets.py"
58+
namespace: dict[str, object] = {"__name__": "release_validator_test", "__file__": str(script)}
59+
exec(compile(script.read_text(encoding="utf-8"), str(script), "exec"), namespace)
60+
expected = namespace["expected_asset_names"]("v0.1.0-rc.2")
61+
for name in expected:
62+
(tmp_path / name).write_bytes(b"release-asset")
63+
64+
assert namespace["validate_release_assets"](tmp_path, "v0.1.0-rc.2") == expected
65+
66+
(tmp_path / "sbom.cdx.json").unlink()
67+
try:
68+
namespace["validate_release_assets"](tmp_path, "v0.1.0-rc.2")
69+
except ValueError as exc:
70+
assert "missing" in str(exc)
71+
else:
72+
raise AssertionError("incomplete release inventory was accepted")

0 commit comments

Comments
 (0)