docs: link install to releases page, add release-process guide

Replaces the "build from source only" Install section with a
Releases-page download path plus a SmartScreen note, and adds
docs/release-process.md so the tag-bump-build flow is captured for
future maintainers.
This commit is contained in:
2026-05-16 12:34:38 +07:00
parent 60cde291bd
commit 5fc9d18c67
6 changed files with 590 additions and 2 deletions
+13 -2
View File
@@ -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/<your-fork>/claude-code-usage-bubble
git clone https://github.com/tiennm99/claude-code-usage-bubble
cd claude-code-usage-bubble
cargo build --release
```
+45
View File
@@ -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.
@@ -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.
<!-- Updated: Validation Session 1 — version-tag match enforcement added per plan.md Validation Log decision 1 -->
<!-- Updated: Validation Session 1 — *.exe fallback at src/update/release.rs:67-69 is out of scope per plan.md Validation Log decision 3 -->
## 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.
@@ -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.
@@ -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
<existing block: git clone + cargo build --release>
```
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).
@@ -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