afterhours-shift-manager/tests/scripts/test_release_tooling.py

71 lines
2.5 KiB
Python
Raw Normal View History

Add changelog-driven releases and App Home tab (#112) * Add changelog-driven releases and App Home tab Version the bot continuously from CHANGELOG.md (the single source of truth for both the version and the staff-readable notes) and surface changes to users in two ways: - A new afterhours-release-notifier Lambda posts a "What's New" message to the shift channel on minor/major releases (patches stay silent). - The bot gains an App Home "About" tab showing what it does, the command list, and the current version's notes. release.yaml runs on Deploy success (not release:published — GITHUB_TOKEN events don't start downstream workflows), checks out the deployed commit, and tags + publishes a GitHub Release + invokes the notifier. It assumes a dedicated, boundary-carrying OIDC role scoped to InvokeFunction on the notifier; the account's cfn role gates role creation on that boundary. The manual Version Bump workflow is retired. A CI guard enforces that a CHANGELOG edit is a clean SemVer bump and that the in-package copy matches. * Harden release workflow and regex against CodeQL findings Address three code-scanning alerts on the PR: - Critical (actions/untrusted-checkout): split release.yaml into a read-only `prepare` job that checks out and runs repo code, and a privileged `publish` job (contents:write + OIDC) that never checks out repo code — it tags, releases, and invokes purely through the GitHub and AWS APIs. Also assert head_branch == main. - High x2 (py/polynomial-redos): rewrite the italic and link regexes in markdown_to_mrkdwn with possessive quantifiers and exclusive character classes so they run in linear time on adversarial input. Adds a regression test. * Move release/announce into Deploy workflow to clear CodeQL The workflow_run-triggered release.yaml kept tripping CodeQL's privileged-context rules (untrusted-checkout, then cache-poisoning) — CodeQL distrusts any workflow_run that checks out a ref, regardless of the main-only guarantee, and there is no autofix. Fold the release job into deploy.yaml gated on `needs: deploy`. A push-to-main run is a trusted context, so checking out and running repo code with write/OIDC is safe there. This still gates on deploy success and serializes via the deploy concurrency group, and removes the separate workflow entirely.
2026-06-11 19:41:31 -04:00
"""Tests for the release tooling scripts (CI guard + changelog CLI)."""
import importlib.util
import json
import pathlib
import sys
ROOT = pathlib.Path(__file__).resolve().parents[2]
def _load(name, relpath):
spec = importlib.util.spec_from_file_location(name, ROOT / relpath)
mod = importlib.util.module_from_spec(spec)
sys.modules[name] = mod
spec.loader.exec_module(mod)
return mod
check = _load("check_changelog", "scripts/check_changelog.py")
cli = _load("changelog_cli", "scripts/changelog_cli.py")
class TestGuardEvaluate:
def test_clean_minor_bump_passes(self):
assert check.evaluate("1.10.0", "v1.9.2", True, True) == []
def test_bad_bump_flagged(self):
problems = check.evaluate("1.11.0", "v1.9.2", True, True) # skips a minor
assert problems and "clean single-step" in problems[0]
def test_unchanged_changelog_is_not_version_checked(self):
# A docs/dependabot PR (no CHANGELOG edit) never trips the bump check.
assert check.evaluate("5.0.0", "v1.9.2", False, True) == []
def test_copy_drift_flagged_even_when_unchanged(self):
problems = check.evaluate("1.9.2", "v1.9.2", False, False)
assert any("out of sync" in p for p in problems)
def test_missing_top_version_flagged_when_changed(self):
problems = check.evaluate(None, "v1.9.2", True, True)
assert any("no version entry" in p for p in problems)
def test_first_release_from_no_tags(self):
assert check.evaluate("0.1.0", "", True, True) == []
class TestChangelogCli:
SAMPLE = "## v1.10.0 — June 11, 2026\n\n**Self-announcing** releases.\n"
def test_top_version(self, tmp_path, capsys):
f = tmp_path / "CHANGELOG.md"
f.write_text(self.SAMPLE)
cli.main(["x", "top-version", str(f)])
assert capsys.readouterr().out == "1.10.0"
def test_bump_kind(self, tmp_path, capsys):
f = tmp_path / "CHANGELOG.md"
f.write_text(self.SAMPLE)
cli.main(["x", "bump-kind", str(f), "v1.9.2"])
assert capsys.readouterr().out == "minor"
def test_payload(self, tmp_path, capsys):
f = tmp_path / "CHANGELOG.md"
f.write_text(self.SAMPLE)
cli.main(["x", "payload", str(f), "1.10.0"])
out = json.loads(capsys.readouterr().out)
assert out["version"] == "1.10.0"
assert out["date_label"] == "June 11, 2026"
# CLI emits raw markdown; Slack conversion happens later, in the Lambda.
assert "**Self-announcing**" in out["notes"]