Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ jobs:
with:
fetch-depth: 0
persist-credentials: false
- name: Test application release metadata guard
run: python3 -B scripts/test_release_preflight.py
- name: Validate source, changelog, signature, and exact-commit push CI
id: release
env:
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ All notable changes to Krillswitch are documented in this file.

### Changed

- Validate the application package version before creating release archives.

- Add signed-tag application releases and no-publish SDK rehearsals with retained tarballs, changelog notes, and checksums.

- Refresh Hono, Radix, Vite, Cloudflare tooling, and GitHub Actions within compatible versions while preserving the Node.js floor and two-day dependency cooldown.
Expand Down
3 changes: 2 additions & 1 deletion RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

Application source releases use signed annotated `vX.Y.Z` tags and
`.github/workflows/release.yml`. The workflow validates dated `CHANGELOG.md`
notes and green push CI on the exact protected-main commit, runs checks, and
notes, a matching root `package.json` version, and green push CI on the exact
protected-main commit, runs checks, and
publishes the source archive with SHA256SUMS. It never deploys the service.

SDK releases keep the public `sdk-vX.Y.Z` namespace and use
Expand Down
2 changes: 2 additions & 0 deletions scripts/release-preflight.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ def main():
version = re.search(r"^## \[?v?(\d+\.\d+\.\d+)\]? - \d{4}-\d{2}-\d{2}", text, re.M)[1]
if not re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?", version):
raise ValueError("release version must be SemVer")
if json.loads(Path("package.json").read_text())["version"] != version:
raise ValueError("package.json version must match the application release")
tag = "v" + version
head = git("rev-parse", "HEAD")
repo = os.environ["GITHUB_REPOSITORY"]
Expand Down
44 changes: 44 additions & 0 deletions scripts/test_release_preflight.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import importlib.util
import os
from pathlib import Path
import tempfile
import unittest
from unittest.mock import patch

spec = importlib.util.spec_from_file_location('release', Path(__file__).with_name('release-preflight.py'))
release = importlib.util.module_from_spec(spec)
spec.loader.exec_module(release)

class ReleasePreflightTests(unittest.TestCase):
def test_mismatched_app_version_fails_before_network(self):
with tempfile.TemporaryDirectory() as directory:
previous = os.getcwd()
os.chdir(directory)
try:
Path('CHANGELOG.md').write_text('## 1.2.3 - 2026-10-12\n- Fix\n')
Path('package.json').write_text('{"version":"1.2.2"}')
with patch.dict(os.environ, {'DRY_RUN':'true','VERSION':'1.2.3'}, clear=True), patch.object(release, 'api') as api:
with self.assertRaisesRegex(ValueError, 'package.json version'): release.main()
api.assert_not_called()
finally: os.chdir(previous)

def test_notes_require_unique_dated_nonempty_section(self):
self.assertEqual(release.notes_for('## 1.2.3 - 2026-10-12\n\n- Fix\n\n## 1.2.2 - 2026-10-11\n- Old\n', '1.2.3'), '- Fix\n')
for text in ['## 1.2.3 - Unreleased\n- Fix', '## 1.2.3 - 2026-10-12\n', '## 1.2.3 - 2026-10-12\n- Fix\n## 1.2.3 - 2026-10-12\n- Duplicate']:
with self.assertRaises(ValueError): release.notes_for(text, '1.2.3')

def test_pull_request_checks_cannot_satisfy_push_gate(self):
with tempfile.TemporaryDirectory() as directory:
previous = os.getcwd()
os.chdir(directory)
try:
Path('CHANGELOG.md').write_text('## 1.2.3 - 2026-10-12\n- Fix\n')
Path('package.json').write_text('{"name":"fixture","version":"1.2.3"}')
def api(path):
if '/branches/' in path: return {'protected': True}
return {'workflow_runs': [{'head_sha':'a'*40,'event':'pull_request','conclusion':'success'}]}
with patch.dict(os.environ, {'DRY_RUN':'true','VERSION':'1.2.3','GITHUB_REPOSITORY':'owner/repo','GITHUB_REF':'refs/heads/main','CI_WORKFLOWS':'ci.yml','PACKAGE_NAME':'fixture'}, clear=True), patch.object(release,'git',return_value='a'*40), patch.object(release,'api',side_effect=api):
with self.assertRaisesRegex(ValueError,'no green push'): release.main()
finally: os.chdir(previous)

if __name__ == '__main__': unittest.main()
Loading