Skip to content

Updates & Distribution

Indexed at commit 51ce1a4 on 2026-08-26 · view on GitHub

Relevant source files

Overview

The Updates & Distribution subsystem covers how cc-analyzer learns its own version, discovers newer releases, downloads and verifies them, and replaces the running binary in place. It also covers the first-install path: the shell and PowerShell scripts under site/public/ and the GitHub Actions workflow that compiles, checksums, and attests every release. The design centers on a single distribution channel — GitHub Releases — reached without an API token by following the /releases/latest redirect (src/core/release.ts:L1-L7).

The public surface is cc-analyzer update [--check], a passive once-a-day "update available" notice appended to fast commands, and the two installer scripts. cc-analyzer ships as a single bun build --compile binary, so self-update means overwriting one executable file rather than a package-manager transaction. Self-update is deliberately narrow: it runs only from a compiled binary on macOS or Linux, delegates Windows to the installer, and refuses when running from source (src/core/update.ts:L191-L265).

Architecture

performUpdate() is the orchestrator: it resolves the running VERSION and the latest published version, compares them, and — when a newer version exists on a supported platform — streams the asset to a temporary file, verifies its checksum, and atomically swaps it over process.execPath (src/core/update.ts:L193-L265). The passive maybeNotifyUpdate() shares the same fetchLatestVersion() and compareVersions() but never downloads anything (src/core/update-check.ts:L64-L92).

Module Layout

ModulePathResponsibility
versionsrc/core/version.tsEmbeds package.json version into the binary as VERSION
releasesrc/core/release.tsResolves latest version and builds asset/checksum URLs
updatesrc/core/update.tsStreams, verifies, and swaps the binary on self-update
update-checksrc/core/update-check.tsPassive once-a-day "update available" notice
checksumsrc/core/checksum.tsParses SHA256SUMS and hashes downloaded files
CLI dispatchsrc/cli/index.tsupdate command, progress rendering, notice trigger
installerssite/public/install.shFirst-install for macOS/Linux and Windows
release CI.github/workflows/release.ymlCross-compiles, checksums, attests, publishes

Sources: src/core/version.ts:L1-L8 src/core/release.ts:L1-L76 src/core/update.ts:L1-L265

Key Components

Version embedding

src/core/version.ts exports VERSION by importing package.json directly. Because bun build --compile bundles the JSON import, the standalone binary carries its own version string; running from source reads the same file. Changesets writes that version in a reviewed chore: version packages PR, and the release workflow creates the matching tag only after that PR lands and its build succeeds. A version therefore cannot reach a binary release without human review of the generated version diff.

Sources: src/core/version.ts:L1-L8 .github/workflows/release.yml:L25-L36

Release discovery

src/core/release.ts resolves the latest version without a GitHub API token. fetchLatestVersion() issues a HEAD request to https://github.com/yorch/cc-analyzer/releases/latest with redirects followed, then reads the tag from the final URL and extracts the leading X.Y.Z core so a re-cut suffixed tag still resolves (src/core/release.ts:L62-L76). This is the same redirect trick the shell installer uses (site/public/install.sh:L44-L57).

compareVersions() compares dotted versions numerically, coercing non-numeric segments such as a -rc suffix to 0 so a prerelease never sorts as newest (src/core/release.ts:L18-L34). assetName() maps process.platform and process.arch to a published asset — darwin/linux × arm64/x64, and a single cc-analyzer-windows-x64.exe that runs on ARM64 via emulation (src/core/release.ts:L37-L44). assetDownloadUrl() and checksumsUrl() build the direct download links for the asset and the SHA256SUMS manifest (src/core/release.ts:L47-L54).

Sources: src/core/release.ts:L9-L76

Self-update

performUpdate() drives the whole flow. It reads VERSION, resolves latest, and returns an up-to-date result when the latest is not newer (src/core/update.ts:L196-L201). On Windows it returns a delegated result telling the user to re-run the PowerShell installer rather than swapping the executable in place (src/core/update.ts:L203-L212). isCompiledBinary() gates self-update to real binaries by checking for the $bunfs marker in import.meta.url, with an allowlist fallback on the executable basename so a renamed interpreter is never overwritten; a source checkout returns an unsupported result advising git pull (src/core/update.ts:L19-L26 src/core/update.ts:L214-L223).

When all guards pass, the asset downloads to a per-pid temp file beside process.execPath, is checksum-verified, and is swapped in (src/core/update.ts:L235-L257). swapBinary() chmods the temp file to 0o755 and renameSync()s it over the target — atomic when both paths share a filesystem, and valid for replacing the running executable on macOS and Linux (src/core/update.ts:L36-L41). Any failure removes the temp file; an EACCES/EPERM error is rewritten into a hint to re-run the installer or set CC_ANALYZER_INSTALL_DIR (src/core/update.ts:L243-L257).

Sources: src/core/update.ts:L19-L41 src/core/update.ts:L191-L265

Streaming download with stall detection

