From 46f3edbcaeb4f82797367caee1df36387873f1c6 Mon Sep 17 00:00:00 2001 From: andromia3 <225720212+andromia3@users.noreply.github.com> Date: Sun, 11 Oct 2026 11:09:41 +0100 Subject: [PATCH] Require evidence for release verifier success Signed-off-by: andromia3 <225720212+andromia3@users.noreply.github.com> --- CHANGELOG.md | 1 + packaging/verify_release.py | 4 ++ .../github-release/scripts/verify_release.py | 4 ++ tests/test_helpers.py | 43 +++++++++++++++++++ 4 files changed, 52 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 962b916..f7f118c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## 0.2.1 (draft, not released) +- Verification rejects empty checksum manifests and requires release metadata when published or immutable state checks are requested. - Gemini Apps package: leave out files without an extension. Gemini's skill uploader rejected 0.2.0 ("The skill folder contains a file with an unsupported file type") because of `templates/gitattributes`; the same package without it was accepted (2026-10-08). - Builder 1.0.1: new `exclude_no_suffix` key in `packages.json`, plus a host invariant that refuses extensionless files in `gemini-apps-only` regardless of config. Regression test added. - host-matrix, packaging and INSTALL docs record what Gemini accepted (`.py`, `.json`, `.md`, `.txt`) and the open question of whether scripts run there. diff --git a/packaging/verify_release.py b/packaging/verify_release.py index 4a4845b..60b7458 100644 --- a/packaging/verify_release.py +++ b/packaging/verify_release.py @@ -52,6 +52,8 @@ def check_sums(assets, out): out.append(('FAIL', 'no %s in %s' % (SUMS, assets))) return {} listed = read_sums(sums_path) + if not listed: + out.append(('FAIL', 'SHA256SUMS contains no assets')) present = {p.name: p for p in assets.iterdir() if p.is_file() and p.name != SUMS} actual = {} for name, want in sorted(listed.items()): @@ -149,6 +151,8 @@ def main(argv=None): print('verify_release ' + __version__) return 0 args = ap.parse_args(argv) + if (args.expect_published or args.expect_immutable) and not args.release_json: + ap.error('--expect-published and --expect-immutable require --release-json') if not args.assets: print('need --assets DIR') return 2 diff --git a/skills/github-release/scripts/verify_release.py b/skills/github-release/scripts/verify_release.py index 4a4845b..60b7458 100644 --- a/skills/github-release/scripts/verify_release.py +++ b/skills/github-release/scripts/verify_release.py @@ -52,6 +52,8 @@ def check_sums(assets, out): out.append(('FAIL', 'no %s in %s' % (SUMS, assets))) return {} listed = read_sums(sums_path) + if not listed: + out.append(('FAIL', 'SHA256SUMS contains no assets')) present = {p.name: p for p in assets.iterdir() if p.is_file() and p.name != SUMS} actual = {} for name, want in sorted(listed.items()): @@ -149,6 +151,8 @@ def main(argv=None): print('verify_release ' + __version__) return 0 args = ap.parse_args(argv) + if (args.expect_published or args.expect_immutable) and not args.release_json: + ap.error('--expect-published and --expect-immutable require --release-json') if not args.assets: print('need --assets DIR') return 2 diff --git a/tests/test_helpers.py b/tests/test_helpers.py index e72c40d..bce83a2 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -94,6 +94,49 @@ def test_verify_release_pass_and_tamper(self): self.assertIn('DIFFERS', r.stdout) +class VerifierEvidenceTests(unittest.TestCase): + def test_state_checks_with_valid_assets_and_metadata(self): + with tempfile.TemporaryDirectory() as td: + root = Path(td) + assets = root / 'assets' + assets.mkdir() + name = 'github-release-0.2.1-test.zip' + archive = assets / name + with zipfile.ZipFile(archive, 'w') as z: + z.writestr('github-release/SKILL.md', '# Test fixture\n') + sums = assets / 'SHA256SUMS.txt' + sums.write_text(sha(archive) + ' ' + name + '\n', encoding='utf-8') + metadata = root / 'release.json' + data = {'tag_name': 'v0.2.1', 'draft': False, 'immutable': True, + 'assets': [{'name': p.name, 'state': 'uploaded', + 'size': p.stat().st_size, 'digest': 'sha256:' + sha(p)} + for p in (archive, sums)]} + for draft, immutable, expected in ((False, True, 0), (True, True, 1), + (False, False, 1)): + with self.subTest(draft=draft, immutable=immutable): + data.update(draft=draft, immutable=immutable) + metadata.write_text(json.dumps(data), encoding='utf-8') + r = run(SKILL / 'scripts/verify_release.py', '--assets', assets, + '--version', '0.2.1', '--release-json', metadata, + '--expect-published', '--expect-immutable') + self.assertEqual(r.returncode, expected, r.stdout + r.stderr) + self.assertIn('GitHub metadata', r.stdout) + + def test_empty_checksum_manifest_fails(self): + with tempfile.TemporaryDirectory() as td: + (Path(td) / 'SHA256SUMS.txt').write_text('', encoding='utf-8') + r = run(SKILL / 'scripts/verify_release.py', '--assets', td) + self.assertEqual(r.returncode, 1, r.stdout) + self.assertIn('contains no assets', r.stdout) + + def test_release_state_flags_require_metadata(self): + for flag in ('--expect-published', '--expect-immutable'): + with self.subTest(flag=flag): + r = run(SKILL / 'scripts/verify_release.py', '--assets', REPO, flag) + self.assertEqual(r.returncode, 2, r.stdout) + self.assertIn('require --release-json', r.stderr) + + class CheckVersionsTests(unittest.TestCase): def test_repo_versions_and_workflow(self): v = json.loads((REPO / '.claude-plugin/plugin.json').read_text())['version']