3e779be6f3
CI / lint (push) Failing after 13m4s
CI / test (3.11, ubuntu-latest) (push) Failing after 2m4s
CI / test (3.13, ubuntu-latest) (push) Successful in 13m30s
CI / test (3.14, ubuntu-latest) (push) Successful in 17m21s
CI / test (3.12, ubuntu-latest) (push) Successful in 17m55s
CI / discover-apps-ps (push) Successful in 1m56s
CI / test (3.9, ubuntu-latest) (push) Successful in 13m17s
CI / test (3.10, ubuntu-latest) (push) Successful in 26m21s
CI / audit (push) Successful in 13m38s
Deploy site / deploy (push) Has been cancelled
CI / test (3.14, ubuntu-24.04-arm) (push) Has been cancelled
196 lines
8.2 KiB
YAML
196 lines
8.2 KiB
YAML
name: Release
|
|
on:
|
|
push:
|
|
# Only REL-prefixed marker tags create a public release. Version
|
|
# tags themselves (v0.4.0, v0.4.0rc1) trigger the four packaging
|
|
# workflows (.deb / RPM / OBS / AUR) via their `v*.*.*` globs but
|
|
# NOT this Release workflow — that's deliberate. kernalix7 found
|
|
# that rapid-iteration tags between v0.1.9 and v0.2.2.1 shipped
|
|
# unverified releases that broke users' working installs
|
|
# (2026-04-29). Releasing now requires pushing a SECOND tag pointed
|
|
# at the same commit as the version tag, namespaced with REL- so
|
|
# the version scheme itself stays clean (PEP 440 / SemVer):
|
|
#
|
|
# git tag v0.4.0 # version, triggers packaging
|
|
# git tag REL-v0.4.0 # marker, triggers this workflow
|
|
# git push origin v0.4.0 REL-v0.4.0
|
|
#
|
|
# The previous `*RTM*` glob was retired 2026-05-05 alongside the
|
|
# PEP 440 versioning move (current versions are `0.4.0` /
|
|
# `0.4.0rc1`, no RTM suffix anywhere). Manual workflow_dispatch
|
|
# still works for emergency one-offs.
|
|
tags:
|
|
- "REL-*"
|
|
workflow_dispatch:
|
|
|
|
permissions:
|
|
contents: write
|
|
# id-token: write # reserved for future PyPI trusted publisher
|
|
|
|
jobs:
|
|
validate:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v5
|
|
- uses: actions/setup-python@v5
|
|
with:
|
|
python-version: "3.13"
|
|
- run: pip install ruff
|
|
- name: Run ruff check
|
|
run: ruff check src/ tests/
|
|
- name: Run ruff format check
|
|
run: ruff format --check src/ tests/
|
|
- name: Install project
|
|
run: pip install -e ".[dev]"
|
|
- name: Run tests
|
|
run: pytest tests/ -v
|
|
# Skip tag/pyproject version parity check on manual dispatch
|
|
- name: Verify tag matches pyproject version
|
|
if: github.event_name == 'push'
|
|
run: |
|
|
# Tag layout: REL-v<pep440-version>. Strip the REL- marker
|
|
# prefix and the leading 'v' to get pyproject.toml's version
|
|
# field (e.g., REL-v0.4.0rc1 -> 0.4.0rc1, REL-v0.4.0 -> 0.4.0).
|
|
tag="${GITHUB_REF_NAME#REL-}"
|
|
tag="${tag#v}"
|
|
pyver=$(python -c "import tomllib,pathlib; print(tomllib.loads(pathlib.Path('pyproject.toml').read_text())['project']['version'])")
|
|
if [ "$tag" != "$pyver" ]; then
|
|
echo "Tag version ($tag) does not match pyproject.toml version ($pyver)" >&2
|
|
exit 1
|
|
fi
|
|
|
|
build:
|
|
needs: validate
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v5
|
|
- uses: actions/setup-python@v5
|
|
with:
|
|
python-version: "3.13"
|
|
- run: python -m pip install build twine
|
|
- name: Build distribution
|
|
run: python -m build
|
|
- name: Check distribution metadata
|
|
run: python -m twine check dist/*
|
|
- uses: actions/upload-artifact@v4
|
|
with:
|
|
name: dist
|
|
path: dist/
|
|
|
|
release:
|
|
needs: build
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v5
|
|
- uses: actions/download-artifact@v4
|
|
with:
|
|
name: dist
|
|
path: dist/
|
|
- name: Extract version from REL tag
|
|
id: version
|
|
run: |
|
|
# REL-v0.4.0 -> 0.4.0; REL-v0.4.0rc1 -> 0.4.0rc1. Used by all
|
|
# subsequent steps so the public Release page reads
|
|
# "v0.4.0" / "0.4.0" instead of the internal "REL-v0.4.0"
|
|
# marker tag name.
|
|
tag="${GITHUB_REF_NAME#REL-}"
|
|
tag="${tag#v}"
|
|
echo "value=$tag" >> "$GITHUB_OUTPUT"
|
|
- name: Extract changelog section
|
|
id: changelog
|
|
run: |
|
|
# REL-v0.4.0 -> 0.4.0; REL-v0.4.0rc1 -> 0.4.0rc1.
|
|
tag="${GITHUB_REF_NAME#REL-}"
|
|
tag="${tag#v}"
|
|
# Determine the previous published version so we can include
|
|
# any intermediate unreleased sections in the notes (sections
|
|
# the user wrote in CHANGELOG but never tagged & released).
|
|
# On 2026-05-05, v0.3.1 was such an unreleased section: it
|
|
# collected Mic92's PRs #62-#65 but no v0.3.1 tag was ever
|
|
# pushed; v0.4.0rc1's release page would have lost those
|
|
# contributor credits if it only matched ## [0.4.0].
|
|
# Strategy: find the most recent CHANGELOG section that
|
|
# corresponds to a published GitHub Release (i.e., has its own
|
|
# tag). Capture from the current section back to (but not
|
|
# including) that already-published one.
|
|
prev=""
|
|
while IFS= read -r section; do
|
|
sver="${section#\#\# \[}"
|
|
sver="${sver%%\] *}"
|
|
# Skip the in-flight version itself and the unreleased header.
|
|
[ "$sver" = "$tag" ] && continue
|
|
[ "$sver" = "Unreleased" ] && continue
|
|
# First section we hit that has a corresponding tag is the
|
|
# previous published release. CHANGELOG is in newest-first
|
|
# order so this is correct on the first match.
|
|
if git rev-parse --verify "v${sver}^{commit}" >/dev/null 2>&1; then
|
|
prev="$sver"
|
|
break
|
|
fi
|
|
done < <(grep -E '^## \[' CHANGELOG.md)
|
|
notes=$(awk -v ver="$tag" -v prev="$prev" '
|
|
$0 ~ "^## \\[" ver "\\]" { flag=1 }
|
|
prev != "" && $0 ~ "^## \\[" prev "\\]" { exit }
|
|
prev == "" && flag && /^## \[/ && $0 !~ "^## \\[" ver "\\]" { exit }
|
|
flag { print }
|
|
' CHANGELOG.md)
|
|
# PEP 440 prereleases (0.4.0rc1) usually share notes with the
|
|
# final (0.4.0) section in CHANGELOG. Fall back to the base
|
|
# version section if the prerelease-specific one is missing.
|
|
if [ -z "$(echo "$notes" | tr -d '[:space:]')" ]; then
|
|
base=$(echo "$tag" | sed -E 's/(rc|alpha|beta)[0-9]*$//')
|
|
if [ "$base" != "$tag" ]; then
|
|
notes=$(awk -v ver="$base" -v prev="$prev" '
|
|
$0 ~ "^## \\[" ver "\\]" { flag=1 }
|
|
prev != "" && $0 ~ "^## \\[" prev "\\]" { exit }
|
|
prev == "" && flag && /^## \[/ && $0 !~ "^## \\[" ver "\\]" { exit }
|
|
flag { print }
|
|
' CHANGELOG.md)
|
|
fi
|
|
fi
|
|
if [ -z "$(echo "$notes" | tr -d '[:space:]')" ]; then
|
|
notes="See CHANGELOG.md for details."
|
|
fi
|
|
{
|
|
echo "notes<<CHANGELOG_EOF"
|
|
echo "$notes"
|
|
echo "CHANGELOG_EOF"
|
|
} >> "$GITHUB_OUTPUT"
|
|
- name: Detect prerelease
|
|
id: prerelease
|
|
run: |
|
|
# Match both PEP 440 ("0.4.0rc1", no separator) and SemVer-
|
|
# hyphen ("0.4.0-rc1") prerelease forms after the REL- prefix
|
|
# is stripped.
|
|
tag="${GITHUB_REF_NAME#REL-}"
|
|
tag="${tag#v}"
|
|
if echo "$tag" | grep -qE -- '(rc|beta|alpha)[0-9]*'; then
|
|
echo "value=true" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "value=false" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
- name: Create release
|
|
uses: softprops/action-gh-release@v2
|
|
with:
|
|
# tag_name + name override the default github.ref_name
|
|
# (REL-v0.4.0 marker) with the clean version tag (v0.4.0) so
|
|
# the public release surface is namespace-free. The marker
|
|
# tag still exists in git as the trigger record.
|
|
tag_name: v${{ steps.version.outputs.value }}
|
|
name: v${{ steps.version.outputs.value }}
|
|
prerelease: ${{ steps.prerelease.outputs.value }}
|
|
# The hand-written CHANGELOG section is the entire release body —
|
|
# including its "### Contributors" subsection, which credits the
|
|
# issue reporters and external PR authors for the release (the repo's
|
|
# house convention; see CONTRIBUTING.md). generate_release_notes is
|
|
# deliberately NOT enabled: on this maintainer-authored repo it dumps
|
|
# a ~120-line "What's Changed" list that credits the owner on nearly
|
|
# every PR and buries the real contributor credits behind it. This
|
|
# step is the SOLE writer of the release body: the packaging
|
|
# workflows (.deb / RPM / OBS / AppImage) only attach assets, never
|
|
# touch the notes, so the release isn't edited/rewritten repeatedly.
|
|
body: ${{ steps.changelog.outputs.notes }}
|
|
files: |
|
|
dist/*.tar.gz
|
|
dist/*.whl
|