Files
claude-code-usage-bubble/plans/260516-1730-github-release-auto-update/phase-01-release-ci-workflow.md
T
tiennm99 5fc9d18c67 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.
2026-05-16 12:34:38 +07:00

7.7 KiB

phase, title, status, priority, effort, dependencies
phase title status priority effort dependencies
1 Release CI workflow in_progress P1 2h

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.

  • 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:

    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

  • .github/workflows/release.yml written
  • permissions: contents: write set
  • Tag-push trigger and workflow_dispatch both wired
  • Cargo.toml-version-vs-tag check step added and fails on mismatch
  • Asset path is target/release/claude-code-usage-bubble.exe exactly
  • --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.