Skip to content

fix(update): ship and fetch native binaries from the GitHub release - #355

Merged
elkaix merged 4 commits into
mainfrom
fix/native-update-assets
Oct 2, 2026
Merged

elkaix merged 4 commits into
mainfrom
fix/native-update-assets

Conversation

@elkaix

@elkaix elkaix commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Requirement or Bug

Native pythinker update fails with native 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

  1. Install 2.1.0 with the native installer.
  2. Run pythinker update and accept 2.4.0 (or 2.4.1).
  3. 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.json comes back as an HTML page.

Release runs 36917123003 (2.4.0) and 36942551995 (2.4.1) fail in Update Homebrew tap and Release lane summary.

Root Cause

Three separate defects. All three are fixed at the root, with no workarounds.

  1. Missing release asset (affects 2.1.0 clients). produce-manifest.mjs writes manifest.json and latest.json entries that point at the bare binary pythinker-code-<target>[.exe]. It hashes that binary and then deletes it, so only .zip, .zst and .tar.gz get uploaded. A 2.1.0 client downloads the bare file and gets a 404.
  2. Wrong download base (affects 2.2.0–2.4.1 clients). chore: upstream reconcile through reference 99eaa993b #323 moved the manifest and binary URLs to 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).
  3. Brew job race (the red X). assertPublishedNpmVersion ran npm view once, 24 s after changeset publish. npm showed 2.4.1 at 23:56:55, about 4 minutes after the check at 23:53:10. The job failed, and Release lane summary fails whenever BREW_RESULT is not success.

