Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 60 additions & 3 deletions .github/workflows/bump.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ on:
description: Version bump level
required: true
options:
- prerelease
- dev
# NOTE: bare "prerelease" is deliberately absent: under the always-on-devN scheme it crashes (from X.Y.Z.devN, poetry-core AssertionError) or skips a number (from X.Y.ZbN.devM -> bN+1 instead of bN). Use an explicit version instead.
- "prerelease --next-phase"
- prepatch
- preminor
Expand Down Expand Up @@ -39,6 +40,12 @@ on:
bumpmsg:
description: Message created by the version bump.
value: ${{ jobs.bump.outputs.bumpmsg }}
version:
description: New version number after the bump (empty on dry-run).
value: ${{ jobs.bump.outputs.version }}
sha:
description: SHA of the bump commit (empty on dry-run).
value: ${{ jobs.bump.outputs.sha }}

jobs:
bump:
Expand All @@ -48,6 +55,8 @@ jobs:
contents: write
outputs:
bumpmsg: ${{ steps.bumping.outputs.BUMP_MSG }}
version: ${{ steps.commit.outputs.VERSION }}
sha: ${{ steps.commit.outputs.SHA }}

steps:
- name: Write App private key to file
Expand Down Expand Up @@ -102,12 +111,57 @@ jobs:
with:
python-version: "3.13"

- name: Install packaging
if: inputs.rule == 'dev'
shell: bash
run: pip install packaging

- name: Compute next dev version
id: devver
if: inputs.rule == 'dev'
shell: python
run: |
import os, subprocess
from packaging.version import Version

# Get current version from poetry
current = Version(subprocess.run(
"poetry version -s",
shell=True,
check=True,
text=True,
capture_output=True,
).stdout.strip())

if current.dev is not None:
# Already on a dev version (with or without pre-release
# segment): increment the dev counter, keep everything else.
pre = f"{current.pre[0]}{current.pre[1]}" if current.pre else ""
nxt = f"{current.base_version}{pre}.dev{current.dev + 1}"
elif current.pre is not None:
# Freshly tagged pre-release (e.g. 0.12.0b0): start dev cycle
# towards the next tag in the same phase (0.12.0b1.dev0).
nxt = (f"{current.base_version}"
f"{current.pre[0]}{current.pre[1] + 1}.dev0")
else:
# Stable release: start dev cycle towards the next patch
# version. The actual release may still become minor/major,
# devN sorts below either, so guessing patch is always safe.
major, minor, patch = (*current.release, 0, 0)[:3]
nxt = f"{major}.{minor}.{patch + 1}.dev0"

with open(os.environ.get("GITHUB_OUTPUT"), "a") as output:
output.write(f"NEXT={nxt}\n")

- name: Bump package Version using Poetry
id: bumping
env:
RULE: ${{ inputs.rule }}
RULE: ${{ inputs.rule == 'dev' && steps.devver.outputs.NEXT || inputs.rule }}
DRY: ${{ inputs.dry-run && '--dry-run' || '' }}
run: echo "BUMP_MSG=$(poetry version $DRY $RULE)" >> $GITHUB_OUTPUT
# Assignment first, so a poetry failure fails this step (command substitution inside echo would swallow the exit code).
run: |
BUMP_MSG=$(poetry version $DRY $RULE)
echo "BUMP_MSG=$BUMP_MSG" >> $GITHUB_OUTPUT

- name: Output debug msg
if: inputs.dry-run
Expand All @@ -118,6 +172,7 @@ jobs:
echo "Dry run only, no actual modification made." >> $GITHUB_STEP_SUMMARY

- name: Commit changes
id: commit
# could also use https://github.com/stefanzweifel/git-auto-commit-action instead
if: ${{ ! inputs.dry-run }}
env:
Expand All @@ -127,5 +182,7 @@ jobs:
git config user.email "217837468+myciapp[bot]@users.noreply.github.com"
git commit -am "$BUMP_MSG"
git push
echo "VERSION=$(poetry version -s)" >> $GITHUB_OUTPUT
echo "SHA=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
echo "## $BUMP_MSG" >> $GITHUB_STEP_SUMMARY
echo "Successfully committed and pushed version bump." >> $GITHUB_STEP_SUMMARY
137 changes: 137 additions & 0 deletions .github/workflows/prepare_release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
name: Prepare release

# Performs the version-bump / draft-release / dev-bump dance:
# 1. Bump the package version to the release version (given rule).
# 2. Create a DRAFT GitHub release whose tag is pinned to the SHA of that bump commit.
# The tag itself is only created by GitHub when a human reviews the (auto-generated + manual) notes and publishes the draft, which then triggers the publish_pypi workflow as before.
# 3. Immediately bump the version to the next dev version, so the release version string never lingers on the branch, even while the draft awaits review.

# Works for both stable releases (rule: patch/minor/major) and internal pre-release tags (rule: preminor/prerelease/...).
# Pre-release versions are automatically marked as "pre-release" on the GitHub release, which the publish workflow uses to skip the PyPI upload.
# Also works for backport releases: dispatch the workflow from a maintenance branch (e.g. v2.x) with rule "patch" and set previous-tag to the last tag of that series so the generated notes compare against the right base.

