From 120747d2fac18d26096fc299490407d762c757d1 Mon Sep 17 00:00:00 2001 From: iamtoruk Date: Tue, 18 Aug 2026 04:11:32 -0700 Subject: [PATCH] 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*`. --- .github/workflows/release-menubar-windows.yml | 99 +++++++++++++++++++ .github/workflows/windows-menubar-ci.yml | 83 ++++++++++++++++ windows/DEVELOPMENT.md | 29 +++--- 3 files changed, 200 insertions(+), 11 deletions(-) create mode 100644 .github/workflows/release-menubar-windows.yml create mode 100644 .github/workflows/windows-menubar-ci.yml diff --git a/.github/workflows/release-menubar-windows.yml b/.github/workflows/release-menubar-windows.yml new file mode 100644 index 00000000..e69c5e66 --- /dev/null +++ b/.github/workflows/release-menubar-windows.yml @@ -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 diff --git a/.github/workflows/windows-menubar-ci.yml b/.github/workflows/windows-menubar-ci.yml new file mode 100644 index 00000000..9a2e0298 --- /dev/null +++ b/.github/workflows/windows-menubar-ci.yml @@ -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 diff --git a/windows/DEVELOPMENT.md b/windows/DEVELOPMENT.md index 6b90bcdc..fee5a3d2 100644 --- a/windows/DEVELOPMENT.md +++ b/windows/DEVELOPMENT.md @@ -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