-
Notifications
You must be signed in to change notification settings - Fork 0
282 lines (262 loc) · 12.1 KB
/
Copy pathrelease.yml
File metadata and controls
282 lines (262 loc) · 12.1 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
---
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
#
# Attach the static musl binaries to a published GitHub Release.
#
# Release Please owns the release itself (version, tag, changelog body) — see
# release-please.yml. This workflow only builds artifacts and uploads them, so
# it must never generate release notes of its own or it would clobber the
# changelog Release Please just wrote.
#
# The container image is built separately by docker.yml, which also triggers on
# `release: published`.
#
# Asset naming — `domarinn_<version>_linux_<arch>`, e.g.
# `domarinn_0.2.0_linux_amd64`. Three properties are deliberate:
#
# * The version is always the second underscore-separated field, so a
# downloaded file identifies itself. Nothing else in a release does: the
# binary was previously named only for its target, and a file sitting in
# ~/Downloads was unidentifiable.
# * `linux_amd64`, not the Rust triple `x86_64-unknown-linux-musl`. The triple
# puts the literal word "unknown" (it is the *vendor* field) exactly where a
# reader expects the version. `musl` is not in the name either — that these
# are fully static is a documented property, not a filename.
# * Underscores, matching the GoReleaser convention this project's assets are
# read against, even though it does not use GoReleaser (see 007df97 for why).
#
# Because the names carry a version, `releases/latest/download/<name>` is no
# longer constructible — GitHub has no wildcard there. Consumers resolve the tag
# first via the `/releases/latest` redirect; README.md, docs/start/install.md
# and .github/actions/domarinn-eval/action.yml all do this.
name: Release
on:
release:
types: [published]
permissions:
contents: read
env:
CARGO_TERM_COLOR: always
jobs:
# ---------------------------------------------------------------------------
# 1. Build the static musl binaries and stage them as workflow artifacts.
# Both targets build natively — no `cross`, no QEMU — because GitHub
# provides arm64 runners free to public repositories. That also means
# neither leg is allowed to fail: an aarch64 binary is a shipped artifact,
# not a best-effort extra.
# ---------------------------------------------------------------------------
binaries:
name: build ${{ matrix.target }}
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
# `target` is the Rust triple the compiler needs; `arch` is the name
# the world uses, and the one that reaches the released filename.
- target: x86_64-unknown-linux-musl
arch: amd64
runner: ubuntu-24.04
- target: aarch64-unknown-linux-musl
arch: arm64
runner: ubuntu-24.04-arm
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# mise installs the pinned Rust/Node/pnpm from .mise/config.toml +
# .mise/mise.lock, the single source of toolchain versions for both CI
# and releases. Caching is off: this workflow publishes artifacts, and a
# poisoned cache entry must never be able to reach a shipped binary.
- uses: jdx/mise-action@3c2e0cf82a5b2e5249f0d3635a4d83d0ae861518 # v4.2.5
with:
install_args: node pnpm rust
cache: false
# The release binary embeds the web UI, so build web/dist first.
- run: pnpm -C web install --frozen-lockfile
- run: pnpm -C web build
# musl-tools gives the C toolchain rusqlite's bundled SQLite (and the
# rustls crypto backend) need; cmake is a hedge for crypto backends.
- name: Install musl toolchain
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends musl-tools cmake
# The `musl-build` task adds the rustup target itself and then builds;
# same command locally and in CI. No rust-cache here (unlike ci.yml):
# release artifacts build from a cold cache.
- name: Build
env:
MUSL_TARGET: ${{ matrix.target }}
run: mise run musl-build
# Only the binary. Checksums are a single release-wide manifest written by
# the upload job, because a per-target file cannot cover the artifacts
# that do not belong to any one target.
- name: Stage artifact
env:
TARGET: ${{ matrix.target }}
ARCH: ${{ matrix.arch }}
TAG: ${{ github.event.release.tag_name }}
run: |
set -euo pipefail
# Release Please tags bare semver (`0.2.0`, not `v0.2.0`), so this
# normally strips nothing — it is here so a future tag-format change
# cannot silently ship `domarinn_v0.2.0_linux_amd64`.
version="${TAG#v}"
mkdir -p dist
name="domarinn_${version}_linux_${ARCH}"
cp "target/${TARGET}/release/domarinn" "dist/${name}"
file "dist/${name}"
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: binary-${{ matrix.arch }}
path: dist/*
if-no-files-found: error
# ---------------------------------------------------------------------------
# 2. Catalogue the dependency graph, once for the whole release.
#
# This is a job of its own rather than a step in the matrix above, and the
# separation is the point: the SBOM is generated from Cargo.lock and
# web/pnpm-lock.yaml, which say nothing about the target triple, so the two
# matrix legs used to produce the same 890-package document twice and ship
# it under two names — implying a per-target dependency graph that does not
# exist. One release, one SBOM.
#
# An empty checkout is also exactly what syft wants. On a pristine tree it
# sees only the two lockfiles; after a build it would additionally crawl
# target/ and node_modules/ — gigabytes of build output, for a worse
# result. And `[profile.release] strip = true` (Cargo.toml) means scanning
# the shipped binary would report 1 package where the lockfiles report 890.
#
# Both lockfiles matter: the binary embeds the built web UI, so the npm
# graph is genuinely part of what ships.
# ---------------------------------------------------------------------------
sbom:
name: catalogue dependencies
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Generate SBOM
uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
with:
path: .
format: spdx-json
output-file: sbom.spdx.json
# This workflow owns what reaches the release; the action must not
# attach assets or artifacts of its own.
upload-artifact: false
upload-release-assets: false
# `.spdx.json`, not `.sbom.json`: the extension is how a consumer tells
# SPDX from CycloneDX without parsing 1.9 MB of JSON. syft also names the
# document after the directory it scanned — literally "." — so stamp the
# release onto it, which is where the version belongs given the filename
# deliberately carries none (see the upload job).
- name: Name the document
env:
TAG: ${{ github.event.release.tag_name }}
run: |
set -euo pipefail
version="${TAG#v}"
mkdir -p dist
# -c because syft writes compact JSON and this step should change one
# field, not the whole encoding. Without it jq re-indents all 900-odd
# packages and the shipped asset grows ~28% (2.03 MB -> 2.60 MB in
# 0.1.3) for nothing a machine reads.
jq -c --arg name "domarinn-${version}" '.name = $name' sbom.spdx.json \
> "dist/domarinn_${version}.spdx.json"
jq -r '"\(.name): \(.spdxVersion), \(.packages | length) packages"' \
"dist/domarinn_${version}.spdx.json"
# Shipped as a release asset so it inherits the checksums and the cosign
# signature. The file is also readable at a raw tag URL without any of
# this — but that URL is unsigned, and a schema is something editors fetch
# and trust.
- name: Stage the config schema
env:
TAG: ${{ github.event.release.tag_name }}
run: |
set -euo pipefail
version="${TAG#v}"
cp domarinn.schema.json "dist/domarinn_${version}.schema.json"
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: sbom
path: dist/*
if-no-files-found: error
# ---------------------------------------------------------------------------
# 3. Checksum, sign, and attach everything to the release Release Please
# already published.
# ---------------------------------------------------------------------------
upload:
name: upload release assets
needs: [binaries, sbom]
runs-on: ubuntu-24.04
permissions:
contents: write
# Keyless cosign: the OIDC token is the signing identity, so there is no
# private key to hold. Same mechanism docker/github-builder already uses
# to sign the container image.
id-token: write
steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
path: dist
merge-multiple: true
# One checksum manifest for the release, the convention every other Go/Rust
# project ships. Generated from inside dist/ because sha256sum records the
# path exactly as given: doing this from the repo root would bake in a
# `dist/` prefix that does not exist for whoever downloads the release,
# and their `sha256sum --check` would fail with "No such file or
# directory". That shipped broken in 0.1.1.
#
# Operands are listed explicitly rather than globbed as `*` for two
# reasons: the manifest must not end up inside itself, and `failglob`
# turns a missing artifact into a failed release instead of a silently
# short manifest.
- name: Generate checksums
env:
TAG: ${{ github.event.release.tag_name }}
run: |
set -euo pipefail
shopt -s failglob
version="${TAG#v}"
cd dist
sha256sum -- "domarinn_${version}_linux_"* "domarinn_${version}.spdx.json" \
"domarinn_${version}.schema.json" \
> "domarinn_${version}_checksums.txt"
cat "domarinn_${version}_checksums.txt"
- name: Install cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
# Sign every artifact — binaries, SBOM, and the checksum manifest alike —
# so a consumer can verify provenance without trusting the download path.
# Bundles are self-contained (certificate + signature + Rekor entry), so
# verification needs no keyserver, only `cosign verify-blob --bundle`.
# They are not themselves checksummed: a bundle carries its own integrity.
- name: Sign artifacts
run: |
set -euo pipefail
shopt -s nullglob
signed=0
for f in dist/*; do
case "$f" in
*.sigstore.json) continue ;;
esac
echo "::group::sign $f"
cosign sign-blob --yes --bundle "${f}.sigstore.json" "$f"
echo "::endgroup::"
signed=$((signed + 1))
done
# A silent zero here would publish an unsigned release that looks fine.
if [ "$signed" -eq 0 ]; then
echo "::error::no artifacts were signed - dist/ was empty"
exit 1
fi
echo "signed ${signed} artifacts"
- name: Upload
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ github.event.release.tag_name }}
GH_REPO: ${{ github.repository }}
# --clobber so re-running the workflow on an existing release replaces
# the assets instead of erroring out.
run: gh release upload "$TAG" dist/* --clobber