Roll up CHANGELOG after release #1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Roll up CHANGELOG after release | |
| # Triggered automatically after `release.yml` succeeds. Performs the | |
| # mechanical CHANGELOG roll-up (move `[Unreleased]` entries under a | |
| # `[<version>]` heading, update footer compare links, bump pyproject.toml + | |
| # uv.lock to the next PATCH). Opens a `chore:` PR against develop for | |
| # owner review + admin-squash-merge. | |
| # | |
| # The script logic + edge cases live in | |
| # `.github/scripts/rollup_changelog.py` with full unit-test coverage in | |
| # `tests/test_rollup_changelog.py`. | |
| on: | |
| workflow_run: | |
| workflows: [Release] | |
| types: [completed] | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: "Released tag (e.g. v0.3.0) — required only for manual replay" | |
| required: true | |
| type: string | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| jobs: | |
| rollup: | |
| name: Open rollup PR | |
| runs-on: ubuntu-latest | |
| # `workflow_run` fires regardless of conclusion; gate so we only act on | |
| # successful release runs (skip when release.yml itself failed). | |
| if: > | |
| github.event_name == 'workflow_dispatch' || | |
| github.event.workflow_run.conclusion == 'success' | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| with: | |
| ref: develop | |
| # full history needed for `git describe --abbrev=0 --tags <ref>^` | |
| # to resolve the prior tag. | |
| fetch-depth: 0 | |
| # Prefer RELEASE_BOT_TOKEN (a non-GITHUB_TOKEN identity) so the | |
| # resulting branch push fires `pull_request` workflows on the | |
| # auto-PR. Falls back to GITHUB_TOKEN when the secret isn't set — | |
| # the auto-PR still opens, but its CI doesn't run until a user | |
| # pushes a commit on top. | |
| token: ${{ secrets.RELEASE_BOT_TOKEN || secrets.GITHUB_TOKEN }} | |
| - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 | |
| with: | |
| python-version: "3.14" | |
| - name: Resolve tags + date | |
| id: resolve | |
| run: | | |
| set -euo pipefail | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | |
| TAG="${{ inputs.tag }}" | |
| else | |
| # release.yml runs on tag pushes; the head_branch on the | |
| # workflow_run payload carries the tag name in that case. | |
| TAG="${{ github.event.workflow_run.head_branch }}" | |
| fi | |
| if [ -z "${TAG}" ]; then | |
| echo "::error::could not resolve released tag from trigger payload" | |
| exit 1 | |
| fi | |
| # Resolve the tag's prior tag. `git describe ... <tag>^` errors when | |
| # there is no prior — catch and treat as empty (first release). | |
| if PRIOR=$(git describe --abbrev=0 --tags --match 'v*.*.*' "${TAG}^" 2>/dev/null); then | |
| echo "prior tag: ${PRIOR}" | |
| else | |
| PRIOR="" | |
| echo "no prior tag (first release)" | |
| fi | |
| # Resolve the release date from the released tag's CHANGELOG.md. | |
| # The release PR is the authoritative source — using `date -u` at | |
| # workflow run time can drift if the workflow fires in a different | |
| # UTC day than the release PR was crafted. Falls back to date -u | |
| # with a warning when the heading isn't found, so a degenerate | |
| # CHANGELOG shape doesn't block the workflow. | |
| VERSION="${TAG#v}" | |
| if DATE=$(git show "${TAG}":CHANGELOG.md \ | |
| | grep -oE "^## \[${VERSION}\] - [0-9]{4}-[0-9]{2}-[0-9]{2}" \ | |
| | head -1 \ | |
| | sed 's/.* - //'); then | |
| if [ -z "${DATE}" ]; then | |
| echo "::warning::could not extract date for ${VERSION} from CHANGELOG.md; falling back to date -u" | |
| DATE=$(date -u +%Y-%m-%d) | |
| else | |
| echo "date from CHANGELOG: ${DATE}" | |
| fi | |
| else | |
| echo "::warning::git show ${TAG}:CHANGELOG.md failed; falling back to date -u" | |
| DATE=$(date -u +%Y-%m-%d) | |
| fi | |
| echo "tag=${TAG}" >> "$GITHUB_OUTPUT" | |
| echo "prior=${PRIOR}" >> "$GITHUB_OUTPUT" | |
| echo "date=${DATE}" >> "$GITHUB_OUTPUT" | |
| echo "branch=chore/changelog-rollup-${TAG}" >> "$GITHUB_OUTPUT" | |
| - name: Run rollup script | |
| run: | | |
| python .github/scripts/rollup_changelog.py \ | |
| --tag "${{ steps.resolve.outputs.tag }}" \ | |
| --prior-tag "${{ steps.resolve.outputs.prior }}" \ | |
| --date "${{ steps.resolve.outputs.date }}" | |
| - name: Open rollup PR | |
| env: | |
| GH_TOKEN: ${{ secrets.RELEASE_BOT_TOKEN || secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| BRANCH="${{ steps.resolve.outputs.branch }}" | |
| TAG="${{ steps.resolve.outputs.tag }}" | |
| DATE="${{ steps.resolve.outputs.date }}" | |
| # Idempotent: if the branch already exists from a previous replay, | |
| # bail rather than force-push (the existing PR is the source of truth). | |
| if git ls-remote --exit-code --heads origin "${BRANCH}" >/dev/null 2>&1; then | |
| echo "::warning::branch ${BRANCH} already exists; skipping push to avoid clobbering an in-flight rollup PR" | |
| exit 0 | |
| fi | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git checkout -b "${BRANCH}" | |
| git add CHANGELOG.md pyproject.toml uv.lock | |
| git commit -m "chore: roll up CHANGELOG [Unreleased] under [${TAG#v}] - ${DATE}" | |
| git push origin "${BRANCH}" | |
| gh pr create \ | |
| --base develop \ | |
| --head "${BRANCH}" \ | |
| --title "chore: roll up CHANGELOG [Unreleased] under [${TAG#v}] - ${DATE}" \ | |
| --body "$(cat <<EOF | |
| Auto-opened by [.github/workflows/changelog-rollup.yml](.github/workflows/changelog-rollup.yml) after the ${TAG} release. | |
| ## What this PR does | |
| - Inserts \`## [${TAG#v}] - ${DATE}\` heading after \`## [Unreleased]\` in CHANGELOG.md. | |
| - Updates \`[Unreleased]: ...compare/${TAG}...HEAD\` footer link. | |
| - Adds \`[${TAG#v}]: ...compare/...${TAG}\` footer link. | |
| - Bumps \`pyproject.toml\` \`[project].version\` and \`uv.lock\` self-version one PATCH (so develop's \`[Unreleased]\` can accumulate again). | |
| See [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) for the cycle. | |
| ## Review | |
| - [ ] CHANGELOG diff matches the released tag's published notes. | |
| - [ ] Version bump is the next PATCH after \`${TAG#v}\` (no skipped bumps). | |
| - [ ] CI green. | |
| After review: \`gh pr merge <N> --admin --squash\`. | |
| EOF | |
| )" |