Skip to content

Commit 4ceded3

Browse files
committed
Automate release publishing with workflows
1 parent 40d1d99 commit 4ceded3

File tree

5 files changed

+193
-25
lines changed

5 files changed

+193
-25
lines changed

.github/workflows/release.yaml

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
# Verify and publish releases created by push to `release/candidate`
2+
name: release
3+
run-name: publish ${{ github.event.workflow_run.head_commit.message }}
4+
5+
on:
6+
workflow_run:
7+
workflows: ["Check and test"]
8+
branches: ["release/candidate"]
9+
types:
10+
- completed
11+
12+
jobs:
13+
build:
14+
runs-on: ubuntu-latest
15+
if: ${{ github.event.workflow_run.conclusion == 'success' }}
16+
outputs:
17+
version: ${{ steps.version.outputs.version }}
18+
steps:
19+
- uses: actions/checkout@v5
20+
with:
21+
ref: ${{ github.event.workflow_run.head_sha }}
22+
- run: pip install poetry
23+
- uses: actions/setup-python@v6
24+
with:
25+
cache: poetry
26+
- name: Get version
27+
id: version
28+
run: |
29+
version=$(poetry version --short)
30+
echo "version=${version}" >> "$GITHUB_OUTPUT"
31+
- run: poetry build
32+
- uses: actions/upload-artifact@v4
33+
with:
34+
name: python-package-dist
35+
path: dist/
36+
37+
verify:
38+
needs: build
39+
runs-on: ubuntu-latest
40+
if: ${{ github.event.workflow_run.conclusion == 'success' }}
41+
steps:
42+
- uses: actions/checkout@v5
43+
with:
44+
ref: ${{ github.event.workflow_run.head_sha }}
45+
- name: Verify release notes
46+
run: pipx run dev/get_release_notes.py
47+
- name: Verify commit message format
48+
run: |
49+
version="${{ needs.build.outputs.version }}"
50+
expected="release: v${version}"
51+
52+
if [[ "$COMMIT_MESSAGE" != "$expected" ]]; then
53+
echo "❌ Invalid commit message format" | tee -a "$GITHUB_STEP_SUMMARY"
54+
echo "❙ Actual: $COMMIT_MESSAGE" | tee -a "$GITHUB_STEP_SUMMARY"
55+
echo "❙ Expected: $expected" | tee -a "$GITHUB_STEP_SUMMARY"
56+
exit 1
57+
fi
58+
env:
59+
COMMIT_MESSAGE: ${{ github.event.workflow_run.head_commit.message }}
60+
61+
# Create release tag vX.Y.Z and merge release candidate into `main`
62+
publish-git:
63+
needs: [verify, build]
64+
permissions:
65+
contents: write
66+
runs-on: ubuntu-latest
67+
steps:
68+
- uses: actions/checkout@v5
69+
with:
70+
fetch-depth: 0
71+
ref: ${{ github.event.workflow_run.head_sha }}
72+
73+
- name: Create and push git tag
74+
continue-on-error: true
75+
run: |
76+
tag="v${VERSION}"
77+
78+
if [ -n "$(git tag -l "${tag}")" ]; then
79+
echo "⚠️ Tag ${tag} already exists, skipping tag creation" | tee -a "$GITHUB_STEP_SUMMARY"
80+
exit 1
81+
fi
82+
83+
git config user.name "github-actions[bot]"
84+
git config user.email "github-actions[bot]@users.noreply.github.com"
85+
git tag "${tag}"
86+
git push origin "${tag}"
87+
echo "✅ Created tag ${tag}" | tee -a "$GITHUB_STEP_SUMMARY"
88+
env:
89+
VERSION: ${{ needs.build.outputs.version }}
90+
91+
- name: Update main branch
92+
run: |
93+
if git push origin HEAD:refs/heads/main; then
94+
echo "✅ Merged release into main branch" | tee -a "$GITHUB_STEP_SUMMARY"
95+
else
96+
echo "⚠️ Failed to merge release into main branche" | tee -a "$GITHUB_STEP_SUMMARY"
97+
fi
98+
99+
publish-github-release:
100+
needs: [verify, build]
101+
permissions:
102+
contents: write
103+
runs-on: ubuntu-latest
104+
steps:
105+
- uses: actions/checkout@v5
106+
with:
107+
ref: ${{ github.event.workflow_run.head_sha }}
108+
- name: Create GitHub Release
109+
run: |
110+
name="v${VERSION}"
111+
112+
if gh release view "${name}" >/dev/null 2>&1; then
113+
echo "⚠️ Release ${name} already exists, skipping release creation" | tee -a "$GITHUB_STEP_SUMMARY"
114+
exit 0
115+
fi
116+
117+
release_notes=$(pipx run dev/get_release_notes.py)
118+
119+
gh release create "${name}" \
120+
--title "${name}" \
121+
--notes "${release_notes}" \
122+
--target "${TARGET_SHA}"
123+
echo "✅ GitHub release [${name}](${{ github.server_url }}/${{ github.repository }}/releases/tag/${name}) created" | tee -a "$GITHUB_STEP_SUMMARY"
124+
env:
125+
GH_TOKEN: ${{ github.token }}
126+
VERSION: ${{ needs.build.outputs.version }}
127+
TARGET_SHA: ${{ github.event.workflow_run.head_sha }}
128+
129+
# Publish the package to PyPi
130+
publish-pypi:
131+
needs: [verify, build]
132+
permissions:
133+
id-token: write
134+
runs-on: ubuntu-latest
135+
environment:
136+
name: pypi
137+
url: https://pypi.org/p/oidc-provider-mock
138+
steps:
139+
- uses: actions/download-artifact@v5
140+
with:
141+
name: python-package-dist
142+
path: dist/
143+
144+
- uses: pypa/gh-action-pypi-publish@release/v1
145+
with:
146+
repository-url: https://upload.pypi.org/legacy/

