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
4 changes: 4 additions & 0 deletions .changie.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ".*"$'
Expand Down
52 changes: 0 additions & 52 deletions .github/workflows/prepare-release.yml

This file was deleted.

95 changes: 16 additions & 79 deletions .github/workflows/python-release.yml
Original file line number Diff line number Diff line change
@@ -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: {}

Expand All @@ -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
Expand All @@ -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
Expand All @@ -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'
Expand Down
40 changes: 40 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Loading