-
Notifications
You must be signed in to change notification settings - Fork 14
713 lines (651 loc) · 29.6 KB
/
Copy pathrelease.yml
File metadata and controls
713 lines (651 loc) · 29.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
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
# Release pipeline (bd-c6l13j79): tag push (or manual dispatch on an
# existing tag) → web payloads → 5-target binary build → checksummed,
# signed artifacts → GitHub Release.
#
# Ported from cscheid/braid's release.yml (study copy in
# external-sources/braid). The artifact naming and checksum layout is a
# contract with install.sh (encoded by
# crates/quarto/tests/integration/bootstrap_sh.rs):
#
# q2-<version>-<platform>.tar.gz single member: q2
# q2-<version>-<platform>.tar.gz.sha256 "<sha256> <filename>"
# q2-<version>-<platform>.tar.gz.minisig minisign (Ed25519) signature;
# trusted comment = the archive
# filename (replay protection —
# install.sh compares it)
# checksums.sha256 all .sha256 lines, combined
#
# <version> has no "v" prefix in filenames; the tag does (v0.1.0 →
# q2-0.1.0-darwin_arm64.tar.gz).
#
# q2 deltas from braid:
#
# - A functional q2 binary embeds THREE payloads via include_dir!:
# the hub MCP bundle (quarto-mcp-launcher), the preview SPA
# (quarto-preview), and the trace viewer (quarto-trace-server). A
# build without them succeeds but ships placeholders (the 2026-05-20
# stale-embed incident class) — the per-target "verify binary" step
# fails the release if any placeholder is detected.
# - The `web-payloads` job builds the target-INDEPENDENT payloads once
# (WASM → preview SPA, trace viewer; the WASM toolchain is heavy and
# identical for every target). The MCP bundle is built per target:
# its keyring native addons must match the *user's* platform
# (KEYRING_PLATFORMS; see ts-packages/quarto-hub-mcp/scripts/
# stage-keyring.mjs).
# - The `hub-mcp-bundle` job ships the same MCP server as a standalone,
# downloadable tarball (quarto-hub-mcp-<version>.tar.gz) so people can
# run it directly (`node index.mjs`) without the q2 binary. ONE
# universal bundle: index.mjs is byte-identical across platforms and
# the keyring loader detects the platform at runtime, so every keyring
# addon co-stages in a single bundle. TEMPORARY/STOPGAP — the clean
# distribution is `npx @quarto/hub-mcp` (bd-3tak0lyy); this tarball
# exists only until the npm channel lands (bd-sca6g1tu; plan
# claude-notes/plans/2026-06-19-release-standalone-hub-mcp-bundle.md).
# - The bundled quarto-hub.com OAuth defaults (QUARTO_HUB_BUNDLED_*)
# are injected from repo secrets/vars into the cargo build env —
# release builds only; see crates/quarto-mcp-launcher/src/defaults.rs
# for why the Desktop-app client secret is safe to embed.
# - Linux targets are gnu on the oldest available runners (glibc 2.35
# floor). Static musl was originally blocked because rusty_v8
# (deno_core → quarto-system-runtime) shipped no musl prebuilts —
# both musl legs 404'd in the v0.1.0 dry-run (run 27449454203). That
# dependency has since been removed (bd-3e3sam51), so musl is now
# viable; we simply haven't switched yet (tracked separately). The
# linux legs build with `--features vendored-openssl` so the binary
# has no runtime libssl dependency (plan D4).
#
# Signing key: MINISIGN_SECRET_KEY repo secret; the public half is pinned
# in install.sh (MINISIGN_PUBKEY) and README. The sign step verifies its
# output against the key extracted from install.sh, so a secret/pinned-key
# mismatch fails the release instead of shipping unverifiable artifacts.
#
# Plan: claude-notes/plans/2026-06-12-q2-github-releases-bundled-mcp.md
name: Release
on:
push:
tags: ['v*']
workflow_dispatch:
inputs:
tag:
description: 'Existing tag to (re-)release, e.g. v0.1.0'
required: true
type: string
permissions:
contents: write
concurrency:
group: release-${{ github.event.inputs.tag || github.ref }}
cancel-in-progress: false
env:
CARGO_TERM_COLOR: always
jobs:
preflight:
name: Verify tag matches Cargo.toml
runs-on: ubuntu-latest
timeout-minutes: 5
if: github.repository == 'quarto-dev/q2'
outputs:
tag: ${{ steps.version.outputs.tag }}
version: ${{ steps.version.outputs.version }}
steps:
- uses: actions/checkout@v6
with:
# On workflow_dispatch the default ref is the branch; build
# from the requested tag so artifacts match what we verify.
ref: ${{ github.event.inputs.tag || github.ref }}
- id: version
env:
INPUT_TAG: ${{ github.event.inputs.tag }}
run: |
TAG="${INPUT_TAG:-$GITHUB_REF_NAME}"
CARGO_VERSION=$(grep -m1 '^version' Cargo.toml | sed 's/.*"\(.*\)".*/\1/')
if [ "v$CARGO_VERSION" != "$TAG" ]; then
echo "::error::tag $TAG does not match workspace Cargo.toml version $CARGO_VERSION"
exit 1
fi
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "version=$CARGO_VERSION" >> "$GITHUB_OUTPUT"
echo "Releasing $TAG (version $CARGO_VERSION)"
# Target-independent embedded payloads, built once: the WASM module
# (wasm32, same bytes for every release target), the preview SPA that
# consumes it, and the trace viewer. Toolchain steps mirror
# hub-client-e2e.yml (the canonical WASM CI setup).
web-payloads:
name: Build web payloads (SPA + trace viewer)
needs: preflight
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v6
with:
ref: ${{ needs.preflight.outputs.tag }}
- name: Set up Rust nightly
uses: dtolnay/rust-toolchain@nightly
with:
targets: wasm32-unknown-unknown
components: rust-src
- name: Set up Clang
uses: egor-tensin/setup-clang@v2
with:
version: latest
- uses: Swatinem/rust-cache@v2
with:
key: release-web-payloads
cache-on-failure: true
# build-wasm.js verifies wasm-bindgen CLI matches the version
# pinned in Cargo.lock; install that exact version.
- name: Install wasm-bindgen-cli
run: |
VERSION=$(awk '/^name = "wasm-bindgen"$/{getline; gsub(/version = |"/, ""); print; exit}' Cargo.lock)
echo "Installing wasm-bindgen-cli $VERSION"
cargo install -f wasm-bindgen-cli --version "$VERSION"
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: '24'
cache: npm
- name: npm ci
run: npm ci
- name: Build WASM (hub-client)
run: npm run build:wasm
working-directory: hub-client
# Same npm invocations as `cargo xtask build-trace-viewer` /
# `build-q2-preview-spa`, without compiling xtask on this runner.
- name: Build trace-viewer
run: npm run build
working-directory: trace-viewer
- name: Build q2-preview-spa
run: npm run build
working-directory: q2-preview-spa
- uses: actions/upload-artifact@v7
with:
name: web-payloads
path: |
trace-viewer/dist
q2-preview-spa/dist
retention-days: 7
if-no-files-found: error
# Standalone hub MCP server bundle (bd-sca6g1tu): the same
# ts-packages/quarto-hub-mcp dist-bundle that gets embedded into q2,
# published as its own downloadable tarball so people can run the MCP
# server directly with an ambient Node.js instead of installing q2.
#
# TEMPORARY/STOPGAP: the clean distribution is `npx @quarto/hub-mcp`
# (bd-3tak0lyy); we ship this tarball only until the npm channel lands
# (we don't want to wire up npm publish credentials yet). Expected
# lifetime ~a couple of months — when bd-3tak0lyy ships, reassess
# whether to drop this job. Plan:
# claude-notes/plans/2026-06-19-release-standalone-hub-mcp-bundle.md.
#
# ONE universal bundle (vs. the per-target binaries): index.mjs is
# byte-identical across platforms; only the @napi-rs/keyring .node
# addons differ, and the keyring loader does runtime platform/arch/libc
# detection, so all platforms co-stage in one bundle. KEYRING_PLATFORMS
# carries every target; non-host addon packages are fetched via
# `npm pack` at the loader's locked version (the path the per-target
# build already relies on — see stage-keyring.mjs).
hub-mcp-bundle:
name: Build standalone hub MCP bundle
needs: preflight
runs-on: ubuntu-latest
timeout-minutes: 15
env:
# Every keyring platform the q2 release matrix covers, co-staged
# into the one universal bundle. Keep in sync with the per-target
# `keyring:` matrix values in the `build` job below.
KEYRING_PLATFORMS: darwin-x64,darwin-arm64,linux-x64-gnu,linux-x64-musl,linux-arm64-gnu,linux-arm64-musl,win32-x64-msvc,win32-arm64-msvc
steps:
- uses: actions/checkout@v6
with:
ref: ${{ needs.preflight.outputs.tag }}
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: '24'
cache: npm
- name: npm ci
run: npm ci
# esbuild compiles the @quarto/* workspace deps from TS source (the
# `source` condition in scripts/bundle.mjs), so no prior workspace
# build is needed here.
- name: Build universal hub MCP bundle
run: npm run bundle -w ts-packages/quarto-hub-mcp
# Fail closed: a real bundle (not the empty/placeholder case), every
# requested keyring addon staged, and the module graph actually
# loads under node (the linux-x64 addon resolves on this runner).
- name: Verify bundle
shell: bash
run: |
DIR=ts-packages/quarto-hub-mcp/dist-bundle
for f in index.mjs build-info.json; do
test -f "$DIR/$f" || { echo "::error::$DIR/$f missing"; exit 1; }
done
IFS=',' read -ra PLATFORMS <<< "$KEYRING_PLATFORMS"
for P in "${PLATFORMS[@]}"; do
test -d "$DIR/node_modules/@napi-rs/keyring-$P" \
|| { echo "::error::keyring addon for $P not staged"; exit 1; }
done
node "$DIR/index.mjs" --help >/dev/null
# Build a copy named for the version so the tarball extracts into a
# self-describing directory, drop in README + NOTICE, then archive.
- name: Package standalone bundle (README + NOTICE + tarball)
shell: bash
env:
VERSION: ${{ needs.preflight.outputs.version }}
run: |
PKG="quarto-hub-mcp-${VERSION}"
cp -r ts-packages/quarto-hub-mcp/dist-bundle "$PKG"
cat > "$PKG/README.md" <<EOF
# Quarto Hub MCP server — standalone bundle (v${VERSION})
The Quarto Hub MCP server (\`@quarto/hub-mcp\`), bundled as one
self-contained ES module. It gives an MCP-capable AI agent read/write
access to files in Quarto Hub projects over Automerge sync, speaking
the Model Context Protocol over stdio.
## Requirements
- **Node.js 24 or newer.** This bundle targets node24 and is NOT
version-guarded (unlike \`q2 mcp\`, which enforces the floor before
launching); older Node may fail in non-obvious ways.
## Run
node index.mjs --help
node index.mjs --server wss://quarto-hub.com/ws
Point your MCP client's server command at \`node /path/to/index.mjs\`.
## Authentication
Unlike the \`q2 mcp\` launcher, this standalone bundle does **not**
embed quarto-hub.com OAuth client credentials. Supply your own via
the environment: \`QUARTO_HUB_MCP_CLIENT_ID\`,
\`QUARTO_HUB_MCP_CLIENT_SECRET\`, and (optionally)
\`QUARTO_HUB_MCP_ISSUER\`. The sync server defaults to
\`wss://quarto-hub.com/ws\` (override with \`--server\`).
## Contents
- \`index.mjs\` the bundled server
- \`build-info.json\` source git commit + build timestamp
- \`node_modules/@napi-rs/\` the OS-keyring native addon (all platforms)
## Status
Experimental, and a temporary distribution channel: the intended
long-term path is \`npx @quarto/hub-mcp\`. See \`build-info.json\` for
the exact source commit this was built from.
EOF
cat > "$PKG/NOTICE" <<EOF
Quarto Hub MCP server — standalone bundle
Copyright (c) Posit, PBC. MIT licensed.
This bundle inlines code from the following third-party packages at
build time (esbuild). Each is distributed under its own license
(all MIT at the versions bundled); consult each project for full
terms:
- @modelcontextprotocol/sdk (MIT)
- jose (MIT)
- oauth4webapi (MIT)
- @automerge/automerge (MIT)
- ws (MIT)
- @napi-rs/keyring (MIT) native addon, node_modules/@napi-rs/
EOF
tar -czf "$PKG.tar.gz" "$PKG"
sha256sum "$PKG.tar.gz" > "$PKG.tar.gz.sha256"
sha256sum -c "$PKG.tar.gz.sha256"
- uses: actions/upload-artifact@v7
with:
name: hub-mcp-bundle
path: |
quarto-hub-mcp-${{ needs.preflight.outputs.version }}.tar.gz
quarto-hub-mcp-${{ needs.preflight.outputs.version }}.tar.gz.sha256
retention-days: 7
if-no-files-found: error
build:
name: Build (${{ matrix.platform }})
needs: [preflight, web-payloads]
runs-on: ${{ matrix.os }}
timeout-minutes: 90
strategy:
fail-fast: false
matrix:
include:
# keyring: the @napi-rs/keyring platform addons staged into
# the MCP bundle. They must match the *user's node* on the
# target platform — both libc flavors on linux (glibc + Alpine
# node), both mac archs (Rosetta mismatch: x64 q2 under an
# arm64 node and vice versa), both win archs (arm64 Windows
# runs x64 q2 under emulation with a native arm64 node).
# cargo_flags: linux legs vendor openssl (no runtime libssl
# dep); macOS/Windows TLS is security-framework/schannel, no
# openssl involved. ubuntu-22.04 runners set the glibc floor
# at 2.35.
- platform: linux_amd64
target: x86_64-unknown-linux-gnu
os: ubuntu-22.04
ext: tar.gz
keyring: linux-x64-gnu,linux-x64-musl
cargo_flags: --features vendored-openssl
- platform: linux_arm64
target: aarch64-unknown-linux-gnu
os: ubuntu-22.04-arm
ext: tar.gz
keyring: linux-arm64-gnu,linux-arm64-musl
cargo_flags: --features vendored-openssl
- platform: darwin_amd64
target: x86_64-apple-darwin
os: macos-15 # arm64 runner; the x86_64 binary runs under Rosetta
ext: tar.gz
keyring: darwin-x64,darwin-arm64
cargo_flags: ''
- platform: darwin_arm64
target: aarch64-apple-darwin
os: macos-15
ext: tar.gz
keyring: darwin-arm64,darwin-x64
cargo_flags: ''
- platform: windows_amd64
target: x86_64-pc-windows-msvc
os: windows-latest
ext: zip
keyring: win32-x64-msvc,win32-arm64-msvc
cargo_flags: ''
steps:
- uses: actions/checkout@v6
with:
ref: ${{ needs.preflight.outputs.tag }}
- name: Set up Rust nightly
uses: dtolnay/rust-toolchain@nightly
with:
targets: ${{ matrix.target }}
# The toolchain action adds the target to the *latest* nightly,
# but cargo resolves the dated nightly pinned in
# rust-toolchain.toml (bd-at72) — which auto-installs with only
# its own declared targets. Add the matrix target to the pinned
# toolchain explicitly, or every cross/musl build dies with
# E0463 "can't find crate for core" (v0.1.0 dry-run, run
# 27448388974).
- name: Add ${{ matrix.target }} to the pinned toolchain
shell: bash
run: rustup target add ${{ matrix.target }}
- uses: Swatinem/rust-cache@v2
with:
key: release-${{ matrix.target }}
# Defender real-time scanning inspects every object/rlib/exe the
# compiler writes; excluding the workspace cuts the MSVC build
# time markedly.
- name: Exclude workspace from Defender scanning
if: runner.os == 'Windows'
shell: pwsh
run: Add-MpPreference -ExclusionPath "${{ github.workspace }}"
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: '24'
cache: npm
- name: npm ci
run: npm ci
# Target-independent payloads from the web-payloads job, placed
# where the include_dir! build scripts look for them.
- uses: actions/download-artifact@v8
with:
name: web-payloads
path: .
# Per-target payload: the MCP bundle, with keyring addons for the
# *target's* users (esbuild compiles workspace TS from source;
# missing addon packages are fetched via npm pack at the loader's
# locked version).
- name: Build hub MCP bundle
shell: bash
env:
KEYRING_PLATFORMS: ${{ matrix.keyring }}
run: npm run bundle -w ts-packages/quarto-hub-mcp
# The bundled quarto-hub.com defaults. Secrets here are masked in
# logs; they are public-client credentials (RFC 8252) kept out of
# the repo only to avoid GOCSPX- scanners — see
# crates/quarto-mcp-launcher/src/defaults.rs.
- name: Build release binary
shell: bash
env:
QUARTO_HUB_BUNDLED_CLIENT_ID: ${{ secrets.QUARTO_HUB_MCP_CLIENT_ID }}
QUARTO_HUB_BUNDLED_CLIENT_SECRET: ${{ secrets.QUARTO_HUB_MCP_CLIENT_SECRET }}
QUARTO_HUB_BUNDLED_SERVER: ${{ vars.QUARTO_HUB_SERVER }}
run: |
for VAR in QUARTO_HUB_BUNDLED_CLIENT_ID QUARTO_HUB_BUNDLED_CLIENT_SECRET QUARTO_HUB_BUNDLED_SERVER; do
if [ -z "${!VAR}" ]; then
echo "::error::$VAR is empty — release builds must carry the bundled hub defaults (repo secrets/vars)"
exit 1
fi
done
cargo build --release --locked --target ${{ matrix.target }} -p quarto ${{ matrix.cargo_flags }}
# Every target in the matrix can execute on its runner (musl
# binaries are static; darwin x86_64 runs under Rosetta; windows
# runs natively), so these checks are unconditional. `shell: bash`
# uses Git Bash on the windows runner.
#
# Beyond braid's version check, this is the anti-stale-embed /
# anti-placeholder gate: a release binary must carry a real MCP
# bundle (with the target's keyring addons), real bundled hub
# defaults, and real SPA payloads.
- name: Verify binary (version, embeds, bundled defaults)
shell: bash
run: |
BIN="./target/${{ matrix.target }}/release/q2"
[ "$RUNNER_OS" = "Windows" ] && BIN="$BIN.exe"
RAW=$("$BIN" --version)
ACTUAL="${RAW##* }"
if [ "$ACTUAL" != "${{ needs.preflight.outputs.version }}" ]; then
echo "::error::binary reports '$RAW', expected version ${{ needs.preflight.outputs.version }}"
exit 1
fi
INFO=$("$BIN" mcp --launcher-info)
printf '%s\n' "$INFO"
FAIL=0
if printf '%s' "$INFO" | grep -q "PLACEHOLDER"; then
echo "::error::release binary embeds the placeholder MCP bundle"
FAIL=1
fi
for VAR in QUARTO_HUB_MCP_CLIENT_ID QUARTO_HUB_MCP_CLIENT_SECRET QUARTO_HUB_SERVER; do
if ! printf '%s' "$INFO" | grep -q "default $VAR: bundled"; then
echo "::error::$VAR is not reported as bundled in the release binary"
FAIL=1
fi
done
IFS=',' read -ra PLATFORMS <<< "${{ matrix.keyring }}"
for P in "${PLATFORMS[@]}"; do
if ! printf '%s' "$INFO" | grep -q "keyring-$P"; then
echo "::error::MCP bundle is missing keyring addon for $P"
FAIL=1
fi
done
exit $FAIL
# Unix targets ship a .tar.gz of the `q2` binary.
- name: Package archive + checksum (tar.gz)
if: matrix.ext == 'tar.gz'
shell: bash
run: |
ARCHIVE="q2-${{ needs.preflight.outputs.version }}-${{ matrix.platform }}.tar.gz"
tar -czf "$ARCHIVE" -C "target/${{ matrix.target }}/release" q2
if command -v sha256sum >/dev/null; then
sha256sum "$ARCHIVE" > "$ARCHIVE.sha256"
sha256sum -c "$ARCHIVE.sha256"
else
shasum -a 256 "$ARCHIVE" > "$ARCHIVE.sha256"
shasum -a 256 -c "$ARCHIVE.sha256"
fi
# Windows ships a .zip of q2.exe. The .sha256 is written in GNU
# coreutils format ("<lowercase-hash> <file>", LF-terminated, no
# BOM) so the ubuntu combine job's `sha256sum -c` accepts it
# alongside the unix lines.
- name: Package archive + checksum (zip)
if: matrix.ext == 'zip'
shell: pwsh
run: |
$version = "${{ needs.preflight.outputs.version }}"
$archive = "q2-$version-${{ matrix.platform }}.zip"
Compress-Archive -Path "target/${{ matrix.target }}/release/q2.exe" -DestinationPath $archive
$hash = (Get-FileHash -Algorithm SHA256 $archive).Hash.ToLower()
[System.IO.File]::WriteAllText((Join-Path (Get-Location) "$archive.sha256"), "$hash $archive`n")
# Signing happens in the release job, NOT here: ubuntu-22.04
# (jammy) has no minisign apt package (v0.1.0 dry-run iteration 3,
# run 27450999322), and centralizing the signature step means the
# secret key is touched by exactly one job and signs the exact
# bytes being published, after their artifact round-trip.
- uses: actions/upload-artifact@v7
if: matrix.ext == 'tar.gz'
with:
name: q2-${{ matrix.platform }}
path: |
q2-${{ needs.preflight.outputs.version }}-${{ matrix.platform }}.tar.gz
q2-${{ needs.preflight.outputs.version }}-${{ matrix.platform }}.tar.gz.sha256
retention-days: 7
if-no-files-found: error
- uses: actions/upload-artifact@v7
if: matrix.ext == 'zip'
with:
name: q2-${{ matrix.platform }}
path: |
q2-${{ needs.preflight.outputs.version }}-${{ matrix.platform }}.zip
q2-${{ needs.preflight.outputs.version }}-${{ matrix.platform }}.zip.sha256
retention-days: 7
if-no-files-found: error
release:
name: Create GitHub Release
needs: [preflight, build, hub-mcp-bundle]
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
with:
ref: ${{ needs.preflight.outputs.tag }}
fetch-depth: 0 # changelog needs the previous tag
- uses: actions/download-artifact@v8
with:
path: artifacts
merge-multiple: true
pattern: q2-*
# The standalone MCP bundle (its own artifact name, so it is not
# swept up by the q2-* platform-completeness check below).
- uses: actions/download-artifact@v8
with:
name: hub-mcp-bundle
path: artifacts
- name: Validate all platforms present, combine and verify checksums
run: |
cd artifacts
VERSION="${{ needs.preflight.outputs.version }}"
MISSING=()
# "<platform>:<ext>" — Windows ships .zip, the rest .tar.gz.
for entry in linux_amd64:tar.gz linux_arm64:tar.gz \
darwin_amd64:tar.gz darwin_arm64:tar.gz \
windows_amd64:zip; do
platform="${entry%%:*}"; ext="${entry##*:}"
for f in "q2-${VERSION}-${platform}.${ext}" \
"q2-${VERSION}-${platform}.${ext}.sha256"; do
[ -f "$f" ] || MISSING+=("$f")
done
done
if [ ${#MISSING[@]} -gt 0 ]; then
echo "::error::missing release files: ${MISSING[*]}"
exit 1
fi
cat -- *.sha256 | sort -k2 > checksums.sha256
sha256sum -c checksums.sha256
cat checksums.sha256
# Trusted comment = archive filename: it is part of the signed
# payload, and install.sh compares it against the file they asked
# for, so a signature cannot be replayed from another artifact.
# The Windows .zip is not signed: install.ps1 does not verify
# signatures (SHA-256 only), so it has no .minisig consumer today.
# Signing lives here (not in the build matrix) so the secret key
# is handled by one job and signs the exact bytes being published;
# ubuntu-22.04 build runners also have no minisign apt package.
- name: Sign tar.gz archives (minisign, Ed25519)
env:
MINISIGN_SECRET_KEY: ${{ secrets.MINISIGN_SECRET_KEY }}
run: |
sudo apt-get update && sudo apt-get install -y minisign
key="$(mktemp)"
trap 'rm -f "$key"' EXIT
chmod 600 "$key"
printf '%s\n' "$MINISIGN_SECRET_KEY" > "$key"
# Verify with the public key pinned in install.sh: a mismatch
# between the repo secret and the pinned key fails the release
# here instead of shipping artifacts no installer can verify.
PUBKEY="$(sed -n 's/^MINISIGN_PUBKEY="\(.*\)"$/\1/p' install.sh)"
test -n "$PUBKEY"
# The q2 platform archives plus the standalone MCP bundle. Each
# gets a signature whose trusted comment is its own filename.
for ARCHIVE in artifacts/q2-*.tar.gz artifacts/quarto-hub-mcp-*.tar.gz; do
NAME="$(basename "$ARCHIVE")"
minisign -Sm "$ARCHIVE" -s "$key" -t "$NAME"
minisign -Vm "$ARCHIVE" -P "$PUBKEY"
done
ls -l artifacts/*.minisig
- name: Generate release notes
env:
TAG: ${{ needs.preflight.outputs.tag }}
VERSION: ${{ needs.preflight.outputs.version }}
run: |
PUBKEY="$(sed -n 's/^MINISIGN_PUBKEY="\(.*\)"$/\1/p' install.sh)"
PREV_TAG=$(git describe --tags --abbrev=0 "$TAG^" 2>/dev/null || true)
{
echo "> **Experimental.** q2 is under active development and not ready for production use."
echo
echo "## Install"
echo
echo '```sh'
echo "curl -fsSL https://raw.githubusercontent.com/${GITHUB_REPOSITORY}/main/install.sh | bash"
echo '```'
echo
echo "On Windows (PowerShell):"
echo
echo '```powershell'
echo "irm https://raw.githubusercontent.com/${GITHUB_REPOSITORY}/main/install.ps1 | iex"
echo '```'
echo
echo "\`q2 mcp\` (the Quarto Hub MCP server) needs [Node.js](https://nodejs.org) 24+ at runtime;"
echo "everything else works standalone. Hub connection defaults for quarto-hub.com are built in."
echo
echo "| Platform | File |"
echo "|---|---|"
echo "| Linux x86_64 (glibc 2.35+) | \`q2-${VERSION}-linux_amd64.tar.gz\` |"
echo "| Linux ARM64 (glibc 2.35+) | \`q2-${VERSION}-linux_arm64.tar.gz\` |"
echo "| macOS Intel | \`q2-${VERSION}-darwin_amd64.tar.gz\` |"
echo "| macOS Apple Silicon | \`q2-${VERSION}-darwin_arm64.tar.gz\` |"
echo "| Windows x86_64 | \`q2-${VERSION}-windows_amd64.zip\` |"
echo
echo "**Verify a manual download** (Unix archives are signed with [minisign](https://jedisct1.github.io/minisign/), Ed25519):"
echo
echo '```sh'
echo "minisign -Vm q2-${VERSION}-<platform>.tar.gz -P ${PUBKEY}"
echo '```'
echo
echo "The trusted comment should name exactly the file you downloaded."
echo "Checksums (all platforms): \`sha256sum -c checksums.sha256 --ignore-missing\`"
echo
echo "## Standalone Quarto Hub MCP server"
echo
echo "\`q2 mcp\` is also published on its own as a self-contained Node bundle for"
echo "running the MCP server **without** installing \`q2\` — e.g. embedding it in"
echo "another tool. Requires [Node.js](https://nodejs.org) **24+**. Download"
echo "\`quarto-hub-mcp-${VERSION}.tar.gz\`, extract, and run \`node index.mjs --help\`."
echo "Unlike \`q2 mcp\`, this bundle does not embed quarto-hub.com credentials — see"
echo "the bundled \`README.md\` for the OAuth env vars to set."
echo "_(Experimental, temporary channel — \`npx\` distribution is planned.)_"
echo
echo "## Changes"
echo
if [ -n "$PREV_TAG" ]; then
git log --pretty='- %s (%h)' "${PREV_TAG}..${TAG}"
else
git log --pretty='- %s (%h)' "$TAG" | head -50
fi
} > notes.md
cat notes.md
- name: Create release
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ needs.preflight.outputs.tag }}
run: |
PRERELEASE=()
case "$TAG" in *-*) PRERELEASE=(--prerelease) ;; esac
gh release create "$TAG" \
--title "q2 $TAG" \
--notes-file notes.md \
--verify-tag \
"${PRERELEASE[@]}" \
artifacts/q2-*.tar.gz artifacts/q2-*.tar.gz.sha256 \
artifacts/q2-*.tar.gz.minisig \
artifacts/q2-*.zip artifacts/q2-*.zip.sha256 \
artifacts/quarto-hub-mcp-*.tar.gz artifacts/quarto-hub-mcp-*.tar.gz.sha256 \
artifacts/quarto-hub-mcp-*.tar.gz.minisig \
artifacts/checksums.sha256