1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# The release automation and the PyPI trusted publisher. This filename is
# registered at PyPI, together with the pypi environment the publish job runs
# in; no other workflow may carry id-token: write.
#
# Both jobs run on every push to the trunk, and to a release/* line when a
# project keeps older lines. release-please maintains the one release pull
# request against the pushed branch — it only bumps pyproject.toml and writes
# the changelog, because skip-github-release in release-please-config.json
# keeps it from tagging — and merging that request is the release: the bump
# push is what makes tag-and-publish tag the commit, build the distributions,
# and publish them over OIDC. On a release/* line the same pair backports a
# patch and tags it there.
name: release-please
permissions:
on:
push:
branches:
jobs:
release-please-pr:
if: github.repository_owner == 'OWNER'
runs-on: ubuntu-latest
permissions:
contents: read
concurrency:
group: release-please-${{ github.ref }}
cancel-in-progress: false
steps:
# The mint is scoped, not blanket: without a permission-* input the token
# carries every permission the installation holds. This half writes the
# bump branch and opens, refreshes, and arms the request, so it takes
# contents and pull-requests, which is the pair release-please documents.
- uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
id: app-token
with:
app-id: ${{ secrets.RELEASE_BOT_APP_ID }}
private-key: ${{ secrets.RELEASE_BOT_APP_PRIVATE_KEY }}
permission-contents: write
permission-pull-requests: write
- uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5
id: release-please
with:
token: ${{ steps.app-token.outputs.token }}
target-branch: ${{ github.ref_name }}
# Arm the request the moment it exists: the forge merges it once every
# required check is green, the trunk style's release decision made once
# at landing; the lines style leaves every merge a human's and this
# step steps aside. The app token matters: auto-merge enabled under
# GITHUB_TOKEN would merge a bump whose push starts no workflow,
# leaving it untagged and unpublished with nothing reporting a failure.
# Arming an armed request is a no-op, so every refresh re-arms.
- name: arm the release request
env:
RELEASE_STYLE: RK_STYLE
GH_TOKEN: ${{ steps.app-token.outputs.token }}
GH_REPO: ${{ github.repository }}
REF: ${{ github.ref_name }}
RELEASE_PR: ${{ steps.release-please.outputs.pr }}
run: |
set -eu
if [ "$RELEASE_STYLE" != "trunk" ]; then
echo "the lines style merges each release by hand; not arming."
exit 0
fi
if [ "$REF" != "master" ]; then
echo "a line's request is never armed; its candidate is what a human validated."
exit 0
fi
number="$(printf '%s' "${RELEASE_PR:-}" | sed -n 's/.*"number":[[:space:]]*\([0-9]*\).*/\1/p' | head -n 1)"
if [ -z "$number" ]; then
echo "no release request to arm."
exit 0
fi
gh pr merge "$number" --auto --squash --delete-branch
echo "armed pull request #$number."
# The publish half. Only the push that lands the bot's own bump releases:
# the detect step compares the committed version against the parent commit
# and against the tags, so an ordinary work merge builds and publishes
# nothing. The app token is what lets the tag land under protection; the
# OIDC token is minted only here, inside the pypi environment the trusted
# publisher names.
tag-and-publish:
if: github.repository_owner == 'OWNER'
runs-on: ubuntu-latest
environment: pypi
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
fetch-depth: 0
persist-credentials: false
- name: detect the pending release
id: bump
env:
SHA: ${{ github.sha }}
run: |
set -eu
version="$(sed -n 's/^version *= *"\([^"]*\)".*/\1/p' pyproject.toml | head -n 1)"
if [ -z "$version" ]; then
echo "::error::could not read the version from pyproject.toml."
exit 1
fi
previous="$(git show "$SHA^:pyproject.toml" | sed -n 's/^version *= *"\([^"]*\)".*/\1/p' | head -n 1)"
if [ "$previous" = "$version" ]; then
echo "this commit does not bump the version; nothing to release."
echo "pending=false" >> "$GITHUB_OUTPUT"
exit 0
fi
if [ -n "$(git ls-remote --tags origin "refs/tags/v$version")" ]; then
echo "v$version is already tagged; no release is pending."
echo "pending=false" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "pending=true" >> "$GITHUB_OUTPUT"
echo "version=$version" >> "$GITHUB_OUTPUT"
# This half creates one tag ref and opens no pull request, so contents
# alone carries it. A target whose tag ruleset blocks creation needs
# administration here too; the convention's ruleset restricts deletion
# and update only.
- uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
if: steps.bump.outputs.pending == 'true'
id: app-token
with:
app-id: ${{ secrets.RELEASE_BOT_APP_ID }}
private-key: ${{ secrets.RELEASE_BOT_APP_PRIVATE_KEY }}
permission-contents: write
- name: tag the release commit
if: steps.bump.outputs.pending == 'true'
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
GH_REPO: ${{ github.repository }}
SHA: ${{ github.sha }}
VERSION: ${{ steps.bump.outputs.version }}
run: |
set -eu
gh api -X POST "repos/$GH_REPO/git/refs" \
-f "ref=refs/tags/v$VERSION" -f "sha=$SHA" >/dev/null
echo "tagged v$VERSION at $SHA."
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
if: steps.bump.outputs.pending == 'true'
with:
python-version: "3.x"
- name: build the distributions
if: steps.bump.outputs.pending == 'true'
run: |
python -m pip install --upgrade build
python -m build
- name: publish over OIDC
if: steps.bump.outputs.pending == 'true'
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
with:
skip-existing: true