afterhours-shift-manager/scripts/check_changelog.py

73 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
#!/usr/bin/env python3
"""CI guard: validate CHANGELOG.md versioning and the in-package copy.
Run on pull requests. Two checks:
1. If CHANGELOG.md changed in the PR, its top version must be a clean
single-step SemVer bump above the latest ``v*`` tag. (Non-changing PRs —
dependabot bumps, docs — are not version-checked, so they don't release.)
2. ``src/slack-bot/CHANGELOG.md`` (the copy that ships with the bot and feeds the
App Home "What's New" tab) must match the canonical root CHANGELOG.md.
The workflow passes context via env: ``PREV_TAG`` (latest tag) and
``CHANGELOG_CHANGED`` ("true"/"false"). Run locally it assumes the changelog
changed so the bump is validated.
"""
import os
import pathlib
import sys
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "src" / "shared"))
from shared.changelog import bump_kind, top_version # noqa: E402
ROOT = pathlib.Path(__file__).resolve().parents[1]
PKG_COPY = ROOT / "src" / "slack-bot" / "CHANGELOG.md"
def evaluate(
top: str | None, prev_tag: str, changelog_changed: bool, copies_match: bool
) -> list[str]:
"""Return a list of problems (empty == pass). Pure, for unit testing."""
problems = []
if not copies_match:
problems.append(
"src/slack-bot/CHANGELOG.md is out of sync with CHANGELOG.md — "
"run scripts/sync_changelog.py"
)
if changelog_changed:
if top is None:
problems.append("CHANGELOG.md changed but has no version entry at the top")
else:
prev = (prev_tag or "v0.0.0").lstrip("v")
if bump_kind(top, prev) is None:
problems.append(
f"top version v{top} is not a clean single-step SemVer bump "
f"above the latest tag v{prev} (expected one of "
f"inc-major / inc-minor / inc-patch)"
)
return problems
def main() -> int:
root_text = (ROOT / "CHANGELOG.md").read_text()
copies_match = PKG_COPY.exists() and PKG_COPY.read_text() == root_text
problems = evaluate(
top=top_version(root_text),
prev_tag=os.environ.get("PREV_TAG", ""),
changelog_changed=os.environ.get("CHANGELOG_CHANGED", "true") == "true",
copies_match=copies_match,
)
if problems:
for problem in problems:
print(f"::error::{problem}")
return 1
print("CHANGELOG guard passed.")
return 0
if __name__ == "__main__":
raise SystemExit(main())