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.
This commit is contained in:
tiennm99 committed 2026-10-21 17:29:56 +07:00
1 parent 81f29f2e4f
commit 9a9b01ff07
4 files changed
+94 -28

No files matched your search

+63 -13
View File
@@ -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 <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[@]}"
+19 -9
View File
@@ -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
+2 -1
View File
@@ -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
@@ -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_<ver>_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 <pid> --updated-to X`
(`git show v0.5.0:src/main.rs`, lines 42 and 54).