chore: bump workspace version to 0.4.1-rc.1 (release dry-run) #9
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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 |