DEVELOPING.md

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -25,26 +25,6 @@ To release a new version of this project follow these steps:
2525
1. Replace the “Upcoming” heading of the changelog with the new version number
2626
and date of release.
2727
1. Update the version in `pyproject.toml`
28-
1. Commit the changes with the commit message “Release vX.Y.Z”
28+
1. Commit the changes with the commit message “release: vX.Y.Z”
2929
1. Push the changes `git push origin` and wait for the build to pass.
30-
1. Tag the branch with a signed and annotated tag: `git tag -as vX.Y.Z`.
31-
Use the version and date as the tag title and the changelog entry as the tag
32-
body. E.g.
33-
34-
```plain
35-
v0.10.0 - 2019-08-25
36-
37-
* Symlink views now support relative symlinks (@daviddavo)
38-
```
39-
40-
1. Push the main branch and tag with `git push --tags`
41-
1. Create a release on Github using the version as the title and the changelog
42-
entries as the description.
43-
1. Publish the new version to PyPI with `poetry publish`.
44-
1. Integrate the release branch
45-
46-
```bash
47-
git checkout main
48-
git merge --ff release/candidate
49-
git push origin
50-
```
30+
1. Wait for the release workflow to publish the release

dev/get_release_notes.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#!/usr/bin/env python
2+
3+
# /// script
4+
# dependencies = ["tomli"]
5+
# ///
6+
7+
8+
import re
9+
from itertools import dropwhile, takewhile
10+
from pathlib import Path
11+
12+
import tomli
13+
14+
15+
def _validate_heading_format(heading: str, version: str):
16+
pattern = rf"^## v{re.escape(version)} - \d{{4}}-\d{{2}}-\d{{2}}$"
17+
if not re.match(pattern, heading.strip()):
18+
raise ValueError(
19+
"Invalid heading format in CHANGELOG.md\n"
20+
f"Actual: {heading.strip()}\n"
21+
f"Expected ## v{version} - YYYY-MM-DD"
22+
)
23+
24+
25+
def _get_release_notes():
26+
with Path("pyproject.toml").open("rb") as f:
27+
data = tomli.load(f)
28+
version = data["tool"]["poetry"]["version"]
29+
release_notes = []
30+
31+
with Path("CHANGELOG.md").open() as lines:
32+
lines = dropwhile(lambda line: not line.startswith("## "), lines)
33+
heading = next(lines)
34+
_validate_heading_format(heading, version)
35+
assert next(lines) == "\n", "Expected empty line after release heading"
36+
release_notes = list(takewhile(lambda line: not line.startswith("## "), lines))
37+
38+
return "".join(release_notes).rstrip()
39+
40+
41+
if __name__ == "__main__":
42+
print(_get_release_notes()) # noqa: T201

poetry.lock

Lines changed: 2 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ ruff = "^0.13.0"
4141
typeguard = "^4.1.5"
4242
pillow = "^11.1.0"
4343
pytest-watcher = "^0.4.3"
44+
tomli = "^2.2.1"
4445

4546
[tool.pytest.ini_options]
4647
addopts = "--cov --cov-report=term --cov-report=html --cov-branch --doctest-modules"

0 commit comments

Comments
 (0)