diff --git a/.github/workflows/release-notes.yml b/.github/workflows/release-notes.yml index 051ede3..93f47be 100644 --- a/.github/workflows/release-notes.yml +++ b/.github/workflows/release-notes.yml @@ -8,6 +8,10 @@ on: tag: description: 'Release tag (e.g., v0.3.3)' required: true + target_ref: + description: 'Branch to check out and update when writing changelog changes' + default: main + required: true permissions: contents: write @@ -18,6 +22,7 @@ jobs: steps: - uses: actions/checkout@v6 with: + ref: ${{ github.event_name == 'release' && github.event.release.target_commitish || inputs.target_ref }} fetch-depth: 0 # full history for tag detection and diff stats - name: Resolve tag @@ -25,8 +30,12 @@ jobs: run: | if [[ "${{ github.event_name }}" == "release" ]]; then echo "tag=${{ github.event.release.tag_name }}" >> "$GITHUB_OUTPUT" + echo "target_ref=${{ github.event.release.target_commitish }}" >> "$GITHUB_OUTPUT" + echo "markdown_post_to=step-summary" >> "$GITHUB_OUTPUT" else echo "tag=${{ inputs.tag }}" >> "$GITHUB_OUTPUT" + echo "target_ref=${{ inputs.target_ref }}" >> "$GITHUB_OUTPUT" + echo "markdown_post_to=release,step-summary" >> "$GITHUB_OUTPUT" fi # ── Surface 1: GitHub Release body + step summary ────────── @@ -39,7 +48,7 @@ jobs: data-format: github-prs head-ref: ${{ steps.tag.outputs.tag }} release-tag: ${{ steps.tag.outputs.tag }} - post-to: release,step-summary + post-to: ${{ steps.tag.outputs.markdown_post_to }} token: ${{ secrets.GITHUB_TOKEN }} # ── Surface 2: Terminal-formatted release asset ──────────── @@ -59,6 +68,7 @@ jobs: # ── Surface 3: Compact notes for changelog ───────────────── - name: Generate release notes (compact) + if: github.event_name == 'workflow_dispatch' uses: ./ id: compact with: @@ -72,7 +82,7 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} - name: Commit changelog updates - if: steps.compact.outcome == 'success' + if: github.event_name == 'workflow_dispatch' && steps.compact.outcome == 'success' run: | if git diff --quiet CHANGELOG.md 2>/dev/null; then echo "No changelog changes to commit." @@ -81,5 +91,5 @@ jobs: git config user.email "github-actions[bot]@users.noreply.github.com" git add CHANGELOG.md git commit -m "docs: update CHANGELOG for ${{ steps.tag.outputs.tag }}" - git push + git push origin "HEAD:${{ steps.tag.outputs.target_ref }}" fi diff --git a/Makefile b/Makefile index ea34a7b..4b508e3 100644 --- a/Makefile +++ b/Makefile @@ -144,15 +144,34 @@ release: build publish gh-release: @VERSION=$$(grep '^version = ' pyproject.toml | sed 's/version = "\(.*\)"/\1/'); \ PROJECT=$$(grep -m1 '^name = ' pyproject.toml | sed 's/name = "\(.*\)"/\1/'); \ + TAG="v$$VERSION"; \ NOTES="site/content/releases/$$VERSION.md"; \ + if [ -n "$$(git status --porcelain)" ]; then echo "Error: working tree is not clean."; exit 1; fi; \ if [ ! -f "$$NOTES" ]; then echo "Error: $$NOTES not found"; exit 1; fi; \ - echo "Creating release v$$VERSION for $$PROJECT..."; \ - git push origin main 2>/dev/null || true; \ - git push origin v$$VERSION 2>/dev/null || true; \ - awk '/^---$$/{c++;next}c>=2' "$$NOTES" | gh release create v$$VERSION \ + if gh release view "$$TAG" >/dev/null 2>&1; then echo "Error: release $$TAG already exists"; exit 1; fi; \ + git fetch origin main --tags; \ + LOCAL=$$(git rev-parse HEAD); \ + REMOTE=$$(git rev-parse origin/main); \ + if [ "$$LOCAL" != "$$REMOTE" ]; then \ + echo "Error: HEAD must match origin/main before releasing."; \ + echo "HEAD=$$LOCAL"; \ + echo "origin/main=$$REMOTE"; \ + exit 1; \ + fi; \ + REMOTE_TAG=$$(git ls-remote origin "refs/tags/$$TAG" | awk '{print $$1}'); \ + if [ -n "$$REMOTE_TAG" ] && [ "$$REMOTE_TAG" != "$$LOCAL" ]; then \ + echo "Error: remote $$TAG points at $$REMOTE_TAG, not $$LOCAL"; \ + exit 1; \ + fi; \ + git tag -f "$$TAG" HEAD; \ + if [ -z "$$REMOTE_TAG" ]; then git push origin "$$TAG"; fi; \ + echo "Creating release $$TAG for $$PROJECT..."; \ + awk '/^---$$/{c++;next}c>=2' "$$NOTES" | gh release create "$$TAG" \ + --verify-tag \ + --target main \ --title "$$PROJECT $$VERSION" \ -F -; \ - echo "✓ GitHub release v$$VERSION created (PyPI publish will run via workflow)"; \ + echo "✓ GitHub release $$TAG created (PyPI publish will run via workflow)"; \ $(MAKE) action-tag # Move the floating major action tag so `uses: lbliii/kida@v0` tracks the latest release. diff --git a/docs/stability-gate.md b/docs/stability-gate.md index d8155e9..12f6486 100644 --- a/docs/stability-gate.md +++ b/docs/stability-gate.md @@ -28,6 +28,41 @@ The package smoke test verifies: - component metadata - sandbox denial for blocked reflection attributes +## Release Automation Gate + +Before running the release target, merge the release-prep PR and work from a +clean checkout whose `HEAD` matches `origin/main`. The release target expects +the version in `pyproject.toml` to match a source page at +`site/content/releases/.md` and creates `v` from the merged +main commit: + +```bash +make gh-release +``` + +The target fails if the worktree is dirty, if `HEAD` differs from `origin/main`, +if the GitHub release already exists, or if an existing remote version tag points +at a different commit. It pushes the version tag, creates the GitHub release from +the curated site release notes, and then moves the floating major action tag +with `make action-tag`. + +After `make gh-release`, verify: + +- `refs/heads/main`, `refs/tags/v`, and `refs/tags/v` point at + the same release commit +- the GitHub release body still contains the curated release notes +- the `Upload Python Package` release workflow succeeded +- `https://pypi.org/pypi/kida-templates//json` returns the released + wheel and sdist metadata +- the docs release page is reachable under + `https://lbliii.github.io/kida/releases//` + +The `Release Notes` workflow dogfoods Kida's release-note templates on release +events, but release events do not rewrite the curated GitHub release body or +commit changelog changes. Use its manual `workflow_dispatch` mode when a +maintainer explicitly wants to regenerate release-note/changelog output for a +tag and target branch. + ## Benchmark Evidence Linux 3.14t benchmark baselines are the performance comparison baseline. Darwin