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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
# The release automation and the crates.io trusted publisher. This filename # RK_IF_REGISTRY
# is registered at crates.io; cargo-dist's release.yml builds binaries and # RK_IF_REGISTRY
# must never be registered there. # RK_IF_REGISTRY
# RK_IF_NO_REGISTRY # The release automation. The crate publishes to no registry: release-plz
# RK_IF_NO_REGISTRY # tags each release and publishes nothing, and cargo-dist's release.yml
# RK_IF_NO_REGISTRY # builds the binaries and the GitHub release on the tag push.
#
# Both halves run on every push to the trunk, and to a RK_LINE_PREFIX* line when a
# project keeps older lines. A third job, release-candidate, runs only when a
# person dispatches it for a line, and mints or promotes that line's release
# candidate. The pull-request half maintains the one release
# pull request against the pushed branch; the release half publishes and tags
# only on the push that lands the bot's own bump, which release_always = false
# recognizes by its release-plz-* head branch. The publish half runs free of
# any concurrency group — cancelling it could skip a release — while the
# pull-request half serializes per ref so racing pushes cannot fight over the
# release pull request.
name: release-plz
permissions:
on:
push:
branches:
# A person mints a release candidate on purpose: the line, and the tip they
# read, so a line that moved since then refuses. Nothing watches a push.
workflow_dispatch:
inputs:
line:
description: The release line, <major>.<minor>, as in 1.4
required: true
type: string
expect:
description: The line's tip you read, as a full commit SHA
required: true
type: string
promote:
description: Promote the line's newest candidate to its stable version instead of minting a new one
required: false
type: boolean
default: false
# RK_GATE_TRIGGER_BEGIN
# RK_GATE_TRIGGER_END
jobs:
# Merging the release pull request is the release: the bump push lands here,
# and this half publishes over OIDC and pushes the tag. The app token is what
# makes the tag trigger release.yml; a tag pushed with GITHUB_TOKEN starts no
# further workflow. On a RK_LINE_PREFIX* line the same job tags the line's patch.
release-plz-release:
if: github.repository_owner == 'OWNER' && github.event_name == 'push'
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # RK_IF_REGISTRY
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
fetch-depth: 0
persist-credentials: false
# The mint is scoped, not blanket: without a permission-* input the token
# carries every permission the installation holds. The release command
# tags and publishes 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@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3
id: app-token
with:
# client-id takes the client ID or the numeric App ID; GitHub accepts either as the JWT issuer: https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-json-web-token-jwt-for-a-github-app
client-id: ${{ secrets.RELEASE_BOT_APP_ID }}
private-key: ${{ secrets.RELEASE_BOT_APP_PRIVATE_KEY }}
permission-contents: write
# RK_DEV_ENV_RELEASE_BEGIN
- uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
- uses: release-plz/action@66b324fd6c72d05d2a08568492ecdd62bfb8a62b # v0.5
id: release
with:
command: release
version: "0.4.3"
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
# RK_DEV_ENV_RELEASE_END
release-plz-pr:
if: github.repository_owner == 'OWNER' && github.event_name == 'push'
runs-on: ubuntu-latest
permissions:
contents: read
concurrency:
group: release-plz-${{ github.ref }}
cancel-in-progress: false
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
fetch-depth: 0
persist-credentials: false
# This half opens and refreshes the release request and then arms it, so
# its token carries pull-requests beside contents. The release half above
# takes contents alone.
- uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3
id: app-token
with:
client-id: ${{ secrets.RELEASE_BOT_APP_ID }}
private-key: ${{ secrets.RELEASE_BOT_APP_PRIVATE_KEY }}
permission-contents: write
permission-pull-requests: write
# RK_DEV_ENV_REQUEST_BEGIN
- uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
# A line that carries a candidate's version keeps this half out of the
# candidate numbering: release-plz would propose the next rc in a
# request. The request returns once the promotion writes the stable
# version, and it then proposes that version with its changelog. Cargo
# reads the version, so any valid manifest form and an inherited
# workspace version count, and a version it cannot read fails the job.
- name: hold the request while the line carries a candidate
id: candidate
env:
REF: ${{ github.ref_name }}
run: |
set -eu
case "$REF" in
RK_LINE_PREFIX*) ;;
*)
echo "held=false" >> "$GITHUB_OUTPUT"
exit 0
;;
esac
metadata="$(cargo metadata --no-deps --format-version 1 --offline)"
version="$(printf '%s' "$metadata" | jq -r --arg manifest "$PWD/Cargo.toml" '.packages[] | select(.manifest_path == $manifest) | .version')"
if [ -z "$version" ]; then
echo "cargo names no package version for $PWD/Cargo.toml; no request opens past an unreadable manifest." >&2
exit 1
fi
case "$version" in
*-*)
echo "$REF carries the candidate $version; no release request until it is promoted."
echo "held=true" >> "$GITHUB_OUTPUT"
;;
*)
echo "held=false" >> "$GITHUB_OUTPUT"
;;
esac
- uses: release-plz/action@66b324fd6c72d05d2a08568492ecdd62bfb8a62b # v0.5
id: release-pr
if: steps.candidate.outputs.held != 'true'
with:
command: release-pr
version: "0.4.3"
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
# RK_DEV_ENV_REQUEST_END
# RK_GATE_ARM_BEGIN
# Arm the request the moment it exists: the forge merges it once every
# required check is green, which is the trunk style's release decision,
# made once at landing rather than per release; the lines style leaves
# every request for a human and this step steps aside. Two things keep
# the arm correct. The app token is one: 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.
# Re-arming is the other: arming an armed request is a no-op, so every
# refresh re-arms and the arm cannot drift out of sync.
- 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-pr.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" != "RK_TRUNK_BRANCH" ]; 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 RK_MERGE_FLAG --delete-branch
echo "armed pull request #$number."
# RK_GATE_ARM_END
# A release candidate, minted on purpose. The job checks out the requested
# line under the app token and persists it, because rk pushes the
# candidate's version before release-plz configures git; the push starts
# the push jobs above, which publish nothing for a commit no release
# request carries. release-plz then releases that commit under a config
# derived from release-plz.toml with release_always = true, the one key
# that releases a commit no request carries. It runs in this file because
# a registry binds its publishing to the workflow file, so a second file
# could not publish. A promotion writes the stable version alone, and the
# line's release request proposes it.
release-candidate:
if: github.repository_owner == 'OWNER' && github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # RK_IF_REGISTRY
concurrency:
group: release-candidate-${{ inputs.line }}
cancel-in-progress: false
steps:
- uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3
id: app-token
with:
client-id: ${{ secrets.RELEASE_BOT_APP_ID }}
private-key: ${{ secrets.RELEASE_BOT_APP_PRIVATE_KEY }}
permission-contents: write
# release-plz looks up the pull requests of the released commit
# before it reads release_always, and that lookup needs this read
# on a private repository.
permission-pull-requests: read
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
ref: RK_LINE_PREFIX${{ inputs.line }}
fetch-depth: 0
# zizmor: ignore[artipacked] the persisted credential is what pushes
# the candidate's version and reads the line's remote tags below.
token: ${{ steps.app-token.outputs.token }}
- name: configure git as the release app
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
APP_SLUG: ${{ steps.app-token.outputs.app-slug }}
run: |
set -eu
uid="$(gh api "/users/${APP_SLUG}[bot]" -q .id)"
git config user.name "${APP_SLUG}[bot]"
git config user.email "${uid}+${APP_SLUG}[bot]@users.noreply.github.com"
- name: derive the candidate's release config
if: ${{ !inputs.promote }}
run: |
set -eu
count="$(grep -c '^release_always = false$' release-plz.toml || true)"
if [ "$count" != "1" ]; then
echo "release-plz.toml carries 'release_always = false' $count times; the candidate's config flips exactly one." >&2
exit 1
fi
sed 's/^release_always = false$/release_always = true/' release-plz.toml > "$RUNNER_TEMP/release-plz-rc.toml"
# RK_DEV_ENV_CANDIDATE_BEGIN
- name: install rk
env:
RK_VERSION: "RK_SELF_VERSION"
run: |
set -eu
curl --proto '=https' --tlsv1.2 -LsSf "RK_SELF_RELEASES/v${RK_VERSION}/release-kit-installer.sh" | RELEASE_KIT_UNMANAGED_INSTALL="$RUNNER_TEMP/rk" sh
echo "$RUNNER_TEMP/rk" >> "$GITHUB_PATH"
- name: mint
env:
LINE: ${{ inputs.line }}
EXPECT: ${{ inputs.expect }}
PROMOTE: ${{ inputs.promote }}
run: |
set -eu
if [ "$PROMOTE" = "true" ]; then
rk lines mint "$LINE" --promote --expect "$EXPECT" --apply
else
rk lines mint "$LINE" --expect "$EXPECT" --apply
fi
- uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
if: ${{ !inputs.promote }}
- uses: release-plz/action@66b324fd6c72d05d2a08568492ecdd62bfb8a62b # v0.5
if: ${{ !inputs.promote }}
with:
command: release
version: "0.4.3"
config: ${{ runner.temp }}/release-plz-rc.toml
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
# RK_DEV_ENV_CANDIDATE_END
# RK_GATE_JOB_BEGIN
# RK_GATE_JOB_END