Repository navigation
397 lines (361 loc) · 15.8 KB
/
Copy pathrelease.yml
File metadata and controls
397 lines (361 loc) · 15.8 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
# Reproducible + signed release workflow.
#
# Triggers on a `vX.Y.Z` (release) or `vX.Y.Z-<suffix>` (pre-release) tag push.
# 0. Gate: the full CI suite (ci.yml) and the security audit (audit.yml) run first
# as reusable workflows; nothing builds unless both pass on the tagged commit.
# 1. A matrix `build` job compiles the native binaries (chela, chela-cli) for each
# target twice in independent target dirs and fails if the two hashes differ -
# the reproducibility check. It uploads the raw binaries (perms restored later,
# since upload-artifact drops the Unix +x bit).
# 2. A `bundle-web` job builds the standalone `chela.html` once (also double-built).
# 3. A `release` job downloads everything and assembles ONE zip,
# `chela-<tag>.zip`: a subfolder per architecture, the standalone chela.html,
# a docs/ folder, and a README.txt. chela.html is also published on its own
# (RECOVERY.md links straight to it). Each published file plus a SHA256SUMS
# aggregate is signed with minisign and attached to the GitHub release.
#
# To cut a release the maintainer must:
# - have generated a minisign keypair (one-time, off-CI).
# - set the `MINISIGN_PRIVATE_KEY` and `MINISIGN_PASSWORD` repo secrets.
# - publish the matching public key in README.md (see "Verifying a release").
# - push a tag like `v1.0.0`, or a pre-release like `v1.0.0-beta.1`, to main.
# See RELEASING.md for the full operator runbook.
#
# Security:
# - Every interpolation of a `${{ ... }}` expression into a shell `run:` block goes
# through an explicit `env:` mapping. The tag patterns below pin the numeric core
# (`v[0-9]+.[0-9]+.[0-9]+`); the optional pre-release suffix after `-` is free-form,
# but the env-var indirection means even an unusual tag name never reaches a shell
# unquoted. The `prerelease`/`make_latest` flags read `github.ref_name` only inside
# action `with:` inputs, never a shell.
# - Every third-party action is pinned to a full commit SHA, not a mutable @v4 / @stable
# tag/branch. An upstream maintainer compromise cannot silently push new code into the
# release job that holds the minisign signing key. Trailing comment is informational;
# bump via Dependabot.
name: Release
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+' # releases: v1.2.3
- 'v[0-9]+.[0-9]+.[0-9]+-*' # pre-releases: v1.2.3-beta.1, v1.2.3-rc.2, ...
permissions:
contents: read # default for all jobs; only the release job overrides to write
env:
CARGO_TERM_COLOR: always
jobs:
# Gate: nothing builds unless the full CI suite and the security audit both pass on
# the tagged commit. They run here as reusable workflows (`workflow_call`).
ci:
name: CI gate
uses: ./.github/workflows/ci.yml
audit:
name: Audit gate
uses: ./.github/workflows/audit.yml
build:
name: Build ${{ matrix.target }}
needs: [ci, audit]
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-gnu
os: ubuntu-latest
bin_suffix: ""
# Strip ELF build-id (random per link) so the binary is byte-stable.
rustflags: "-C link-arg=-Wl,--build-id=none"
- target: x86_64-apple-darwin
os: macos-latest
bin_suffix: ""
# Strip Mach-O LC_UUID (random per link).
rustflags: "-C link-arg=-Wl,-no_uuid"
- target: aarch64-apple-darwin
os: macos-latest
bin_suffix: ""
rustflags: "-C link-arg=-Wl,-no_uuid"
- target: x86_64-pc-windows-msvc
os: windows-latest
bin_suffix: ".exe"
# Make the PE timestamp deterministic (MSVC linker flag).
rustflags: "-C link-arg=/Brepro"
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 (Node 24)
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 # master @ 2026-05
with:
toolchain: stable
targets: ${{ matrix.target }}
- name: Build (pass 1)
shell: bash
env:
CARGO_TARGET_DIR: build-1
RUSTFLAGS: ${{ matrix.rustflags }}
TARGET: ${{ matrix.target }}
run: |
cargo build --release --locked --target "$TARGET" \
-p chela-tui -p chela-cli
- name: Build (pass 2, independent target dir)
shell: bash
env:
CARGO_TARGET_DIR: build-2
RUSTFLAGS: ${{ matrix.rustflags }}
TARGET: ${{ matrix.target }}
run: |
cargo build --release --locked --target "$TARGET" \
-p chela-tui -p chela-cli
- name: Verify reproducibility (hash both passes)
shell: bash
env:
TARGET: ${{ matrix.target }}
BIN_SUFFIX: ${{ matrix.bin_suffix }}
run: |
set -euo pipefail
fail=0
for bin in chela chela-cli; do
b1="build-1/${TARGET}/release/${bin}${BIN_SUFFIX}"
b2="build-2/${TARGET}/release/${bin}${BIN_SUFFIX}"
if [ ! -f "$b1" ] || [ ! -f "$b2" ]; then
echo "::error::expected binary $bin missing"
fail=1
continue
fi
h1=$(sha256sum "$b1" | awk '{print $1}')
h2=$(sha256sum "$b2" | awk '{print $1}')
if [ "$h1" != "$h2" ]; then
echo "::error::$bin not reproducible: $h1 != $h2"
fail=1
else
echo "$bin reproducible: $h1"
fi
done
exit $fail
# Upload the raw pass-1 binaries under a per-target subfolder; the release job
# collects every target into one archive. upload-artifact drops the Unix +x bit,
# which the release job restores before packing.
- name: Stage binaries
shell: bash
env:
TARGET: ${{ matrix.target }}
BIN_SUFFIX: ${{ matrix.bin_suffix }}
run: |
set -euo pipefail
dest="dist/${TARGET}"
mkdir -p "$dest"
for bin in chela chela-cli; do
cp "build-1/${TARGET}/release/${bin}${BIN_SUFFIX}" "$dest/"
done
- name: Upload binaries
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 (Node 24)
with:
name: bin-${{ matrix.target }}
path: dist
bundle-web:
name: Bundle standalone chela.html
needs: [ci, audit]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 (Node 24)
- uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 # master @ 2026-05
with:
toolchain: stable
targets: wasm32-unknown-unknown
- name: Build chela-bundle and produce chela.html (pass 1)
shell: bash
env:
CARGO_TARGET_DIR: build-1
run: |
cargo build --release --locked --bin chela-bundle
./build-1/release/chela-bundle build-1/chela.html
- name: Build chela-bundle and produce chela.html (pass 2)
shell: bash
env:
CARGO_TARGET_DIR: build-2
run: |
cargo build --release --locked --bin chela-bundle
./build-2/release/chela-bundle build-2/chela.html
- name: Verify chela.html reproducibility
shell: bash
run: |
h1=$(sha256sum build-1/chela.html | awk '{print $1}')
h2=$(sha256sum build-2/chela.html | awk '{print $1}')
if [ "$h1" != "$h2" ]; then
echo "::error::chela.html not reproducible: $h1 != $h2"
exit 1
fi
echo "chela.html reproducible: $h1"
- name: Stage chela.html
shell: bash
# NB: not `web/` - that's the repo's GitHub Pages dir, and `path: web` would
# upload the whole explainer site.
run: |
mkdir -p htmlout
cp build-1/chela.html htmlout/chela.html
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 (Node 24)
with:
name: web
path: htmlout
release:
name: Sign + publish GitHub release
needs: [build, bundle-web]
runs-on: ubuntu-latest
permissions:
contents: write # this job attaches signed artifacts to the GitHub release
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 (Node 24)
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 (Node 24)
with:
path: artifacts
merge-multiple: true
- name: Install minisign + zip
shell: bash
run: sudo apt-get update && sudo apt-get install -y minisign zip
# One archive with everything: a subfolder per architecture, the standalone
# chela.html, docs/, and a README.txt. chela.html is also emitted on its own
# (the recovery guide links straight to it).
- name: Assemble the combined bundle
shell: bash
env:
TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
src=$(git -C . log -1 --pretty=%ct) # SOURCE_DATE_EPOCH for deterministic tar
bundle="chela-${TAG}"
mkdir -p out "${bundle}/docs"
# merge-multiple put each build's binaries under artifacts/<target>/ and the
# web bundle at artifacts/chela.html.
for d in artifacts/*/; do
t="$(basename "$d")"
# Only the per-target binary dirs hold a chela binary; skip anything else.
[ -e "${d}chela" ] || [ -e "${d}chela.exe" ] || continue
mkdir -p "${bundle}/${t}"
cp "${d}"chela* "${bundle}/${t}/"
# restore the +x bit upload-artifact strips (harmless on the .exe).
chmod +x "${bundle}/${t}/"* 2>/dev/null || true
done
cp artifacts/chela.html "${bundle}/chela.html"
cp artifacts/chela.html "out/chela-${TAG}-web.html"
cp README.md SPEC.md RECOVERY.md MANUAL_RECOVERY.md AUDITORS.md \
LICENSE-MIT LICENSE-APACHE "${bundle}/docs/"
cat > "${bundle}/README.txt" <<EOF
chela ${TAG}
One archive, every platform. Pick your architecture's subfolder:
x86_64-unknown-linux-gnu/ Linux (Intel/AMD 64-bit)
x86_64-apple-darwin/ macOS (Intel)
aarch64-apple-darwin/ macOS (Apple Silicon)
x86_64-pc-windows-msvc/ Windows (64-bit)
Each holds chela (the wizard TUI) and chela-cli (scriptable CLI). chela.html
is the standalone browser app - no install, open it in any modern browser.
docs/ has README.md, SPEC.md, RECOVERY.md, MANUAL_RECOVERY.md, AUDITORS.md,
and the two LICENSE files.
EOF
cat >> "${bundle}/README.txt" <<'EOF'
macOS - "chela cannot be opened because the developer cannot be verified":
The binaries are minisign-signed (see Verifying below) but are not notarized
by Apple, so macOS Gatekeeper quarantines them after download. From the macOS
subfolder, in Terminal:
chmod +x chela chela-cli
xattr -d com.apple.quarantine chela chela-cli
then run ./chela. GUI alternative: System Settings > Privacy & Security, then
"Open Anyway". Only do this for binaries whose minisign signature you verified.
(Linux is the same minus the xattr line; on Windows just run chela.exe.)
EOF
cat >> "${bundle}/README.txt" <<EOF
Verifying: check the minisign signature (chela-${TAG}.zip.minisig) against the
public key in README.md:
https://github.com/SecretSplitKit/Chela#verifying-a-release
EOF
# Deterministic zip: normalise mtimes, add entries in sorted order, drop the
# extra (uid/gid/extended-time) fields. zip keeps the Unix +x bits in the
# external attributes, which unzip and macOS Archive Utility restore.
find "${bundle}" -exec touch -d "@${src}" {} +
find "${bundle}" | sort | TZ=UTC zip -X -9 -@ "out/chela-${TAG}.zip" >/dev/null
ls -la out/
- name: Sign artifacts and build SHA256SUMS
shell: bash
env:
MINISIGN_PRIVATE_KEY: ${{ secrets.MINISIGN_PRIVATE_KEY }}
MINISIGN_PASSWORD: ${{ secrets.MINISIGN_PASSWORD }}
TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
if [ -z "${MINISIGN_PRIVATE_KEY:-}" ]; then
echo "::error::MINISIGN_PRIVATE_KEY repo secret is not set - see RELEASING.md"
exit 1
fi
if [ -z "${MINISIGN_PASSWORD:-}" ]; then
echo "::error::MINISIGN_PASSWORD repo secret is not set - see RELEASING.md"
exit 1
fi
umask 077
printf '%s' "$MINISIGN_PRIVATE_KEY" > /tmp/minisign.key
cd out
# SHA256SUMS aggregate (canonical, signed) + a per-file .sha256 (BSD-style).
: > SHA256SUMS
for f in $(ls -1 chela-*.zip chela-*.html 2>/dev/null | sort); do
sha256sum "$f" >> SHA256SUMS
sha256sum "$f" > "${f}.sha256"
done
# minisign (the C tool) has no password env var - it reads the passphrase from
# stdin when there is no tty (the CI case). printf is a bash builtin, so the
# secret never appears in the process list.
for f in chela-*.zip chela-*.html SHA256SUMS; do
[ -f "$f" ] || continue
printf '%s\n' "$MINISIGN_PASSWORD" \
| minisign -S -m "$f" -s /tmp/minisign.key -t "chela ${TAG} $(basename "$f")"
done
shred -u /tmp/minisign.key
- name: Build release notes body
shell: bash
env:
TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
cd out
{
cat <<HEADER
Reproducible signed release.
## Downloads
- \`chela-${TAG}.zip\` - everything in one archive: a subfolder per
architecture (Linux, macOS Intel, macOS Apple Silicon, Windows), each with
\`chela\` (the wizard) and \`chela-cli\`, plus the standalone \`chela.html\`,
a \`docs/\` folder, and a README.
- \`chela-${TAG}-web.html\` - just the standalone browser app (for recovery).
## Verifying
The chela minisign public key is in [README.md](https://github.com/SecretSplitKit/Chela#verifying-a-release).
Every file has a matching \`.minisig\`:
\`\`\`sh
minisign -V -p chela.pub -m chela-${TAG}.zip
minisign -V -p chela.pub -m SHA256SUMS
sha256sum -c SHA256SUMS
\`\`\`
## SHA-256
\`\`\`
HEADER
cat SHA256SUMS
echo '```'
} > release_body.md
echo "--- release body ---"
cat release_body.md
- name: List release files
shell: bash
run: ls -la out/
- name: Create GitHub release
uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.0 (Node 24)
with:
files: |
out/chela-${{ github.ref_name }}.zip
out/chela-${{ github.ref_name }}.zip.sha256
out/chela-${{ github.ref_name }}.zip.minisig
out/chela-${{ github.ref_name }}-web.html
out/chela-${{ github.ref_name }}-web.html.sha256
out/chela-${{ github.ref_name }}-web.html.minisig
out/SHA256SUMS
out/SHA256SUMS.minisig
fail_on_unmatched_files: false
body_path: out/release_body.md
# A tag with a pre-release suffix (the `-` in v1.0.0-beta.1) publishes as a
# GitHub pre-release and is never marked "Latest"; a plain vX.Y.Z does both.
prerelease: ${{ contains(github.ref_name, '-') }}
make_latest: ${{ !contains(github.ref_name, '-') }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}