Skip to content

🆙 Changelog & versions #5297

🆙 Changelog & versions

🆙 Changelog & versions #5297

Workflow file for this run

---
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"