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
name: release
on:
push:
tags:
- 'v*.*.*'
permissions:
contents: read
env:
CARGO_TERM_COLOR: always
jobs:
test:
name: test
runs-on: ubuntu-latest
# The same acknowledgement as ci.yml's `test` and `coverage` jobs:
# tests/device_node_size.rs needs a loop device, this runner is not
# root, and without the variable the test fails rather than skips.
# Declared on the job so every suite run below has it.
env:
AM_FS_CORE_ALLOW_UNPRIVILEGED_SKIP: "1"
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: 1.95.0
components: clippy
- uses: Swatinem/rust-cache@v2
- name: verify packaged output-budget script
shell: bash
run: |
files="$(cargo package --locked --list)"
grep -Fqx 'scripts/output-budget.sh' <<<"$files" || {
echo 'package omits scripts/output-budget.sh' >&2
exit 1
}
- name: verify tag matches Cargo.toml version
run: |
tag="${GITHUB_REF_NAME#v}"
crate_ver=$(grep -m1 '^version' Cargo.toml | sed -E 's/version *= *"(.*)"/\1/')
if [ "$tag" != "$crate_ver" ]; then
echo "tag $tag does not match Cargo.toml version $crate_ver" >&2
exit 1
fi
- name: test (debug, quiet and budgeted)
run: |
bash scripts/tier.sh "test (debug)" debug 750 50000 -- cargo test --locked --all-targets --features cli
bash scripts/core.sh test-floor debug 330
# A RELEASE-PROFILE RUN, DECIDED IN #111. The profile published is
# the release one, and optimisation can change behaviour the debug
# run never sees: `debug_assert!` bodies vanish, overflow wraps.
# It runs here, on a tag, not in ci.yml, so pull requests don't pay
# for a second full suite.
#
# Do NOT set EXPECT_OVERFLOW_CHECKS on this run. Overflow checks are
# meant to be off under --release, so the handshake would arm an
# assertion that fails every green run. tests/ci_profile.rs refuses
# that combination.
- name: test (release, quiet and budgeted)
run: |
bash scripts/tier.sh "test (release)" release 750 50000 -- cargo test --locked --release --all-targets --features cli
bash scripts/core.sh test-floor release 330
- name: keep the full test transcripts
if: always()
uses: actions/upload-artifact@v4
with:
name: tier-logs-release
path: tmp/logs/
if-no-files-found: warn
retention-days: 14
# Twice: once as a consumer's static library builds this crate (no
# features), and once with the `cli` feature the tools turn on, so a
# lint in either shape is caught.
- run: cargo clippy --locked --all-targets -- -D warnings
- run: cargo clippy --locked --all-targets --features cli -- -D warnings
publish:
name: publish to crates.io
needs: test
runs-on: ubuntu-latest
environment: release
# The only job with any write grant, because it is the only one that
# needs one: an OIDC token for crates.io trusted publishing and for
# signing the attestation, the attestation store, and the release the
# .crate is attached to.
permissions:
id-token: write
attestations: write
contents: write
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable, 2026-09-27
with:
toolchain: 1.95.0
# PROVENANCE. A version on crates.io says nothing about where it was
# built, so the crate is packaged here, checked after the publish
# against the checksum crates.io serves, and only then attested and
# attached to the GitHub release for the tag. Packaged on every run,
# published or not, so a re-run of a tag whose publish succeeded but
# whose attestation did not can still finish the job. The same
# packaging `cargo publish` does, and cargo's archive is
# reproducible: the file `cargo publish` uploads is this one.
# THE NOTES BEFORE THE UPLOAD (#209): the release body is this
# version's CHANGELOG section, and a tag the CHANGELOG does not
# describe stops here, before crates.io has anything.
- name: the CHANGELOG describes this version
shell: bash
run: |
set -euo pipefail
bash scripts/release-notes.sh "$GITHUB_REF_NAME" > "$RUNNER_TEMP/release-notes.md"
- name: package the crate
shell: bash
run: cargo package --no-verify
# ALREADY PUBLISHED IS NOT A FAILURE. A crate's first
# version under a new name cannot come from here: trusted publishing
# needs the crate to exist before its publisher can be registered, so
# that version is published by hand from this tag's commit. The run
# then has nothing to upload, and the checksum step below still holds
# the version crates.io serves to the archive built here, so a hand
# publish of anything else fails this job.
- name: is this version already on crates.io
id: published
shell: bash
run: |
set -euo pipefail
name="$(grep -m1 '^name = ' Cargo.toml | sed 's/name = //; s/"//g')"
version="$(grep -m1 '^version = ' Cargo.toml | sed 's/version = //; s/"//g')"
code="$(curl -s -o /dev/null -w '%{http_code}' \
-A "${GITHUB_REPOSITORY} release (github.com/${GITHUB_REPOSITORY})" \
"https://crates.io/api/v1/crates/$name/$version")"
echo "crates.io answered $code for $name $version"
if [ "$code" = 200 ]; then echo "already=true" >> "$GITHUB_OUTPUT"; else echo "already=false" >> "$GITHUB_OUTPUT"; fi
- uses: rust-lang/crates-io-auth-action@bbd81622f20ce9e2dd9622e3218b975523e45bbe # v1.0.4
if: steps.published.outputs.already != 'true'
id: auth
- name: cargo publish
if: steps.published.outputs.already != 'true'
env:
CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}
run: cargo publish
# Nothing is attested on trust: the file is compared with the
# checksum crates.io records for this version, so the attestation
# speaks for the crates.io download, not merely for a file that was
# on this runner. The index can trail the upload by a few seconds.
- name: the packaged crate is the one crates.io serves
shell: bash
run: |
set -euo pipefail
name="$(grep -m1 '^name = ' Cargo.toml | sed 's/name = //; s/"//g')"
version="$(grep -m1 '^version = ' Cargo.toml | sed 's/version = //; s/"//g')"
crate="target/package/$name-$version.crate"
local_sum="$(sha256sum "$crate" | cut -d' ' -f1)"
remote_sum=""
for _ in 1 2 3 4 5 6 7 8 9 10; do
remote_sum="$(curl -sf "https://crates.io/api/v1/crates/$name/$version" \
-H 'User-Agent: release-workflow' | jq -r '.version.checksum')" && break
sleep 6
done
if [ "$local_sum" != "$remote_sum" ]; then
echo "::error::$crate is sha256 $local_sum but crates.io records ${remote_sum:-nothing} for $name $version; the file here is not the published one, so it is not attested"
exit 1
fi
echo "$crate is the crates.io download: sha256 $local_sum"
- uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
with:
subject-path: target/package/*.crate
# So `gh attestation verify` has a file to check that anyone can
# fetch beside the tag, and so the crates.io download can be
# compared with it.
- name: attach the crate to the GitHub release
shell: bash
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
run: |
set -euo pipefail
if gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1; then
gh release edit "$GITHUB_REF_NAME" --notes-file "$RUNNER_TEMP/release-notes.md"
else
gh release create "$GITHUB_REF_NAME" --verify-tag --title "$GITHUB_REF_NAME" \
--notes-file "$RUNNER_TEMP/release-notes.md"
fi
gh release upload "$GITHUB_REF_NAME" target/package/*.crate --clobber