fix(update): ship and fetch native binaries from the GitHub release - #355
Conversation
The release manifest and the CDN latest.json name a bare binary per platform, but the release only uploaded zip/zst/tar.gz, so every native update from 2.1.0 hit HTTP 404. Clients since #323 also fetched from a CDN /binaries/ route that only serves the site HTML. - produce-manifest uploads the bare binary + sha256 sidecar it names - client resolves manifest and binaries on the GitHub release again - release gate HEADs every advertised download, not only the version - brew bump drops the one-shot npm view that raced publish propagation (red X on the 2.4.0 and 2.4.1 release runs); the tarball poll gates it
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository: PyModel/pythinker-code/.coderabbit.yaml Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (13)
Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughNative binaries now use versioned GitHub release assets, and release checks test the download URLs advertised by CDN and GitHub manifests. The Homebrew formula update script relies on tarball polling rather than an exact-version npm query. ChangesNative binary release assets
Homebrew formula update gate
Priority: ➖ Normal Estimated code review effort: 3 (Moderate) | ~25 minutes Change: Bug fix · Severity of issue fixed: Medium Sequence Diagram(s)sequenceDiagram
participant Verifier as verify-release-consistency
participant CDN
participant GitHub as GitHub release
participant Assets as Advertised download URLs
Verifier->>CDN: Fetch latest.json
Verifier->>GitHub: Fetch manifest.json
Verifier->>Assets: HEAD-check collected URLs
Assets-->>Verifier: Return response status
Merge Risk: ⚪ Minimal · up to Native updates now use published GitHub release assets, release checks verify advertised downloads, and Homebrew retains its tarball availability gate. No concrete merge-blocking issue is established; merge after normal checks pass. Security Architecture ReviewSecurity architecture risk: 🔵 Low · up to The new download source aligns with the release publisher, while existing version, checksum and activation checks remain in place. No introduced security vulnerability was established. Some uncertainty remains about control of the externally hosted update metadata and the release checker’s outbound requests. Retained concerns Security review detailsSecurity Blast Radius
Security Findings and Attack Paths
Trust Boundaries and Controls
Resilience and Maintainability Implications
Hardening Proposals
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Docstring CoverageExplanation Docstring coverage is 41.67% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 12 functions across 11 files. (2 skipped: 2 unsupported.)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
commit: |
…#356) ## Requirement or Bug Ship the Homebrew formula as the native binary, the way CodexBar's tap does: per-platform release tarballs, bumped by the release run, verified by a real install. Stacked on #355 (shares `update-brew-formula.mjs`). ## Bug Reproduction Steps N/A (feature). ## Root Cause N/A (feature). ## Code Changes - `update-brew-formula.mjs`: `renderFormula` writes the whole formula from a template instead of patching one `url` line with a regex. The template has `on_macos`/`on_linux` × `Hardware::CPU.arm?`, one native `pythinker-code-<target>.tar.gz` url + sha256 per target, `bin.install "pythinker"`, and a `test do` that asserts `pythinker --version`. The sha256 comes from the downloaded bytes, not from the sidecars. All four downloads share one 600 s poll that retries on 404. After the push, the script reads `Formula/pythinker-code.rb` back from the tap's `main` and fails if it differs. A rejected push gets up to 3 attempts with `pull --rebase` between them. - `release.yml`: - `update-brew-tap` now also needs `publish-native-assets`. On 2.4.1 the tarballs appeared about 13 min after `release` finished. - New `verify-brew-install` job (macos-latest + ubuntu-latest) runs `brew tap`, `brew install`, `brew test` and compares `--version` with `package.json`. - `Release lane summary` reads `BREW_RESULT` from `verify-brew-install`. - `cli/update/source.ts`: a native binary under a Homebrew Cellar counts as a `homebrew` install. `detectNativeInstall()` returns false there, so the binary never stages or swaps itself inside the Cellar, and `brew upgrade` is its update path. Every caller of `detectNativeInstall()` (update download, startup swap, source detection) goes through this one function. - `.agents/skills/release/SKILL.md`: the brew section now describes this flow. How this differs from CodexBar: CodexBar dispatches a tap-side workflow with a PAT. This PR keeps the existing GitHub App token, which pushes the formula directly. That needs no new secret and no workflow in the second repo. The substance is the same: a native per-platform formula, a 404 retry on assets, and a content check after the bump. ## Behavior Changes and Affected Users | Behavior | Before | After | Who relies on the old behavior | Escape hatch | |---|---|---|---|---| | What `brew install pymodel/tap/pythinker-code` installs | npm tarball + `depends_on "node"` | native binary, no Node.js | Homebrew users on macOS and Linux | `npm i -g @pymodel/pythinker-code` | | `node` formula after `brew upgrade` | a dependency | orphaned (`brew autoremove` may delete it) | users who rely on brew-managed `node` only through this formula | `brew install node` | | opentui caveat in the formula | printed | removed (the native binary bundles it) | nobody | n/a | | Install source of a native binary in `/opt/homebrew/Cellar/…` or `/home/linuxbrew/.linuxbrew/Cellar/…` | `native` (would stage and swap itself) | `homebrew` (shows the `brew upgrade` hint) | nobody; only this PR puts native binaries into the Cellar | n/a | | Release lane brew result | the tap push job | the real install on macOS + Ubuntu | release operators | `RELEASE_LANE_BREW=disabled` (unchanged) | Inventory of the old npm-formula path and where each item went: | Old item | New place | |---|---| | `depends_on "node"` | dropped; the binary bundles Node | | opentui caveat | dropped | | npm tarball sha256 from the registry | sha256 of each release tarball from the downloaded bytes | | npm 404 poll (600 s) | the same poll, shared across the 4 release tarballs | | `--version` test | kept, now on the native binary | Test coverage: - `source.test.ts`: Cellar paths on macOS and Linux → `homebrew`, and `detectNativeInstall` returns false. Both failed before the change. - `update-brew-formula.test.mjs`: formula pairs in order, a missing target throws, an invalid sha256 throws. - `release-workflows.test.mjs`: the brew needs and `BREW_RESULT` wiring. - Manual check with real Homebrew on the 2.4.1 tarballs, done with `brew install` + `brew test`: - macOS arm64: the Developer ID signature stays intact. - Linux x64 (`homebrew/brew` container): `--version` prints 2.4.1. - `pnpm test:release` 58/58; app update suites 329/329; `tsc`, lint, actionlint clean. ## Checklist - [x] I have read the [CONTRIBUTING](https://github.com/PyModel/pythinker-code/blob/main/CONTRIBUTING.md) document. - [x] I have linked a related issue (external PRs: issue must have a maintainer's `/approve`). - [x] I have added tests that prove my feature works. - [x] The behavior-change table above is complete, and every removed behavior or flipped default is named in the changeset and either has an escape hatch or was explicitly approved by a maintainer in this PR. - [x] Ran `gen-changesets` skill, or this PR needs no changeset. - [x] Ran `gen-docs` skill, or this PR needs no doc update.
This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated. # Releases ## @pymodel/pythinker-code@2.5.0 ### Minor Changes - [#356](#356) [`d1b4e58`](d1b4e58) Thanks [@elkaix](https://github.com/elkaix)! - Homebrew now installs the native `pythinker` binary on macOS and Linux, without Node.js. ### Patch Changes - [#355](#355) [`34b854a`](34b854a) Thanks [@elkaix](https://github.com/elkaix)! - Native `pythinker update` downloads the new binary from the GitHub release again; native installs on 2.2.0–2.4.1 need one reinstall to receive it (see [#354](#354)). ## @pymodel/pythinker-desktop@1.6.0 ### Minor Changes - [#356](#356) [`d1b4e58`](d1b4e58) Thanks [@elkaix](https://github.com/elkaix)! - Homebrew now installs the native `pythinker` binary on macOS and Linux, without Node.js. ### Patch Changes - [#355](#355) [`34b854a`](34b854a) Thanks [@elkaix](https://github.com/elkaix)! - Native `pythinker update` downloads the new binary from the GitHub release again; native installs on 2.2.0–2.4.1 need one reinstall to receive it (see [#354](#354)). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Requirement or Bug
Native
pythinker updatefails withnative binary download returned HTTP 404, and the 2.4.0 and 2.4.1 release runs on main end red. Related: #354.Bug Reproduction Steps
pythinker updateand accept 2.4.0 (or 2.4.1).error: failed to download update 2.4.0: native binary download returned HTTP 404.On 2.2.0–2.4.1 the same command fails earlier, because
manifest.jsoncomes back as an HTML page.Release runs 36917123003 (2.4.0) and 36942551995 (2.4.1) fail in
Update Homebrew tapandRelease lane summary.Root Cause
Three separate defects. All three are fixed at the root, with no workarounds.
produce-manifest.mjswritesmanifest.jsonandlatest.jsonentries that point at the bare binarypythinker-code-<target>[.exe]. It hashes that binary and then deletes it, so only.zip,.zstand.tar.gzget uploaded. A 2.1.0 client downloads the bare file and gets a 404.code.pythinker.com/pythinker-code/binaries/<v>/…. That route has never served a file: the site answers it with its SPA page and HTTP 200. These clients cannot update, and only a reinstall fixes them (Native installs 2.2.0–2.4.1: reinstall once to get updates again #354).assertPublishedNpmVersionrannpm viewonce, 24 s afterchangeset publish. npm showed 2.4.1 at 23:56:55, about 4 minutes after the check at 23:53:10. The job failed, andRelease lane summaryfails wheneverBREW_RESULTis not success.Code Changes
produce-manifest.mjs: copy the binary into the upload set under the name the manifest uses, with a.sha256sidecar. The existingdist-native-release/*upload step then ships it.constant/app.ts,native-manifest.ts,native-stage.ts: replacepythinkerCodeCdnBinariesBase()withpythinkerCodeReleaseAssetUrl(version, filename). It returns the GitHub release URL with the encoded tag, which is the same shape as 2.1.0 and aslatest.json. The.zst-first staging path does not change.update-brew-formula.mjs: delete the one-shotnpm viewcheck.downloadNpmTarballalready polls for 600 s, retries on 404, and refuses when the budget runs out, so it is the only gate now.cdn-consistency.mjs+verify-release-consistency.mjs: the release gate now HEADs every URL thatlatest.jsonand the releasemanifest.jsonadvertise, and fails on anything unreachable. It retries 403/429. Against the live 2.4.1 release it reports the 6 missing bare binaries, so it would have caught defect 1 on 2.3.0.README.md: a pointer for stuck native installs to Native installs 2.2.0–2.4.1: reinstall once to get updates again #354.Behavior Changes and Affected Users
.zip,.zst,.tar.gz(+ sidecars).sha256code.pythinker.com/pythinker-code/binaries/<v>/…github.com/PyModel/pythinker-code/releases/download/<encoded tag>/…RELEASE_LANE_BREW=disabled(unchanged)Populations:
Contract file touched:
apps/pythinker-code/src/cli/update/*(CLI tripwire). The removed/binaries/base never served data, so no client from a previous release loses anything.Test coverage:
release-artifacts.test.ts: every file the manifest names exists in the upload set. Failed before the fix, passes after.native-manifest.test.ts: exact encoded GitHub URLs.cdn-consistency.test.mjs: URL collection, 404s, 403/429 retry.update-brew-formula.test.mjs: the poll is the only gate.pnpm test:release54/54; app update suites 325/325;tscclean;pnpm lint0 errors.Checklist
/approve).gen-changesetsskill, or this PR needs no changeset.gen-docsskill, or this PR needs no doc update.Summary by CodeRabbit
pythinker updatenow downloads the latest binary from the GitHub release, restoring updates for supported native installations.pythinker updatefor future updates.