mirror of
https://github.com/tiennm99/claude-code-usage-bubble.git
synced 2026-10-23 18:17:21 +00:00
Rename the crate, binary, product name, identifier, settings folder, log file, autostart entry, user agent, updater endpoint and release assets. Legacy Win32 clients still update: they fall back to the first .exe asset, and the legacy rollback workflow reads the package name from the ref it builds. Upgraded installs start with default settings.
266 lines
9.3 KiB
YAML
266 lines
9.3 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 }}
|
|
persist-credentials: false
|
|
|
|
- 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 }}
|
|
persist-credentials: false
|
|
|
|
- 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/usage-monitor.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/usage-monitor.exe
|
|
target/release/usage-monitor.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 }}
|
|
persist-credentials: false
|
|
|
|
- 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="usage-monitor_${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 }}
|
|
persist-credentials: false
|
|
|
|
- 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
|