From 9a9b01ff073da6018ea8988fd6fbbb1018fb497f Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Wed, 21 Oct 2026 17:29:56 +0700 Subject: [PATCH] ci: scope the updater signing key to signing steps and split the legacy publish job The signing key moves from job-level to step-level env, so npm install scripts, setup actions and crate build scripts never see it. Linux compiles without the key and signs in a separate tauri bundle step. The legacy rollback builds and signs under a read-only, unpersisted token with the locked Tauri CLI, and a separate job holds the write token to publish. --- .github/workflows/release-legacy.yml | 76 +++++++++++++++---- .github/workflows/release.yml | 28 ++++--- docs/release-process.md | 3 +- .../phase-10-ci-release.md | 15 ++-- 4 files changed, 94 insertions(+), 28 deletions(-) diff --git a/.github/workflows/release-legacy.yml b/.github/workflows/release-legacy.yml index e1599ae..c6adc15 100644 --- a/.github/workflows/release-legacy.yml +++ b/.github/workflows/release-legacy.yml @@ -6,6 +6,10 @@ name: Release legacy # the signed `latest.json`, and Tauri Linux clients find no entry for their # platform, which they report as up to date. The Win32 app accepts the # `--wait-pid --updated-to X` relaunch the Tauri updater uses. +# +# The build job holds a read-only token that is never written to disk, and +# only the steps that check or use the updater signing key see it. The write +# token lives in the publish job, which builds nothing. on: workflow_dispatch: @@ -31,11 +35,9 @@ jobs: win-x64: name: Windows x64 (legacy) runs-on: windows-latest - permissions: - contents: write + outputs: + sha: ${{ steps.sha.outputs.sha }} env: - TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} - TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} VERSION: ${{ inputs.version }} TAG: ${{ inputs.tag }} defaults: @@ -43,6 +45,8 @@ jobs: shell: bash steps: - name: Check the inputs and the signing key + env: + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} run: | if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "::error::version '$VERSION' is not X.Y.Z." @@ -60,13 +64,19 @@ jobs: - uses: actions/checkout@v7 with: ref: ${{ inputs.ref }} + persist-credentials: false - # The legacy ref predates the release script, so take it from the - # default branch. + # The legacy ref predates package.json and the release script, so take + # both from the default branch. - uses: actions/checkout@v7 with: ref: ${{ github.event.repository.default_branch }} path: current + persist-credentials: false + + - name: Record the commit the release points at + id: sha + run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" - uses: dtolnay/rust-toolchain@stable @@ -75,6 +85,12 @@ jobs: - uses: actions/setup-node@v7 with: node-version: lts/* + cache: npm + cache-dependency-path: current/package-lock.json + + # The Tauri CLI that signs the exe comes from the reviewed lockfile, + # not from a version resolved at run time. + - run: npm ci --prefix current # `cargo update --workspace` rewrites only the package's own version # in Cargo.lock, so the locked build below keeps every dependency. @@ -90,21 +106,55 @@ jobs: - run: cargo build --release --locked - # The legacy ref has no package.json, so the Tauri CLI comes from npx. + # `--no` refuses to download a CLI that the lockfile did not install. - name: Sign the exe + env: + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} run: | exe=target/release/claude-code-usage-bubble.exe - npx --yes @tauri-apps/cli@2 signer sign --app-version "$VERSION" "$exe" + npm exec --prefix current --no -- tauri signer sign --app-version "$VERSION" "$exe" test -s "$exe.sig" - mkdir -p dist - cp "$exe" "$exe.sig" dist/ + + - uses: actions/upload-artifact@v7 + with: + name: release-legacy-win-x64 + path: | + target/release/claude-code-usage-bubble.exe + target/release/claude-code-usage-bubble.exe.sig + retention-days: 7 + if-no-files-found: error + + publish: + name: Publish + needs: win-x64 + runs-on: ubuntu-latest + permissions: + contents: write + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ inputs.tag }} + TARGET: ${{ needs.win-x64.outputs.sha }} + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ github.event.repository.default_branch }} + persist-credentials: false + + - uses: actions/setup-node@v7 + with: + node-version: lts/* + + - uses: actions/download-artifact@v8 + with: + name: release-legacy-win-x64 + path: dist - name: Write the Windows-only latest.json and check the asset set - run: node current/scripts/make-latest-json.js --windows-only "$TAG" dist + run: node scripts/make-latest-json.js --windows-only "$TAG" dist - name: Create the release env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} DRAFT: ${{ inputs.draft }} run: | if [ "$DRAFT" = true ]; then @@ -112,5 +162,5 @@ jobs: else mode=(--latest) fi - gh release create "$TAG" dist/* --target "$(git rev-parse HEAD)" \ + gh release create "$TAG" dist/* --repo "$GITHUB_REPOSITORY" --target "$TARGET" \ --title "$TAG" --generate-notes "${mode[@]}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fa63c99..ce898f8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -76,12 +76,14 @@ jobs: name: Windows x64 needs: prepare runs-on: windows-latest - env: - TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} - TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + # The updater signing key decides what every installed client will + # install, so only the steps that check or use it see it: never + # `npm ci` install scripts, the setup actions or crate build scripts. steps: - name: Refuse a missing signing key shell: bash + env: + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} run: | if [ -z "${TAURI_SIGNING_PRIVATE_KEY//[[:space:]]/}" ]; then echo "::error::The TAURI_SIGNING_PRIVATE_KEY secret is empty; an unsigned build cannot be released." @@ -113,6 +115,8 @@ jobs: shell: bash env: VERSION: ${{ needs.prepare.outputs.version }} + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} run: | exe=target/release/claude-code-usage-bubble.exe npx tauri signer sign --app-version "$VERSION" "$exe" @@ -131,11 +135,11 @@ jobs: name: Linux x64 needs: prepare runs-on: ubuntu-22.04 - env: - TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} - TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + # As on Windows, only the steps that check or use the signing key see it. steps: - name: Refuse a missing signing key + env: + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} run: | if [ -z "${TAURI_SIGNING_PRIVATE_KEY//[[:space:]]/}" ]; then echo "::error::The TAURI_SIGNING_PRIVATE_KEY secret is empty; an unsigned build cannot be released." @@ -163,12 +167,18 @@ jobs: - run: npm ci - - name: Build the AppImage and deb with updater signatures + # Compile without the key, then bundle and sign with it: `tauri bundle` + # packages the binary already built and runs no cargo build scripts. + - run: npm run tauri build -- --no-bundle -- --locked + + - name: Bundle the AppImage and deb with updater signatures + env: + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} run: >- - npm run tauri build -- + npm run tauri bundle -- --bundles appimage,deb --config '{"bundle":{"createUpdaterArtifacts":true}}' - -- --locked # The bundler names outputs after productName, which has spaces, and # GitHub rewrites spaces in asset names, so the URLs in latest.json diff --git a/docs/release-process.md b/docs/release-process.md index 55214ad..64ecaee 100644 --- a/docs/release-process.md +++ b/docs/release-process.md @@ -28,7 +28,8 @@ below matters: bump Cargo *before* you tag. The build jobs need the `TAURI_SIGNING_PRIVATE_KEY` and `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` secrets and a non-empty `plugins.updater.pubkey` in `tauri.conf.json`; the workflow fails - before building without them. + before building without them. Only the steps that sign see the key: + dependency installs and compilation run without it. ## Testing without a real tag diff --git a/plans/261021-1028-tauri-v2-port/phase-10-ci-release.md b/plans/261021-1028-tauri-v2-port/phase-10-ci-release.md index 8e97f99..53a456a 100644 --- a/plans/261021-1028-tauri-v2-port/phase-10-ci-release.md +++ b/plans/261021-1028-tauri-v2-port/phase-10-ci-release.md @@ -30,7 +30,9 @@ host has no patchelf and no display. - Triggers: tag `v*.*.*`, or `workflow_dispatch` with a `tag` input (a draft release). Keep the tag-equals-`Cargo.toml`-version check. - Every build job runs `npm ci` and passes `--config '{"bundle":{"createUpdaterArtifacts":true}}'` where bundles are - built, with `TAURI_SIGNING_PRIVATE_KEY` and `_PASSWORD` in the environment. + built. `TAURI_SIGNING_PRIVATE_KEY` and `_PASSWORD` are step-level env on + the steps that check or use them only, never job-level, so `npm ci` + install scripts, setup actions and crate build scripts never see the key. A signing step fails hard when the key variable is empty. - Keep the step that fails before any build when `plugins.updater.pubkey` in `tauri.conf.json` is empty. `build.rs` only checks the key when @@ -44,7 +46,8 @@ host has no patchelf and no display. - Upload the exe and its `.sig`. - **linux-x64** job (`ubuntu-22.04`): - Install the Phase 01 apt list plus `patchelf`. - - `npm run tauri build -- --bundles appimage,deb --config …`. + - `npm run tauri build -- --no-bundle -- --locked` without the key, then + `npm run tauri bundle -- --bundles appimage,deb --config …` with it. - Rename every output to space-free kebab names before upload, because the bundler names them after `productName` and GitHub rewrites spaces in asset names: `claude-code-usage-bubble__amd64.AppImage`, the matching @@ -67,10 +70,12 @@ host has no patchelf and no display. rollback for Tauri clients; `workflow_dispatch` with `ref`, `version`, `tag` and a `draft` input that defaults to `true`): checks out `ref`, patches the `Cargo.toml` version, runs `cargo build --release --locked`, signs the exe - with `tauri signer sign` (CLI from `npx --yes @tauri-apps/cli@2`, since the - legacy ref has no `package.json`), runs `make-latest-json.js --windows-only` + with `tauri signer sign` (CLI from `npm ci --prefix current` on the default + branch's lockfile, since the legacy ref has no `package.json`), runs `make-latest-json.js --windows-only` (from the current default branch) and publishes `claude-code-usage-bubble.exe`, - its `.sig` and the Windows-only `latest.json`. Tauri Linux clients get + its `.sig` and the Windows-only `latest.json`. The build job runs with + `contents: read` and `persist-credentials: false` and uploads an artifact; a + separate publish job holds `contents: write`, as in `release.yml`. Tauri Linux clients get `TargetNotFound`, which Phase 09 maps to Up to date. The v0.5.0 sources accept the Tauri relaunch args `--wait-pid --updated-to X` (`git show v0.5.0:src/main.rs`, lines 42 and 54).