🆙 Changelog & versions #5297
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: 🆙 Changelog & versions | |
| "on": | |
| workflow_call: | |
| secrets: | |
| REPOMATIC_PAT: | |
| required: false | |
| workflow_dispatch: | |
| schedule: | |
| # Run daily at 6:00 UTC for bump-version job. | |
| - cron: "0 6 * * *" | |
| push: | |
| branches: | |
| - main | |
| paths: | |
| - changelog.md | |
| - "**/pyproject.toml" | |
| # Trigger on any workflow change to make sure version gets hard-coded everywhere. | |
| - .github/workflows/*.yaml | |
| # Trigger on lockfile changes so bump-version recreates its PRs before they conflict. | |
| - uv.lock | |
| # Trigger on source and docs changes too, so a reconciliation commit that | |
| # touches only these gets an immediate PR re-base instead of waiting for the | |
| # workflow_run backstop below (see the prepare-release job's `if:`). That | |
| # backstop re-bases the PR after every Build & release regardless of paths, so | |
| # a commit landing entirely outside this list — a skill, a composite action | |
| # under .github/actions/, readme.md or claude.md — is caught then rather than | |
| # leaving the PR stale. | |
| - "**/*.py" | |
| - "docs/**" | |
| # Trigger after release workflow completes to ensure tags exist before bump-version. | |
| # This avoids race conditions where changelog workflow checks for tags before they're pushed. | |
| workflow_run: | |
| workflows: | |
| # Must match the release workflow's `name:` exactly (emoji included): | |
| # GitHub silently ignores the trigger when the name has no match. | |
| - "🚀 Build & release" | |
| types: | |
| - completed | |
| branches: | |
| - main | |
| permissions: {} | |
| concurrency: | |
| # Include event_name to prevent cross-event cancellation. | |
| # See repomatic/github/actions.py for rationale. | |
| group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| # Supply-chain cooldown: no package published within the window can be resolved by | |
| # any command in this workflow. Set here, not per command, so it also covers the | |
| # `metadata` bootstrap and any step added later; a workflow-level `env:` cannot | |
| # reference `needs`, so the window is a literal kept equal to `[tool.repomatic] | |
| # minimum-release-age`. repomatic's own test suite enforces that upstream; a | |
| # synced copy is kept in step by hand. Deliberate bypasses are per-package CLI | |
| # flags (`--exclude-newer-package`, `--min-release-age-exclude`). | |
| # See claude.md for the rationale. | |
| env: | |
| NPM_CONFIG_MIN_RELEASE_AGE: 7 | |
| UV_EXCLUDE_NEWER: "1 week" | |
| jobs: | |
| metadata: | |
| name: 🧬 Project metadata | |
| # Run on schedule, manual dispatch, push, or after release workflow completes successfully. | |
| # Push events keep bump-version PRs conflict-free by recreating them on every main change. | |
| if: > | |
| github.event_name == 'schedule' || | |
| github.event_name == 'workflow_dispatch' || | |
| github.event_name == 'push' || | |
| (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success') | |
| runs-on: ubuntu-26.04 | |
| timeout-minutes: 15 | |
| outputs: | |
| metadata: ${{ steps.metadata.outputs.metadata }} | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| # Use github.sha, not workflow_run.head_sha (stale after release cycle). | |
| # See repomatic/github/actions.py for rationale. | |
| ref: ${{ github.sha }} | |
| fetch-tags: true | |
| - uses: astral-sh/setup-uv@ae62891fec2bb8e7d6c99fc78c9fec3a63790f8d # v10.0.0 | |
| with: | |
| version: "0.12.3" | |
| - name: Run repomatic metadata | |
| id: metadata | |
| run: > | |
| uv --no-progress run --frozen -- repomatic metadata | |
| --format github-json --output "$GITHUB_OUTPUT" | |
| minor_bump_allowed major_bump_allowed release_commits_matrix | |
| fix-changelog: | |
| name: 📋 Fix changelog | |
| needs: | |
| - metadata | |
| # Skip during release cycle (push event with release commits). | |
| # The workflow_run event after "🚀 Build & release" completes handles | |
| # the post-release case, when the GitHub release is published and | |
| # visible to the public API. | |
| if: >- | |
| github.event_name == 'workflow_run' | |
| || !fromJSON(needs.metadata.outputs.metadata).release_commits_matrix | |
| runs-on: ubuntu-26.04 | |
| timeout-minutes: 15 | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: ${{ github.sha }} | |
| fetch-tags: true | |
| - uses: astral-sh/setup-uv@ae62891fec2bb8e7d6c99fc78c9fec3a63790f8d # v10.0.0 | |
| with: | |
| version: "0.12.3" | |
| - name: Fix changelog dates and admonitions | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: uv --no-progress run --frozen -- repomatic lint-changelog --fix | |
| - name: Sync pull request | |
| # This job checks out a detached HEAD at `github.sha`; pr-sync falls | |
| # back to the default branch from the event payload as its base. | |
| # Fail fast instead of hanging when the GitHub API is rate-limit starved. | |
| timeout-minutes: 10 | |
| env: | |
| GH_TOKEN: ${{ secrets.REPOMATIC_PAT || github.token }} | |
| run: > | |
| uv --no-progress run --frozen -- repomatic | |
| pr-sync --template fix-changelog | |
| bump-version: | |
| name: 🆙 Bump versions | |
| needs: | |
| - metadata | |
| # Always run since metadata already filters appropriate events. | |
| runs-on: ubuntu-26.04 | |
| timeout-minutes: 15 | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| strategy: | |
| matrix: | |
| part: | |
| - minor | |
| - major | |
| # The condition must be repeated on each gated step because: | |
| # 1. Job-level `if:` is evaluated before matrix expansion, so `matrix.*` isn't available there | |
| # 2. GitHub Actions lacks conditional step groups to skip multiple steps with one condition | |
| # Checkout and setup-uv always run so the cleanup step can use `uvx --from .` | |
| # on the inverse path (when the bump is no longer allowed). | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| # Use github.sha, not workflow_run.head_sha (stale after release cycle). | |
| ref: ${{ github.sha }} | |
| # Fetch tags so the close-stale-bump-pr step's is_version_bump_allowed | |
| # re-check resolves the latest release tag. Without tags it falls back | |
| # to "allow" and silently skips closing the orphan PR, leaving the | |
| # decision inconsistent with the metadata job (which fetches tags). | |
| fetch-tags: true | |
| - uses: astral-sh/setup-uv@ae62891fec2bb8e7d6c99fc78c9fec3a63790f8d # v10.0.0 | |
| with: | |
| version: "0.12.3" | |
| - name: ${{ matrix.part }} version bump | |
| if: fromJSON(needs.metadata.outputs.metadata)[format('{0}_bump_allowed', matrix.part)] | |
| run: > | |
| uv --no-progress run --frozen -- repomatic | |
| run bump-my-version -- bump --verbose ${{ matrix.part }} | |
| - name: Sync uv.lock | |
| # Plain `uv lock` (not `--upgrade`, not `uv sync`) is deliberate: a version | |
| # bump must refresh only the project's own version entry, never drag | |
| # dependencies forward. `uv lock --upgrade` re-resolves everything, so with | |
| # a `"0 day"` exclude-newer-package bypass it fast-forwards that dependency | |
| # across a release still inside its cooldown, on an unreviewed bump PR. | |
| # `uv sync` leaves the lock in a state the autofix `sync-uv-lock` job | |
| # rewrites into a redundant follow-up PR. Plain `uv lock` updates the lock | |
| # conservatively, avoiding both; genuine dependency upgrades stay the | |
| # `sync-uv-lock` job's responsibility, in their own reviewable PR. | |
| if: fromJSON(needs.metadata.outputs.metadata)[format('{0}_bump_allowed', matrix.part)] | |
| run: uv --no-progress lock | |
| - name: Sync pull request | |
| if: fromJSON(needs.metadata.outputs.metadata)[format('{0}_bump_allowed', matrix.part)] | |
| # Detached HEAD at `github.sha`; the base falls back to the default | |
| # branch from the event payload. The PR stays a draft on every sync | |
| # (via the template's frontmatter), not just at creation: its content | |
| # is regenerated underneath it and must never look ready to merge. | |
| # Runs after the bump, so the body's attribution footer names the bumped version rather | |
| # than the code that rendered it: see repomatic/github/pr_body.py::render_template. | |
| # Fail fast instead of hanging when the GitHub API is rate-limit starved. | |
| timeout-minutes: 10 | |
| env: | |
| GH_TOKEN: ${{ secrets.REPOMATIC_PAT || github.token }} | |
| PR_BRANCH: ${{ matrix.part }}-version-increment | |
| PR_PART: ${{ matrix.part }} | |
| run: > | |
| uv --no-progress run --frozen -- repomatic | |
| pr-sync | |
| --template bump-version --part "$PR_PART" --branch "$PR_BRANCH" | |
| - name: Close stale ${{ matrix.part }} bump PR | |
| # Runs on the inverse path: when a bump is no longer allowed, reconcile | |
| # any orphan PR left by an earlier run that raced a competing merge. | |
| # See repomatic/cli.py::close_stale_bump_pr for rationale. | |
| if: ${{ !fromJSON(needs.metadata.outputs.metadata)[format('{0}_bump_allowed', matrix.part)] }} | |
| env: | |
| GH_TOKEN: ${{ secrets.REPOMATIC_PAT || github.token }} | |
| run: > | |
| uv --no-progress run --frozen -- repomatic | |
| close-stale-bump-pr --part ${{ matrix.part }} | |
| prepare-release: | |
| name: 🎬 Prepare release | |
| # Skip only schedule (that trigger exists for the bump-version job alone). | |
| # workflow_run is intentionally allowed: it fires after every "🚀 Build & | |
| # release" (so after every push to main) and re-bases the PR onto the current | |
| # HEAD. That is the backstop for a push that did not match this workflow's | |
| # `paths:` filter — one touching only skills, a composite action, or an unlisted | |
| # markdown file — which would otherwise leave the PR stale. It double-runs with the | |
| # push trigger on paths-matching commits, but create-pull-request no-ops when | |
| # the branch is already current, so the extra run is cheap and idempotent. | |
| if: github.event_name != 'schedule' | |
| runs-on: ubuntu-26.04 | |
| timeout-minutes: 15 | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| # PAT required: bare `git push` authenticates via checkout's own | |
| # credentials, not the `GH_TOKEN` env set below. See | |
| # repomatic/github/token.py module docstring. | |
| token: ${{ secrets.REPOMATIC_PAT || github.token }} | |
| - uses: astral-sh/setup-uv@ae62891fec2bb8e7d6c99fc78c9fec3a63790f8d # v10.0.0 | |
| with: | |
| version: "0.12.3" | |
| # --- Freeze commit: freeze everything to the release version. --- | |
| - name: Strip dev suffix for release | |
| # Bump the "dev" part: .dev0 → release (omitted), producing a clean X.Y.Z version. | |
| run: > | |
| uv --no-progress run --frozen -- repomatic | |
| run bump-my-version -- bump --verbose dev | |
| - name: Extract version | |
| id: get_version | |
| run: > | |
| echo "current_version=$( | |
| uv --no-progress run --frozen -- repomatic | |
| run bump-my-version -- show current_version | |
| )" | tee -a "$GITHUB_OUTPUT" | |
| - name: Prepare release | |
| # Updates changelog and citation dates, comparison URL, and removes warning. | |
| # Also hard-codes version in workflow URLs for kdeldycke/repomatic repository. | |
| run: uv --no-progress run --frozen -- repomatic prepare-release | |
| - name: Sync uv.lock | |
| # Plain `uv lock`, not `--upgrade`: see the bump-version job above. Every | |
| # other version bump in this file re-locks; skipping it here left the tag | |
| # with pyproject ahead of uv.lock, and `uv run --frozen` reinstalls the | |
| # project on the next sync to close that gap. The release build runs two | |
| # nested syncs (the workflow's, then the one `repomatic run` opens for a | |
| # needs_venv tool), so the second one deleted the console script the | |
| # first had just written, which is a sharing violation on Windows. | |
| run: uv --no-progress lock | |
| - name: Prepare repository | |
| env: | |
| GIT_AUTHOR: ${{ github.actor }} | |
| run: | | |
| git config --global user.name "$GIT_AUTHOR" | |
| git config --global user.email "$GIT_AUTHOR@users.noreply.github.com" | |
| git clean -fd | |
| - name: Create freeze commit | |
| env: | |
| CURRENT_VERSION: ${{ steps.get_version.outputs.current_version }} | |
| run: git commit --all --message="[changelog] Release v${CURRENT_VERSION}" | |
| # --- Unfreeze commit: revert to development references. --- | |
| - name: Re-target main branch in workflows | |
| # This step is only used in the original repository to automate remote URL tagging. | |
| if: github.repository == 'kdeldycke/repomatic' | |
| run: uv --no-progress run --frozen -- repomatic prepare-release --post-release | |
| - name: Add new changelog entry | |
| run: uv --no-progress run --frozen -- repomatic changelog ./changelog.md | |
| - name: Version bump | |
| run: > | |
| uv --no-progress run --frozen -- repomatic | |
| run bump-my-version -- bump --verbose patch | |
| - name: Sync uv.lock | |
| # Plain `uv lock`, not `--upgrade`: see the bump-version job above. A | |
| # release-time version bump refreshes only the project's version entry, | |
| # never dependencies. | |
| run: uv --no-progress lock | |
| - name: Create unfreeze commit | |
| env: | |
| CURRENT_VERSION: ${{ steps.get_version.outputs.current_version }} | |
| run: > | |
| git commit --all --message="[changelog] Post-release bump | |
| v${CURRENT_VERSION} → v$(uv --no-progress run --frozen -- repomatic | |
| run bump-my-version -- show current_version)" | |
| - name: Sync pull request | |
| # The freeze and unfreeze commits above are already committed, so this | |
| # step carries them through as two commits rather than squashing them: | |
| # the release lane rebase-merges this PR and both must land intact. | |
| # Runs after the post-release bump, so the body's attribution footer names that bumped | |
| # version rather than the code that rendered it: see | |
| # repomatic/github/pr_body.py::render_template. | |
| # Fail fast instead of hanging when the GitHub API is rate-limit starved. | |
| timeout-minutes: 10 | |
| env: | |
| # Authenticated read so the checklist can link the draft dev pre-release | |
| # (drafts are visible only to maintainers) and resolve the previous tag. | |
| # We need custom PAT with workflows permission to hard-code version numbers in URLs in | |
| # .github/workflows/*.yaml files. | |
| CURRENT_VERSION: ${{ steps.get_version.outputs.current_version }} | |
| GH_TOKEN: ${{ secrets.REPOMATIC_PAT || github.token }} | |
| run: > | |
| uv --no-progress run --frozen -- repomatic | |
| pr-sync --template prepare-release --version "$CURRENT_VERSION" |