Repository navigation
239 lines (213 loc) · 9.08 KB
/
Copy pathrelease.yml
File metadata and controls
239 lines (213 loc) · 9.08 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
name: Release
# Every push to main runs release-please, which keeps a release PR open with
# the next version and changelog. Merging that PR creates the tag and GitHub
# Release, and the jobs below then attach attested binaries and publish the
# crate to crates.io. The release runs in this workflow, not on the tag, so
# it never depends on a tag push starting a run, and a pushed tag releases
# nothing.
# If a release fails after release-please created it, rerun the failed jobs,
# or dispatch this workflow on main with the release tag: either rebuilds,
# re-attests and re-uploads that release and retries the crates.io publish.
# Users verify a binary against this file's path and refs/heads/main, so
# renaming the file or running it from another ref fails the README's check.
on:
push:
branches: [main]
workflow_dispatch:
inputs:
tag:
description: Existing release tag to rebuild and publish, e.g. v0.1.0
required: true
type: string
permissions:
contents: read
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
jobs:
release-please:
runs-on: ubuntu-latest
timeout-minutes: 15
outputs:
release_created: ${{ steps.release.outputs.release_created }}
tag_name: ${{ steps.release.outputs.tag_name }}
# On a dispatch the steps skip but the job still succeeds: a release
# created here would otherwise lose its build to the dispatched tag, and a
# skipped job would skip every job after it.
steps:
# The App token, not GITHUB_TOKEN, opens the release PR, so CI runs on it.
- uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
if: github.event_name == 'push'
id: app-token
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
# What release-please and the assignee step use, and all the
# release App is granted: asking for more fails the token request.
permission-contents: write
permission-pull-requests: write
- uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5
if: github.event_name == 'push'
id: release
with:
token: ${{ steps.app-token.outputs.token }}
- name: Assign the release PR to the maintainer
if: ${{ steps.release.outputs.pr }}
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
GH_REPO: ${{ github.repository }}
# Via env: the PR JSON carries branch and title text.
PR_JSON: ${{ steps.release.outputs.pr }}
# REST, not gh pr edit, which also queries project fields an App
# token cannot read.
run: |
number=$(jq -er .number <<< "$PR_JSON")
gh api --method POST "repos/$GH_REPO/issues/$number/assignees" -f "assignees[]=27Bslash6" > /dev/null
check:
needs: release-please
if: >-
needs.release-please.outputs.release_created == 'true'
|| (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main')
runs-on: ubuntu-latest
timeout-minutes: 15
outputs:
tag: ${{ steps.tag.outputs.tag }}
sha: ${{ steps.sha.outputs.sha }}
env:
# A dispatch names the tag; a push releases what release-please created.
TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || needs.release-please.outputs.tag_name }}
steps:
- name: Tag is a release version
id: tag
run: |
if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::$TAG is not a release tag such as v0.1.0"
exit 1
fi
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# Fully qualified, so a branch with the tag's name is never built.
ref: refs/tags/${{ steps.tag.outputs.tag }}
fetch-depth: 0
persist-credentials: false
- name: Tagged commit is on main
id: sha
# A dispatch can name any tag; only one on main may be built and
# attested as a run from main. Later jobs check out this exact commit,
# so a tag moved after this check cannot change what ships.
run: |
git merge-base --is-ancestor HEAD origin/main \
|| { echo "::error::$TAG is not on main"; exit 1; }
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- name: Tag matches the crate version
run: |
version=$(cargo metadata --locked --no-deps --format-version 1 | jq -r '.packages[0].version')
if [ "$TAG" != "v$version" ]; then
echo "::error::tag $TAG does not match the crate version $version"
exit 1
fi
- name: Package the crate
# Builds the packaged crate, so the publish job can skip that build.
run: cargo package --locked
build:
name: build (${{ matrix.target }})
needs: check
runs-on: ${{ matrix.os }}
timeout-minutes: 30
strategy:
matrix:
include:
# musl builds are static, so they run on any Linux distribution.
- { os: ubuntu-latest, target: x86_64-unknown-linux-musl }
- { os: ubuntu-24.04-arm, target: aarch64-unknown-linux-musl }
- { os: macos-latest, target: aarch64-apple-darwin }
env:
TAG: ${{ needs.check.outputs.tag }}
TARGET: ${{ matrix.target }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ needs.check.outputs.sha }}
persist-credentials: false
- name: Install the musl toolchain
if: runner.os == 'Linux'
# ring compiles C, which needs musl-gcc.
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends musl-tools
- name: Add the target
run: rustup target add "$TARGET"
- name: Test the shipped target
run: cargo test --locked --release --target "$TARGET"
- name: Build
run: cargo build --locked --release --target "$TARGET"
- name: Archive
run: |
dir="ck-$TAG-$TARGET"
mkdir "$dir"
cp "target/$TARGET/release/ck" LICENSE README.md "$dir/"
tar -czf "$dir.tar.gz" "$dir"
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ matrix.target }}
path: ck-*.tar.gz
if-no-files-found: error
release:
needs: [check, build]
runs-on: ubuntu-latest
timeout-minutes: 15
# Signing happens here rather than in the build jobs, so dependency build
# scripts never run with a token that can sign or publish.
permissions:
contents: write # upload to the release
id-token: write # sign the attestation
attestations: write # store the attestation
steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
path: dist
merge-multiple: true
- name: Unpack the binaries
# Attesting the binary too lets a user verify ck after unpacking it.
run: for archive in dist/*.tar.gz; do tar -xzf "$archive" -C dist; done
- name: Attest the archives and the binaries in them
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
with:
subject-path: |
dist/*.tar.gz
dist/ck-*/ck
- name: Upload to the GitHub Release
# release-please created the release. --clobber lets a rerun replace
# the archives with its own attested ones.
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
TAG: ${{ needs.check.outputs.tag }}
run: gh release upload "$TAG" --clobber dist/*.tar.gz
publish:
needs: [check, release]
runs-on: ubuntu-latest
timeout-minutes: 15
# crates.io accepts a token minted here only from this workflow in this
# environment: trusted publishing, so the repo holds no registry secret.
environment: release
permissions:
contents: read
id-token: write # exchange for a crates.io token
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ needs.check.outputs.sha }}
persist-credentials: false
# Needs the crate to exist on crates.io with this workflow and the
# `release` environment registered as its trusted publisher. crates.io
# attaches a publisher only to an existing crate, so 0.0.0 is published
# by hand with an API token first. Every released version comes after it,
# because this job cannot publish a version that already exists.
- uses: rust-lang/crates-io-auth-action@c6f97d42243bad5fab37ca0427f495c86d5b1a18 # v1.0.5
id: auth
- name: Publish to crates.io
# The check job already built the packaged crate. A version can never
# be republished, so this runs last, after the GitHub Release exists.
env:
CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}
run: cargo publish --locked --no-verify