The download streams to disk with live progress and a per-chunk stall timeout, replacing an earlier buffered download that could hang silently. downloadTo() bounds only the connect/headers phase with an AbortController on a 30-second timer, then hands the response body to pumpStream() (src/core/update.ts:L108-L157). pumpStream() reads chunks through withStall(), which rejects if no chunk arrives within stallMs (30 seconds); the timer resets on every chunk, so a slow-but-moving download is never killed — only a true stall aborts (src/core/update.ts:L50-L106). Each chunk is awaited into a Bun.FileSink, applying backpressure and surfacing disk-full errors cleanly (src/core/update.ts:L134-L152).

pumpStream() reports { received, total } per chunk via onProgress, and downloadTo() compares the received byte count against Content-Length, throwing on a short download so a cleanly-ended-but-truncated stream never installs a partial binary (src/core/update.ts:L94-L96 src/core/update.ts:L153-L157). The CLI renders this as a live percentage line on a TTY via renderDownloadProgress(), kept off piped output so redirected output stays clean (src/cli/index.ts:L172-L180).

Sources: src/core/update.ts:L43-L157 src/cli/index.ts:L172-L204

Checksum verification

src/core/checksum.ts handles the SHA256SUMS manifest. parseChecksums() reads the standard sha256sum format — a 64-hex digest, whitespace, an optional * binary-mode marker, and the filename — into a filename-to-hash map (src/core/checksum.ts:L12-L21). expectedHash() looks up an asset, and fileSha256() computes a file's lowercase-hex digest with Bun.CryptoHasher (src/core/checksum.ts:L24-L33).

verifyChecksum() in src/core/update.ts fetches the manifest for the target version and aborts the update if it cannot be fetched, if it lists no entry for the asset, or if the computed hash does not match (src/core/update.ts:L159-L189). Verification is required for the self-updater: an attacker who can tamper with an asset can usually also make the manifest fetch fail, so failing open would defeat the check (src/core/update.ts:L159-L165). This is an integrity guard, not a signature — the manifest ships from the same release, so it protects against corrupted downloads and partial tampering but is not a substitute for signing (src/core/checksum.ts:L1-L9).

Sources: src/core/checksum.ts:L1-L33 src/core/update.ts:L159-L189

Passive update notice

src/core/update-check.ts prints a passive "update available" line without ever delaying a command. maybeNotifyUpdate() reads a cached result for an instant notice and refreshes the cache at most once a day with a short 1-second-timeout request at the tail of the command (src/core/update-check.ts:L6-L10 src/core/update-check.ts:L64-L92). notifyEnabled() gates it off when CC_ANALYZER_NO_UPDATE_CHECK or CI is set, or when stderr is not a TTY (src/core/update-check.ts:L20-L24). The whole function is wrapped so it never throws and never affects the exit code (src/core/update-check.ts:L64-L92).

The CLI triggers the notice only for a fixed set of quick commands and only when --json is absent, so structured output is never polluted (src/cli/index.ts:L206-L207 src/cli/index.ts:L272-L276). When a fetch fails, the cache keeps any previously known version and advances lastCheck, so an offline user does not pay the timeout on every command for the rest of the day (src/core/update-check.ts:L72-L83).

Sources: src/core/update-check.ts:L1-L92 src/cli/index.ts:L206-L276

Data Flow

cmdUpdate() handles --check by resolving the latest version and printing whether an update exists, without downloading; otherwise it calls performUpdate() with the progress renderer and prints the result message, exiting non-zero only when the status is unsupported (src/cli/index.ts:L182-L204).

Sources: src/cli/index.ts:L182-L204 src/core/update.ts:L191-L265

First-install scripts and release build

The installers mirror the self-updater's logic for users who have no binary yet. site/public/install.sh detects OS and architecture, pins latest to a concrete tag via the same redirect, restricts transfers to HTTPS with no protocol downgrade, and verifies the checksum with whichever of sha256sum or shasum is present (site/public/install.sh:L24-L90). The Windows script uses Invoke-WebRequest and Get-FileHash, and cleans its temp file on every failure path (site/public/install.ps1:L26-L63). Unlike the self-updater, both installers skip verification gracefully for releases that predate the manifest (site/public/install.sh:L69-L90 site/public/install.ps1:L36-L57).

.github/workflows/release.yml runs on pushes to main. It first uses Changesets to open or update a reviewed version PR; ordinary merges then stop because their current version is already released. Merging the version PR re-runs lint, both typechecks, tests, and the host build; embeds the web UI; and cross-compiles five binaries — Linux and macOS on x64 and arm64, plus Windows x64. It generates SHA256SUMS from the dist/ basenames, validates the exact expected asset set, creates the vX.Y.Z tag on the publishing commit, and attaches the binaries and manifest to one GitHub release. The workflow also runs actions/attest-build-provenance, which signs each binary with a workflow identity via OpenID Connect (OIDC) that in-job code cannot forge, verifiable with gh attestation verify. The checksums guard integrity; the provenance attestation adds supply-chain traceability that the same-job manifest cannot.

Sources: site/public/install.sh:L1-L111 site/public/install.ps1:L1-L75 .github/workflows/release.yml:L1-L85