mirror of
https://github.com/tiennm99/claude-code-usage-bubble.git
synced 2026-10-22 10:16:07 +00:00
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.
262 lines
9.2 KiB
YAML
262 lines
9.2 KiB
YAML
name: Release
|
|
|
|
# A pushed plain vX.Y.Z tag publishes the release that every client updates
|
|
# from: legacy Win32 clients (v0.5.0 and earlier) through the GitHub API's
|
|
# `/releases/latest`, Tauri clients through its `latest.json` asset. A
|
|
# dispatch run builds the same assets into a draft, which neither reaches.
|
|
# Suffixed tags (vX.Y.Z-suffix) are dispatch-only: both client comparators
|
|
# ignore the suffix, so publishing one would ship an rc as a stable update
|
|
# and hide the final vX.Y.Z behind it.
|
|
|
|
on:
|
|
push:
|
|
tags: ['v[0-9]+.[0-9]+.[0-9]+']
|
|
workflow_dispatch:
|
|
inputs:
|
|
tag:
|
|
description: 'Existing tag to build into a draft release (e.g. v0.0.0-test)'
|
|
required: true
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
prepare:
|
|
name: Check the tag and the updater key
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
tag: ${{ steps.tag.outputs.tag }}
|
|
version: ${{ steps.tag.outputs.version }}
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
with:
|
|
ref: ${{ inputs.tag || github.ref }}
|
|
|
|
- name: Verify Cargo.toml version matches tag
|
|
id: tag
|
|
env:
|
|
INPUT_TAG: ${{ inputs.tag }}
|
|
run: |
|
|
if [[ "$GITHUB_REF" == refs/tags/* ]]; then
|
|
tag="${GITHUB_REF#refs/tags/}"
|
|
else
|
|
tag="$INPUT_TAG"
|
|
fi
|
|
if ! [[ "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
|
|
echo "::error::Tag '$tag' is not of the form vX.Y.Z or vX.Y.Z-suffix."
|
|
exit 1
|
|
fi
|
|
if [ "$GITHUB_EVENT_NAME" != workflow_dispatch ] && [[ "$tag" == *-* ]]; then
|
|
echo "::error::Tag '$tag' has a suffix; suffixed tags are for draft dispatch runs only and are never published."
|
|
exit 1
|
|
fi
|
|
expected="${tag#v}"
|
|
cargo_version=$(sed -nE 's/^version[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p' Cargo.toml | head -n 1)
|
|
if [ "$cargo_version" != "$expected" ]; then
|
|
echo "::error::Tag ($tag -> $expected) does not match Cargo.toml version ($cargo_version). Bump Cargo.toml before tagging."
|
|
exit 1
|
|
fi
|
|
echo "Cargo version $cargo_version matches tag $tag"
|
|
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
|
echo "version=$expected" >> "$GITHUB_OUTPUT"
|
|
|
|
# An exe without the updater public key can never verify, and so never
|
|
# install, a later update; every install of it would be stranded.
|
|
# build.rs checks this only for a build that has the signing key in its
|
|
# environment, so check it here, before any build runs.
|
|
- name: Refuse an empty updater public key
|
|
run: |
|
|
pubkey=$(jq -r '.plugins.updater.pubkey // ""' tauri.conf.json)
|
|
if [ -z "${pubkey//[[:space:]]/}" ]; then
|
|
echo "::error::plugins.updater.pubkey in tauri.conf.json is empty: put the updater public key there before releasing."
|
|
exit 1
|
|
fi
|
|
|
|
win-x64:
|
|
name: Windows x64
|
|
needs: prepare
|
|
runs-on: windows-latest
|
|
# 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."
|
|
exit 1
|
|
fi
|
|
|
|
- uses: actions/checkout@v7
|
|
with:
|
|
ref: ${{ needs.prepare.outputs.tag }}
|
|
|
|
- uses: dtolnay/rust-toolchain@stable
|
|
|
|
- uses: Swatinem/rust-cache@v2
|
|
|
|
- uses: actions/setup-node@v7
|
|
with:
|
|
node-version: lts/*
|
|
cache: npm
|
|
|
|
- run: npm ci
|
|
|
|
# The Windows release is the portable exe; no installer is built.
|
|
- run: npm run tauri build -- --no-bundle -- --locked
|
|
|
|
# `signer sign` writes a separate .sig and leaves the exe bytes as
|
|
# built, so the digest GitHub reports for the uploaded exe, which
|
|
# legacy clients verify, is the digest of the file they run.
|
|
- name: Sign the exe
|
|
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"
|
|
test -s "$exe.sig"
|
|
|
|
- uses: actions/upload-artifact@v7
|
|
with:
|
|
name: release-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
|
|
|
|
linux-x64:
|
|
name: Linux x64
|
|
needs: prepare
|
|
runs-on: ubuntu-22.04
|
|
# 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."
|
|
exit 1
|
|
fi
|
|
|
|
- uses: actions/checkout@v7
|
|
with:
|
|
ref: ${{ needs.prepare.outputs.tag }}
|
|
|
|
- uses: dtolnay/rust-toolchain@stable
|
|
|
|
- uses: Swatinem/rust-cache@v2
|
|
|
|
- uses: actions/setup-node@v7
|
|
with:
|
|
node-version: lts/*
|
|
cache: npm
|
|
|
|
- name: Install Linux build prerequisites
|
|
run: |
|
|
sudo apt-get update
|
|
sudo apt-get install -y libwebkit2gtk-4.1-dev build-essential curl wget file \
|
|
libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
|
|
|
- run: npm ci
|
|
|
|
# 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 bundle --
|
|
--bundles appimage,deb
|
|
--config '{"bundle":{"createUpdaterArtifacts":true}}'
|
|
|
|
# The bundler names outputs after productName, which has spaces, and
|
|
# GitHub rewrites spaces in asset names, so the URLs in latest.json
|
|
# would no longer match. A signature covers the content, not the name.
|
|
- name: Rename the bundles to space-free names
|
|
env:
|
|
VERSION: ${{ needs.prepare.outputs.version }}
|
|
run: |
|
|
set -euo pipefail
|
|
shopt -s nullglob
|
|
one() {
|
|
if [ "$#" -ne 1 ]; then
|
|
echo "::error::Expected exactly one bundle, found $#: $*" >&2
|
|
return 1
|
|
fi
|
|
printf '%s' "$1"
|
|
}
|
|
appimage=$(one target/release/bundle/appimage/*_amd64.AppImage)
|
|
deb=$(one target/release/bundle/deb/*_amd64.deb)
|
|
name="claude-code-usage-bubble_${VERSION}_amd64"
|
|
mkdir -p dist
|
|
cp "$appimage" "dist/$name.AppImage"
|
|
cp "$appimage.sig" "dist/$name.AppImage.sig"
|
|
cp "$deb" "dist/$name.deb"
|
|
test -s "dist/$name.AppImage.sig"
|
|
ls -l dist
|
|
|
|
- uses: actions/upload-artifact@v7
|
|
with:
|
|
name: release-linux-x64
|
|
path: dist/
|
|
retention-days: 7
|
|
if-no-files-found: error
|
|
|
|
publish:
|
|
name: Publish
|
|
needs: [prepare, win-x64, linux-x64]
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: write
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
TAG: ${{ needs.prepare.outputs.tag }}
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
with:
|
|
ref: ${{ needs.prepare.outputs.tag }}
|
|
|
|
- uses: actions/setup-node@v7
|
|
with:
|
|
node-version: lts/*
|
|
|
|
- uses: actions/download-artifact@v8
|
|
with:
|
|
pattern: release-*
|
|
path: dist
|
|
merge-multiple: true
|
|
|
|
- name: Write latest.json and check the asset set
|
|
run: node scripts/make-latest-json.js "$TAG" dist
|
|
|
|
# Legacy clients read only `/releases/latest`, so a tag release must be
|
|
# a non-draft, non-prerelease release marked latest.
|
|
- name: Create the release
|
|
run: |
|
|
if [ "$GITHUB_EVENT_NAME" = workflow_dispatch ]; then
|
|
mode=(--draft)
|
|
else
|
|
mode=(--latest)
|
|
fi
|
|
gh release create "$TAG" dist/* --verify-tag --title "$TAG" --generate-notes "${mode[@]}"
|
|
|
|
- name: Check that /releases/latest is this release
|
|
if: github.event_name != 'workflow_dispatch'
|
|
run: |
|
|
latest=$(gh api "repos/$GITHUB_REPOSITORY/releases/latest" -q .tag_name)
|
|
if [ "$latest" != "$TAG" ]; then
|
|
echo "::error::/releases/latest returns $latest, not $TAG; legacy clients would not see this release."
|
|
exit 1
|
|
fi
|