Skip to content

Commit eb7c066

Browse files
authored
fix(release): read the Homebrew lane version from the formula's version 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 -->
1 parent 0f518b5 commit eb7c066

2 files changed

Lines changed: 31 additions & 5 deletions

File tree

‎scripts/release/release-status.mjs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ async function fetchText(fetchImpl, url, label, init = {}) {
8383
}
8484

8585
function brewFormulaVersion(formula) {
86-
const match = typeof formula === 'string' ? /pythinker-code-(\d+\.\d+\.\d+)\.tgz/u.exec(formula) : null;
86+
const match = typeof formula === 'string' ? /^\s*version "([^"]+)"\s*$/mu.exec(formula) : null;
8787
return match === null ? undefined : validVersion(match[1]);
8888
}
8989

‎scripts/release/release-status.test.mjs‎

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,19 +47,30 @@ function json(body, status = 200) {
4747
});
4848
}
4949

50+
function brewFormulaText(version) {
51+
const tag = encodeURIComponent(`@pymodel/pythinker-code@${version}`);
52+
return [
53+
'class PythinkerCode < Formula',
54+
` version "${version}"`,
55+
` url "https://github.com/PyModel/pythinker-code/releases/download/${tag}/pythinker-code-darwin-arm64.tar.gz"`,
56+
'',
57+
].join('\n');
58+
}
59+
5060
function fixtureFetch({
5161
cliAssets = expectedCliAssets,
5262
nightlyAssets = desktopAssets(desktopNightlyVersion, 'nightly'),
5363
brewVersion = '1.3.0',
64+
brewFormula,
5465
} = {}) {
5566
return async (input) => {
5667
const url = new URL(String(input));
5768
if (url.hostname === 'registry.npmjs.org') return json({ latest: '1.3.0' });
5869
if (url.hostname === 'raw.githubusercontent.com') {
59-
return new Response(
60-
`url "https://registry.npmjs.org/@pymodel/pythinker-code/-/pythinker-code-${brewVersion}.tgz"\n`,
61-
{ status: 200, headers: { 'content-type': 'text/plain' } },
62-
);
70+
return new Response(brewFormula ?? brewFormulaText(brewVersion), {
71+
status: 200,
72+
headers: { 'content-type': 'text/plain' },
73+
});
6374
}
6475
if (url.hostname === 'code.pythinker.com') {
6576
return json({
@@ -165,6 +176,21 @@ void test('fails when the Homebrew formula lags the published CLI version', asyn
165176
assert.equal(brew?.observed, '1.2.0');
166177
});
167178

179+
void test('fails when the Homebrew formula has no version stanza', async (t) => {
180+
const rootDir = await fixtureRoot(t);
181+
const result = await collectReleaseStatus({
182+
rootDir,
183+
desktopCommitCount,
184+
fetchImpl: fixtureFetch({ brewFormula: 'class PythinkerCode < Formula\n' }),
185+
});
186+
187+
assert.equal(result.ok, false);
188+
const brew = result.rows.find((row) => row.lane === 'Homebrew');
189+
assert.equal(brew?.ok, false);
190+
assert.equal(brew?.observed, 'unavailable');
191+
assert.match(brew?.details ?? '', /no pythinker-code tarball version/u);
192+
});
193+
168194
void test('fails when the current desktop Nightly release is incomplete', async (t) => {
169195
const rootDir = await fixtureRoot(t);
170196
const result = await collectReleaseStatus({

0 commit comments

Comments
 (0)