From 5f26b0e064709345f96e1a8bb7d42e481714abe1 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Mon, 18 May 2026 14:50:50 -0700 Subject: [PATCH 1/7] Harden release tool command resolution (cherry picked from commit e9dfa974d05cd7b388802ce4925babec0abcb042) --- .github/workflows/release.yml | 6 +++--- docs/specs/deploy.md | 9 +++++---- scripts/sign-and-deploy.sh | 6 +++++- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9e4197f..5e25df5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -118,7 +118,7 @@ jobs: run: pnpm --filter dormouse build - name: Package extension - run: cd vscode-ext && npx vsce package --no-dependencies + run: pnpm --dir vscode-ext exec vsce package --no-dependencies - name: Upload .vsix uses: actions/upload-artifact@v4 @@ -156,7 +156,7 @@ jobs: working-directory: vscode-ext run: | for i in 1 2 3; do - npx vsce publish --packagePath *.vsix --no-dependencies && exit 0 + pnpm exec vsce publish --packagePath *.vsix --no-dependencies && exit 0 echo "Attempt $i failed, retrying in 10s..." sleep 10 done @@ -168,7 +168,7 @@ jobs: working-directory: vscode-ext run: | for i in 1 2 3; do - npx ovsx publish --packagePath *.vsix --no-dependencies && exit 0 + pnpm exec ovsx publish --packagePath *.vsix --no-dependencies && exit 0 echo "Attempt $i failed, retrying in 10s..." sleep 10 done diff --git a/docs/specs/deploy.md b/docs/specs/deploy.md index 8e00036..571e74b 100644 --- a/docs/specs/deploy.md +++ b/docs/specs/deploy.md @@ -89,15 +89,15 @@ Runs on `ubuntu-latest`: 2. `pnpm install --frozen-lockfile` at the repo root 3. `pnpm --filter dormouse-lib test` 4. `pnpm --filter dormouse build:frontend && pnpm --filter dormouse build` -5. `npx vsce package --no-dependencies` +5. `pnpm --dir vscode-ext exec vsce package --no-dependencies` 6. Upload `.vsix` as artifact ### Job: `publish-vscode` Runs after `build-vscode` succeeds: 1. Download `.vsix` artifact -2. `npx vsce publish --packagePath *.vsix --no-dependencies` -3. `npx ovsx publish --packagePath *.vsix --no-dependencies` +2. `pnpm exec vsce publish --packagePath *.vsix --no-dependencies` +3. `pnpm exec ovsx publish --packagePath *.vsix --no-dependencies` This runs in CI because VSCode Marketplace publishing uses PAT tokens (no hardware key needed). @@ -113,7 +113,8 @@ This runs in CI because VSCode Marketplace publishing uses PAT tokens (no hardwa brew install gh jsign gh auth login xcode-select --install -tauri signer generate # creates the Tauri update signing keypair +pnpm install --frozen-lockfile +pnpm --dir standalone exec tauri signer generate # creates the Tauri update signing keypair ``` ### Two signing layers diff --git a/scripts/sign-and-deploy.sh b/scripts/sign-and-deploy.sh index f85a967..ff8b06f 100755 --- a/scripts/sign-and-deploy.sh +++ b/scripts/sign-and-deploy.sh @@ -562,6 +562,10 @@ sign_updates() { log "Signing update bundles with Tauri key..." + check_command pnpm "Install pnpm with corepack: corepack enable pnpm" + pnpm --dir "$REPO_ROOT/standalone" exec tauri --version &>/dev/null \ + || error "Tauri CLI not found in workspace dependencies. Run 'pnpm install --frozen-lockfile' before signing updates." + prompt_secret_multiline TAURI_SIGNING_PRIVATE_KEY "Enter Tauri signing private key" local release_dir="$WORK_DIR/release-assets" @@ -602,7 +606,7 @@ sign_updates() { # Use tauri signer to sign the bundle TAURI_SIGNING_PRIVATE_KEY="$TAURI_SIGNING_PRIVATE_KEY" \ TAURI_SIGNING_PRIVATE_KEY_PASSWORD="${TAURI_SIGNING_PRIVATE_KEY_PASSWORD:-}" \ - npx --prefix "$REPO_ROOT/standalone" tauri signer sign \ + pnpm --dir "$REPO_ROOT/standalone" exec tauri signer sign \ --private-key "$TAURI_SIGNING_PRIVATE_KEY" \ "$bundle" fi From 25e45e2235c3f145c2b4b86bf71746d13bea00a6 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Mon, 18 May 2026 14:58:18 -0700 Subject: [PATCH 2/7] Pin GitHub Actions to commit SHAs (cherry picked from commit c1a6c3866ab1be798d77d9a5cd264cacb40a95ef) --- .github/dependabot.yml | 10 ++++++++++ .github/workflows/chromatic.yml | 8 ++++---- .github/workflows/ci.yml | 14 +++++++------- .github/workflows/release.yml | 30 +++++++++++++++--------------- 4 files changed, 36 insertions(+), 26 deletions(-) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..439005f --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: weekly + groups: + github-actions: + patterns: + - "*" diff --git a/.github/workflows/chromatic.yml b/.github/workflows/chromatic.yml index 73e77d0..b039704 100644 --- a/.github/workflows/chromatic.yml +++ b/.github/workflows/chromatic.yml @@ -16,17 +16,17 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: fetch-depth: 0 - name: Setup Node - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: 22 - name: Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa # v4 with: version: 10 @@ -35,7 +35,7 @@ jobs: working-directory: lib - name: Run Chromatic - uses: chromaui/action@latest + uses: chromaui/action@1fd1c4b0d4411b6de61818251f04b047850bf500 # latest with: projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} workingDir: lib diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 590117a..b3a0869 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,13 +10,13 @@ jobs: name: Build & Test runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: 22 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa # v4 with: version: 10 @@ -33,17 +33,17 @@ jobs: name: Standalone Smoketest runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: 22 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa # v4 with: version: 10 - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable - name: Install system dependencies run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5e25df5..12edd56 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,25 +22,25 @@ jobs: artifact-name: standalone-win-x64 runs-on: ${{ matrix.platform }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: 22 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa # v4 with: version: 10 - name: Install workspace dependencies run: pnpm install --frozen-lockfile - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable with: targets: ${{ matrix.target }} - name: Rust cache - uses: swatinem/rust-cache@v2 + uses: swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae # v2 with: workspaces: standalone/src-tauri @@ -51,7 +51,7 @@ jobs: sudo apt-get install -y -qq libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf - name: Build Tauri app - uses: tauri-apps/tauri-action@v0 + uses: tauri-apps/tauri-action@fce9c6108b31ea247710505d3aaaa893ee6768d4 # v0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Dummy key so Tauri generates updater artifacts; real signing happens locally @@ -76,7 +76,7 @@ jobs: shell: bash - name: Upload artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: ${{ matrix.artifact-name }} path: | @@ -95,13 +95,13 @@ jobs: name: Build VSCode Extension runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: 22 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa # v4 with: version: 10 @@ -121,7 +121,7 @@ jobs: run: pnpm --dir vscode-ext exec vsce package --no-dependencies - name: Upload .vsix - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: vscode-extension path: vscode-ext/*.vsix @@ -133,13 +133,13 @@ jobs: - build-vscode runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: 22 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa # v4 with: version: 10 @@ -147,7 +147,7 @@ jobs: run: pnpm install --frozen-lockfile - name: Download .vsix - uses: actions/download-artifact@v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: name: vscode-extension path: vscode-ext From 89dcd3e1415b54693cef27a8da2023fc3f47288f Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Mon, 18 May 2026 16:33:43 -0700 Subject: [PATCH 3/7] Set explicit workflow permissions (cherry picked from commit f106dafaab72ed1fbdd1e58cbd0a5a831e867f84) --- .github/workflows/chromatic.yml | 3 +++ .github/workflows/ci.yml | 3 +++ .github/workflows/release.yml | 3 +++ docs/specs/deploy.md | 9 +++++++++ 4 files changed, 18 insertions(+) diff --git a/.github/workflows/chromatic.yml b/.github/workflows/chromatic.yml index b039704..576b4a2 100644 --- a/.github/workflows/chromatic.yml +++ b/.github/workflows/chromatic.yml @@ -10,6 +10,9 @@ on: paths: - 'lib/**' +permissions: + contents: read + jobs: chromatic: name: Visual Regression Tests diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b3a0869..97ee1d7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,9 @@ on: branches: [main] pull_request: +permissions: + contents: read + jobs: build-and-test: name: Build & Test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 12edd56..475f53a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,6 +5,9 @@ on: tags: - 'v*' +permissions: + contents: read + jobs: build-standalone: name: Build Standalone (${{ matrix.target }}) diff --git a/docs/specs/deploy.md b/docs/specs/deploy.md index 571e74b..26cd4e2 100644 --- a/docs/specs/deploy.md +++ b/docs/specs/deploy.md @@ -57,6 +57,15 @@ Stage 2: Local (sign-and-deploy.sh) Triggered by tag push `v*`. Three parallel jobs: +The workflow defaults `GITHUB_TOKEN` to read-only repository access with: + +```yaml +permissions: + contents: read +``` + +No release job currently requests `id-token: write`; add that only if a job starts verifying or publishing with OIDC-backed provenance. + ### Job: `build-standalone` (matrix) Runs on `ubuntu-22.04` (linux), `macos-latest` (mac), and `windows-latest` (win). Uses `tauri-apps/tauri-action@v0`. From cc7019d21aed854154b82f8b9f14e8b1c8d1d4eb Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Mon, 18 May 2026 17:14:28 -0700 Subject: [PATCH 4/7] Gate extension publishing with protected environment (cherry picked from commit 138176969a7f5c6975becfe5f9723d70ac35c8d5) --- .github/workflows/release.yml | 2 ++ docs/specs/deploy.md | 16 +++++++++------- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 475f53a..16891d5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -135,6 +135,8 @@ jobs: - build-standalone - build-vscode runs-on: ubuntu-latest + environment: + name: vscode-extension-publish steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 diff --git a/docs/specs/deploy.md b/docs/specs/deploy.md index 26cd4e2..93bfcbb 100644 --- a/docs/specs/deploy.md +++ b/docs/specs/deploy.md @@ -42,7 +42,8 @@ Code signing for Windows requires a physical USB hardware key (EV cert via PIV). ``` Stage 1: CI (GitHub Actions) → Build unsigned Tauri apps (win, mac, linux) - → Build + publish VSCode extension + → Build VSCode extension + → Publish VSCode extension after protected environment approval → Upload unsigned Tauri artifacts Stage 2: Local (sign-and-deploy.sh) @@ -104,11 +105,12 @@ Runs on `ubuntu-latest`: ### Job: `publish-vscode` Runs after `build-vscode` succeeds: -1. Download `.vsix` artifact -2. `pnpm exec vsce publish --packagePath *.vsix --no-dependencies` -3. `pnpm exec ovsx publish --packagePath *.vsix --no-dependencies` +1. Enter the `vscode-extension-publish` GitHub environment +2. Download `.vsix` artifact +3. `pnpm exec vsce publish --packagePath *.vsix --no-dependencies` +4. `pnpm exec ovsx publish --packagePath *.vsix --no-dependencies` -This runs in CI because VSCode Marketplace publishing uses PAT tokens (no hardware key needed). +This runs in CI because VSCode Marketplace publishing uses PAT tokens (no hardware key needed). The `vscode-extension-publish` environment must require reviewer approval and allow deployments only from `v*` tags. Store `VSCE_PAT` and `OVSX_PAT` as environment secrets there, not broad repository secrets. **Migration note:** This replaces the existing `.github/workflows/publish-vscode.yml`, which was triggered by `vscode-ext/v*` tags and has never been run. That workflow should be deleted when the unified release workflow is created. Fixes from the old workflow: use `ubuntu-latest` instead of `macos-latest`, upgrade to Node 22, and unify under the `v*` tag convention. @@ -222,8 +224,8 @@ If you edit `CHANGELOG.md` manually outside `/release-notes` and want to preview | Secret | Where | Purpose | |--------|-------|---------| -| `VSCE_PAT` | GitHub Actions secret | VS Code Marketplace publish | -| `OVSX_PAT` | GitHub Actions secret | OpenVSX publish | +| `VSCE_PAT` | `vscode-extension-publish` GitHub environment secret | VS Code Marketplace publish | +| `OVSX_PAT` | `vscode-extension-publish` GitHub environment secret | OpenVSX publish | | `GITHUB_TOKEN` | GitHub Actions (automatic) | Artifact upload | | `APPLE_SIGNING_IDENTITY` | Local keychain | macOS codesign | | `APPLE_ID` | Local env / prompted | Notarization | From a38e356f05aa1c069748c2054a3320b23bf442c1 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Mon, 18 May 2026 21:07:57 -0700 Subject: [PATCH 5/7] Verify CI artifacts before signing (cherry picked from commit db8fcab4de0c297b74c65eb97cc776dd15cc99da) --- .github/workflows/release.yml | 81 ++++++++++++++++++++++- docs/specs/deploy.md | 28 +++++++- scripts/sign-and-deploy.sh | 118 ++++++++++++++++++++++++++++++---- 3 files changed, 210 insertions(+), 17 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 16891d5..bb35b8a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,6 +11,10 @@ permissions: jobs: build-standalone: name: Build Standalone (${{ matrix.target }}) + permissions: + contents: read + id-token: write + attestations: write strategy: matrix: include: @@ -78,11 +82,51 @@ jobs: standalone/src-tauri/target/${{ matrix.target }}/release/nsis/x64/plugins/ shell: bash + - name: Generate artifact manifest + shell: bash + run: | + set -euo pipefail + + cd standalone + release_dir="src-tauri/target/${{ matrix.target }}/release" + manifest="artifact-manifest.sha256" + + { + [[ -f "$release_dir/dormouse.exe" ]] && printf '%s\n' "$release_dir/dormouse.exe" + if [[ -d "$release_dir/bundle" ]]; then + find -L "$release_dir/bundle" -type f \( \ + -name "*.exe" -o \ + -name "*.msi" -o \ + -name "*.dmg" -o \ + -path "*.app/*" -o \ + -name "*.AppImage" -o \ + -path "*/nsis/*" \ + \) -print + fi + [[ -d "$release_dir/nsis" ]] && find -L "$release_dir/nsis" -type f -print + [[ -d sidecar ]] && find -L sidecar -type f -print + [[ -d src-tauri/binaries ]] && find -L src-tauri/binaries -type f -print + } | sort -u | while IFS= read -r file; do + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$file" + else + shasum -a 256 "$file" + fi + done > "$manifest" + + [[ -s "$manifest" ]] + + - name: Attest artifact manifest + uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 + with: + subject-path: standalone/artifact-manifest.sha256 + - name: Upload artifacts uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: ${{ matrix.artifact-name }} path: | + standalone/artifact-manifest.sha256 standalone/src-tauri/target/${{ matrix.target }}/release/dormouse.exe standalone/src-tauri/target/${{ matrix.target }}/release/bundle/**/*.exe standalone/src-tauri/target/${{ matrix.target }}/release/bundle/**/*.msi @@ -97,6 +141,10 @@ jobs: build-vscode: name: Build VSCode Extension runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + attestations: write steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 @@ -123,11 +171,42 @@ jobs: - name: Package extension run: pnpm --dir vscode-ext exec vsce package --no-dependencies + - name: Generate artifact manifest + shell: bash + run: | + set -euo pipefail + shopt -s nullglob + + cd vscode-ext + manifest="artifact-manifest.sha256" + files=(*.vsix) + + { + for path in "${files[@]}"; do + [[ -f "$path" ]] && printf '%s\n' "$path" + done + } | sort -u | while IFS= read -r file; do + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$file" + else + shasum -a 256 "$file" + fi + done > "$manifest" + + [[ -s "$manifest" ]] + + - name: Attest artifact manifest + uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 + with: + subject-path: vscode-ext/artifact-manifest.sha256 + - name: Upload .vsix uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: vscode-extension - path: vscode-ext/*.vsix + path: | + vscode-ext/*.vsix + vscode-ext/artifact-manifest.sha256 publish-vscode: name: Publish VSCode Extension diff --git a/docs/specs/deploy.md b/docs/specs/deploy.md index 93bfcbb..d6e3bd7 100644 --- a/docs/specs/deploy.md +++ b/docs/specs/deploy.md @@ -43,11 +43,13 @@ Code signing for Windows requires a physical USB hardware key (EV cert via PIV). Stage 1: CI (GitHub Actions) → Build unsigned Tauri apps (win, mac, linux) → Build VSCode extension + → Generate and attest artifact manifests → Publish VSCode extension after protected environment approval → Upload unsigned Tauri artifacts Stage 2: Local (sign-and-deploy.sh) → Download CI artifacts + → Verify artifact attestations and hashes → Sign macOS (codesign + notarize) → Sign Windows (jsign + PIV hardware key) → Generate Tauri update manifest with signatures @@ -65,7 +67,16 @@ permissions: contents: read ``` -No release job currently requests `id-token: write`; add that only if a job starts verifying or publishing with OIDC-backed provenance. +Only the build jobs request additional permissions, and only for provenance: + +```yaml +permissions: + contents: read + id-token: write + attestations: write +``` + +The publish job stays on the workflow read-only default and is separately gated by the `vscode-extension-publish` environment. ### Job: `build-standalone` (matrix) @@ -88,7 +99,9 @@ Each matrix leg: 2. Install workspace dependencies once from the repo root with `pnpm install --frozen-lockfile` 3. Install system deps (Linux: libgtk, libwebkit, etc.) 4. Build via `tauri-action` — but **skip signing** (no `APPLE_SIGNING_IDENTITY`, no `TAURI_SIGNING_PRIVATE_KEY`) -5. Upload artifacts (installers + bundles) via `actions/upload-artifact` +5. Generate `artifact-manifest.sha256` with SHA-256 hashes for the files that will be uploaded +6. Publish a GitHub artifact attestation for the manifest +7. Upload the manifest plus artifacts (installers + bundles) via `actions/upload-artifact` **Note:** We do NOT use `tauri-action`'s built-in GitHub Release creation. We create the release locally after signing. @@ -100,7 +113,9 @@ Runs on `ubuntu-latest`: 3. `pnpm --filter dormouse-lib test` 4. `pnpm --filter dormouse build:frontend && pnpm --filter dormouse build` 5. `pnpm --dir vscode-ext exec vsce package --no-dependencies` -6. Upload `.vsix` as artifact +6. Generate `artifact-manifest.sha256` for the `.vsix` +7. Publish a GitHub artifact attestation for the manifest +8. Upload the manifest plus `.vsix` as artifact ### Job: `publish-vscode` @@ -118,6 +133,13 @@ This runs in CI because VSCode Marketplace publishing uses PAT tokens (no hardwa `scripts/sign-and-deploy.sh` is the source of truth for the local pipeline (download, sign, notarize, package, release). Run with no args or `--help` to see subcommands. +Before any local signing step runs, downloaded CI artifacts must pass two checks: + +1. `gh attestation verify` must prove the artifact manifest was attested by `.github/workflows/release.yml` in `diffplug/dormouse`, for `refs/tags/vX.Y.Z`, at the exact commit SHA resolved by the local tag. +2. `sha256sum -c` or `shasum -a 256 -c` must prove every downloaded file listed in `artifact-manifest.sha256` still has the hash CI recorded before upload. + +The manifest itself is the attested subject, not the final signed app. This closes the gap between CI artifact production and the local machine that holds signing credentials: stale cached artifacts, wrong-tag artifacts, and tampered downloads are rejected before codesign, jsign, notarization, Tauri signing, or release upload can run. + ### One-time setup ```bash diff --git a/scripts/sign-and-deploy.sh b/scripts/sign-and-deploy.sh index ff8b06f..f23d9e0 100755 --- a/scripts/sign-and-deploy.sh +++ b/scripts/sign-and-deploy.sh @@ -101,6 +101,11 @@ check_command() { command -v "$1" &>/dev/null || error "Required command not found: $1. Install with: $2" } +check_gh_attestation_support() { + gh attestation verify --help &>/dev/null \ + || error "GitHub CLI does not support 'gh attestation verify'. Upgrade gh before signing release artifacts." +} + # Returns 0 if a specific artifact has already been downloaded artifact_downloaded() { local name="$1" @@ -254,6 +259,83 @@ find_release_run_id() { | head -1 } +validate_sha256_manifest_paths() { + local manifest="$1" + + awk ' + NF < 2 { exit 1 } + { + path = $0 + sub(/^[0-9a-fA-F]+[[:space:]]+[* ]?/, "", path) + if (path == "" || path ~ /^\// || path ~ /(^|\/)\.\.($|\/)/) { + exit 1 + } + } + ' "$manifest" +} + +check_sha256_manifest() { + local artifact_dir="$1" + local manifest_rel="$2" + + if command -v sha256sum &>/dev/null; then + (cd "$artifact_dir" && sha256sum -c "$manifest_rel" >/dev/null) + elif command -v shasum &>/dev/null; then + (cd "$artifact_dir" && shasum -a 256 -c "$manifest_rel" >/dev/null) + else + error "Required command not found: sha256sum or shasum" + fi +} + +verify_downloaded_artifact() { + local name="$1" + local tag="$2" + local tag_sha="$3" + local artifact_dir="$DOWNLOAD_DIR/$name" + + [[ -d "$artifact_dir" ]] || error "$name: artifact directory not found at $artifact_dir" + + local manifest_count + manifest_count=$(find "$artifact_dir" -name artifact-manifest.sha256 -type f | wc -l | tr -d '[:space:]') + [[ "$manifest_count" == "1" ]] \ + || error "$name: expected exactly one artifact-manifest.sha256, found $manifest_count" + + local manifest + manifest=$(find "$artifact_dir" -name artifact-manifest.sha256 -type f -print | head -1) + [[ -s "$manifest" ]] || error "$name: artifact-manifest.sha256 is empty" + + local manifest_rel="${manifest#"$artifact_dir"/}" + [[ "$manifest_rel" != "$manifest" ]] || error "$name: could not resolve manifest path relative to artifact directory" + + validate_sha256_manifest_paths "$manifest" \ + || error "$name: artifact manifest contains an absolute or parent-relative path" + + local identity="https://github.com/$GITHUB_REPO/.github/workflows/release.yml@refs/tags/$tag" + + log " $name: verifying artifact attestation" + gh attestation verify "$manifest" \ + --repo "$GITHUB_REPO" \ + --cert-identity "$identity" \ + --cert-oidc-issuer "https://token.actions.githubusercontent.com" \ + --source-ref "refs/tags/$tag" \ + --source-digest "$tag_sha" >/dev/null \ + || error "$name: artifact manifest attestation failed" + + log " $name: verifying artifact hashes" + check_sha256_manifest "$artifact_dir" "$manifest_rel" \ + || error "$name: downloaded artifact hash verification failed" +} + +verify_downloaded_artifacts() { + local tag="$1" + local tag_sha="$2" + + log "Verifying artifact attestations and hashes..." + for name in "${ARTIFACT_NAMES[@]}"; do + verify_downloaded_artifact "$name" "$tag" "$tag_sha" + done +} + # ============================================================================= # Download CI Artifacts (per-artifact caching) # ============================================================================= @@ -262,23 +344,29 @@ find_release_run_id() { # Artifacts are stored in $DOWNLOAD_DIR and NEVER modified after download. download_artifacts_from_run() { local run_id="$1" + local tag="$2" + local tag_sha="$3" mkdir -p "$DOWNLOAD_DIR" for name in "${ARTIFACT_NAMES[@]}"; do if artifact_downloaded "$name"; then - log " $name: already downloaded, skipping" + log " $name: already downloaded, verifying" + verify_downloaded_artifact "$name" "$tag" "$tag_sha" continue fi log " $name: downloading..." + rm -rf "$DOWNLOAD_DIR/$name" if gh run download "$run_id" \ --repo "$GITHUB_REPO" \ --name "$name" \ --dir "$DOWNLOAD_DIR/$name"; then + verify_downloaded_artifact "$name" "$tag" "$tag_sha" touch "$DOWNLOAD_DIR/.downloaded-$name" log " $name: done" else + rm -rf "$DOWNLOAD_DIR/$name" warn " $name: download failed (will retry on next run)" fi done @@ -294,17 +382,19 @@ download_artifacts() { local version="$1" local tag="v$version" - if all_artifacts_downloaded; then - log "All artifacts already downloaded, skipping" - return - fi - local tag_sha tag_sha=$(resolve_tag_sha "$tag") log "Finding workflow run for tag $tag ($tag_sha)..." check_command gh "brew install gh && gh auth login" + check_gh_attestation_support + + if all_artifacts_downloaded; then + log "All artifacts already downloaded, verifying cache" + verify_downloaded_artifacts "$tag" "$tag_sha" + return + fi local run_id="" local attempts=0 @@ -330,24 +420,26 @@ download_artifacts() { log "Workflow completed successfully!" log "Downloading artifacts..." - download_artifacts_from_run "$run_id" + download_artifacts_from_run "$run_id" "$tag" "$tag_sha" } resume_download() { local version="$1" local tag="v$version" - if all_artifacts_downloaded; then - log "All artifacts already downloaded, skipping" - return - fi - local tag_sha tag_sha=$(resolve_tag_sha "$tag") log "Finding completed workflow run for tag $tag ($tag_sha)..." check_command gh "brew install gh && gh auth login" + check_gh_attestation_support + + if all_artifacts_downloaded; then + log "All artifacts already downloaded, verifying cache" + verify_downloaded_artifacts "$tag" "$tag_sha" + return + fi local run_id="" run_id=$(find_release_run_id "$tag" "$tag_sha") @@ -362,7 +454,7 @@ resume_download() { log "Found completed workflow run: $run_id" log "Downloading artifacts..." - download_artifacts_from_run "$run_id" + download_artifacts_from_run "$run_id" "$tag" "$tag_sha" } # ============================================================================= From e92bcc86254fd71f858387eca6d194e91c5dbc34 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Mon, 18 May 2026 21:16:27 -0700 Subject: [PATCH 6/7] Make release artifact selection strict (cherry picked from commit 1113debc85004318ed87a722d6d02c68080e2994) --- docs/specs/deploy.md | 13 ++ scripts/sign-and-deploy.sh | 257 +++++++++++++++++++++++++------------ 2 files changed, 185 insertions(+), 85 deletions(-) diff --git a/docs/specs/deploy.md b/docs/specs/deploy.md index d6e3bd7..e21715f 100644 --- a/docs/specs/deploy.md +++ b/docs/specs/deploy.md @@ -140,6 +140,19 @@ Before any local signing step runs, downloaded CI artifacts must pass two checks The manifest itself is the attested subject, not the final signed app. This closes the gap between CI artifact production and the local machine that holds signing credentials: stale cached artifacts, wrong-tag artifacts, and tampered downloads are rejected before codesign, jsign, notarization, Tauri signing, or release upload can run. +The local script must also select release artifacts by strict expected paths instead of broad `find | head` matches. Release signing fails closed unless the expected files exist at the expected locations: + +| Artifact | Expected local path under `release-signed/work` | +|----------|-------------------------------------------------| +| macOS app bundle | `standalone-mac-aarch64/src-tauri/target/aarch64-apple-darwin/release/bundle/macos/Dormouse.app` | +| Windows app executable | `standalone-win-x64/src-tauri/target/x86_64-pc-windows-msvc/release/dormouse.exe` | +| Windows installer | `standalone-win-x64/src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/Dormouse_X.Y.Z_x64-setup.exe` | +| NSIS script | `standalone-win-x64/src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/installer.nsi` | +| NSIS plugin | `standalone-win-x64/src-tauri/target/x86_64-pc-windows-msvc/release/nsis/x64/plugins/nsis_tauri_utils.dll` | +| Linux AppImage | `standalone-linux-x64/src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/Dormouse_X.Y.Z_amd64.AppImage` | + +Release upload likewise uses only the three stable output filenames (`Dormouse-macos-aarch64.tar.gz`, `Dormouse-windows-x64-setup.exe`, `Dormouse-linux-x86_64.AppImage`) and fails if `release-signed/release-assets` contains any other files. + ### One-time setup ```bash diff --git a/scripts/sign-and-deploy.sh b/scripts/sign-and-deploy.sh index f23d9e0..11abeba 100755 --- a/scripts/sign-and-deploy.sh +++ b/scripts/sign-and-deploy.sh @@ -106,6 +106,102 @@ check_gh_attestation_support() { || error "GitHub CLI does not support 'gh attestation verify'. Upgrade gh before signing release artifacts." } +require_file() { + local description="$1" + local path="$2" + + [[ -f "$path" ]] || error "$description not found at expected path: $path" + printf '%s\n' "$path" +} + +require_directory() { + local description="$1" + local path="$2" + + [[ -d "$path" ]] || error "$description not found at expected path: $path" + printf '%s\n' "$path" +} + +require_single_find_match() { + local description="$1" + local root="$2" + shift 2 + + [[ -d "$root" ]] || error "$description search root not found: $root" + + local matches=() + while IFS= read -r match; do + [[ -n "$match" ]] && matches+=("$match") + done < <(find "$root" "$@" -print | sort) + + if [[ "${#matches[@]}" -eq 1 ]]; then + printf '%s\n' "${matches[0]}" + return + fi + + { + echo "[ERROR] $description: expected exactly one match, found ${#matches[@]}" + echo "Search root: $root" + if [[ "${#matches[@]}" -gt 0 ]]; then + echo "Matches:" + printf ' %s\n' "${matches[@]}" + fi + } >&2 + exit 1 +} + +mac_app_path() { + require_directory \ + "macOS app bundle" \ + "$SIGN_DIR/standalone-mac-aarch64/src-tauri/target/aarch64-apple-darwin/release/bundle/macos/Dormouse.app" +} + +windows_release_dir() { + printf '%s\n' "$SIGN_DIR/standalone-win-x64/src-tauri/target/x86_64-pc-windows-msvc/release" +} + +windows_exe_path() { + require_file \ + "Windows app executable" \ + "$(windows_release_dir)/dormouse.exe" +} + +windows_installer_path() { + local version="${1:-}" + local pattern="Dormouse_*_x64-setup.exe" + if [[ -n "$version" ]]; then + pattern="Dormouse_${version}_x64-setup.exe" + fi + + require_single_find_match \ + "Windows NSIS installer" \ + "$(windows_release_dir)/bundle/nsis" \ + -type f \ + -name "$pattern" +} + +linux_appimage_path() { + local version="$1" + + require_single_find_match \ + "Linux AppImage" \ + "$SIGN_DIR/standalone-linux-x64/src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage" \ + -type f \ + -name "Dormouse_${version}_amd64.AppImage" +} + +nsis_script_path() { + require_file \ + "NSIS script" \ + "$(windows_release_dir)/bundle/nsis/installer.nsi" +} + +nsis_plugin_path() { + require_file \ + "NSIS Tauri plugin" \ + "$(windows_release_dir)/nsis/x64/plugins/nsis_tauri_utils.dll" +} + # Returns 0 if a specific artifact has already been downloaded artifact_downloaded() { local name="$1" @@ -178,13 +274,6 @@ prepare_sign_dir() { done } -find_nsis_script() { - find "$SIGN_DIR/standalone-win-x64" \ - -name "installer.nsi" \ - -print \ - | head -1 -} - rebuild_windows_installer() { local signed_exe="$1" local installer_path="$2" @@ -192,8 +281,7 @@ rebuild_windows_installer() { check_command makensis "Install NSIS: brew install makensis" local script_path - script_path=$(find_nsis_script) - [[ -n "$script_path" ]] || error "NSIS script not found in downloaded artifacts; ensure release.yml uploads the nsis staging directory." + script_path=$(nsis_script_path) local script_dir script_dir="$(cd "$(dirname "$script_path")" && pwd)" @@ -204,16 +292,10 @@ rebuild_windows_installer() { artifact_dir="$(cd "$SIGN_DIR/standalone-win-x64" && pwd)" perl "$SCRIPT_DIR/patch-nsis-paths.pl" "$script_path" "$artifact_dir" - # Patch ADDITIONALPLUGINSPATH separately — it is outside the checkout tree. + # Patch ADDITIONALPLUGINSPATH separately; it is outside the checkout tree. local plugin_dir - plugin_dir=$(find "$SIGN_DIR/standalone-win-x64" -name "nsis_tauri_utils.dll" -exec dirname {} \; | head -1) - if [[ -n "$plugin_dir" ]]; then - local abs_plugin_dir - abs_plugin_dir="$(cd "$plugin_dir" && pwd)" - sed -i '' "s|^!define ADDITIONALPLUGINSPATH .*|!define ADDITIONALPLUGINSPATH \"$abs_plugin_dir\"|" "$script_path" - else - warn "nsis_tauri_utils.dll not found in artifacts; makensis may fail" - fi + plugin_dir="$(cd "$(dirname "$(nsis_plugin_path)")" && pwd)" + sed -i '' "s|^!define ADDITIONALPLUGINSPATH .*|!define ADDITIONALPLUGINSPATH \"$plugin_dir\"|" "$script_path" local installer_name installer_name="$(basename "$installer_path")" @@ -225,12 +307,9 @@ rebuild_windows_installer() { makensis -NOCD "$(basename "$script_path")" ) - # makensis outputs whatever filename the .nsi defines; find it - local output_exe - output_exe=$(find "$script_dir" -maxdepth 1 -name "*.exe" -newer "$script_path" | head -1) - [[ -n "$output_exe" ]] || error "NSIS rebuild did not produce an installer" - log "NSIS produced: $(basename "$output_exe")" - mv "$output_exe" "$installer_path" + [[ -f "$installer_path" ]] \ + || error "NSIS rebuild did not produce expected installer: $installer_path" + log "NSIS produced: $(basename "$installer_path")" } resolve_tag_sha() { @@ -295,13 +374,8 @@ verify_downloaded_artifact() { [[ -d "$artifact_dir" ]] || error "$name: artifact directory not found at $artifact_dir" - local manifest_count - manifest_count=$(find "$artifact_dir" -name artifact-manifest.sha256 -type f | wc -l | tr -d '[:space:]') - [[ "$manifest_count" == "1" ]] \ - || error "$name: expected exactly one artifact-manifest.sha256, found $manifest_count" - local manifest - manifest=$(find "$artifact_dir" -name artifact-manifest.sha256 -type f -print | head -1) + manifest=$(require_single_find_match "$name artifact manifest" "$artifact_dir" -type f -name artifact-manifest.sha256) [[ -s "$manifest" ]] || error "$name: artifact-manifest.sha256 is empty" local manifest_rel="${manifest#"$artifact_dir"/}" @@ -531,9 +605,9 @@ sign_macos() { log "Starting macOS code signing..." local app - app=$(find "$SIGN_DIR/standalone-mac-aarch64" -name "*.app" -type d | head -1) + app=$(mac_app_path) - [[ -n "$app" ]] && sign_macos_app "$app" "aarch64" + sign_macos_app "$app" "aarch64" log "All macOS signing complete" } @@ -575,27 +649,25 @@ notarize_macos() { prompt_secret APPLE_SIGN_PASS "Enter Apple ID password (or app-specific password)" local app - app=$(find "$SIGN_DIR/standalone-mac-aarch64" -name "*.app" -type d | head -1) + app=$(mac_app_path) - [[ -n "$app" ]] && notarize_macos_app "$app" "aarch64" + notarize_macos_app "$app" "aarch64" # Re-package signed+notarized app into .dmg and .tar.gz - if [[ -n "$app" ]]; then - local app_name - app_name=$(basename "$app") - - log "Creating $FNAME_MAC..." - # COPYFILE_DISABLE=1 stops macOS's tar from writing ._* AppleDouble - # sidecar files (resource-fork metadata) into the archive. Without - # this the Tauri updater's extraction fails with - # `failed to unpack ._Dormouse.app`. - COPYFILE_DISABLE=1 tar -czf "$SIGN_DIR/$FNAME_MAC" -C "$(dirname "$app")" "$app_name" - - # Defense in depth: if any ._* slipped in anyway, fail loudly here - # rather than shipping a tarball the updater can't unpack. - if tar -tzf "$SIGN_DIR/$FNAME_MAC" | grep -E '(^|/)\._' >/dev/null; then - error "$FNAME_MAC contains AppleDouble (._*) entries — macOS metadata leaked into the archive" - fi + local app_name + app_name=$(basename "$app") + + log "Creating $FNAME_MAC..." + # COPYFILE_DISABLE=1 stops macOS's tar from writing ._* AppleDouble + # sidecar files (resource-fork metadata) into the archive. Without + # this the Tauri updater's extraction fails with + # `failed to unpack ._Dormouse.app`. + COPYFILE_DISABLE=1 tar -czf "$SIGN_DIR/$FNAME_MAC" -C "$(dirname "$app")" "$app_name" + + # Defense in depth: if any ._* slipped in anyway, fail loudly here + # rather than shipping a tarball the updater can't unpack. + if tar -tzf "$SIGN_DIR/$FNAME_MAC" | grep -E '(^|/)\._' >/dev/null; then + error "$FNAME_MAC contains AppleDouble (._*) entries — macOS metadata leaked into the archive" fi log "All macOS notarization and packaging complete" @@ -606,15 +678,15 @@ notarize_macos() { # ============================================================================= sign_windows() { + local version="${1:-}" + log "Starting Windows code signing..." check_command jsign "brew install jsign" prompt_secret EV_SIGN_PIN "Enter PIV PIN for Windows signing" - # Find the inner exe local exe_path - exe_path=$(find "$SIGN_DIR/standalone-win-x64" \( -name "Dormouse.exe" -o -name "dormouse.exe" \) -not -name "*setup*" -not -name "*install*" | head -1) - [[ -n "$exe_path" ]] || error "Windows executable not found" + exe_path=$(windows_exe_path) log "Signing inner executable: $exe_path" jsign \ @@ -625,22 +697,19 @@ sign_windows() { --tsmode RFC3161 \ "$exe_path" - # Find the NSIS installer local installer_path - installer_path=$(find "$SIGN_DIR/standalone-win-x64" -name "*setup*.exe" -o -name "*install*.exe" | head -1) - - if [[ -n "$installer_path" ]]; then - rebuild_windows_installer "$exe_path" "$installer_path" - log "Signing installer: $installer_path" - jsign \ - --storetype PIV \ - --storepass "$EV_SIGN_PIN" \ - --alias "$JSIGN_ALIAS" \ - --tsaurl "$TSA_URL" \ - --tsmode RFC3161 \ - "$installer_path" + installer_path=$(windows_installer_path "$version") + + rebuild_windows_installer "$exe_path" "$installer_path" + log "Signing installer: $installer_path" + jsign \ + --storetype PIV \ + --storepass "$EV_SIGN_PIN" \ + --alias "$JSIGN_ALIAS" \ + --tsaurl "$TSA_URL" \ + --tsmode RFC3161 \ + "$installer_path" - fi log "Windows signing complete" } @@ -672,21 +741,13 @@ sign_updates() { # Windows NSIS installer. With Tauri v2 createUpdaterArtifacts=true, # the installer itself is the updater bundle; there is no .nsis.zip. local signed_setup - signed_setup=$(find "$SIGN_DIR/standalone-win-x64" \ - -path "*/release/bundle/nsis/*setup*.exe" \ - -type f \ - | head -1) - [[ -n "$signed_setup" ]] || error "Windows NSIS installer not found. Run Windows signing first." + signed_setup=$(windows_installer_path "$version") cp "$signed_setup" "$release_dir/$FNAME_WIN" # Linux AppImage. With Tauri v2 createUpdaterArtifacts=true, # the AppImage itself is the updater bundle; there is no .AppImage.tar.gz. local linux_update - linux_update=$(find "$SIGN_DIR/standalone-linux-x64" \ - -path "*/release/bundle/appimage/*.AppImage" \ - -type f \ - | head -1) - [[ -n "$linux_update" ]] || error "Linux AppImage not found in signed work directory." + linux_update=$(linux_appimage_path "$version") cp "$linux_update" "$release_dir/$FNAME_LINUX" # Generate .sig files for update bundles using Tauri CLI @@ -755,16 +816,38 @@ create_release() { local version="$1" local tag="v$version" local release_dir="$WORK_DIR/release-assets" + local release_assets=( + "$release_dir/$FNAME_MAC" + "$release_dir/$FNAME_WIN" + "$release_dir/$FNAME_LINUX" + ) log "Creating GitHub Release $tag..." check_command gh "brew install gh && gh auth login" [[ -d "$release_dir" ]] || error "Release assets not found at $release_dir. Run signing steps first." - for asset in "$FNAME_MAC" "$FNAME_WIN" "$FNAME_LINUX"; do - [[ -f "$release_dir/$asset" ]] || error "Release asset missing: $release_dir/$asset. Run sign-updates first." + for asset in "${release_assets[@]}"; do + [[ -f "$asset" ]] || error "Release asset missing: $asset. Run sign-updates first." done + local unexpected_assets=() + while IFS= read -r asset; do + [[ -n "$asset" ]] && unexpected_assets+=("$asset") + done < <(find "$release_dir" -type f \ + ! -name "$FNAME_MAC" \ + ! -name "$FNAME_WIN" \ + ! -name "$FNAME_LINUX" \ + -print | sort) + + if [[ "${#unexpected_assets[@]}" -gt 0 ]]; then + { + echo "[ERROR] Release asset directory contains unexpected files:" + printf ' %s\n' "${unexpected_assets[@]}" + } >&2 + exit 1 + fi + # Extract changelog for this version local notes_file="$WORK_DIR/release-notes.md" if [[ -f "$REPO_ROOT/CHANGELOG.md" ]]; then @@ -786,7 +869,7 @@ create_release() { gh release upload "$tag" \ --repo "$GITHUB_REPO" \ --clobber \ - "$release_dir"/* + "${release_assets[@]}" gh release edit "$tag" \ --repo "$GITHUB_REPO" \ --title "$tag" \ @@ -799,7 +882,7 @@ create_release() { --title "$tag" \ --verify-tag \ --notes-file "$notes_file" \ - "$release_dir"/* + "${release_assets[@]}" fi rm -f "$notes_file" @@ -820,7 +903,7 @@ Commands: resume VERSION Resume: download completed CI artifacts, sign, release sign-mac Re-sign macOS app bundles notarize Re-notarize macOS apps - sign-win Re-sign Windows executable + sign-win VERSION Re-sign Windows executable and installer sign-updates VER Re-generate Tauri update signatures and manifest from existing signed work release VERSION Re-create GitHub Release from existing signed assets @@ -834,6 +917,7 @@ Examples: $(basename "$0") all 0.1.0 # Full pipeline $(basename "$0") resume 0.1.0 # Resume after CI completed $(basename "$0") sign-mac # Re-sign macOS only + $(basename "$0") sign-win 0.1.0 # Re-sign Windows only $(basename "$0") sign-updates 0.1.0 # Re-sign update bundles only $(basename "$0") release 0.1.0 # Re-create GitHub Release EOF @@ -862,7 +946,7 @@ main() { prepare_sign_dir sign_macos notarize_macos - sign_windows + sign_windows "$version" sign_updates "$version" create_release "$version" ;; @@ -875,7 +959,7 @@ main() { prepare_sign_dir sign_macos notarize_macos - sign_windows + sign_windows "$version" sign_updates "$version" create_release "$version" ;; @@ -888,8 +972,11 @@ main() { notarize_macos ;; sign-win) + local version="${2:-}" + [[ -z "$version" ]] && error "Usage: $(basename "$0") sign-win " + ensure_version "$version" prepare_sign_dir - sign_windows + sign_windows "$version" ;; sign-updates) local version="${2:-}" From 8f41be657d13d308fd5261984ff2deda220d5671 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Mon, 18 May 2026 21:27:58 -0700 Subject: [PATCH 7/7] Generate CI-only Tauri updater key (cherry picked from commit 11f6a286e676ca3a49467a50125729065e8c63d1) --- .github/workflows/release.yml | 16 +++++++++++++--- docs/specs/deploy.md | 11 +++++++---- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bb35b8a..406c3f6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -57,13 +57,23 @@ jobs: sudo apt-get update -qq sudo apt-get install -y -qq libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf + - name: Generate ephemeral Tauri updater key + shell: bash + run: | + set -euo pipefail + + key_path="$RUNNER_TEMP/tauri-ci-updater.key" + pnpm --dir standalone exec tauri signer generate \ + --ci \ + --write-keys "$key_path" \ + --force + + echo "TAURI_SIGNING_PRIVATE_KEY_PATH=$key_path" >> "$GITHUB_ENV" + - name: Build Tauri app uses: tauri-apps/tauri-action@fce9c6108b31ea247710505d3aaaa893ee6768d4 # v0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # Dummy key so Tauri generates updater artifacts; real signing happens locally - TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} - TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} with: projectPath: standalone tauriScript: pnpm tauri diff --git a/docs/specs/deploy.md b/docs/specs/deploy.md index e21715f..a91a7ad 100644 --- a/docs/specs/deploy.md +++ b/docs/specs/deploy.md @@ -98,13 +98,16 @@ Each matrix leg: 1. Checkout, setup Node 22, pnpm 10, Rust stable 2. Install workspace dependencies once from the repo root with `pnpm install --frozen-lockfile` 3. Install system deps (Linux: libgtk, libwebkit, etc.) -4. Build via `tauri-action` — but **skip signing** (no `APPLE_SIGNING_IDENTITY`, no `TAURI_SIGNING_PRIVATE_KEY`) -5. Generate `artifact-manifest.sha256` with SHA-256 hashes for the files that will be uploaded -6. Publish a GitHub artifact attestation for the manifest -7. Upload the manifest plus artifacts (installers + bundles) via `actions/upload-artifact` +4. Generate an ephemeral, per-job Tauri updater key with `pnpm --dir standalone exec tauri signer generate --ci --write-keys "$RUNNER_TEMP/tauri-ci-updater.key" --force` +5. Build via `tauri-action` with `TAURI_SIGNING_PRIVATE_KEY_PATH` pointing at that ephemeral key, but **no real updater signing secret** and no `APPLE_SIGNING_IDENTITY` +6. Generate `artifact-manifest.sha256` with SHA-256 hashes for the files that will be uploaded +7. Publish a GitHub artifact attestation for the manifest +8. Upload the manifest plus artifacts (installers + bundles) via `actions/upload-artifact` **Note:** We do NOT use `tauri-action`'s built-in GitHub Release creation. We create the release locally after signing. +The CI updater key exists only so Tauri emits updater-shaped artifacts during unsigned builds. It is generated inside the runner, is not stored in source control or GitHub Secrets, and its public key is not the public key trusted by shipped apps. The final release bundles are re-signed locally by `scripts/sign-and-deploy.sh` with the production Tauri updater key before upload. + ### Job: `build-vscode` Runs on `ubuntu-latest`: