Skip to content

Commit 41e4360

Browse files
committed
feat(tools): add verify_release.py to validate release assets + SHA256SUMS
Both SDKs download platform bundles from a GitHub Release and verify them against SHA256SUMS, but nothing checked that a published release is internally consistent — a missing asset or a checksum mismatch only surfaced when a user's pip/npm install failed. Add tools/verify_release.py <tag> (stdlib only): - Fetches the release via the GitHub API (GITHUB_TOKEN raises the rate limit; a missing tag exits non-zero with a clear message). - Asserts every required platform asset is present and named exactly as the SDK tables expect. The expected names are imported from the Python SDK's _ASSETS, and the Node ASSETS table is parsed and checked to agree, so the two installers can't drift. linux-x64 is required today; win-x64 and mac-* are verified only once published. - Parses SHA256SUMS and verifies each listed hash against the asset: by default against the asset's API digest plus a size/state sanity check, and with --full by downloading and re-hashing each asset. Flags an asset with no checksum entry and a SHA256SUMS line naming no real asset. - Exits non-zero with a per-check report on any problem. Add tools/tests/test_verify_release.py: evaluate() is pure (release dict + SHA256SUMS body in, Report out), so every failure mode — missing required asset, missing SHA256SUMS, wrong hash, orphan checksum line, failed sanity, --full re-hash mismatch, mismatched SDK tables — is covered offline. Picked up by the existing pytest tools/tests CI step. Add a release-triggered workflow (.github/workflows/verify-release.yml, also workflow_dispatch) so a broken release fails loudly, and document the tool in CONTRIBUTING.md. Verified against the current v151.0.7908.0 (passes; correctly skips the unpublished mac assets) and on a nonexistent tag (exits non-zero). Closes #14
1 parent c33bb1e commit 41e4360

4 files changed

Lines changed: 420 additions & 0 deletions

File tree

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: verify-release
2+
3+
# When a release is published, confirm it is internally consistent — every expected bundle
4+
# asset is present and SHA256SUMS matches — so a broken release fails loudly here instead of
5+
# in a user's pip/npm install. Also runnable on demand against any existing tag.
6+
on:
7+
release:
8+
types: [published]
9+
workflow_dispatch:
10+
inputs:
11+
tag:
12+
description: release tag to verify (e.g. v151.0.7908.0)
13+
required: true
14+
15+
permissions:
16+
contents: read
17+
18+
jobs:
19+
verify:
20+
name: verify release assets
21+
runs-on: ubuntu-latest
22+
steps:
23+
- uses: actions/checkout@v4
24+
- uses: actions/setup-python@v5
25+
with:
26+
python-version: "3.12"
27+
- name: Verify release assets + SHA256SUMS
28+
run: python tools/verify_release.py "${{ github.event.release.tag_name || inputs.tag }}"
29+
env:
30+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

CONTRIBUTING.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ a runtime library.
3333
`build/apply-patches.sh`, so always add your patch to `series`.
3434
- **`build/apply-patches.sh`** applies the series onto a Chromium `src/`.
3535
- **`tools/gauntlet.py`** — the live detection harness (CreepJS / Sannysoft / BrowserScan).
36+
- **`tools/verify_release.py <tag>`** — checks a published release is internally consistent:
37+
every expected bundle asset is present and `SHA256SUMS` matches the SDK tables. Runs
38+
automatically when a release is published; run it locally with `--full` to re-hash assets.
3639

3740
Full build instructions: [docs/BUILD_NATIVE.md](docs/BUILD_NATIVE.md). Expect a multi-hour first
3841
compile; incremental rebuilds after a one-line patch are minutes.

