Skip to content

Release

Release #1

Workflow file for this run

name: Release
on:
workflow_run:
workflows:
- CI
types:
- completed
permissions:
contents: write
concurrency:
group: release-${{ github.event.workflow_run.head_branch }}
cancel-in-progress: true
jobs:
release:
if: >-
${{
github.event.workflow_run.conclusion == 'success' &&
(github.event.workflow_run.head_branch == 'main' || github.event.workflow_run.head_branch == 'master')
}}
runs-on: ubuntu-latest
steps:
- name: Checkout tested commit
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.event.workflow_run.head_sha }}
- name: Install uv
uses: astral-sh/setup-uv@v7
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Ensure tested commit is branch tip
id: tip
env:
BRANCH_NAME: ${{ github.event.workflow_run.head_branch }}
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
run: |
git fetch origin "${BRANCH_NAME}" --tags
REMOTE_SHA="$(git rev-parse "origin/${BRANCH_NAME}")"
if [ "${REMOTE_SHA}" != "${HEAD_SHA}" ]; then
echo "Branch ${BRANCH_NAME} moved from ${HEAD_SHA} to ${REMOTE_SHA}; skipping stale release."
echo "is_tip=false" >> "${GITHUB_OUTPUT}"
exit 0
fi
echo "is_tip=true" >> "${GITHUB_OUTPUT}"
- name: Decide release bump
if: steps.tip.outputs.is_tip == 'true'
id: bump
run: |
python - <<'PY' >> "${GITHUB_OUTPUT}"
import re
import subprocess
import sys
def git(*args: str) -> str:
return subprocess.check_output(["git", *args], text=True).strip()
try:
last_tag = git("describe", "--tags", "--abbrev=0")
except subprocess.CalledProcessError:
last_tag = ""
rev_range = f"{last_tag}..HEAD" if last_tag else "HEAD"
raw = git("log", rev_range, "--format=%s%n%b%x00")
entries = [entry.strip() for entry in raw.split("\x00") if entry.strip()]
if not entries:
print("should_release=false")
print("reason=no_commits")
sys.exit(0)
bump = None
release_notes = []
for entry in entries:
lines = entry.splitlines()
subject = lines[0].strip()
body = "\n".join(lines[1:])
text = f"{subject}\n{body}"
lower = subject.lower()
if "breaking change" in text.lower() or re.match(r"^[^:]+!:", subject):
bump = "major"
release_notes.append(subject)
break
if lower.startswith("feat:"):
bump = "minor" if bump != "major" else bump
release_notes.append(subject)
continue
if lower.startswith("fix:"):
if bump not in {"major", "minor"}:
bump = "patch"
release_notes.append(subject)
continue
print(f"last_tag={last_tag}")
print(f"should_release={'true' if bump else 'false'}")
print(f"bump={bump or ''}")
PY
- name: Stop when no release is needed
if: steps.tip.outputs.is_tip == 'true' && steps.bump.outputs.should_release != 'true'
run: echo "No feat/fix/breaking changes since the last tag."
- name: Bump version
if: steps.tip.outputs.is_tip == 'true' && steps.bump.outputs.should_release == 'true'
run: uv version --bump "${{ steps.bump.outputs.bump }}" --frozen
- name: Read version
if: steps.tip.outputs.is_tip == 'true' && steps.bump.outputs.should_release == 'true'
id: version
run: echo "value=$(uv version --short)" >> "${GITHUB_OUTPUT}"
- name: Commit release change
if: steps.tip.outputs.is_tip == 'true' && steps.bump.outputs.should_release == 'true'
env:
VERSION: ${{ steps.version.outputs.value }}
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git switch -C "${{ github.event.workflow_run.head_branch }}"
git add pyproject.toml
git commit -m "chore: release ${VERSION} [skip ci]"
- name: Create tag
if: steps.tip.outputs.is_tip == 'true' && steps.bump.outputs.should_release == 'true'
env:
VERSION: ${{ steps.version.outputs.value }}
run: git tag -a "v${VERSION}" -m "Release v${VERSION}"
- name: Push release commit and tag
if: steps.tip.outputs.is_tip == 'true' && steps.bump.outputs.should_release == 'true'
env:
BRANCH_NAME: ${{ github.event.workflow_run.head_branch }}
VERSION: ${{ steps.version.outputs.value }}
run: |
git push origin "HEAD:${BRANCH_NAME}"
git push origin "v${VERSION}"