Repository navigation
314 lines (296 loc) · 13.5 KB
/
Copy pathrelease.yml
File metadata and controls
314 lines (296 loc) · 13.5 KB
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
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
# Trusted pipeline for pushes to main: run the full validation (ci.yml, the same jobs a PR gets),
# and only once it is green let release-please cut a release, attach the CLI binaries, publish it,
# and move the floating major tag. Nothing here runs for pull requests, so write tokens never meet
# unreviewed code. Modelled on gitops-reverser's release.yml.
#
# release-please lives here rather than behind a tag trigger: tags created with GITHUB_TOKEN never
# trigger other workflows, so anything published for a release must chain behind it in this run.
#
# A release is cut as a draft (release-please-config.json), every asset is attached to the draft,
# and only then is it published. A failed publish job leaves an unpublished draft, never a
# half-populated release. Immutable releases are enabled, so a published release refuses new assets
# and its tag cannot move: everything must be attached while it is a draft.
#
# A draft left behind by a failed run is finished by hand: run this workflow (workflow_dispatch)
# with its tag. It builds from the draft's own commit, attaches the assets, publishes and moves the
# major tag. release-please has already marked that release as tagged, so a new push would not.
#
# To list a release on the Marketplace, publish-release waits for an approval on the `release`
# environment; see that job and docs/github-action.md §6.
name: Release
on:
push:
branches: [main]
workflow_dispatch:
inputs:
tag:
description: An existing draft release to finish, e.g. v0.1.0
required: true
permissions:
contents: read
# Never cancel a release mid-run; a follow-up push queues behind it.
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
jobs:
ci:
name: CI
uses: ./.github/workflows/ci.yml
permissions:
contents: read
packages: write
secrets: inherit
release-please:
name: Release Please
runs-on: ubuntu-latest
timeout-minutes: 10
needs: ci
if: github.event_name == 'push'
permissions:
contents: write
pull-requests: write
outputs:
release_created: ${{ steps.release.outputs.release_created }}
tag_name: ${{ steps.release.outputs.tag_name }}
steps:
- name: Run release-please
id: release
uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
config-file: release-please-config.json
manifest-file: .release-please-manifest.json
# Cut releases here, but do not open the next release PR: the release is still a draft,
# which release-please's "latest release" lookup cannot see, so a PR computed now would
# cover the whole history. release-pr.yml refreshes the PR on other pushes, and the
# release-please-pr job below opens the next one once the release is published.
skip-github-pull-request: true
# Which release the publish jobs finish: the one release-please just cut, or the draft named by
# workflow_dispatch. Either way the binaries are built from the release's own commit.
target:
name: Release target
runs-on: ubuntu-latest
timeout-minutes: 5
needs: [ci, release-please]
if: >-
always() && needs.ci.result == 'success' &&
(needs.release-please.outputs.release_created == 'true' || github.event_name == 'workflow_dispatch')
permissions:
contents: write # a read-only token cannot see draft releases
outputs:
tag_name: ${{ steps.target.outputs.tag_name }}
version: ${{ steps.target.outputs.version }}
major: ${{ steps.target.outputs.major }}
commit: ${{ steps.target.outputs.commit }}
steps:
- name: Resolve the release
id: target
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG_NAME: ${{ needs.release-please.outputs.tag_name || inputs.tag }}
run: |
set -euo pipefail
release="$(gh release view "${TAG_NAME}" --repo "${GITHUB_REPOSITORY}" --json isDraft,targetCommitish)"
if [[ "$(jq -r .isDraft <<< "${release}")" != "true" ]]; then
echo "::error::${TAG_NAME} is not a draft release; a published release is not changed" >&2
exit 1
fi
commit="$(jq -r .targetCommitish <<< "${release}")"
if [[ ! "${commit}" =~ ^[0-9a-f]{40}$ ]]; then
echo "::error::${TAG_NAME} targets ${commit}, not a commit SHA" >&2
exit 1
fi
version="${TAG_NAME#v}"
{
echo "tag_name=${TAG_NAME}"
echo "version=${version}"
echo "major=${version%%.*}"
echo "commit=${commit}"
} >> "$GITHUB_OUTPUT"
echo "Finishing ${TAG_NAME} (${commit})"
# Build, checksum, sign and attest the CLI binaries, and attach them to the draft release. The
# build is `task dist` in the CI image, so it reproduces locally with the same command; the
# binaries of one commit are byte for byte the same on every build.
#
# Two ways to verify, as in gitops-reverser: `gh attestation verify` is the stronger claim, and
# the sha256sums file is what a `curl | sha256sum -c` line in another CI can use without a token.
publish-cli:
name: Publish CLI binaries
runs-on: ubuntu-latest
timeout-minutes: 20
needs: [ci, target]
# Explicit: the implicit success() also looks at release-please, which a dispatch skips.
if: ${{ !cancelled() && needs.target.result == 'success' }}
permissions:
contents: write # upload release assets
packages: read # pull the CI image
id-token: write # cosign keyless signing + attestation OIDC
attestations: write
env:
CI_CONTAINER: ${{ needs.ci.outputs.ci-image }}
CI_WORKDIR: /workspaces/${{ github.event.repository.name }}
steps:
- name: Checkout the release commit
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ needs.target.outputs.commit }}
persist-credentials: false
- name: Login to Docker registry
run: |
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
- name: Build the binaries for every published platform
env:
VERSION: ${{ needs.target.outputs.version }}
run: |
docker run --rm \
-v "${GITHUB_WORKSPACE}:${CI_WORKDIR}" \
-w "${CI_WORKDIR}" \
"${CI_CONTAINER}" \
bash -lc '
set -e
git config --global --add safe.directory "$PWD"
task dist
'
# task dist checks the version the binary prints against the manifest; this checks the
# manifest against the release that was just cut.
./dist/settings-image_linux_amd64 version | grep -qF "version=${VERSION} " || {
echo "the binary does not report ${VERSION}" >&2
exit 1
}
ls -l dist
- name: Install cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
with:
cosign-release: v3.1.4
# dist/ was written by the container as root, so the signature bundles go beside it.
- name: Sign the checksums file (cosign keyless)
run: cosign sign-blob --bundle settings-image.sha256sums.sigstore.json --yes dist/settings-image.sha256sums
# GitHub stores attestations only for a public repository on the organisation's plan: this
# step fails if settings-image is ever made private again.
- name: Attest SLSA provenance for the binaries
id: attest
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
with:
subject-path: dist/settings-image_*
- name: Rename provenance bundle to the Scorecard-recognized suffix
run: cp "${{ steps.attest.outputs.bundle-path }}" settings-image.intoto.jsonl
- name: Upload the binaries as release assets
uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
with:
tag_name: ${{ needs.target.outputs.tag_name }}
draft: true
append_body: true
fail_on_unmatched_files: true
files: |
dist/settings-image_linux_amd64
dist/settings-image_linux_arm64
dist/settings-image_darwin_arm64
dist/settings-image.sha256sums
settings-image.sha256sums.sigstore.json
settings-image.intoto.jsonl
body: |
## GitHub Action
```yaml
- uses: ConfigButler/settings-image@${{ needs.target.outputs.tag_name }}
with:
path: configs/shop
repository: ghcr.io/acme/shop-settings
```
## CLI
Binaries for linux/amd64, linux/arm64 and darwin/arm64, with a checksums file:
```bash
curl -fsSLO https://github.com/${{ github.repository }}/releases/download/${{ needs.target.outputs.tag_name }}/settings-image_linux_amd64
curl -fsSL https://github.com/${{ github.repository }}/releases/download/${{ needs.target.outputs.tag_name }}/settings-image.sha256sums | sha256sum -c --ignore-missing
```
Verify the checksums file was signed by this repository's release workflow:
```bash
curl -fsSLO https://github.com/${{ github.repository }}/releases/download/${{ needs.target.outputs.tag_name }}/settings-image.sha256sums.sigstore.json
cosign verify-blob settings-image.sha256sums \
--bundle settings-image.sha256sums.sigstore.json \
--certificate-identity-regexp '^https://github.com/${{ github.repository }}/\.github/workflows/release\.yml@' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
```
Each binary also carries SLSA provenance:
```bash
gh attestation verify settings-image_linux_amd64 --repo ${{ github.repository }}
```
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Publish the release only after every asset is attached. Publishing creates the tag.
#
# The `release` environment is the pause for a Marketplace listing (docs/github-action.md §6).
# Without protection rules it passes straight through. With a required reviewer, this job waits,
# with the complete draft, for an approval: a maintainer publishes the draft in the UI with
# "Publish this Action to the GitHub Marketplace" ticked (no API can tick it, and an immutable
# release cannot be listed afterwards), then approves, and the jobs below finish the release.
publish-release:
name: Publish GitHub Release
runs-on: ubuntu-latest
timeout-minutes: 10
needs: [target, publish-cli]
if: ${{ !cancelled() && needs.publish-cli.result == 'success' }}
environment: release
permissions:
contents: write
steps:
- name: Publish the draft release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG_NAME: ${{ needs.target.outputs.tag_name }}
run: |
set -euo pipefail
if [[ "$(gh release view "${TAG_NAME}" --repo "${GITHUB_REPOSITORY}" --json isDraft --jq .isDraft)" != "true" ]]; then
echo "::notice::${TAG_NAME} was already published by hand (a Marketplace listing); nothing to publish"
exit 0
fi
gh release edit "${TAG_NAME}" --draft=false --repo "${GITHUB_REPOSITORY}"
# Move the floating major tag (v0, later v1) to the release, the Actions convention behind
# `uses: ConfigButler/settings-image@v0` (docs/github-action.md §4). Only a tag is force-pushed,
# never a branch.
move-major-tag:
name: Move the major tag
runs-on: ubuntu-latest
timeout-minutes: 10
needs: [target, publish-release]
if: ${{ !cancelled() && needs.publish-release.result == 'success' }}
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Point the major tag at the release
env:
TAG_NAME: ${{ needs.target.outputs.tag_name }}
MAJOR: v${{ needs.target.outputs.major }}
run: |
set -euo pipefail
git fetch --force --tags origin
commit="$(git rev-parse "${TAG_NAME}^{commit}")"
git tag --force "${MAJOR}" "${commit}"
git push --force origin "refs/tags/${MAJOR}"
echo "${MAJOR} -> ${TAG_NAME} (${commit})"
# The PR half of release-please, after the release is published, so it sees that release instead
# of the draft. It only opens or refreshes the next release PR; it never tags or releases.
release-please-pr:
name: Release Please (PR)
runs-on: ubuntu-latest
timeout-minutes: 10
needs: [publish-release]
if: ${{ !cancelled() && needs.publish-release.result == 'success' }}
# Shared with release-pr.yml, so the two never rewrite the PR branch at once.
concurrency:
group: release-pr-${{ github.ref }}
cancel-in-progress: false
permissions:
contents: write
pull-requests: write
steps:
- name: Open or refresh the next release PR
uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
config-file: release-please-config.json
manifest-file: .release-please-manifest.json
skip-github-release: true