tools/tests/test_verify_release.py

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
"""
2+
Tests for tools/verify_release.py — the release consistency checker.
3+
4+
evaluate() is pure: it takes an already-fetched release dict + SHA256SUMS body and returns
5+
a Report, so every failure mode (missing asset, wrong hash, orphan checksum line, mismatched
6+
SDK tables) can be driven offline without touching the GitHub API.
7+
8+
Run: pytest tools/tests -q
9+
"""
10+
from __future__ import annotations
11+
import sys
12+
from pathlib import Path
13+
14+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
15+
import verify_release as vr # noqa: E402
16+
17+
# The SDK table the checker validates against (linux is required; win/mac optional-if-present).
18+
SDK_ASSETS = {
19+
"linux-x64": "tilion-fortress-linux-x64.tar.gz",
20+
"win-x64": "tilion-fortress-win-x64.zip",
21+
"mac-arm64": "tilion-fortress-mac-arm64.tar.gz",
22+
"mac-x64": "tilion-fortress-mac-x64.tar.gz",
23+
}
24+
NODE_NAMES = set(SDK_ASSETS.values())
25+
26+
LINUX = SDK_ASSETS["linux-x64"]
27+
WIN = SDK_ASSETS["win-x64"]
28+
H_LINUX = "f4e0e83a38b08ec62ec07cb7f0c54d8eae5e7260798a91e7d703e547de53207c"
29+
H_WIN = "a538de3341d9e7bf1c87f81b0c6e91ec9c2bde3f80872a8f626dd074d1161a45"
30+
31+
32+
def asset(name, digest=None, size=100, state="uploaded"):
33+
a = {"name": name, "size": size, "state": state, "url": f"https://api/{name}"}
34+
if digest:
35+
a["digest"] = f"sha256:{digest}"
36+
return a
37+
38+
39+
def release(*assets):
40+
return {"assets": list(assets)}
41+
42+
43+
def sums(*pairs):
44+
return "".join(f"{h} {n}\n" for h, n in pairs)
45+
46+
47+
def evaluate(rel, sums_text, **kw):
48+
kw.setdefault("node_asset_names", NODE_NAMES)
49+
return vr.evaluate(rel, sums_text, SDK_ASSETS, **kw)
50+
51+
52+
# --------------------------------------------------------------------------- the happy path
53+
def test_consistent_release_passes():
54+
# linux + win present and checksummed, SHA256SUMS present, mac absent (optional).
55+
rel = release(
56+
asset("SHA256SUMS"),
57+
asset(LINUX, digest=H_LINUX),
58+
asset(WIN, digest=H_WIN),
59+
)
60+
rep = evaluate(rel, sums((H_LINUX, LINUX), (H_WIN, WIN)))
61+
assert rep.failures == []
62+
63+
64+
def test_linux_only_release_passes():
65+
rel = release(asset("SHA256SUMS"), asset(LINUX, digest=H_LINUX))
66+
rep = evaluate(rel, sums((H_LINUX, LINUX)))
67+
assert rep.failures == []
68+
69+
70+
# --------------------------------------------------------------------------- missing assets
71+
def test_missing_required_linux_asset_fails():
72+
rel = release(asset("SHA256SUMS"), asset(WIN, digest=H_WIN))
73+
rep = evaluate(rel, sums((H_WIN, WIN)))
74+
assert f"asset present: {LINUX}" in rep.failures
75+
76+
77+
def test_missing_sha256sums_fails():
78+
rel = release(asset(LINUX, digest=H_LINUX))
79+
rep = evaluate(rel, sums((H_LINUX, LINUX)))
80+
assert "SHA256SUMS present" in rep.failures
81+
82+
83+
def test_missing_optional_mac_is_skipped_not_failed():
84+
rel = release(asset("SHA256SUMS"), asset(LINUX, digest=H_LINUX))
85+
rep = evaluate(rel, sums((H_LINUX, LINUX)))
86+
assert rep.failures == [] # mac-arm64 / mac-x64 absent -> skipped, not a failure
87+
88+
89+
# --------------------------------------------------------------------------- checksum problems
90+
def test_wrong_hash_via_api_digest_fails():
91+
rel = release(asset("SHA256SUMS"), asset(LINUX, digest="deadbeef" * 8))
92+
rep = evaluate(rel, sums((H_LINUX, LINUX)))
93+
assert f"sha256 (API digest): {LINUX}" in rep.failures
94+
95+
96+
def test_asset_without_sha256sums_entry_fails():
97+
rel = release(asset("SHA256SUMS"), asset(LINUX, digest=H_LINUX))
98+
rep = evaluate(rel, sums()) # empty SHA256SUMS
99+
assert f"checksummed: {LINUX}" in rep.failures
100+
101+
102+
def test_orphan_sha256sums_entry_fails():
103+
rel = release(asset("SHA256SUMS"), asset(LINUX, digest=H_LINUX))
104+
rep = evaluate(rel, sums((H_LINUX, LINUX), ("cafef00d" * 8, "ghost-asset.tar.gz")))
105+
assert "no orphan SHA256SUMS entries" in rep.failures
106+
107+
108+
def test_unuploaded_or_empty_asset_fails_sanity():
109+
rel = release(asset("SHA256SUMS"), asset(LINUX, digest=H_LINUX, size=0, state="starter"))
110+
rep = evaluate(rel, sums((H_LINUX, LINUX)))
111+
assert f"asset sane: {LINUX}" in rep.failures
112+
113+
114+
# --------------------------------------------------------------------------- --full re-hashing
115+
def test_full_mode_rehash_match_passes():
116+
rel = release(asset("SHA256SUMS"), asset(LINUX)) # no digest; --full hashes instead
117+
rep = evaluate(rel, sums((H_LINUX, LINUX)), full=True, hasher=lambda n: H_LINUX)
118+
assert rep.failures == []
119+
120+
121+
def test_full_mode_rehash_mismatch_fails():
122+
rel = release(asset("SHA256SUMS"), asset(LINUX))
123+
rep = evaluate(rel, sums((H_LINUX, LINUX)), full=True, hasher=lambda n: "0" * 64)
124+
assert f"sha256 (re-hashed): {LINUX}" in rep.failures
125+
126+
127+
# --------------------------------------------------------------------------- SDK table parity
128+
def test_mismatched_sdk_tables_fails():
129+
rel = release(asset("SHA256SUMS"), asset(LINUX, digest=H_LINUX))
130+
rep = evaluate(rel, sums((H_LINUX, LINUX)), node_asset_names={"something-else.tar.gz"})
131+
assert "SDK asset tables agree (python == node)" in rep.failures
132+
133+
134+
# --------------------------------------------------------------------------- parsing
135+
def test_parse_sha256sums_handles_binary_marker_and_case():
136+
parsed = vr.parse_sha256sums(f"AA11BB22 {LINUX}\ncafef00d *{WIN}\n")
137+
assert parsed == {LINUX: "aa11bb22", WIN: "cafef00d"}

0 commit comments

Comments
 (0)