on:
workflow_call:
inputs:
rule:
type: string
description: Poetry version bump rule for the release version
required: true
notes:
type: string
description: >
Manual release notes section, shown above the auto-generated
notes. Can also be added/edited later in the draft release UI.
required: false
default: ""
tag-prefix:
type: string
description: Prefix for the git tag created from the version number
required: false
default: "v"
branch:
type: string
description: >
Branch (ref) to release from. Defaults to the ref the workflow
was dispatched on, so releasing from a maintenance branch (e.g.
v2.x) only requires selecting that branch in the Actions UI.
required: false
default: ${{ github.ref_name }}
previous-tag:
type: string
description: >
Tag to use as comparison base for the auto-generated release
notes. Leave empty to let GitHub choose automatically (fine for
regular releases from main). Set explicitly for backport
releases from maintenance branches, where releases are created
out of version order and GitHub may pick the wrong base (e.g.
set to "v2.1.0" when releasing 2.1.1 after 2.5.0 exists).
required: false
default: ""
outputs:
version:
description: The release version number.
value: ${{ jobs.BumpRelease.outputs.version }}
draft-url:
description: URL of the created draft release.
value: ${{ jobs.Draft.outputs.url }}

jobs:
BumpRelease:
name: Bump to release version
uses: ./.github/workflows/bump.yml
secrets: inherit
with:
rule: ${{ inputs.rule }}
branch: ${{ inputs.branch }}

Draft:
name: Create draft release
runs-on: ubuntu-latest
needs: BumpRelease
permissions:
contents: write
outputs:
url: ${{ steps.draft.outputs.url }}

steps:
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.13"

- name: Install packaging
shell: bash
run: pip install packaging

- name: Determine release type
id: reltype
env:
VERSION: ${{ needs.BumpRelease.outputs.version }}
shell: python
run: |
import os, sys
from packaging.version import Version

version = Version(os.environ.get("VERSION"))

# Dev versions are never tagged or released.
if version.is_devrelease:
sys.exit(f"::error:: Refusing to create a release for dev "
f"version {version!s}; check the bump rule.")

# Pre-releases (aN/bN/rcN) become internal-only tags: marked as pre-release on GitHub, skipped by the PyPI publish workflow.
with open(os.environ.get("GITHUB_OUTPUT"), "a") as output:
output.write(
f"PRERELEASE={str(version.is_prerelease).lower()}\n")

- name: Create draft release
id: draft
uses: softprops/action-gh-release@v2
with:
tag_name: "${{ inputs.tag-prefix }}${{ needs.BumpRelease.outputs.version }}"
target_commitish: ${{ needs.BumpRelease.outputs.sha }}
draft: true
prerelease: ${{ steps.reltype.outputs.PRERELEASE == 'true' }}
generate_release_notes: true
previous_tag: ${{ inputs.previous-tag }}
body: ${{ inputs.notes }}

- name: Create step summary
env:
DRAFT_URL: ${{ steps.draft.outputs.url }}
VERSION: ${{ needs.BumpRelease.outputs.version }}
SHA: ${{ needs.BumpRelease.outputs.sha }}
run: |
echo "## Draft release for $VERSION created" >> $GITHUB_STEP_SUMMARY
echo "Tag will be created at \`$SHA\` upon publishing." >> $GITHUB_STEP_SUMMARY
echo "Review and publish here: $DRAFT_URL" >> $GITHUB_STEP_SUMMARY

BumpDev:
name: Bump to next dev version
needs: Draft
uses: ./.github/workflows/bump.yml
secrets: inherit
with:
rule: dev
branch: ${{ inputs.branch }}
24 changes: 14 additions & 10 deletions .github/workflows/publish_pypi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ jobs:
with:
python-version: "3.13"

- name: Verify tag matches package version
env:
TAG: ${{ github.event.release.tag_name }}
run: |
PKGVER=$(poetry version -s)
if [ "${TAG#v}" != "$PKGVER" ]; then
echo "::error::Release tag $TAG does not match package version $PKGVER at the tagged commit."
exit 1
fi

- name: Create step summary
run: echo "## Building $(poetry version)" >> $GITHUB_STEP_SUMMARY

Expand All @@ -45,7 +55,8 @@ jobs:
name: Publish package to PyPI
runs-on: ubuntu-latest
needs: Build
if: needs.build.result == 'success'
# Internal pre-release tags (aN/bN/rcN) get built and attached to the GitHub release above, but are never published to PyPI.
if: ${{ needs.Build.result == 'success' && !github.event.release.prerelease }}
steps:
- name: Checkout code
uses: actions/checkout@v6
Expand Down Expand Up @@ -81,12 +92,5 @@ jobs:
SUBURL="${PKGVER// //}"
echo "https://pypi.org/project/$SUBURL/" >> $GITHUB_STEP_SUMMARY

Bump:
name: Bump to (next) dev version
needs: Publish
if: ${{ needs.publish.result == 'success' }}
uses: ./.github/workflows/bump.yml
secrets: inherit
with:
rule: prerelease
branch: main
# NOTE: The former trailing "Bump to (next) dev version" job has moved to prepare_release.yml, which bumps to the next dev version
# immediately after creating the draft release, so the release version string never lingers on main while the draft awaits review.
Loading
Loading