mirror of
https://github.com/tiennm99/claude-code-usage-bubble.git
synced 2026-10-23 20:17:31 +00:00
ci.yml is now callable with a ref to test. The release workflow calls it on the prepared tag and both build jobs wait for it, so a tag whose sources fail tests, lint or the build publishes nothing. A called run gets its own concurrency group and is never cancelled. The release steps now push main and wait for green CI before pushing the tag.
282 lines
9.8 KiB
YAML
282 lines
9.8 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
|
|
|
|
# Nothing is built for release from a tag whose sources fail CI.
|
|
verify:
|
|
name: CI on the tag
|
|
needs: prepare
|
|
permissions:
|
|
contents: read
|
|
uses: ./.github/workflows/ci.yml
|
|
with:
|
|
ref: ${{ needs.prepare.outputs.tag }}
|
|
|
|
win-x64:
|
|
name: Windows x64
|
|
needs: [prepare, verify]
|
|
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, verify]
|
|
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 and bundle without the key: the AppImage bundler downloads
|
|
# and runs linuxdeploy tools at whatever version is current, so the key
|
|
# must never be in its environment. The AppImage is signed on its own
|
|
# after the rename.
|
|
- run: npm run tauri build -- --no-bundle -- --locked
|
|
|
|
- name: Bundle the AppImage and deb
|
|
run: npm run tauri bundle -- --bundles appimage,deb
|
|
|
|
# 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.
|
|
- 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 "$deb" "dist/$name.deb"
|
|
|
|
# As on Windows, `signer sign` writes a separate .sig next to the file
|
|
# and leaves the AppImage bytes as bundled.
|
|
- name: Sign the AppImage
|
|
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: |
|
|
appimage="dist/usage-monitor_${VERSION}_amd64.AppImage"
|
|
npx tauri signer sign --app-version "$VERSION" "$appimage"
|
|
test -s "$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
|