Code Changes

  • produce-manifest.mjs: copy the binary into the upload set under the name the manifest uses, with a .sha256 sidecar. The existing dist-native-release/* upload step then ships it.
  • constant/app.ts, native-manifest.ts, native-stage.ts: replace pythinkerCodeCdnBinariesBase() with pythinkerCodeReleaseAssetUrl(version, filename). It returns the GitHub release URL with the encoded tag, which is the same shape as 2.1.0 and as latest.json. The .zst-first staging path does not change.
  • update-brew-formula.mjs: delete the one-shot npm view check. downloadNpmTarball already 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 that latest.json and the release manifest.json advertise, 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

Behavior Before After Who relies on the old behavior Escape hatch
Native release assets .zip, .zst, .tar.gz (+ sidecars) also the bare binary + .sha256 nobody; it adds an asset that manifests already named n/a
Native client download URLs (built from this tree) code.pythinker.com/pythinker-code/binaries/<v>/… github.com/PyModel/pythinker-code/releases/download/<encoded tag>/… nobody: the CDN route never returned a file (SPA HTML, 200) n/a
Brew tap job when npm is slow to show the version fails at once polls the tarball up to 600 s nobody; the old failure was a false negative RELEASE_LANE_BREW=disabled (unchanged)
Release consistency gate checks version strings only also fails on any unreachable advertised download release operators none needed; it only reports real 404s

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:release 54/54; app update suites 325/325; tsc clean; pnpm lint 0 errors.

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked a related issue (external PRs: issue must have a maintainer's /approve).
  • I have added tests that prove my feature works.
  • 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.
  • Ran gen-changesets skill, or this PR needs no changeset.
  • Ran gen-docs skill, or this PR needs no doc update.

Summary by CodeRabbit

  • Bug Fixes
    • Native pythinker update now downloads the latest binary from the GitHub release, restoring updates for supported native installations.
    • If you’re using a native install on version 2.2.0–2.4.1, rerun the installation command once to receive a version that can update normally. After reinstalling, use pythinker update for future updates.

elkaix added 4 commits October 1, 2026 20:16
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
@coderabbitai

coderabbitai Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

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 configuration

Configuration used: Repository: PyModel/pythinker-code/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: db7c5365-972f-458a-a006-06f02d36a46e

📥 Commits

Reviewing files that changed from the base of the PR and between 5d166ef and 5dc72ec.

📒 Files selected for processing (13)
  • .changeset/native-update-release-assets.md
  • README.md
  • apps/pythinker-code/scripts/native/produce-manifest.mjs
  • apps/pythinker-code/src/cli/update/native-manifest.ts
  • apps/pythinker-code/src/cli/update/native-stage.ts
  • apps/pythinker-code/src/constant/app.ts
  • apps/pythinker-code/test/cli/update/native-manifest.test.ts
  • apps/pythinker-code/test/scripts/native/release-artifacts.test.ts
  • scripts/release/cdn-consistency.mjs
  • scripts/release/cdn-consistency.test.mjs
  • scripts/release/update-brew-formula.mjs
  • scripts/release/update-brew-formula.test.mjs
  • scripts/release/verify-release-consistency.mjs

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

Native 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.

Changes

Native binary release assets

Layer / File(s) Summary
Package and resolve release assets
apps/pythinker-code/scripts/native/produce-manifest.mjs, apps/pythinker-code/src/constant/app.ts, apps/pythinker-code/src/cli/update/*, apps/pythinker-code/test/cli/update/*, apps/pythinker-code/test/scripts/native/*, README.md, .changeset/*
Manifest generation copies bare binaries and writes checksum sidecars. Native manifest and binary URLs now point to versioned GitHub release assets. Tests verify the URL paths and generated artifacts. The README and changeset describe the update limitation for native installs on versions 2.2.0–2.4.1.
Verify advertised download URLs
scripts/release/cdn-consistency.mjs, scripts/release/cdn-consistency.test.mjs, scripts/release/verify-release-consistency.mjs
The release check collects URLs from CDN and GitHub manifests, then checks reachability. It retries transport errors, 5xx, 403, and 429 responses. Tests cover URL collection, failures, and successful retries.

Homebrew formula update gate

Layer / File(s) Summary
Use tarball polling for npm availability
scripts/release/update-brew-formula.mjs, scripts/release/update-brew-formula.test.mjs
The script removes the exact-version npm view check and proceeds to its existing tarball polling flow. Tests for the removed check are deleted; tarball download tests remain.

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
Loading

Merge Risk: ⚪ Minimal · up to 5dc72

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 Review

Security architecture risk: 🔵 Low · up to 5dc72

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
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The principal sensitive outcome is replacement of installed native executables with release-owned bytes. Compromise of the authority supplying both a release manifest and its binaries could affect clients installing that release within their local execution privileges; manifest checksums alone do not authenticate a compromised publisher. No such compromise was demonstrated.

Security Findings and Attack Paths

  • observed — The new production checker passes externally served latest.json platform URLs to outbound HEAD requests without an origin restriction. This creates a metadata-to-network boundary requiring publication-authority assessment; attacker write access and effective network exposure were not established, so it is not a verified attack path.

Trust Boundaries and Controls

  • observed — The routed public-entrypoint ranges are tests with injected network functions, not production services. Production checks issue HEAD requests without attaching the deployment webhook credential. Native asset publication requests repository-content write permission, while the separate tap token is restricted to content writes in homebrew-tap.

Resilience and Maintainability Implications

  • observed — The existing activation path rechecks checksum and executable version, rejects non-newer versions, evaluates automatic-update policy and serializes swaps with a mutex. Failed installation attempts restore the prior executable where possible and retain recovery artifacts when restoration fails. The documented interruption window during replacement predates this PR’s URL change.

Hardening Proposals

  • proposed — Constrain advertised URLs and redirect destinations to approved HTTPS download origins before the release checker fetches them. This would make its network boundary explicit without treating the unproven metadata-control scenario as an observed vulnerability.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning 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:… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title uses the required fix prefix, stays within 72 characters, uses imperative wording, and accurately describes the native binary release and download changes.
Description check ✅ Passed The description includes the required bug details, reproduction steps, root cause, code changes, behavior-change table, affected users, test coverage, and completed checklist. It is directly related t…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

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.)

  • Fix all pre-merge checks with AI
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@pkg-pr-new

pkg-pr-new Bot commented Oct 2, 2026

Copy link
Copy Markdown
pnpm dlx https://pkg.pr.new/@pymodel/pythinker-code@5dc72ec
npx https://pkg.pr.new/@pymodel/pythinker-code@5dc72ec

commit: 5dc72ec

@elkaix
elkaix merged commit 34b854a into main Oct 2, 2026
27 checks passed
@elkaix
elkaix deleted the fix/native-update-assets branch October 2, 2026 01:19
elkaix added a commit that referenced this pull request Oct 2, 2026
…#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.
elkaix pushed a commit that referenced this pull request Oct 2, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant