Skip to content

feat(release): ship the Homebrew formula from native release tarballs - #356

Merged
elkaix merged 7 commits into
mainfrom
feat/brew-native-formula
Oct 2, 2026
Merged

elkaix merged 7 commits into
mainfrom
feat/brew-native-formula

Conversation

@elkaix

@elkaix elkaix commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

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

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

elkaix added 6 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
Port the CodexBar Homebrew channel: the formula installs the native
per-platform tarball (macOS/Linux x arm64/x64) with a --version test
instead of the npm tarball plus a node dependency.

- update-brew-formula renders the whole formula, hashes downloaded
  tarballs (404 poll), pushes with rebase retry, reads it back
- update-brew-tap waits for publish-native-assets
- verify-brew-install installs from the public tap on macOS + Linux and
  is the Homebrew lane result in the release summary
- a native binary under a Homebrew Cellar reports source homebrew and
  never stages or swaps itself
- release skill documents the lane
@coderabbitai

coderabbitai Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 18 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

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

Review profile: CHILL

Plan: Advanced

Run ID: 0964c1ee-b433-4f6f-83f8-c9ec7a2a7654

📥 Commits

Reviewing files that changed from the base of the PR and between 34b854a and d32f391.

📒 Files selected for processing (8)
  • .agents/skills/release/SKILL.md
  • .changeset/brew-native-formula.md
  • .github/workflows/release.yml
  • apps/pythinker-code/src/cli/update/source.ts
  • apps/pythinker-code/test/cli/update/source.test.ts
  • scripts/release/release-workflows.test.mjs
  • scripts/release/update-brew-formula.mjs
  • scripts/release/update-brew-formula.test.mjs
  • 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@d32f391
npx https://pkg.pr.new/@pymodel/pythinker-code@d32f391

commit: d32f391

Base automatically changed from fix/native-update-assets to main October 2, 2026 01:18
# Conflicts:
#	scripts/release/update-brew-formula.mjs
#	scripts/release/update-brew-formula.test.mjs
@elkaix
elkaix enabled auto-merge (squash) October 2, 2026 01:24
@elkaix
elkaix merged commit d1b4e58 into main Oct 2, 2026
25 checks passed
@elkaix
elkaix deleted the feat/brew-native-formula branch October 2, 2026 01:27
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>
elkaix added a commit that referenced this pull request Oct 2, 2026
…on stanza (#358)

## Requirement or Bug

The nightly workflow has failed on `main` every scheduled run since
2026-09-28 (runs 36405927270, 36551889134, 36698004633, 36814079854,
36847459152). After PR #356 shipped the Homebrew formula from native
release tarballs, the release-reconciliation script can no longer read
the formula's version, so the Homebrew lane fails on every nightly.

## Bug Reproduction Steps

1. Let a release publish with the #356 formula shape (done:
`@pymodel/pythinker-code@2.5.0`).
2. Run `node scripts/release/release-status.mjs` on `main` (commit
before this PR).
3. The Homebrew lane reports `FAIL | Formula/pythinker-code.rb (no
version)` — the step exits 1 and the nightly run goes red (drift issue
"Release lane drift detected").

## Root Cause

`brewFormulaVersion` extracted the version from the old npm-tarball
formula shape via the regex `/pythinker-code-(\d+\.\d+\.\d+)\.tgz/`. PR
#356 rewrote the generated formula to pin native per-platform tarballs
(`pythinker-code-<target>.tar.gz`, no version in the filename) and to
declare an explicit `version "X.Y.Z"` stanza instead. The lane checker —
a consumer of the formula shape — was not updated, so it now always
reports the formula as unreadable. This is a fundamental fix: the parser
now reads the stanza the generator actually writes (`renderFormula` in
`scripts/release/update-brew-formula.mjs`).

## Code Changes

- `scripts/release/release-status.mjs` — `brewFormulaVersion` now
extracts the version from the formula's `version "…"` stanza.
- `scripts/release/release-status.test.mjs` — the fixture formula now
renders the native-tarball shape (version stanza + native tarball URL),
and a new test proves a formula without a version stanza fails the lane
with "no pythinker-code tarball version".

## Behavior Changes and Affected Users

| Behavior | Before | After | Who relies on the old behavior | Escape
hatch |
|---|---|---|---|---|
| Homebrew lane version detection in nightly reconciliation | version
parsed from a `pythinker-code-<version>.tgz` npm-tarball URL | version
parsed from the formula's `version "…"` stanza | maintainers reading the
drift issue's "Observed" column for a tap still serving the pre-#356
npm-tarball formula: they now see "unavailable" instead of the old
version number | none — the pre-#356 formula shape is no longer produced
by any pipeline |

Nothing else observable changes: the CLI, desktop app, extension,
installers, and updaters do not consume `brewFormulaVersion`; it is
internal to the release reconciliation script and the nightly workflow.

Test coverage: `scripts/release/release-status.test.mjs` — lane-aligned
test (native-shape fixture), lagging-formula test, and the new
missing-stanza test (`pnpm run test:release`, 59/59).

## 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 is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Homebrew release status now reflects the version declared in the
formula, rather than inferring it from the release archive’s filename.
This keeps the reported version accurate when archive naming differs
from the formula. If the formula does not contain a valid version
declaration, the version is reported as unavailable.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
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