ci: build and release the Windows menubar

windows-menubar-ci.yml runs on windows/** changes: tsc, clippy with -D warnings,
and cargo test on both windows-latest and ubuntu-latest, plus a release-profile
`tauri build --no-bundle` on Windows. The Linux leg exists because most of the
crate's cfg(windows) code cannot be compiled anywhere else, so the reverse -
keeping the ksni paths and every shared helper clean off Windows - has to be
checked somewhere too.

release-menubar-windows.yml mirrors release-menubar.yml: a `windows-v*` tag (or
a manual dispatch) builds the MSI and publishes it, with a sha256, to a
"Windows Menubar vX" release.

The two release-desktop-*.yml files on the source branch are deliberately not
imported: their tag names and "Desktop" release titles would collide with the
Electron app in app/, which already owns `desktop-v*`.
This commit is contained in:
iamtoruk 2026-08-18 04:11:32 -07:00
parent 7e57fb8d4f
commit 120747d2fa
3 changed files with 200 additions and 11 deletions

View file

@ -0,0 +1,99 @@
name: Release Windows Menubar
# Triggers on a `windows-v*` tag push (e.g. `git tag windows-v0.9.20 && git push origin
# windows-v0.9.20`), or manually via the Actions tab. Mirrors release-menubar.yml, which
# does the same job for the macOS menubar under the `mac-v*` tags. The produced `.msi` is
# unsigned; users see a SmartScreen prompt on first run until we add signing.
on:
push:
tags:
- 'windows-v*'
workflow_dispatch:
inputs:
version:
description: 'Version label for the bundle (e.g. v0.9.20 or dev-preview)'
required: true
default: 'dev-preview'
permissions:
contents: write # Needed to create the release + upload assets.
jobs:
build:
runs-on: windows-latest
timeout-minutes: 45
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Resolve version label
id: version
shell: bash
run: |
if [[ "${GITHUB_REF}" == refs/tags/windows-v* ]]; then
echo "value=${GITHUB_REF#refs/tags/windows-}" >> "$GITHUB_OUTPUT"
else
echo "value=${{ github.event.inputs.version }}" >> "$GITHUB_OUTPUT"
fi
- uses: actions/setup-node@v6
with:
node-version: 22.13.0
cache: npm
cache-dependency-path: windows/package-lock.json
- uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-pc-windows-msvc
- uses: Swatinem/rust-cache@v2
with:
workspaces: windows/src-tauri
- name: Install dependencies
working-directory: windows
run: npm ci
- name: Build MSI bundle
working-directory: windows
run: npm run tauri build
- name: Collect artifacts
shell: bash
run: |
set -euo pipefail
mkdir -p release-artifacts
find windows/src-tauri/target/release/bundle -type f -name '*.msi' \
-exec cp -v {} release-artifacts/ \;
(cd release-artifacts && for f in *.msi; do sha256sum "$f" > "$f.sha256"; done)
ls -la release-artifacts
- name: Upload artifact (for manual runs)
if: github.event_name == 'workflow_dispatch'
uses: actions/upload-artifact@v6
with:
name: CodeBurnMenubar-Windows-${{ steps.version.outputs.value }}
path: release-artifacts/*
if-no-files-found: error
- name: Create / update GitHub Release
if: startsWith(github.ref, 'refs/tags/windows-v')
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ github.ref_name }}
name: Windows Menubar ${{ steps.version.outputs.value }}
body: |
Download the `.msi` below and run it. The tray app reads everything through the
CodeBurn CLI, so install that first:
```
npm install -g codeburn
```
Requires codeburn 0.9.9 or newer and the WebView2 Runtime (preinstalled on
Windows 11 and recent Windows 10 updates; installed on demand otherwise).
The bundle is unsigned, so Windows SmartScreen warns on first run: click
"More info", then "Run anyway". Signing is planned.
files: release-artifacts/*
fail_on_unmatched_files: true

View file

@ -0,0 +1,83 @@
name: Windows Menubar CI
# The Windows menubar (windows/) is a Tauri app: a React frontend plus a Rust binary whose
# interesting code is `#[cfg(windows)]` and therefore only ever compiled on a Windows runner.
# ubuntu-latest is in the matrix because the same crate has to stay clean on the ksni/Linux
# paths and because contributors develop it on non-Windows machines.
on:
push:
branches: [main]
paths:
- .github/workflows/windows-menubar-ci.yml
- windows/**
pull_request:
paths:
- .github/workflows/windows-menubar-ci.yml
- windows/**
permissions:
contents: read
jobs:
check:
runs-on: ${{ matrix.os }}
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
os: [windows-latest, ubuntu-latest]
steps:
- uses: actions/checkout@v6
# webkit2gtk + libayatana are what the Tauri and ksni crates link against; without
# them the Linux leg cannot even typecheck the Rust side.
- name: Install Linux system dependencies
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y \
libwebkit2gtk-4.1-dev \
libayatana-appindicator3-dev \
librsvg2-dev \
libssl-dev \
libxdo-dev \
libgtk-3-dev \
build-essential
- uses: actions/setup-node@v6
with:
node-version: 22.13.0
cache: npm
cache-dependency-path: windows/package-lock.json
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- uses: Swatinem/rust-cache@v2
with:
workspaces: windows/src-tauri
- name: Install dependencies
working-directory: windows
run: npm ci
- name: Typecheck frontend
working-directory: windows
run: npx tsc --noEmit
- name: Clippy
working-directory: windows/src-tauri
run: cargo clippy --all-targets -- -D warnings
- name: Rust tests
working-directory: windows/src-tauri
run: cargo test
# Release-profile compile of the real Windows binary. `--no-bundle` skips the WiX
# download and MSI packaging, which belong to the release workflow, not to every PR.
- name: Release build smoke
if: runner.os == 'Windows'
working-directory: windows
run: npm run tauri build -- --no-bundle

View file

@ -112,15 +112,21 @@ process, so the cadence follows popover visibility.
The Plan pill (visible on the Claude tab, or when Claude is the only detected provider) reads
Claude Code's OAuth credentials from `~/.claude/.credentials.json`, calls
`https://api.anthropic.com/api/oauth/usage`, refreshes the token once on 401, and stores one
snapshot per window under `~/.cache/codeburn/subscription-snapshots.json` (`CODEBURN_CACHE_DIR`
override) so a freshly reset window can still show last cycle's final. This is the same file
format the macOS app writes. Nothing is logged: the credential blob never leaves the Rust side.
`https://api.anthropic.com/api/oauth/usage`, and stores one snapshot per window under
`~/.cache/codeburn/subscription-snapshots.json` (`CODEBURN_CACHE_DIR` override) so a freshly
reset window can still show last cycle's final. This is the same file format the macOS app
writes. Nothing is logged: the credential blob never leaves the Rust side.
On a 401 we do **not** call the token refresh endpoint. Claude's refresh token is single-use
and rotates, so spending it would invalidate the token Claude Code itself is holding and break
the user's login. Like `ClaudeCredentialStore.refreshAfter401` on macOS, we re-read Claude's
own credential file for a token it has already rotated, and report a transient failure when
there isn't one yet.
## Build a production package
```bash
# Windows (.msi + NSIS .exe): run from a Windows host
# Windows (.msi): run from a Windows host
npm run tauri build
# Linux (experimental): produces .deb, .rpm, .AppImage under src-tauri/target/release/bundle/
@ -139,13 +145,14 @@ npm run tauri build
- **Config writes**: `~/.config/codeburn/config.json` writes run under a POSIX `flock` on
`~/.config/codeburn/.config.lock`. On Windows the same path uses a create-new lock file. Note
that this lock is advisory *between instances of this app only* - the codeburn CLI does not
take it - so it narrows, but does not eliminate, a concurrent-write race. A lock left behind
by a crash is never deleted by another process; it expires after 30 s of inactivity and the
next writer retries.
take it - so it narrows, but does not eliminate, a concurrent-write race. A live holder keeps
its file handle open and Windows will not unlink an open file, so the staleness sweep can only
ever reclaim a lock whose owner is gone (after 30 s).
- **Snapshot writes**: `subscription-snapshots.json` refuses a symlinked target and is written
0600 on unix, mirroring `mac/Sources/CodeBurnMenubar/Security/SafeFile.swift`.
- **Credentials**: the Plan view reads `~/.claude/.credentials.json` with a 64 KB cap and refuses
symlinks; tokens are only ever sent to the Anthropic usage and token endpoints over TLS.
symlinks; the access token is only ever sent to the Anthropic usage endpoint over TLS, and the
refresh token is never read or sent at all.
- **FX fetches**: Frankfurter response is parsed as JSON and the rate is clamped to
`[0.0001, 1_000_000]` before it touches displayed numbers. Stale cache preferred over poisoned
fresh data.
@ -158,8 +165,8 @@ npm run tauri build
`cargo clippy -D warnings` and `cargo test` on windows-latest + ubuntu-latest, plus a release
build smoke on Windows.
- `windows-v*` tag (e.g. `windows-v0.9.20`) triggers
`.github/workflows/release-menubar-windows.yml`; publishes the `.msi` and NSIS `.exe` to a
"Windows Menubar vX" release. Unsigned for now, so Windows SmartScreen prompts on first run
`.github/workflows/release-menubar-windows.yml`; publishes the `.msi` (plus its sha256) to
a "Windows Menubar vX" release. Unsigned for now, so Windows SmartScreen prompts on first run
until a signing cert is in place.
## Pending work