mirror of
https://github.com/tiennm99/claude-code-usage-bubble.git
synced 2026-06-06 06:10:12 +00:00
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:
@@ -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
|
||||
```
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user