Repository navigation
299 lines (291 loc) · 12.7 KB
/
Copy pathrelease.yml
File metadata and controls
299 lines (291 loc) · 12.7 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
# Release: on a version tag, run the full test suite, then publish every
# workspace crate to crates.io in dependency order via Trusted Publishing
# (GitHub OIDC — no long-lived token anywhere). Publishing is triggered only
# by a maintainer pushing a tag — never automatically.
name: release
on:
push:
# Full versions only: the floating v1 action tag moves on releases and
# must not re-trigger publishing.
tags: ["v[0-9]*.[0-9]*.[0-9]*"]
# Manual re-run for resuming a rate-limited publish; skips crates whose
# version is already on the registry.
workflow_dispatch:
inputs:
tag:
description: >-
The release tag this run is finishing, e.g. v2.0.0. Required to
move the floating major tag; a resumed publish that left `vN`
behind is a release whose documented ref still points at the
previous version, which is the failure `float-major-tag` exists
to prevent.
required: false
permissions:
contents: read
# Never cancel: eleven crates published in dependency order is not a thing to
# stop halfway, and a second run for the same tag should queue behind the
# first rather than race it for the index.
concurrency:
group: release-${{ github.event_name == 'push' && github.ref_name || inputs.tag }}
cancel-in-progress: false
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install pinned toolchain
run: rustup show
- uses: taiki-e/install-action@183e4297cca2404691e9380e1307288dced5c82a # v2.87.25
with:
tool: just,cargo-deny
- run: just ci
publish:
needs: test
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
# Trusted Publishing: crates.io trusts this repo+workflow via OIDC and
# mints a short-lived token; no secret lives anywhere.
id-token: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install pinned toolchain
run: rustup show
- name: Authenticate with crates.io (OIDC, tokenless)
id: auth
uses: rust-lang/crates-io-auth-action@c6f97d42243bad5fab37ca0427f495c86d5b1a18 # v1.0.5
- name: Publish in dependency order
env:
CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}
run: |
set -euo pipefail
version=$(cargo metadata --format-version 1 --no-deps \
| python3 -c "import json,sys; print(next(p['version'] for p in json.load(sys.stdin)['packages'] if p['name']=='launchbound-cli'))")
# The tag is the contract: on a tag push, refuse to publish a
# manifest that disagrees with it. This is the real barrier — the
# trigger glob is only defense-in-depth (GitHub's `*` matches any
# characters, so v1.2.3-rc1 style tags still trigger).
if [ "${GITHUB_REF_TYPE:-}" = "tag" ]; then
if [ "v$version" != "$GITHUB_REF_NAME" ]; then
echo "::error::tag $GITHUB_REF_NAME does not match Cargo.toml version $version — refusing to publish"
exit 1
fi
else
echo "::warning::manual dispatch publishes main's Cargo.toml version ($version); versions already on crates.io are skipped"
fi
echo "publishing version $version"
for crate in \
launchbound-space \
launchbound-bench \
launchbound-build \
launchbound-prune \
launchbound-search \
launchbound-model \
launchbound-metal \
launchbound-report \
launchbound-runner \
launchbound-tui \
launchbound-cli; do
if curl -fsS -A "launchbound-release (github.com/vyncint/launchbound)" "https://crates.io/api/v1/crates/$crate/$version" >/dev/null 2>&1; then
echo "== $crate $version already on crates.io — skipping =="
continue
fi
echo "== publishing $crate =="
for attempt in $(seq 1 12); do
if cargo publish -p "$crate" --no-verify 2>&1 | tee /tmp/publish.log; then
break
fi
if grep -q "already uploaded\|already exists" /tmp/publish.log; then
echo "$crate already published (crossed with a previous run)"; break
fi
if [ "$attempt" = 12 ]; then
echo "publishing $crate failed after 12 attempts"; exit 1
fi
if grep -q "429 Too Many Requests" /tmp/publish.log; then
echo "rate-limited; waiting 10 minutes (crates.io new-crate bucket refills ~1/10min)"
sleep 610
else
echo "retry $attempt for $crate in 30s (index propagation)"
sleep 30
fi
done
done
# The GitHub Release, from the changelog rather than from generated notes.
#
# This workflow had no `gh release` step at all: it tested, published
# eleven crates, moved the floating tag and notified the testing repo, and
# never created the release. Every one has been cut by hand, so skipping
# one took only a distraction -- 2.2.1 went to crates.io on 2026-09-10 and
# the repository still showed 2.2.0 as Latest twelve days later, for the
# release whose whole content was "the documentation people land on now
# works".
#
# `--generate-notes` would be a downgrade: CHANGELOG.md here is written for
# a reader, marks `bench:` entries, and is the document the release was cut
# from, so the two cannot be allowed to disagree.
github-release:
needs: publish
if: >-
(github.event_name == 'push' && github.ref_type == 'tag')
|| (github.event_name == 'workflow_dispatch' && inputs.tag != '')
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: write # creates the release
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
ref: ${{ github.event_name == 'push' && github.ref_name || inputs.tag }}
- name: Extract this version's changelog section
env:
TAG: ${{ github.event_name == 'push' && github.ref_name || inputs.tag }}
run: |
set -euo pipefail
version="${TAG#v}"
awk -v v="$version" '
$0 ~ "^## \\[" v "\\]" { inside = 1; next }
inside && /^## \[/ { exit }
inside { print }
' CHANGELOG.md > notes.md
if [ ! -s notes.md ]; then
echo "::error::CHANGELOG.md has no '## [$version]' section; write it before releasing"
exit 1
fi
{
echo
echo '## Install'
echo
echo '```sh'
echo 'cargo install launchbound-cli'
echo '```'
echo
echo 'In CI, the prune gate is three lines:'
echo
echo '```yaml'
echo '- uses: actions/checkout@v5'
echo "- uses: vyncint/launchbound/action@v2"
echo ' with:'
echo ' cc: "8.6"'
echo '```'
} >> notes.md
echo "release notes:"; cat notes.md
- name: Create the release
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ github.event_name == 'push' && github.ref_name || inputs.tag }}
run: |
set -euo pipefail
if gh release view "$TAG" >/dev/null 2>&1; then
echo "release $TAG exists; replacing its notes"
gh release edit "$TAG" --notes-file notes.md
else
gh release create "$TAG" --title "launchbound ${TAG#v}" --notes-file notes.md
fi
# Move the floating major tag to the release that was just published.
#
# The README tells users to write `vyncint/launchbound/action@v1`, so that
# tag is the contract for everyone who followed the documentation. It was a
# manual step, and a manual step in a release is a step that gets skipped:
# `v1` sat on 1.0.2 through both 1.1.0 and 1.2.0, so every consumer on the
# documented ref kept getting an action pinned to reconverge 0.1.11 while
# 1.2.0's entire subject was moving that pin to 0.3.0.
#
# Runs only after a successful publish, and only for a tag push, so the
# floating tag can never point at a release that did not reach crates.io.
# The trigger filter above requires three numeric components, so pushing
# `v1` here cannot re-enter this workflow.
float-major-tag:
needs: [publish, github-release]
# A tag push carries the tag in `ref_name`; a dispatch has to be told.
# Runs for both, because RELEASING documents resuming a rate-limited
# publish by dispatch — and a resumed release that skipped this job is a
# release whose documented `@vN` still points at the previous version.
if: >-
(github.event_name == 'push' && github.ref_type == 'tag')
|| (github.event_name == 'workflow_dispatch' && inputs.tag != '')
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write # moves refs/tags/vN
steps:
# The ref moves through the API rather than by pushing, so this job --
# the only one here that writes a ref -- needs no git credential left
# in its working tree, and the checkout can keep
# `persist-credentials: false` like every other one in this repository.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
fetch-depth: 0
- name: Point vN at the release tag
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
TAG: ${{ github.event_name == 'push' && github.ref_name || inputs.tag }}
run: |
set -euo pipefail
# A dispatch can be handed anything, so the tag has to exist and be
# a version before a floating pointer is aimed at it.
sha="$(git rev-parse -q --verify "refs/tags/${TAG}^{commit}")" || {
echo "::error::no such tag: '$TAG'"; exit 1
}
case "$TAG" in
v[0-9]*.[0-9]*.[0-9]*) ;;
*) echo "::error::'$TAG' is not a release tag (expected vX.Y.Z)"; exit 1 ;;
esac
major="v${TAG#v}"
major="${major%%.*}"
case "$major" in
v[0-9]*) ;;
*) echo "::error::cannot derive a major tag from '$TAG'"; exit 1 ;;
esac
if gh api "repos/$REPO/git/ref/tags/$major" >/dev/null 2>&1; then
gh api -X PATCH "repos/$REPO/git/refs/tags/$major" \
-f "sha=$sha" -F force=true >/dev/null
else
gh api -X POST "repos/$REPO/git/refs" \
-f "ref=refs/tags/$major" -f "sha=$sha" >/dev/null
fi
echo "$major now points at $TAG (${sha:0:8})"
# Tell the repository that tests this tool against real kernels that a new
# version exists, so the deep run happens now rather than at its next
# scheduled tick.
#
# Best-effort by design. launchbound-action-demo resolves the newest
# published launchbound-cli from crates.io on a schedule of its own, so a
# dispatch that never arrives -- an expired token, a release cut by hand --
# costs latency and nothing else. This job never fails the release: a
# published crate is published whether or not the notification landed.
#
# Needs a PAT with `contents: write` on launchbound-action-demo, stored as
# TESTING_REPO_DISPATCH_TOKEN. Without it the job says so and exits clean.
notify-testing-repo:
name: notify-testing-repo
needs: publish
if: github.event_name == 'push' && github.ref_type == 'tag'
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- name: Dispatch to launchbound-action-demo
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.TESTING_REPO_DISPATCH_TOKEN }}
TAG: ${{ github.ref_name }}
run: |
if [ -z "${GH_TOKEN:-}" ]; then
echo "::notice::TESTING_REPO_DISPATCH_TOKEN is not set; launchbound-action-demo will pick this up on its own schedule instead"
exit 0
fi
gh api repos/vyncint/launchbound-action-demo/dispatches \
-f event_type=tool-published \
-F "client_payload[version]=${TAG#v}" \
&& echo "dispatched tool-published to launchbound-action-demo" \
|| echo "::warning::could not dispatch to launchbound-action-demo; it will pick this up on its own schedule"