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
# The whole release pipeline: there is no registry, so publishing is tagging
# and the tarball attached to the release page is the distribution.
#
# Both release jobs 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. Each job calls rk, which holds the release logic
# and reads the driver, the style, and the integration from the landing record.
# release-request runs rk release request, which keeps the one release pull
# request, VERSION plus the changelog, against the pushed branch
# (release-engine:one-request-per-base). Merging that request is the release:
# the release job runs rk release build, which releases only the commit that
# merges the request (release-engine:a-final-version-releases-at-its-request),
# then attests the artifacts, then runs rk release publish, which creates the
# release page and its tag last (release-engine:the-tag-is-created-last).
#
# The attestation stays in this workflow, because only the run that built the
# artifacts holds the identity that signs them. rk cannot mint it.
name: release
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:
# The release request. rk opens it on rk-release/<base>, refreshes it in
# place on every push, and arms it where the landing arms it
# (release-engine:the-request-arms-where-the-projection-arms).
release-request:
if: github.repository_owner == 'OWNER' && github.event_name == 'push'
runs-on: ubuntu-latest
permissions:
contents: read
concurrency:
group: release-request-${{ 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 pushes the
# request branch and opens, refreshes, and arms the request, so it takes
# contents and pull-requests.
- 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
permission-pull-requests: write
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
fetch-depth: 0
# zizmor: ignore[artipacked] the persisted credential is the app
# token rk pushes the request branch with.
token: ${{ steps.app-token.outputs.token }}
# RK_DEV_ENV_REQUEST_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: install git-cliff
run: pipx install git-cliff==2.14.2
# RK_DEV_ENV_REQUEST_END
# The token is the release app's, never GITHUB_TOKEN: a push made with
# the default token starts no workflow, and rk refuses it
# (release-engine:the-request-pushes-as-the-bot).
- name: maintain the release request
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: RK_DEV_RUN rk release request
# The release itself. rk release build decides from the commit and the
# forge whether this push releases, and skips every push that does not.
# Where it releases, it builds the tarball with the project's build command
# and reports the artifacts. The attestation follows, and only then does rk
# release publish create the release and its tag, so nothing publicly
# reachable exists before its attestation does.
release:
if: github.repository_owner == 'OWNER' && github.event_name == 'push'
runs-on: ubuntu-latest
permissions:
contents: read
# Minting the build-provenance attestation: the OIDC token identifies
# this run to Sigstore, and the other two write the attestation back.
id-token: write
attestations: write
artifact-metadata: write
steps:
# This half creates the release, whose publication creates the tag, and
# reads the pull request the pushed commit merges, because only that
# merge releases a final version. It opens no pull request, so it takes
# contents and a read of pull-requests. 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: ${{ secrets.RELEASE_BOT_APP_ID }}
private-key: ${{ secrets.RELEASE_BOT_APP_PRIVATE_KEY }}
permission-contents: write
permission-pull-requests: read
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
fetch-depth: 0
persist-credentials: false
# RK_DEV_ENV_RELEASE_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: install git-cliff
run: pipx install git-cliff==2.14.2
# RK_DEV_ENV_RELEASE_END
- name: install the build packages
run: sudo apt-get update -qq && sudo apt-get install -y --no-install-recommends RK_BUILD_PACKAGES
# The build writes its state, release, resume, or skip, and the
# artifacts it built to the step outputs.
- name: build
id: build
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: RK_DEV_RUN rk release build
# Provenance, minted before anything is publishable. The run that built
# the artifacts signs them, so a consumer can check which workflow and
# commit produced them instead of trusting the release page the
# checksum also sits on. A rerun at the same commit rebuilds the same
# tarball and attests it again before rk touches the page.
- name: attest
if: steps.build.outputs.state == 'release' || steps.build.outputs.state == 'resume'
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4
with:
subject-path: ${{ steps.build.outputs.artifacts }}
# The tag is created here and nowhere else: rk creates a draft release,
# uploads every asset, and publishes the draft, and the publication
# creates the tag. It skips where the build skipped, finishes a draft a
# failed run left, and stops on a tag at another commit
# (release-engine:every-step-converges).
- name: publish
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: RK_DEV_RUN rk release publish
# A release candidate, minted on purpose. The job checks out the requested
# line under the app token and persists it, because rk lines mint reads the
# line's remote tags and pushes the candidate's version, VERSION,
# with it. A push made with the default token starts no workflow, and the
# release job above runs on that push: rk release build releases a
# candidate at the commit that writes it, and rk release publish creates
# its prerelease page and its tag (release-engine:a-candidate-is-a-prerelease).
# So this job publishes nothing and creates no tag. A promotion writes the
# stable version alone, its push releases nothing, and the line's release
# request proposes it (release-candidates:a-promotion-writes-only-tested-code).
release-candidate:
if: github.repository_owner == 'OWNER' && github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
permissions:
contents: read
concurrency:
group: release-candidate-${{ inputs.line }}
cancel-in-progress: false
steps:
# rk reads the line's tags and pushes one commit, and opens, reads, and
# tags nothing else, so the token 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
- 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"
# 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"
# RK_DEV_ENV_CANDIDATE_END
# A candidate the line already carries with no tag yet is pending: rk
# reports it, writes and pushes nothing, and exits 0, and a rerun of
# the release job publishes it
# (release-candidates:a-pending-candidate-is-published-not-reminted).
- name: mint
env:
LINE: ${{ inputs.line }}
EXPECT: ${{ inputs.expect }}
PROMOTE: ${{ inputs.promote }}
run: |
set -eu
if [ "$PROMOTE" = "true" ]; then
RK_DEV_RUN rk lines mint "$LINE" --promote --expect "$EXPECT" --apply
else
RK_DEV_RUN rk lines mint "$LINE" --expect "$EXPECT" --apply
fi
# RK_GATE_JOB_BEGIN
# RK_GATE_JOB_END