-
Notifications
You must be signed in to change notification settings - Fork 5
319 lines (307 loc) · 16.2 KB
/
Copy pathrelease-cli.yml
File metadata and controls
319 lines (307 loc) · 16.2 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
# Attach prebuilt `areev` binaries to the GitHub Release.
#
# Until this existed, the only way to get the CLI was `cargo install areev` —
# a full Rust build. That put the web console (`areev ui`, including the Areev Loop
# review queue) out of reach of anywhere without a toolchain: notebooks, Colab,
# a scratch container, an ops box evaluating the tool.
#
# Trigger: on a published GitHub Release (tag `vX.Y.Z`) or manual dispatch.
# A plain manual dispatch builds and uploads artifacts but attaches nothing,
# so it is safe to use as a dry run. Giving it `attach_to_tag: vX.Y.Z`
# additionally runs `publish` against that EXISTING release — the backfill
# path for when `release`'s own run fails partway (a bad matrix leg, one
# platform's toolchain) after the Release is already public and pypi/npm may
# already have shipped, so re-publishing the Release to retrigger everything
# is not an option. It goes through the exact same attest-then-upload steps
# as a real release, so a backfilled asset is attested identically to one
# that landed on the first try. Guarded on the input being non-empty so a
# stray dispatch can never attach by accident; requires the same repo-write
# access dispatching this workflow already needs.
#
# GLIBC baseline: the Linux assets are built on the `ubuntu-latest` /
# `ubuntu-24.04-arm` images, so they need **GLIBC 2.39 or newer** (Ubuntu
# 24.04+, Debian 13+, RHEL 10+). On an older distro — Ubuntu 22.04 carries
# 2.35 and is supported until 2027 — the dynamic linker fails with a raw
# `version 'GLIBC_2.39' not found`, which says nothing useful. Use the
# container image, `cargo install areev`, or build from source there. This is
# stated on the Releases page and in docs/quickstart.md; keep the three in
# step if the runner image moves.
#
# No secrets. `GITHUB_TOKEN` with `contents: write` is enough to upload assets.
name: release-cli
on:
release:
types: [published]
workflow_dispatch:
inputs:
attach_to_tag:
description: >-
Backfill: attach this run's binaries to an EXISTING published
release (e.g. v1.6.4). Leave empty for the normal dry run that
builds and uploads artifacts but attaches nothing.
required: false
default: ""
permissions:
contents: read
jobs:
build:
name: ${{ matrix.target }}${{ matrix.suffix }}
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
# Build aarch64-linux NATIVELY on a free arm64 runner. Cross-gcc
# cannot compile turso's mimalloc/zstd C deps ("no option
# -Wdate-time"); a native arm64 gcc has no such issue. Same reason
# release-pypi.yml uses this runner — keep the two in step.
- { runner: ubuntu-latest, target: x86_64-unknown-linux-gnu, ext: "", features: "", suffix: "" }
- { runner: ubuntu-24.04-arm, target: aarch64-unknown-linux-gnu, ext: "", features: "", suffix: "" }
# macOS runners are arm64; x86_64 cross-compiles cleanly there
# because Apple ships both SDKs.
- { runner: macos-latest, target: x86_64-apple-darwin, ext: "", features: "", suffix: "" }
- { runner: macos-latest, target: aarch64-apple-darwin, ext: "", features: "", suffix: "" }
- { runner: windows-latest, target: x86_64-pc-windows-msvc, ext: ".exe", features: "", suffix: "" }
# Server-tier Linux builds (#123). The default assets deliberately
# carry NEITHER postgres nor TLS — that is what keeps the edge
# binary dependency-light. But the docs point at Postgres as the
# server-tier backend, so without these the released binary was the
# one artifact that could not run the deployment shape the docs
# recommend, and the alternative was a 15-minute compile.
#
# `postgres-tls`, not `postgres`: the plain feature REFUSES a DSN
# carrying `sslmode=` (STO-E003), and Azure Flexible Server, RDS
# with `rds.force_ssl` and Cloud SQL all require TLS. A
# postgres-only asset would hit a second wall one step later.
# Linux only — the server tier does not deploy on macOS/Windows.
- { runner: ubuntu-latest, target: x86_64-unknown-linux-gnu, ext: "", features: "postgres-tls", suffix: "-postgres" }
- { runner: ubuntu-24.04-arm, target: aarch64-unknown-linux-gnu, ext: "", features: "postgres-tls", suffix: "-postgres" }
# A console that fronts a postgres memory on the public internet
# needs an identity stronger than a shared proxy secret, because an
# OIDC principal may answer a HITL ask and a proxy-asserted one may
# not. That combination — postgres storage AND native OIDC — was
# buildable and not published, so the only way to run it was to
# compile on the box. Linux only, for the same reason as the row
# above: the server tier does not deploy on macOS/Windows.
- { runner: ubuntu-latest, target: x86_64-unknown-linux-gnu, ext: "", features: "postgres-tls,oidc", suffix: "-postgres-oidc" }
- { runner: ubuntu-24.04-arm, target: aarch64-unknown-linux-gnu, ext: "", features: "postgres-tls,oidc", suffix: "-postgres-oidc" }
steps:
- uses: actions/checkout@v7
with:
# A backfill labels its assets with attach_to_tag's version and
# uploads them to THAT release with --clobber, so they must be built
# from that tag. Left to its default this checks out whatever ref the
# run was dispatched against (main), and the backfill would overwrite
# a published release's binaries with different code under the old
# version's name — the one outcome a signed, attested asset is
# supposed to make impossible. Empty string keeps the default ref for
# a real release and for the plain dry-run dispatch.
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.attach_to_tag || '' }}
- name: Install Rust
run: rustup update stable && rustup target add ${{ matrix.target }}
# `shell: bash` explicitly: `windows-latest` defaults to PowerShell,
# where a bare `\` is not a line continuation — it parsed `--target` as
# PowerShell's unary `--` operator and failed the whole leg, which
# (matrix jobs report pass/fail as one unit) skipped `publish` for
# every platform even though the other seven legs had already
# succeeded. Every other `run:` step below already says so explicitly;
# this one didn't, because before #123 it was a one-liner with nothing
# shell-specific to get wrong.
- name: Build areev
shell: bash
run: |
cargo build --release --locked -p areev --bin areev \
--target ${{ matrix.target }} \
${{ matrix.features != '' && format('--features {0}', matrix.features) || '' }}
# Fail here rather than shipping a binary that cannot start. A release
# asset nobody ran is worth less than no asset at all.
#
# Skipped only for the one cross-compiled target: the macOS runners are
# arm64, so the x86_64 build cannot be relied on to execute there
# (Rosetta may or may not be present). Every other target is native.
- name: Smoke-test the binary
if: matrix.target != 'x86_64-apple-darwin'
shell: bash
run: |
BIN="target/${{ matrix.target }}/release/areev${{ matrix.ext }}"
"$BIN" --version
"$BIN" add --db "$RUNNER_TEMP/smoke.db" --ns caller john prefers tea
"$BIN" recall --db "$RUNNER_TEMP/smoke.db" --ns caller --subject john | grep -q tea
# The whole point of the -postgres asset is that it HAS the backend,
# so prove it rather than trusting the feature flag reached the
# build. Port 1 refuses instantly: a working build fails to CONNECT,
# a mis-built one fails with "lacks the postgres backend". The
# sslmode= is deliberate — it also proves this is postgres-tls and
# not plain postgres, which would refuse the DSN with STO-E003.
if [ -n "${{ matrix.features }}" ]; then
out="$("$BIN" recall --db 'postgresql://u:p@127.0.0.1:1/none?sslmode=require&schema=smoke' \
--ns caller --subject john 2>&1 || true)"
printf '%s\n' "$out"
case "$out" in
*"lacks the postgres backend"*) echo "::error::postgres feature missing from the -postgres asset"; exit 1 ;;
*"STO-E003"*) echo "::error::built with plain postgres, not postgres-tls"; exit 1 ;;
esac
fi
# Same argument for oidc, which is the ENTIRE reason the
# -postgres-oidc asset exists: a feature-flag typo still builds and
# still passes every check above, and the miss only surfaces when a
# deployed console rejects its own --oidc-issuer. The two builds
# disagree in a way that is cheap to read: without the feature the
# flag is refused outright ("no native OIDC"), with it the flag is
# accepted and then asks for its companions.
case "${{ matrix.features }}" in
*oidc*)
out="$("$BIN" ui --db "$RUNNER_TEMP/smoke.db" \
--oidc-issuer https://login.microsoftonline.com/smoke/v2.0 2>&1 || true)"
printf '%s\n' "$out"
case "$out" in
*"no native OIDC"*) echo "::error::oidc feature missing from the -postgres-oidc asset"; exit 1 ;;
*"needs --oidc-client-id"*) echo "oidc present" ;;
*) echo "::error::unrecognised --oidc-issuer response; the smoke test can no longer tell the two builds apart"; exit 1 ;;
esac
;;
esac
- name: Package
shell: bash
run: |
set -euo pipefail
VERSION="${GITHUB_REF_NAME#v}"
# workflow_dispatch has no tag to read a version from — UNLESS this
# is a backfill dispatch, in which case attach_to_tag names the
# real version being packaged (must match what `publish` uploads
# to below, or the asset names on the release would disagree with
# SHA256SUMS's own filenames).
if [ "$GITHUB_EVENT_NAME" != "release" ]; then
VERSION="${{ inputs.attach_to_tag }}"
VERSION="${VERSION#v}"
[ -n "$VERSION" ] || VERSION="dev"
fi
NAME="areev-${VERSION}-${{ matrix.target }}${{ matrix.suffix }}"
mkdir -p "dist/$NAME"
cp "target/${{ matrix.target }}/release/areev${{ matrix.ext }}" "dist/$NAME/"
cp README.md LICENSE-MIT LICENSE-APACHE "dist/$NAME/"
cd dist
if [ "${{ matrix.ext }}" = ".exe" ]; then
7z a "${NAME}.zip" "$NAME" >/dev/null
ASSET="${NAME}.zip"
else
tar czf "${NAME}.tar.gz" "$NAME"
ASSET="${NAME}.tar.gz"
fi
# Per-asset checksum; the job that publishes concatenates them, since
# each target is built on a different machine.
if command -v sha256sum >/dev/null; then
sha256sum "$ASSET" > "${ASSET}.sha256"
else
shasum -a 256 "$ASSET" > "${ASSET}.sha256"
fi
echo "ASSET=$ASSET" >> "$GITHUB_ENV"
- uses: actions/upload-artifact@v7
with:
name: cli-${{ matrix.target }}${{ matrix.suffix }}
path: |
dist/*.tar.gz
dist/*.zip
dist/*.sha256
if-no-files-found: error
# A bill of materials for the binary's dependency graph (#81). Generated
# once, on one runner: the graph comes from Cargo.lock, which is committed
# and identical for every target, so five copies would be five identical
# files with different names.
#
# cargo-cyclonedx is pinned rather than floating: an SBOM whose generator
# changes between releases produces diffs that are the tool moving, not the
# dependencies, and the point of publishing one is that a consumer can
# diff two releases and see what actually changed.
sbom:
name: SBOM
runs-on: ubuntu-latest
# A bill of materials is documentation about a build, not the build. It
# must never be able to withhold a shipped binary from users — which is
# exactly what it did on the 1.5.2 tag, where a wrong flag in this job
# left five green platform builds unattached to the release. `publish`
# no longer waits on it either; both halves of that lesson matter.
continue-on-error: true
steps:
- uses: actions/checkout@v7
- name: Install Rust
run: rustup update stable
- name: Install cargo-cyclonedx
run: cargo install cargo-cyclonedx --version 0.5.7 --locked
- name: Generate CycloneDX SBOM
run: |
set -euo pipefail
VERSION="${GITHUB_REF_NAME#v}"
if [ "$GITHUB_EVENT_NAME" != "release" ]; then
VERSION="${{ inputs.attach_to_tag }}"
VERSION="${VERSION#v}"
[ -n "$VERSION" ] || VERSION="dev"
fi
# cargo-cyclonedx takes NEITHER `--locked` nor `-p`, and it ignores
# `--override-filename`: it writes `<package>.cdx.json` next to each
# manifest it describes. `--top-level` is also deliberately absent —
# it truncates the document to DIRECT dependencies (17 of 341 here),
# which is not a bill of materials.
cargo cyclonedx --manifest-path crates/areev-cli/Cargo.toml \
--format json --spec-version 1.5
mkdir -p dist
cp crates/areev-cli/areev.cdx.json "dist/areev-${VERSION}-cargo.cdx.json"
test -s "dist/areev-${VERSION}-cargo.cdx.json"
- uses: actions/upload-artifact@v7
with:
name: cli-sbom
path: dist/*.cdx.json
if-no-files-found: error
publish:
name: attach to the release
# `needs: sbom` for ORDERING only — the job is `continue-on-error`, and a
# continue-on-error job that fails still satisfies `needs`. So the SBOM is
# attached alongside the binaries when it works, and cannot hold them
# hostage when it does not.
needs: [build, sbom]
# A real release always has something to attach to. A manual run does
# too, but ONLY when explicitly told which existing release (backfill);
# otherwise it stops after build, which is what makes a plain
# `workflow_dispatch` a safe dry run.
if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && inputs.attach_to_tag != '')
runs-on: ubuntu-latest
permissions:
contents: write
# Build provenance (#81): a consumer can prove a downloaded asset was
# produced by THIS workflow in THIS repo, rather than on a maintainer's
# laptop or through a stolen publish token. Sigstore-backed, keyless —
# no key for us to lose, which is the whole reason to prefer it.
id-token: write
attestations: write
steps:
- uses: actions/download-artifact@v8
with:
pattern: cli-*
merge-multiple: true
path: dist
- name: Collect checksums
run: |
set -euo pipefail
cd dist
cat ./*.sha256 | sort -k2 > SHA256SUMS
rm -f ./*.sha256
cat SHA256SUMS
# Attest BEFORE upload: an asset on the release that no attestation
# covers is worse than one with none at all, because the release notes
# claim every asset is attested.
- name: Attest build provenance
uses: actions/attest-build-provenance@v4
with:
subject-path: |
dist/*.tar.gz
dist/*.zip
- name: Upload assets
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# $GITHUB_REF_NAME is the TAG on a real release, but on a backfill
# dispatch it is whatever ref this run was dispatched against
# (e.g. `main`) — the release to attach to is attach_to_tag, not
# that ref. `gh release upload` fails loudly if the named tag has
# no release, rather than silently attaching to the wrong one.
TARGET_TAG: ${{ github.event_name == 'release' && github.ref_name || inputs.attach_to_tag }}
run: gh release upload "$TARGET_TAG" dist/* --clobber --repo "$GITHUB_REPOSITORY"