Skip to content

chore(develop): release 1.2.2 #21

chore(develop): release 1.2.2

chore(develop): release 1.2.2 #21

name: Release Please
# Release automation, in two phases that both run from this one file.
#
# Work lands on develop. main is moved only by this workflow, so it always
# equals the last released commit.
#
# 1. Every push to develop, release-please recomputes what the next version
# would be from the Conventional Commit subjects since the last release, and
# opens or rewrites a "chore(develop): release X.Y.Z" PR carrying the version
# bumps and the CHANGELOG entry.
# 2. Merging that PR pushes to develop, which runs this workflow again. This
# time release-please sees a merged release PR and creates the tag and the
# GitHub release. The jobs below then fast-forward main onto that commit,
# ship to PyPI, and publish the docs.
#
# Everything downstream lives in this same workflow on purpose. A release - or a
# push - made with the default GITHUB_TOKEN does not trigger other workflows, so
# an `on: push: tags` publish, or a pages build watching main, would silently
# never fire. Calling them through `needs` sidesteps that entirely.
on:
push:
branches:
- develop
# Rebuild the release PR on demand. release-please leaves an open release PR
# alone when the version it computes has not changed, so a config fix that
# only alters the PR's *contents* is not picked up on its own - delete the
# release branch and run this to have it rebuilt.
workflow_dispatch:
permissions:
contents: read
jobs:
# The last moment before a tag exists. release-please creates the tag and the
# GitHub release in a single step, so anything asserted after it - as the
# version-consistency check was, from the publish job - reports a problem that
# has already shipped and needs a second release to correct. Everything that
# must be true of a release is therefore asserted here, and release-please does
# not run unless it holds.
#
# Only a release merge is checked. On an ordinary push CHANGELOG.md carries a
# `## [Unreleased]` heading, which is correct there and would fail the version
# comparison, so the check would cry wolf on every commit.
preflight:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Assert every version and the CHANGELOG agree
shell: pwsh
run: |
$subject = git log -1 --pretty=%s
if ($subject -notmatch '^chore\(develop\): release ') {
Write-Host "Not a release merge - nothing to assert."
Write-Host " head commit: $subject"
exit 0
}
Write-Host "Release merge detected: $subject"
./scripts/check-version-consistency.ps1
release-please:
needs: preflight
runs-on: ubuntu-latest
permissions:
contents: write # tags, releases, and the release branch
pull-requests: write # open and rewrite the release PR
issues: write # the autorelease:* labels are an issues-scope API
outputs:
# `releases_created` is always set to the string 'true' or 'false'. Compare
# it explicitly: any non-empty string is truthy in a GitHub Actions
# expression, so a bare `if: ...outputs.releases_created` is always true.
releases_created: ${{ steps.release.outputs.releases_created }}
tag_name: ${{ steps.release.outputs.tag_name }}
# The release commit on develop. main is fast-forwarded onto exactly this.
sha: ${{ steps.release.outputs.sha }}
steps:
- uses: googleapis/release-please-action@v5
id: release
with:
config-file: release-please-config.json
manifest-file: .release-please-manifest.json
# Without this release-please works against the repository's default
# branch. That is develop today, but stating it keeps this workflow
# correct if the default is ever changed back.
target-branch: develop
# Releases and PRs made with GITHUB_TOKEN do not trigger workflows, so
# CI does not run on the release PR itself. Supply a PAT or GitHub App
# token here if you want the PR gated by the test matrix before merge.
# token: ${{ secrets.RELEASE_PLEASE_TOKEN }}
# uv.lock records the project's own version, so every version bump leaves it a
# release behind and fails `uv lock --check`. release-please cannot fix that:
# its updaters re-serialise the file they edit, which is safe for a small hand
# written manifest and emphatically not for a generated 900-line lockfile. So
# uv regenerates it, on the release PR, before anyone merges it.
sync-lockfile:
needs: release-please
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: read
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
# Asking for the PR rather than reading release-please's `pr` output: that
# output is only set on the run that creates or updates the PR, and
# release-please deliberately leaves an open release PR alone when the
# version it computes has not changed. Every later push to develop would
# therefore skip this job with the PR sitting there unsynced.
- name: Find the open release PR
id: find
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
branch=$(gh pr list --state open --base develop --json headRefName --jq '[.[] | select(.headRefName | startswith("release-please--"))][0].headRefName // ""')
if [ -z "$branch" ]; then
echo "No open release PR - nothing to sync."
else
echo "Release PR branch: $branch"
fi
echo "branch=$branch" >> "$GITHUB_OUTPUT"
- uses: astral-sh/setup-uv@v7
if: steps.find.outputs.branch != ''
with:
version: "latest"
- name: Regenerate uv.lock and commit it if it moved
if: steps.find.outputs.branch != ''
env:
BRANCH: ${{ steps.find.outputs.branch }}
run: |
git checkout -B "$BRANCH" "origin/$BRANCH"
uv lock
if git diff --quiet -- uv.lock; then
echo "uv.lock is already in step with pyproject.toml."
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git commit -m "chore: sync uv.lock with the release version" -- uv.lock
git push origin "$BRANCH"
echo "uv.lock synced onto $BRANCH."
# release-please is configured with skip-changelog, so CHANGELOG.md stays
# hand-written - but it still creates the GitHub release with its own notes,
# generated from commit subjects. Those are thinner than the entry a human
# wrote, so replace them with it.
release-notes:
needs: release-please
if: ${{ needs.release-please.outputs.releases_created == 'true' }}
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v7
- name: Use the hand-written CHANGELOG entry as the release notes
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ needs.release-please.outputs.tag_name }}
run: |
CLEAN_VERSION="${TAG#v}"
# Matched with index()/substr() rather than a dynamic regex: awk
# treats "\[" in a string as a plain "[", which silently turns the
# pattern into a character class that matches every "## [" heading,
# so the notes become whichever section happens to be first.
awk -v hdr="## [$CLEAN_VERSION]" '
index($0, hdr) == 1 { found = 1; next }
found && substr($0, 1, 4) == "## [" { exit }
found { print }
' CHANGELOG.md > release_notes.md
if [ -s release_notes.md ]; then
gh release edit "$TAG" --notes-file release_notes.md
echo "Release notes taken from the CHANGELOG entry for $CLEAN_VERSION."
else
# Not fatal: the release exists either way, and the publish job's
# version-consistency check is what actually fails a missing entry.
echo "::warning::No CHANGELOG section for $CLEAN_VERSION - keeping the generated notes."
fi
publish:
needs: release-please
if: ${{ needs.release-please.outputs.releases_created == 'true' }}
uses: ./.github/workflows/publish-mcp.yml
secrets: inherit
# main is a pointer to the last released commit, moved only from here. This is
# a plain fast-forward, so it fails loudly rather than rewriting anything if
# main carries commits develop does not - which happens only if someone pushed
# to main directly.
fast-forward-main:
needs: release-please
if: ${{ needs.release-please.outputs.releases_created == 'true' }}
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Fast-forward main onto the released commit
env:
SHA: ${{ needs.release-please.outputs.sha }}
TAG: ${{ needs.release-please.outputs.tag_name }}
run: |
if git push origin "$SHA:refs/heads/main"; then
echo "main now points at $SHA ($TAG)."
else
echo "::error::Could not fast-forward main onto $SHA ($TAG). main has commits that develop does not; merge main into develop and re-run this workflow."
exit 1
fi
# Docs describe the released version, so they publish from a release rather
# than from every push. Chained rather than triggered: the fast-forward above
# is a GITHUB_TOKEN push and so does not start pages.yml on its own.
docs:
needs: [release-please, fast-forward-main]
if: ${{ needs.release-please.outputs.releases_created == 'true' }}
permissions:
contents: read
pages: write
id-token: write
uses: ./.github/workflows/pages.yml