From 7d55a68fd738174cc3a11b76244a905a0e4e25b2 Mon Sep 17 00:00:00 2001 From: Michael van Tellingen Date: Mon, 26 Oct 2026 22:24:12 +0100 Subject: [PATCH] ci: release with changie and changie-release-action v2 Replace the manual prepare-release workflow with a release job in the test workflow. It maintains a release PR and, once merged, tags the version and creates the GitHub release with the release app token. python-release.yml (the PyPI trusted publisher) now runs on that release instead of on CHANGELOG.md pushes: it builds, publishes to PyPI, attaches the dist files and SBOMs to the release and uploads the SBOM to DependencyTrack, as before. --- .changie.yaml | 4 ++ .github/workflows/prepare-release.yml | 52 --------------- .github/workflows/python-release.yml | 95 +++++---------------------- .github/workflows/test.yml | 40 +++++++++++ README.md | 4 +- 5 files changed, 63 insertions(+), 132 deletions(-) delete mode 100644 .github/workflows/prepare-release.yml diff --git a/.changie.yaml b/.changie.yaml index 8768d7d..d400e50 100644 --- a/.changie.yaml +++ b/.changie.yaml @@ -25,12 +25,16 @@ kinds: - label: Security key: security auto: patch + - label: Dependency + key: dependency + auto: patch newlines: afterChangelogHeader: 1 afterChangelogVersion: 1 beforeKind: 1 afterKind: 1 endOfVersion: 1 +# uv.lock also contains the version, the release job runs `uv lock` replacements: - path: pyproject.toml find: '^version = ".*"$' diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml deleted file mode 100644 index a14d4b9..0000000 --- a/.github/workflows/prepare-release.yml +++ /dev/null @@ -1,52 +0,0 @@ -name: Prepare release PR - -on: - workflow_dispatch: - inputs: - version: - description: "Release version, or major/minor/patch/auto" - required: true - default: "auto" - -permissions: - contents: write - pull-requests: write - -jobs: - prepare-release: - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: Batch changelog fragments - uses: miniscruff/changie-action@3223cb29b92a0972dedc864ace3da01e04780ae4 # v3.0.1 - with: - version: latest - args: batch ${{ inputs.version }} --allow-no-changes=false - - - name: Merge changelog - uses: miniscruff/changie-action@3223cb29b92a0972dedc864ace3da01e04780ae4 # v3.0.1 - with: - version: latest - args: merge - - - name: Read release version - id: latest - uses: miniscruff/changie-action@3223cb29b92a0972dedc864ace3da01e04780ae4 # v3.0.1 - with: - version: latest - args: latest - - - name: Create release pull request - uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 - with: - title: Release ${{ steps.latest.outputs.output }} - branch: release/${{ steps.latest.outputs.output }} - commit-message: Release ${{ steps.latest.outputs.output }} - body: | - Batches changie fragments and updates the changelog for `${{ steps.latest.outputs.output }}`. - - Merge this pull request to publish the package and create the GitHub release. diff --git a/.github/workflows/python-release.yml b/.github/workflows/python-release.yml index 87f6bf3..253efae 100644 --- a/.github/workflows/python-release.yml +++ b/.github/workflows/python-release.yml @@ -1,13 +1,11 @@ name: Release +# The release job in test.yml creates the GitHub release (and tag) with the +# release app token once the changie release PR is merged, which triggers this +# workflow. It is the PyPI trusted publisher, so keep its name and environment. on: - push: - branches: ["main", "master"] - paths: - - "CHANGELOG.md" - # Retry a release whose publish failed; the existing-release check below - # still prevents publishing the same version twice. - workflow_dispatch: + release: + types: [created] permissions: {} @@ -28,91 +26,37 @@ jobs: fetch-depth: 0 persist-credentials: false - - name: Set up Python - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 - with: - python-version: "3.13" - - name: Install uv uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 with: enable-cache: false - - name: Read release version + - name: Check release version id: release - run: | - version=$(python - <<'PY' - import tomllib - from pathlib import Path - - pyproject = tomllib.loads(Path("pyproject.toml").read_text()) - print(pyproject["project"]["version"]) - PY - ) - notes=".changes/${version}.md" - if [ ! -f "$notes" ]; then - echo "Missing release notes: $notes" >&2 - exit 1 - fi - echo "version=$version" >> "$GITHUB_OUTPUT" - echo "notes=$notes" >> "$GITHUB_OUTPUT" - - - name: Check version changed - id: version-changed env: - BEFORE_SHA: ${{ github.event.before }} - EVENT_NAME: ${{ github.event_name }} - VERSION: ${{ steps.release.outputs.version }} + TAG: ${{ github.event.release.tag_name }} run: | - # Manual runs have no "before" commit; rely on the existing-release check. - if [ "${EVENT_NAME}" = "workflow_dispatch" ]; then - echo "changed=true" >> "$GITHUB_OUTPUT" - exit 0 - fi - previous_version=$(git show "${BEFORE_SHA}:pyproject.toml" | python -c ' - import sys - import tomllib - - print(tomllib.loads(sys.stdin.read())["project"]["version"]) - ' - ) - if [ "$previous_version" = "${VERSION}" ]; then - echo "changed=false" >> "$GITHUB_OUTPUT" - else - echo "changed=true" >> "$GITHUB_OUTPUT" - fi - - - name: Check for existing release - id: existing-release - if: steps.version-changed.outputs.changed == 'true' - env: - GH_TOKEN: ${{ github.token }} - VERSION: ${{ steps.release.outputs.version }} - run: | - if gh release view "${VERSION}" >/dev/null 2>&1; then - echo "exists=true" >> "$GITHUB_OUTPUT" - else - echo "exists=false" >> "$GITHUB_OUTPUT" + version=$(uv version --short) + if [ "${version}" != "${TAG}" ]; then + echo "::error::Release ${TAG} does not match the package version ${version}" + exit 1 fi + echo "version=${version}" >> "$GITHUB_OUTPUT" - name: Build package - if: steps.version-changed.outputs.changed == 'true' && steps.existing-release.outputs.exists == 'false' run: uv build # Package plus locked runtime dependencies only, so the SBOM matches what # users install from PyPI rather than the dev/test toolchain. - name: Install runtime environment - if: steps.version-changed.outputs.changed == 'true' && steps.existing-release.outputs.exists == 'false' run: uv sync --frozen --no-default-groups --no-editable - uses: anchore/sbom-action/download-syft@66cbf4bc1f1c0d2edc94016e65bc221b6bb0ad6c # v0.24.3 - if: steps.version-changed.outputs.changed == 'true' && steps.existing-release.outputs.exists == 'false' id: syft # Scan site-packages only; .venv/bin links to the interpreter outside the env. # SPDX for the release page, CycloneDX 1.6 for DependencyTrack. - name: Generate SBOM - if: steps.version-changed.outputs.changed == 'true' && steps.existing-release.outputs.exists == 'false' env: SYFT: ${{ steps.syft.outputs.cmd }} SYFT_FILE_METADATA_SELECTION: none @@ -129,24 +73,18 @@ jobs: django-postcode-lookup.raw.cdx.json > django-postcode-lookup.cdx.json - name: Publish package - if: steps.version-changed.outputs.changed == 'true' && steps.existing-release.outputs.exists == 'false' uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 - - name: Create GitHub release - if: steps.version-changed.outputs.changed == 'true' && steps.existing-release.outputs.exists == 'false' + - name: Upload release assets env: GH_TOKEN: ${{ github.token }} - VERSION: ${{ steps.release.outputs.version }} - NOTES: ${{ steps.release.outputs.notes }} + TAG: ${{ github.event.release.tag_name }} run: | - gh release create "${VERSION}" \ - --title "${VERSION}" \ - --notes-file "${NOTES}" \ + gh release upload "${TAG}" \ dist/* django-postcode-lookup.spdx.json django-postcode-lookup.cdx.json - name: Mark release published id: published - if: steps.version-changed.outputs.changed == 'true' && steps.existing-release.outputs.exists == 'false' run: echo "released=true" >> "$GITHUB_OUTPUT" - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 @@ -157,8 +95,7 @@ jobs: retention-days: 7 # Separate job so the DT key is never present while repo code runs - # (uv build / uv sync above execute the build backend). Releases are created - # with GITHUB_TOKEN, which doesn't trigger release workflows, so this lives here. + # (uv build / uv sync above execute the build backend). dependencytrack: needs: release if: needs.release.outputs.released == 'true' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f0a1ee1..c91e131 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -52,3 +52,43 @@ jobs: - name: Run tests run: uv run --group test pytest + + release: + runs-on: ubuntu-latest + needs: [tests] + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + permissions: + contents: read + concurrency: + group: release + cancel-in-progress: false + steps: + - name: Create release app token + id: app-token + uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0 + with: + app-id: ${{ vars.RELEASE_APP_ID }} + private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} + permission-contents: write + permission-pull-requests: write + + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Install uv + uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 + with: + enable-cache: false + + # Creates or updates the release PR, or tags and releases the version + # once it is merged. The GitHub release is created with the app token, + # so it triggers python-release.yml, which publishes to PyPI. + - name: Release + uses: labd/changie-release-action@059406879b04f6e073547b325fbbff9a573038e3 # v2.0.0 + with: + github-token: ${{ steps.app-token.outputs.token }} + # uv.lock contains the project version as well + version-command: uv lock + create-release: true diff --git a/README.md b/README.md index 17c0523..a832dab 100644 --- a/README.md +++ b/README.md @@ -84,4 +84,6 @@ Add a changelog fragment in pull requests with: changie new ``` -To prepare a release, run the `Prepare release PR` GitHub Actions workflow. It batches unreleased fragments, updates `CHANGELOG.md` and version files, then opens a release pull request. Merging that release PR publishes the package and creates the GitHub release from the changelog entry. +When fragments are merged to `master`, a release pull request is opened (or +updated) automatically. Merging that pull request tags the release, creates the +GitHub release and publishes the package to PyPI.