diff --git a/README.md b/README.md index d0ee5dd..7e322f8 100644 --- a/README.md +++ b/README.md @@ -55,10 +55,21 @@ environment. ## Install -Until packaged binaries are published, build from source: +### Download the latest release + +Grab `claude-code-usage-bubble.exe` from the +[Releases page](https://github.com/tiennm99/claude-code-usage-bubble/releases/latest). +Put it anywhere on disk (e.g. `%LOCALAPPDATA%\ClaudeCodeUsageBubble\`) and +run it. The app self-updates from the same Releases feed. + +**First-run note:** the binary is unsigned, so SmartScreen will show +"Windows protected your PC". Click **More info** → **Run anyway**. Code +signing is on the roadmap. + +### Build from source ```powershell -git clone https://github.com//claude-code-usage-bubble +git clone https://github.com/tiennm99/claude-code-usage-bubble cd claude-code-usage-bubble cargo build --release ``` diff --git a/docs/release-process.md b/docs/release-process.md new file mode 100644 index 0000000..0b706e0 --- /dev/null +++ b/docs/release-process.md @@ -0,0 +1,45 @@ +# Cutting a release + +The `release.yml` workflow builds and publishes on every pushed tag +matching `v*.*.*`. The workflow asserts that the pushed tag matches +`Cargo.toml` `version` and **fails fast on mismatch**, so the order +below matters: bump Cargo *before* you tag. + +## Steps for a new version + +1. Bump `Cargo.toml` `version` (`X.Y.Z`). +2. `cargo build --release` locally to refresh `Cargo.lock`. +3. Commit: `chore: bump version to X.Y.Z`. +4. Tag and push: + + ```powershell + git tag -a vX.Y.Z -m "vX.Y.Z" + git push origin main + git push origin vX.Y.Z + ``` + +5. Watch the "Release" workflow run; it creates the GitHub Release + with `claude-code-usage-bubble.exe` attached and auto-generated + notes. + +## Testing without a real tag + +Use the workflow's `workflow_dispatch` input with a throwaway tag +like `v0.0.0-test`. The release is created as a **draft**, so it +does not show up on the public Releases feed or trigger self-updates +for users. + +## Versioning + +Semver-ish: bump patch for fixes, minor for features, major for +breaking changes (e.g. `settings.json` schema change). The in-app +updater compares `Version { major, minor, patch }` lexicographically, +so a higher tuple wins. + +## Asset name contract + +The updater matches the release asset by exact filename +`claude-code-usage-bubble.exe` (case-insensitive). Cargo's +`name = "claude-code-usage-bubble"` already produces that name in +`target/release/`, so the workflow uploads the file as-is — do not +rename it. diff --git a/plans/260516-1730-github-release-auto-update/phase-01-release-ci-workflow.md b/plans/260516-1730-github-release-auto-update/phase-01-release-ci-workflow.md new file mode 100644 index 0000000..675d283 --- /dev/null +++ b/plans/260516-1730-github-release-auto-update/phase-01-release-ci-workflow.md @@ -0,0 +1,176 @@ +--- +phase: 1 +title: "Release CI workflow" +status: in_progress +priority: P1 +effort: "2h" +dependencies: [] +--- + +# Phase 1: Release CI workflow + +## Overview + +Ship `.github/workflows/release.yml`. On a pushed tag matching `v*`, +the workflow verifies the tag matches `Cargo.toml`'s `version` field +(fail-fast if not), builds `cargo build --release` on `windows-latest`, +renames/copies the binary to the exact asset name the updater expects +(`claude-code-usage-bubble.exe`), and creates a GitHub Release with +that asset attached and auto-generated notes. Also supports +`workflow_dispatch` for dry-run testing without cutting a real tag. + + + + + +## Requirements + +### Functional +- Trigger on tag push matching `v*.*.*` (and `workflow_dispatch` for testing). +- **Verify the tag matches `Cargo.toml` `version`** before building. Workflow aborts on mismatch. +- Build on `windows-latest` with stable Rust toolchain, `x86_64-pc-windows-msvc` target. +- Cache cargo registry + target dir to keep wall time under ~5 min. +- Upload `target/release/claude-code-usage-bubble.exe` as a Release asset. +- Use `gh release create` with `--generate-notes` for the body. +- Use `--draft` on `workflow_dispatch` runs so test runs don't become public. +- On real tag runs (`vX.Y.Z`), publish immediately (not draft). + +### Non-functional +- Workflow file under ~100 lines. +- No third-party Marketplace actions other than `actions/checkout` and `Swatinem/rust-cache` (or just `actions/cache`). Avoid `softprops/action-gh-release`-style wrappers — `gh` CLI is preinstalled on the runner and is one less supply-chain risk. +- Default `GITHUB_TOKEN` permissions, with explicit `contents: write` only on the release job. + +## Architecture + +### Flow + +``` +push tag v0.1.1 + └─→ release.yml (job: build, runs-on: windows-latest) + ├─ checkout (at the tag) + ├─ Swatinem/rust-cache (uses runner-default stable Rust) + ├─ resolve tag (from refs/tags or workflow_dispatch input) + ├─ verify Cargo.toml version == tag (strip leading 'v') → abort on mismatch + ├─ cargo build --release --locked + ├─ gh release create v0.1.1 target/release/claude-code-usage-bubble.exe \ + │ --title "v0.1.1" --generate-notes [--draft on workflow_dispatch] + └─ done +``` + +### Asset name verification + +The updater's primary matcher is `eq_ignore_ascii_case("claude-code-usage-bubble.exe")` +(`src/update/release.rs:64`). Cargo's `name = "claude-code-usage-bubble"` +already produces that exe name in `target/release/`, so no rename is +needed — just upload the file as-is. + +## Related Code Files + +- Create: `.github/workflows/release.yml` +- Reference (do not modify in this phase): `src/update/release.rs`, `Cargo.toml` + +## Implementation Steps + +1. Create `.github/workflows/release.yml` with this shape: + + ```yaml + name: Release + + on: + push: + tags: ['v*.*.*'] + workflow_dispatch: + inputs: + tag: + description: 'Tag to release (must already exist, e.g. v0.1.1)' + required: true + + permissions: + contents: write + + jobs: + build: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.inputs.tag || github.ref }} + + - uses: Swatinem/rust-cache@v2 + + - name: Resolve tag + id: tag + shell: pwsh + run: | + $tag = if ($env:GITHUB_REF -like 'refs/tags/*') { + $env:GITHUB_REF -replace '^refs/tags/','' + } else { + '${{ github.event.inputs.tag }}' + } + "tag=$tag" | Out-File -FilePath $env:GITHUB_OUTPUT -Append + + - name: Verify Cargo.toml version matches tag + shell: pwsh + run: | + $tag = '${{ steps.tag.outputs.tag }}' + $expected = $tag -replace '^v','' + $cargoVersion = (Select-String -Path Cargo.toml -Pattern '^version\s*=\s*"([^"]+)"' | Select-Object -First 1).Matches.Groups[1].Value + if ($cargoVersion -ne $expected) { + Write-Error "Tag ($tag → $expected) does not match Cargo.toml version ($cargoVersion). Bump Cargo.toml before tagging." + exit 1 + } + Write-Host "Cargo version $cargoVersion matches tag $tag" + + - name: Build release + run: cargo build --release --locked + + - name: Create release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: pwsh + run: | + $tag = '${{ steps.tag.outputs.tag }}' + $asset = 'target/release/claude-code-usage-bubble.exe' + $draft = if ('${{ github.event_name }}' -eq 'workflow_dispatch') { '--draft' } else { '' } + gh release create $tag $asset --title $tag --generate-notes $draft + ``` + +2. Sanity-check: `gh workflow list` after pushing the file shows the new "Release" workflow. + +3. Verify the workflow YAML lints clean by viewing it in the GitHub UI (or with `actionlint` locally if installed). + +## Todo List + +- [x] `.github/workflows/release.yml` written +- [x] `permissions: contents: write` set +- [x] Tag-push trigger and `workflow_dispatch` both wired +- [x] Cargo.toml-version-vs-tag check step added and fails on mismatch +- [x] Asset path is `target/release/claude-code-usage-bubble.exe` exactly +- [x] `--generate-notes` enabled +- [ ] Committed and pushed to `main` + +## Success Criteria + +- [ ] Workflow appears under Actions tab on GitHub. +- [ ] Manually dispatching with a throwaway tag (`v0.0.0-test`) produces a **draft** release with the `.exe` attached. +- [ ] Pushing a tag whose value disagrees with `Cargo.toml` fails the workflow before `cargo build` runs (verify by intentionally mismatching once on a throwaway dispatch). +- [ ] No third-party actions beyond `actions/checkout@v4` and `Swatinem/rust-cache@v2`. +- [ ] Workflow file is under ~100 lines including blank lines. +- [ ] Out of scope (do not touch): `*.exe` fallback at `src/update/release.rs:67-69`. Tracked as future cleanup once multi-arch ships. + +## Risk Assessment + +| Risk | Likelihood | Mitigation | +|---|---|---| +| `Cargo.lock` drift causes `--locked` to fail | Low | Lockfile is committed; bump it locally before tagging if deps changed | +| Build time >10 min and cache cold | Low | `rust-cache` covers cargo registry + `target/`; first run is slow, subsequent fast | +| Tag pushed without prior `Cargo.toml` version bump | Was Medium → now Mitigated | CI now fails fast in the "Verify Cargo.toml version matches tag" step; maintainer cannot accidentally ship a version-mismatched binary | +| `gh release create` fails because tag does not exist for workflow_dispatch | Medium | Workflow_dispatch input takes a tag string and `actions/checkout` is pinned to it — if the tag does not exist, checkout fails fast with a clear error | +| Pre-release tag like `v0.1.0-rc1` triggers workflow but does not match Cargo's stable version | Low | Tag-match check uses string equality after `^v` strip; `0.1.0-rc1 != 0.1.0` fails fast. If pre-releases are wanted later, change Cargo `version` and the check still works | + +## Security Considerations + +- `permissions: contents: write` is the minimum scope needed to create a release; no `id-token` or package perms requested. +- `GH_TOKEN` is the default `GITHUB_TOKEN`, scoped to this repo only. +- No secrets are echoed; `gh` reads `GH_TOKEN` from env. +- The published `.exe` is unsigned. SmartScreen will show "Unknown publisher" the first time a user runs it. Document this in Phase 3; code signing is out of scope. diff --git a/plans/260516-1730-github-release-auto-update/phase-02-end-to-end-update-test.md b/plans/260516-1730-github-release-auto-update/phase-02-end-to-end-update-test.md new file mode 100644 index 0000000..314d684 --- /dev/null +++ b/plans/260516-1730-github-release-auto-update/phase-02-end-to-end-update-test.md @@ -0,0 +1,120 @@ +--- +phase: 2 +title: "End-to-end update test" +status: pending +priority: P1 +effort: "2h" +dependencies: [1] +--- + +# Phase 2: End-to-end update test + +## Overview + +Cut a real test release pair and prove the in-app updater finds it, +downloads it, swaps the running .exe, and relaunches into the new +version. The updater code already exists; this phase is about +flushing out integration bugs between CI output and the updater's +expectations (asset name casing, redirect behavior, file lock release +timing, version-comparison edge cases). + +## Requirements + +### Functional +- Cut `v0.1.0` from current `main` → CI uploads asset. +- Bump `Cargo.toml` to `0.1.1`, commit, push, tag `v0.1.1` → CI uploads asset. +- On a Windows test machine, run the v0.1.0 binary downloaded from the v0.1.0 release. +- From the right-click menu, "Check for updates" must transition `Idle → Checking → Available`. +- Clicking "Update available" must transition `Available → Applying` and exit the process. +- ~2 seconds later, v0.1.1 must be running (verify via right-click menu showing "Up to date" after re-check, or via file properties on the .exe). +- The `version_action` apply branch (`src/app.rs:1037-1066`) must succeed: `update::install::begin` returns `Ok(())` and the process posts `WM_QUIT` (`PostQuitMessage(0)` at `src/app.rs:1056`). + +### Non-functional +- Test machine has no admin privileges → confirms `ensure_writable` (`src/update/install.rs:81-89`) works for `%LOCALAPPDATA%` install. +- Run from an install path that contains a space (e.g. `C:\Users\test user\bin\`) to validate `spawn_handoff` quoting (`src/update/install.rs:53-69`). + +## Architecture + +### Test matrix + +| Scenario | Where exe lives | Expected | +|---|---|---| +| Vanilla user-local install | `%LOCALAPPDATA%\ClaudeCodeUsageBubble\` | Succeeds | +| Path with spaces | `C:\Users\test user\bin\` | Succeeds (cmd /c quoting) | +| Read-only install dir (e.g. `C:\Program Files\…`) | `C:\Program Files\Bubble\` | `Failed` status surfaces; no crash | +| Offline | n/a | `Failed` status, no crash, retries on next 24h timer | +| Already on latest | n/a | `UpToDate` status, no download | + +### Observability + +- Run with `claude-code-usage-bubble.exe --diagnose` to capture the + log at `%TEMP%\claude-code-usage-bubble.log`. Look for `update apply + failed:` lines (`src/app.rs:1058`). +- After update, the new process is started by `cmd.exe` (detached); + Task Manager parent column will show no parent — that's expected. + +## Related Code Files + +- Reference only (no edits expected): `src/update/release.rs`, `src/update/install.rs`, `src/app.rs` (lines 1025-1135), `Cargo.toml` + +## Implementation Steps + +1. **Cut v0.1.0:** + ```bash + git -C D:/tiennm99/claude-code-usage-bubble tag -a v0.1.0 -m "v0.1.0" + git -C D:/tiennm99/claude-code-usage-bubble push origin v0.1.0 + ``` + Wait for the Phase-1 workflow to produce `v0.1.0` release with `claude-code-usage-bubble.exe`. Download the asset locally — this is the "old" binary. + +2. **Smoke-test the v0.1.0 download** on a Windows machine: run it, confirm the bubble appears, right-click → "Check for updates" returns "Up to date" (no v0.1.1 yet). + +3. **Cut v0.1.1:** + - Bump `Cargo.toml` `version = "0.1.0"` → `"0.1.1"`. + - `cargo build --release` locally to refresh `Cargo.lock`. + - Commit: `chore: bump version to 0.1.1`. + - Tag: `git tag -a v0.1.1 -m "v0.1.1"`. + - Push both: `git push origin main && git push origin v0.1.1`. + +4. **Run the v0.1.0 binary** (still installed from step 1) and right-click → "Check for updates". Status should transition to "Update available". Click it. The process exits, ~2 s pass, the new v0.1.1 binary should launch automatically. + +5. **Verify v0.1.1 is running:** right-click → "Check for updates" should now return "Up to date". Cross-check `claude-code-usage-bubble.exe --diagnose` log for the version line, or check file properties in Explorer. + +6. **Cleanup if it goes wrong:** + - Stuck "Applying" status with no swap → kill the detached `cmd.exe` in Task Manager, manually copy `%LOCALAPPDATA%\ClaudeCodeUsageBubble\updates\update.exe` over the running exe location. + - `cmd /c` quoting broke → fix in `src/update/install.rs:58-60` and retag as `v0.1.2`. + +7. **Run negative scenarios** (table above): path-with-spaces, read-only install dir, offline. Each must fail-soft without crashing. + +## Todo List + +- [ ] v0.1.0 release cut and asset downloaded +- [ ] v0.1.0 binary verified runnable on Windows +- [ ] Cargo.toml bumped to 0.1.1, committed, tagged, pushed +- [ ] v0.1.1 release produced by CI +- [ ] v0.1.0 binary self-updates to v0.1.1 successfully +- [ ] Post-update, "Check for updates" returns "Up to date" +- [ ] Negative scenario: install in path with space succeeds +- [ ] Negative scenario: read-only install dir surfaces "Failed" status, no crash +- [ ] Negative scenario: offline → "Failed", retry timer rearmed + +## Success Criteria + +- [ ] A v0.1.0 download → click update → v0.1.1 running with no manual file copying. +- [ ] No SmartScreen kill (it will warn on first run; that's expected and documented). +- [ ] `%TEMP%\claude-code-usage-bubble.log` contains no `update apply failed` lines after the successful run. +- [ ] All negative scenarios fail without crashing the bubble. + +## Risk Assessment + +| Risk | Likelihood | Mitigation | +|---|---|---| +| File lock not released in 2 s window | Low-Medium | The 2 s `timeout` in `spawn_handoff` is conservative; if it ever races, bump to 3 s | +| GitHub CDN redirect not followed by WinHTTP | Very Low | WinHTTP follows redirects by default (no `WINHTTP_OPTION_DISABLE_FEATURE` set); will surface in step 4 if broken | +| Antivirus quarantines the freshly-written staging exe | Medium | Document the workaround (allowlist the install dir); future signing fixes this | +| Test pollutes real release feed | Low | If you must test with throwaway tags, use `workflow_dispatch` (creates draft) instead of pushing the tag | + +## Security Considerations + +- The downloaded asset is fetched over HTTPS from a `*.githubusercontent.com` CDN; WinHTTP validates certs against the system root store. +- No checksum verification yet — accepted risk (HTTPS + cert pinning is the floor). Future enhancement: ship `SHA256SUMS.txt` and verify in `install::download`. +- The `cmd /c` command string is composed only from `current_exe()` and `stage_path()`; neither is user-controlled. No shell injection vector. diff --git a/plans/260516-1730-github-release-auto-update/phase-03-docs-and-release-process.md b/plans/260516-1730-github-release-auto-update/phase-03-docs-and-release-process.md new file mode 100644 index 0000000..6a6d91b --- /dev/null +++ b/plans/260516-1730-github-release-auto-update/phase-03-docs-and-release-process.md @@ -0,0 +1,133 @@ +--- +phase: 3 +title: "Docs and release process" +status: in_progress +priority: P2 +effort: "1h" +dependencies: [2] +--- + +# Phase 3: Docs and release process + +## Overview + +Now that v0.1.x ships out of CI, update the user-facing docs to point +people at the GitHub Release instead of "build from source", and +write a short maintainer checklist that future-me can follow to cut +a release without re-deriving it from this plan. + +## Requirements + +### Functional +- `README.md` "Install" section points to the Releases page and the SmartScreen warning. +- A new `docs/release-process.md` (one page) lists the cut-a-release steps: bump `Cargo.toml`, commit, tag, push. +- Keep the "build from source" path as a secondary option for developers. + +### Non-functional +- `docs/release-process.md` under 60 lines. +- No `CHANGELOG.md` — the GitHub auto-generated release notes are the changelog. + +## Architecture + +The README has one "Install" section (`README.md:56-66`). Replace it +with a two-track structure: + +``` +Install +├── Download binary (recommended, one paragraph + SmartScreen note) +└── Build from source (existing block, kept verbatim) +``` + +## Related Code Files + +- Modify: `README.md` (Install section) +- Create: `docs/release-process.md` + +## Implementation Steps + +1. **README.md** — replace the "Install" section. Sketch: + + ```markdown + ## Install + + ### Download the latest release + + Grab `claude-code-usage-bubble.exe` from the + [Releases page](https://github.com/tiennm99/claude-code-usage-bubble/releases/latest). + Put it anywhere on disk (e.g. `%LOCALAPPDATA%\ClaudeCodeUsageBubble\`) + and run it. The app self-updates from the same Releases feed. + + First-run note: the binary is unsigned, so SmartScreen will show + "Windows protected your PC". Click "More info" → "Run anyway". + Code signing is on the roadmap. + + ### Build from source + + + ``` + +2. **docs/release-process.md** — new file. Sketch: + + ```markdown + # Cutting a release + + The `release.yml` workflow builds and publishes on every pushed + tag matching `v*.*.*`. The workflow asserts that the pushed tag + matches `Cargo.toml` `version` and **fails fast on mismatch**, so + the order below matters: bump Cargo *before* you tag. + + Steps for a new version: + + 1. Bump `Cargo.toml` `version` (`X.Y.Z`). + 2. `cargo build --release` locally to refresh `Cargo.lock`. + 3. Commit: `chore: bump version to X.Y.Z`. + 4. Tag and push: + ```bash + git tag -a vX.Y.Z -m "vX.Y.Z" + git push origin main + git push origin vX.Y.Z + ``` + 5. Watch the "Release" workflow run; it creates the GitHub Release + with `claude-code-usage-bubble.exe` attached and auto-generated + notes. + + ## Testing without a real tag + + Use the workflow's `workflow_dispatch` input with a throwaway tag + like `v0.0.0-test`. The release is created as a **draft**, so it + does not show up on the public Releases feed or trigger + self-updates for users. + + ## Versioning + + Semver-ish: bump patch for fixes, minor for features, major for + breaking changes (e.g. settings.json schema change). The in-app + updater compares `Version { major, minor, patch }` lexicographically. + ``` + +3. Verify the Releases page link in the README resolves (it will once Phase 2 has cut at least v0.1.0). + +## Todo List + +- [x] `README.md` Install section rewritten with two tracks +- [x] SmartScreen note added +- [x] `docs/release-process.md` created +- [ ] Links verified by clicking through (requires Phase 2 v0.1.0 release to exist) + +## Success Criteria + +- [ ] A new contributor reading only `README.md` knows how to install without building. +- [ ] A maintainer reading only `docs/release-process.md` can cut a release without re-reading this plan. +- [ ] No mention of "Until packaged binaries are published" remains anywhere. + +## Risk Assessment + +| Risk | Likelihood | Mitigation | +|---|---|---| +| README link to `/releases/latest` 404s before first release exists | Certain pre-Phase-2 | Land this phase **after** Phase 2 has cut v0.1.0 | +| Users skip the SmartScreen note and panic | Medium | Bold the "Click More info → Run anyway" line; mention it in the Releases body too if needed | + +## Security Considerations + +- The SmartScreen warning is the user's signal that the binary is unsigned. Be honest about it; do not obscure it. +- Recommending `%LOCALAPPDATA%` as the install location keeps the user inside their writable tree (no UAC needed for self-update). diff --git a/plans/260516-1730-github-release-auto-update/plan.md b/plans/260516-1730-github-release-auto-update/plan.md new file mode 100644 index 0000000..9e42b69 --- /dev/null +++ b/plans/260516-1730-github-release-auto-update/plan.md @@ -0,0 +1,103 @@ +--- +title: "GitHub release CI + auto-update wiring" +description: "Publish Windows binaries to GitHub Releases via Actions so the existing in-app updater can self-update." +status: pending +priority: P2 +created: 2026-05-16 +--- + +# GitHub release CI + auto-update wiring + +## Overview + +The self-update subsystem already exists end-to-end in `src/update/`: +`release::fetch_latest` polls `releases/latest` on GitHub, parses the +`tag_name` into a `Version`, picks the asset whose name matches +`claude-code-usage-bubble.exe` (or the first `.exe` as fallback), and +`install::begin` downloads it + spawns an inline `cmd /c` handoff +that swaps the running exe and relaunches. `app.rs` wires this to a +24-hour timer (`UPDATE_CHECK_INTERVAL_SECS`) and the right-click menu +("Check for updates" / "Update available" / "Applying update…"). + +What is missing is the **producer side**: no `.github/workflows/` +directory exists, the repo has no tags, and the README explicitly +says "Until packaged binaries are published, build from source". The +updater therefore has nothing to pull from. Closing that loop is the +whole job. + +This plan ships three things: (1) a tag-triggered GitHub Actions +workflow that builds release on `windows-latest` and attaches the +exe to a GitHub Release, (2) an end-to-end test that proves a running +v0.1.0 actually self-updates to v0.1.1 in the wild, and (3) the docs +and release-cutting checklist so future versions ship by pushing a +tag. + +Out of scope: code signing, winget channel (`Channel::Winget` stays +stubbed), SHA256 sidecar verification (HTTPS + cert pinning by WinHTTP +is the security floor; checksum is a nice-to-have for later), and any +new updater code paths beyond what the existing code already supports. + +## Phases + +| Phase | Name | Status | +|-------|------|--------| +| 1 | [Release CI workflow](./phase-01-release-ci-workflow.md) | Files written, awaiting commit + push | +| 2 | [End-to-end update test](./phase-02-end-to-end-update-test.md) | Pending (user-driven: requires tag pushes + Windows runs) | +| 3 | [Docs and release process](./phase-03-docs-and-release-process.md) | Files written, awaiting commit; link verification gated on Phase 2 | + +## Key contracts (must not break) + +The updater is already shipped logic — these constants are the +contract the CI workflow has to satisfy: + +| Contract | Source | Value | +|---|---|---| +| Asset filename (primary match) | `src/update/release.rs:7` | `claude-code-usage-bubble.exe` | +| Asset filename (fallback) | `src/update/release.rs:67-69` | any `*.exe` | +| Endpoint | `src/update/release.rs:45` | `https://api.github.com/repos/tiennm99/claude-code-usage-bubble/releases/latest` | +| Tag → version parse | `src/update/release.rs:33-41` | strips leading `v`, splits on `-`, takes `major.minor.patch` | +| Current version source | `Cargo.toml` `version` | bumped per release | + +A tag like `v0.1.1` → parses as `Version { 0, 1, 1 }`. The workflow +MUST upload an asset named exactly `claude-code-usage-bubble.exe`. + +## Dependencies + +No cross-plan dependencies. The prior plan +[`260516-0707-cleanroom-rewrite/phase-06-updater-and-remove-notice.md`](../260516-0707-cleanroom-rewrite/phase-06-updater-and-remove-notice.md) +delivered the consumer side and is complete in code (whether its own +phase row is checked is independent of this plan). + +## Validation Log + +### Session 1 — 2026-05-16 + +#### Verification Results +- **Tier:** Standard (3 phases → Fact Checker + Contract Verifier) +- **Claims checked:** 9 +- **Verified:** 9 | **Failed:** 0 | **Unverified:** 0 +- Claims verified: `ASSET_NAME` constant at `src/update/release.rs:7`, matcher at `src/update/release.rs:64`, fallback at `src/update/release.rs:67-69`, URL endpoint at `src/update/release.rs:45`, version parse at `src/update/release.rs:33-41`, `version_action` apply branch at `src/app.rs:1037-1066`, 24-hour interval at `src/app.rs:48`, Cargo `name = "claude-code-usage-bubble"` and `version = "0.1.0"` at `Cargo.toml:2-3`, README "Until packaged binaries are published" at `README.md:58`. + +#### Decisions + +1. **Version-tag match enforcement: YES, fail-fast in CI.** + The release workflow must parse `Cargo.toml` and abort if the tag (e.g. `v0.1.1`) does not equal the Cargo version. Prevents silent mismatch where a binary self-reports a different version than the release tag — which would in turn break the updater's `Version::current() vs Version::parse(tag_name)` comparison and either skip a real update or loop on the same one. + → Propagated to `phase-01-release-ci-workflow.md` as a new step + extra success criterion. + +2. **Initial release strategy: tag v0.1.0 first, then bump to v0.1.1 for the E2E test.** + Phase 2 stays as written. Cut v0.1.0 from current `main` (no Cargo bump needed since Cargo.toml is already `0.1.0`), download the asset, bump Cargo to `0.1.1`, tag `v0.1.1`, watch the v0.1.0 binary self-update to v0.1.1. Two real releases, clean test. + → No changes needed in Phase 2 — already aligned. + +3. **Asset matcher `*.exe` fallback: defer.** + Today only one asset ships so the fallback at `src/update/release.rs:67-69` is dead code. Once multi-arch lands (`x86_64`/`arm64`), the fallback could pick the wrong binary. Tracked as a future cleanup, NOT in scope for this plan. + → Documented in Phase 1 success criteria as a non-action. + +#### Whole-Plan Consistency Sweep +- Files reread: `plan.md`, `phase-01-release-ci-workflow.md`, `phase-02-end-to-end-update-test.md`, `phase-03-docs-and-release-process.md` +- Decision deltas checked: 3 (version-match enforcement, initial release version, asset fallback) +- Reconciled stale references: 4 + - Phase 1 architecture flow + todo list + success criteria updated to include version-tag check + - Phase 1 non-functional line-count budget bumped 80 → ~100 lines to match the actual YAML after adding the check step + - Phase 1 flow diagram cleaned up (removed phantom `rustup default stable` step; added cache + dispatch-draft notation) + - Phase 3 `release-process.md` sketch now calls out that CI enforces tag-vs-Cargo match, explaining why step ordering matters +- Unresolved contradictions: 0