-
Notifications
You must be signed in to change notification settings - Fork 34
499 lines (461 loc) · 20.6 KB
/
Copy pathrelease.yml
File metadata and controls
499 lines (461 loc) · 20.6 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
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
name: Release
on:
push:
tags:
- "v*"
permissions:
id-token: write
attestations: write
contents: write
jobs:
# Gate the whole release on a dependency audit of the TAGGED commit.
#
# CI owns the audit job, but CI is triggered by the push to `main` while
# this workflow is triggered independently by the tag. Pushing the commit
# and the tag back-to-back starts the release before CI can report, so a
# lockfile advisory published since the last green run ships regardless.
# Cutting v0.21.18 hit exactly that: RUSTSEC-2026-0253 landed between the
# last green run and the tag, and `sbom` had already created a PUBLIC
# release with three targets' binaries by the time CI came back red.
# Recovery meant deleting a published release and re-pushing the tag.
#
# This runs ahead of `sbom` specifically because `sbom` is what creates the
# release object — nothing is public until the audit passes.
#
# Scoped to the audit and NOT the rest of CI on purpose: the audit is the
# one check whose verdict changes without the commit changing, since a
# newly published advisory flips it red on identical code. fmt / clippy /
# tests are deterministic per commit, so a SHA that was green stays green;
# re-running them here would add ~7 min to every release and catch nothing.
preflight:
name: Audit dependencies
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: ./.github/actions/audit
sbom:
name: Generate SBOM
# Nothing public exists until the audit clears — see `preflight`.
needs: preflight
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
# Install from crates.io rather than psastras/sbom-rs/actions/…@cargo-sbom-latest:
# that tag is stale (2023, no actions/ dir) so the action ref fails to resolve.
- name: Install cargo-sbom
run: cargo install cargo-sbom --locked
# cargo-sbom writes to stdout (no -o flag) and the SPDX value is
# spdx_json_2_3 (not spdx_2_3).
- name: Generate SPDX SBOM
run: cargo sbom --output-format spdx_json_2_3 > sbom.spdx.json
- name: Upload SBOM as release asset
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: sbom.spdx.json
tag: ${{ github.ref }}
overwrite: true
- name: Upload SBOM as workflow artifact
uses: actions/upload-artifact@v4
with:
name: sbom
path: sbom.spdx.json
build:
name: ${{ matrix.target }}
# Wait for the SBOM: the "Download SBOM" step below pulls the `sbom`
# artifact, and attestation needs it. Without this the jobs race and
# build can hit Download SBOM before sbom uploads (or after it fails),
# dying before it uploads the binaries.
needs: sbom
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
# Static musl build — portable across Linux distros (no glibc
# version coupling). Works because TLS is rustls, not OpenSSL.
- os: ubuntu-latest
target: x86_64-unknown-linux-musl
# Cross-compile the Intel target on the Apple-Silicon runner.
# `macos-13` (Intel) runners are scarce and frequently sit
# queued until the 24h timeout; `macos-latest` is plentiful and
# ships both-arch SDKs. Build the no-plugin set (same as
# Windows): `evil_janet`'s C lib only compiles for the host
# arch, so the Janet `plugin` feature can't link for x86_64 when
# cross-building. The native arm64 build below keeps the plugin.
- os: macos-latest
target: x86_64-apple-darwin
cargo_flags: "--no-default-features --features windows-default"
- os: macos-latest
target: aarch64-apple-darwin
# Windows MSVC can't build the Janet `plugin` feature
# (evil_janet needs _setjmp; janetrs 0.8 passes i64 to the
# Windows fseek). Build the default set minus plugin.
- os: windows-latest
target: x86_64-pc-windows-msvc
cargo_flags: "--no-default-features --features windows-default"
steps:
- uses: actions/checkout@v7
- name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
target: ${{ matrix.target }}
# musl needs the cross linker/toolchain.
- name: Install musl tools
if: matrix.target == 'x86_64-unknown-linux-musl'
run: sudo apt-get update && sudo apt-get install -y musl-tools
- name: Build
run: cargo build --release --target ${{ matrix.target }} ${{ matrix.cargo_flags }}
# Package the `dirge` binary (named via [[bin]] regardless of the
# `dirge-agent` crate name). Windows → .zip, Unix → .tar.gz; emit a
# sha256 alongside so users can verify the download.
- name: Package (Unix)
if: matrix.os != 'windows-latest'
run: |
archive=dirge-${{ matrix.target }}.tar.gz
tar czf "$archive" -C "target/${{ matrix.target }}/release" dirge
shasum -a 256 "$archive" > "$archive.sha256"
- name: Package (Windows)
if: matrix.os == 'windows-latest'
shell: pwsh
run: |
$archive = "dirge-${{ matrix.target }}.zip"
Compress-Archive -Path "target/${{ matrix.target }}/release/dirge.exe" -DestinationPath $archive
(Get-FileHash $archive -Algorithm SHA256).Hash | Out-File -Encoding ascii "$archive.sha256"
- name: Download SBOM
uses: actions/download-artifact@v4
with:
name: sbom
- name: Attest binary (Unix)
if: matrix.os != 'windows-latest'
uses: actions/attest@v4
with:
subject-path: dirge-${{ matrix.target }}.tar.gz
sbom-path: sbom.spdx.json
- name: Attest binary (Windows)
if: matrix.os == 'windows-latest'
uses: actions/attest@v4
with:
subject-path: dirge-${{ matrix.target }}.zip
sbom-path: sbom.spdx.json
- name: Upload release assets
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: dirge-${{ matrix.target }}.*
file_glob: true
tag: ${{ github.ref }}
overwrite: true
nix-binary-package:
name: Bump Nix binary package
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
ref: main
# Full history so the post-build commit can rebase+push to main.
fetch-depth: 0
- name: Install Nix
uses: DeterminateSystems/nix-installer-action@v22
- name: Prefetch release assets and update nix/bin.nix
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
tag="$GITHUB_REF_NAME"
version="${tag#v}"
assets="$(gh release view "$tag" -R "$GITHUB_REPOSITORY" --json assets --jq '.assets[].name')"
python3 - <<'PY' > /tmp/dirge-nix-assets.tsv
import re
from pathlib import Path
text = Path("nix/bin.nix").read_text()
for match in re.finditer(r'"(?P<system>[^"]+)" = \{\s*triple = "(?P<triple>[^"]+)";', text):
print(f"{match.group('system')}\t{match.group('triple')}")
PY
hashes_json="{}"
while IFS=$'\t' read -r system triple; do
asset="dirge-${triple}.tar.gz"
if ! grep -Fxq "$asset" <<< "$assets"; then
echo "Missing release asset: $asset" >&2
exit 1
fi
url="https://github.com/${GITHUB_REPOSITORY}/releases/download/${tag}/${asset}"
hash="$(nix store prefetch-file --hash-type sha256 --json "$url" | jq -r .hash)"
hashes_json="$(jq --arg system "$system" --arg hash "$hash" '. + {($system): $hash}' <<< "$hashes_json")"
done < /tmp/dirge-nix-assets.tsv
HASHES_JSON="$hashes_json" VERSION="$version" python3 - <<'PY'
import json
import os
import re
from pathlib import Path
path = Path("nix/bin.nix")
text = path.read_text()
text = re.sub(r'version = "[^"]+";', f'version = "{os.environ["VERSION"]}";', text, count=1)
hashes = json.loads(os.environ["HASHES_JSON"])
for system, hash_ in hashes.items():
pattern = rf'("{re.escape(system)}" = \{{\n\s+triple = "[^"]+";\n\s+hash = ")[^"]+(";)'
text = re.sub(pattern, rf'\g<1>{hash_}\2', text, count=1)
path.write_text(text)
PY
# Commit the bump straight to `main` instead of opening a PR. The
# PR route (peter-evans/create-pull-request) failed on every release
# with "GitHub Actions is not permitted to create or approve pull
# requests" — a repo Actions toggle that's off. `main` is unprotected
# and this job has contents: write (explicit permissions), so a
# direct push works and needs no extra setting or PAT. A GITHUB_TOKEN
# push doesn't re-trigger workflows, so this can't loop.
- name: Commit nix/bin.nix bump
run: |
set -euo pipefail
if git diff --quiet -- nix/bin.nix; then
echo "nix/bin.nix already current for ${GITHUB_REF_NAME} — nothing to commit"
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add nix/bin.nix
git commit -m "Bump Nix dirge-bin to ${GITHUB_REF_NAME}"
# The release job can run minutes after the tag; rebase on any
# main movement so the push fast-forwards.
git pull --rebase origin main
git push origin HEAD:main
# Bump the Homebrew tap (dirge-code/homebrew-dirge) to the just-released
# version + checksums. Runs after every target has uploaded its assets, so
# the .sha256 files exist to read back. Needs a HOMEBREW_TAP_TOKEN secret —
# a PAT (or fine-grained token) with contents:write on the tap repo, since
# the default GITHUB_TOKEN can't push to another repository. If the secret
# is absent the checkout step fails loudly but the binary release above is
# already complete and unaffected.
homebrew:
name: Bump Homebrew formula
needs: build
runs-on: ubuntu-latest
steps:
- name: Resolve version + checksums from the release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
tag="$GITHUB_REF_NAME"
mkdir -p shas
gh release download "$tag" -R "$GITHUB_REPOSITORY" -p '*.tar.gz.sha256' -D shas
{
echo "VERSION=${tag#v}"
echo "ARM_MAC=$(awk '{print $1}' shas/dirge-aarch64-apple-darwin.tar.gz.sha256)"
echo "X86_MAC=$(awk '{print $1}' shas/dirge-x86_64-apple-darwin.tar.gz.sha256)"
echo "X86_LINUX=$(awk '{print $1}' shas/dirge-x86_64-unknown-linux-gnu.tar.gz.sha256)"
} >> "$GITHUB_ENV"
- name: Check out the tap
uses: actions/checkout@v7
with:
repository: dirge-code/homebrew-dirge
token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
path: tap
- name: Update the formula in place
run: |
f=tap/Formula/dirge.rb
# version line + the version segment in every release URL
sed -i \
-e "s|version \"[^\"]*\"|version \"$VERSION\"|" \
-e "s|/releases/download/v[0-9][^/]*/|/releases/download/v$VERSION/|g" \
"$f"
# rewrite each sha256 by the URL that immediately precedes it
awk -v arm="$ARM_MAC" -v xmac="$X86_MAC" -v xlin="$X86_LINUX" '
/aarch64-apple-darwin\.tar\.gz"/ { want="arm" }
/x86_64-apple-darwin\.tar\.gz"/ { want="xmac" }
/x86_64-unknown-linux-gnu\.tar\.gz"/ { want="xlin" }
/sha256 "/ {
v = (want=="arm" ? arm : (want=="xmac" ? xmac : xlin))
sub(/sha256 "[^"]*"/, "sha256 \"" v "\"")
want=""
}
{ print }
' "$f" > "$f.tmp" && mv "$f.tmp" "$f"
- name: Commit and push
run: |
cd tap
if git diff --quiet; then
echo "Formula already at $VERSION — nothing to do."
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git commit -am "dirge $VERSION"
git push
# Publish to crates.io (dirge-agent). Was manual, and 0.20.0 never reached
# the registry as a result — `cargo install dirge-agent` went 0.19.29 →
# 0.21.0 (dirge-a9pc). Needs a CARGO_REGISTRY_TOKEN repo secret scoped to
# publish-update on the crate.
#
# `needs: build` because publishing is PERMANENT — a version can be yanked
# but never replaced. Gating on all five targets building means a tag that
# doesn't compile everywhere doesn't become an immovable registry entry.
crates-io:
name: Publish to crates.io
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
# A tag whose version doesn't match the manifest would publish the
# WRONG version under the right tag, which no later check can undo.
- name: Check the tag matches Cargo.toml
run: |
set -euo pipefail
tag="${GITHUB_REF_NAME#v}"
manifest="$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].version')"
if [ "$tag" != "$manifest" ]; then
echo "::error::tag ${GITHUB_REF_NAME} disagrees with Cargo.toml version ${manifest}" >&2
exit 1
fi
echo "tag and manifest agree on ${manifest}"
- name: Publish
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: |
set -euo pipefail
if [ -z "${CARGO_REGISTRY_TOKEN:-}" ]; then
echo "::error::CARGO_REGISTRY_TOKEN is not set — add it as a repo secret" >&2
exit 1
fi
# Idempotent: re-running a completed release must not fail. cargo
# refuses to overwrite an existing version, and that refusal is the
# success case here.
set -o pipefail
if cargo publish --locked 2>&1 | tee /tmp/publish.log; then
exit 0
fi
if grep -qE "already (uploaded|exists)|crate version .* is already" /tmp/publish.log; then
echo "dirge-agent ${GITHUB_REF_NAME#v} is already on crates.io — nothing to do"
exit 0
fi
exit 1
# Bump the version shown next to the logo on https://dirge-code.github.io/.
# Also manual until now, and also missed for 0.20.0 — the site still read
# v0.19.29 when 0.21.0 was cut (dirge-a9pc). Same shape as the homebrew job:
# needs a SITE_REPO_TOKEN secret (PAT with contents:write on the site repo)
# because GITHUB_TOKEN can't push to another repository.
site:
name: Bump site version
needs: build
runs-on: ubuntu-latest
steps:
- name: Check out the site
uses: actions/checkout@v7
with:
repository: dirge-code/dirge-code.github.io
token: ${{ secrets.SITE_REPO_TOKEN }}
path: site
- name: Rewrite the brand-ver span
run: |
set -euo pipefail
version="${GITHUB_REF_NAME#v}"
f=site/index.html
sed -i "s|\(class=\"brand-ver\">\)v[0-9][^<]*|\1v${version}|" "$f"
# An unmatched sed is a silent no-op, which is exactly the failure
# mode this job exists to remove. Prove the string is there now.
grep -q "class=\"brand-ver\">v${version}<" "$f" || {
echo "::error::brand-ver span did not end up at v${version} — has the markup changed?" >&2
exit 1
}
- name: Commit and push
run: |
set -euo pipefail
cd site
if git diff --quiet; then
echo "Site already at ${GITHUB_REF_NAME} — nothing to do."
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git commit -am "Bump version to ${GITHUB_REF_NAME}"
git push
# The actual fix for dirge-a9pc. Every job above can report success while
# shipping nothing: the homebrew and site seds no-op silently if the markup
# moves, and a skipped job is green by default. This reads each channel back
# from its published source and fails if any of them disagrees with the tag,
# so "a channel didn't ship" is a red X instead of something noticed two
# releases later.
verify-channels:
name: Verify all release channels
needs: [build, nix-binary-package, homebrew, crates-io, site]
runs-on: ubuntu-latest
steps:
- name: Check every channel carries this version
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -uo pipefail
tag="$GITHUB_REF_NAME"
version="${tag#v}"
failed=0
fail() { echo "::error::$1"; failed=1; }
ok() { echo "ok: $1"; }
# Fetch to a file, THEN match. `gh api … | grep -q` looks equivalent
# and isn't: grep -q exits on the first match, gh takes SIGPIPE, and
# pipefail reports the pipeline as failed — a missing-channel error
# for a channel that is fine. It only shows up on inputs long enough
# to still be streaming (index.html), which is the worst way to find
# out.
fetch() { gh api "$1" -H "Accept: application/vnd.github.raw" > "$2"; }
# 1. GitHub release — every packaged target plus its checksum.
assets="$(gh release view "$tag" -R "$GITHUB_REPOSITORY" --json assets --jq '.assets[].name')"
for want in \
dirge-x86_64-unknown-linux-gnu.tar.gz \
dirge-x86_64-unknown-linux-musl.tar.gz \
dirge-x86_64-apple-darwin.tar.gz \
dirge-aarch64-apple-darwin.tar.gz \
dirge-x86_64-pc-windows-msvc.zip \
sbom.spdx.json
do
if grep -Fxq "$want" <<< "$assets"; then ok "release asset $want"
else fail "release $tag is missing asset $want"; fi
case "$want" in
sbom.spdx.json) ;;
*) if grep -Fxq "$want.sha256" <<< "$assets"; then ok "release asset $want.sha256"
else fail "release $tag is missing checksum $want.sha256"; fi ;;
esac
done
# 2. crates.io. The sparse index is CDN-cached, so give it a few
# minutes before calling it missing.
on_registry() {
curl -sf https://index.crates.io/di/rg/dirge-agent > /tmp/crates-index.json || return 1
[ "$(jq -sr --arg v "$version" 'any(.[]; .vers == $v)' /tmp/crates-index.json)" = true ]
}
for attempt in $(seq 1 10); do
on_registry && break
echo "crates.io index has no ${version} yet (attempt ${attempt}/10)"
[ "$attempt" -lt 10 ] && sleep 30
done
if on_registry
then ok "crates.io has dirge-agent ${version}"
else fail "crates.io has no dirge-agent ${version}"; fi
# 3. nix/bin.nix, committed back to main by the bump job.
fetch "repos/$GITHUB_REPOSITORY/contents/nix/bin.nix?ref=main" /tmp/bin.nix
if grep -q "version = \"${version}\";" /tmp/bin.nix
then ok "nix/bin.nix on main is at ${version}"
else fail "nix/bin.nix on main is not at ${version}"; fi
# 4. Homebrew tap.
fetch repos/dirge-code/homebrew-dirge/contents/Formula/dirge.rb /tmp/dirge.rb
if grep -q "version \"${version}\"" /tmp/dirge.rb
then ok "homebrew formula is at ${version}"
else fail "homebrew formula is not at ${version}"; fi
# 5. Site.
fetch repos/dirge-code/dirge-code.github.io/contents/index.html /tmp/index.html
if grep -q "class=\"brand-ver\">v${version}<" /tmp/index.html
then ok "site brand-ver is at v${version}"
else fail "site brand-ver is not at v${version}"; fi
if [ "$failed" -ne 0 ]; then
echo "::error::one or more release channels did not receive ${tag}"
exit 1
fi
echo "all six channels carry ${tag}"