Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions packaging/verify_release.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()):
Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions skills/github-release/scripts/verify_release.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()):
Expand Down Expand Up @@ -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
Expand Down
43 changes: 43 additions & 0 deletions tests/test_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand Down