fix(release): advertise only published versions on the update channel - #37
Conversation
The CDN manifest took its version from apps/pythinker-code/package.json and the site autodeploys on every push to main, so a `ci: release packages` merge advertised the next version before — and, when a changeset landed while the version PR was open, without — npm and the GitHub release assets ever getting it. Clients then polled GitHub for assets that did not exist for about six minutes on every launch. Derive the advertised version from the npm dist-tag instead, take publishedAt from npm's own publish timestamp so unrelated site deploys stop re-anchoring the client rollout window, and run the release consistency check on version-bump merges that published nothing — it was gated on a successful publish, so it skipped exactly the case where the version and the published artifacts diverge.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughThe release flow now advertises published npm versions only. The CDN manifest records npm publication time, and release verification compares the CDN version with npm latest. ChangesPublished CDN release handling
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant ReleaseWorkflow
participant BuildCDN
participant NpmRegistry
participant CDNManifest
participant ConsistencyCheck
ReleaseWorkflow->>BuildCDN: Build CDN release
BuildCDN->>NpmRegistry: Query latest published version and timestamp
NpmRegistry-->>BuildCDN: Return release metadata
BuildCDN->>CDNManifest: Write latest.json
ReleaseWorkflow->>ConsistencyCheck: Verify release consistency
ConsistencyCheck->>CDNManifest: Fetch latest.json
ConsistencyCheck->>NpmRegistry: Read npm latest version
ConsistencyCheck-->>ReleaseWorkflow: Report CDN version status
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Comment |
commit: |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/site/scripts/build-cdn.mjs`:
- Around line 52-53: Validate the trimmed PYTHINKER_CDN_VERSION override in the
pinned-version path before returning it from the surrounding manifest version
function. Accept and write the override only when it satisfies the manifest’s
existing SemVer contract; otherwise reject or ignore it according to the
established invalid-configuration behavior, preserving the generated timestamp
and normal version resolution for valid or absent overrides.
- Around line 67-70: Update the publication timestamp handling around
publishedAt in the CDN build metadata so missing or non-string
view.time[version] values cause the build to fail instead of falling back to the
current time. Preserve valid npm-provided timestamps unchanged and report the
invalid or missing release metadata clearly.
In `@scripts/release/verify-release-consistency.mjs`:
- Around line 15-20: Update compareRelease to account for prerelease identifiers
when comparing SEMVER values, or validate dist-tags.latest before invoking it so
prerelease values are rejected; ensure a CDN version such as 1.2.3 is treated as
newer than npm latest 1.2.3-rc.1 and causes the consistency check to fail rather
than report deploy lag.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 5c23aed3-a6af-48c8-bbed-8d47e0a8b9bb
📒 Files selected for processing (4)
.changeset/update-channel-published-versions-only.md.github/workflows/release.ymlapps/site/scripts/build-cdn.mjsscripts/release/verify-release-consistency.mjs
…lish time The pinned-version escape hatch skipped the semver check the registry path applies, so an operator typo shipped a latest.json that every installed client fails to parse. It now answers to the same shape rule. publishedAt fell back to build time when npm's metadata was unreadable, which is exactly the re-anchoring of the rollout window the function's own comment records as the bug. An unreadable timestamp is a failed registry read, and a failed read already fails the build.
Resolves apps/site/scripts/build-cdn.mjs by keeping both sides: main's RELEASE_VERSION guard and strict publishedAt from the #37 review, plus this branch's resolvePlatformArtifacts, MIN_REQUIRED_VERSION and the manifest fields that name each platform's artifact. Verified by running the merged script: it writes 0.9.2 with npm's own publish time and six platform entries with real checksums.
) ## Related Issue No issue — reported directly. The problem is described below. ## Problem A user's terminal showed `↑ Update available — v0.11.0` on the banner while `/update` answered `Update to v0.10.0 already in progress`, and nothing ever changed. Investigating it found a whole class of defects behind that one screen: - The manifest advertised a **version**, so the client had to guess a GitHub asset URL and poll for it. When the guess was wrong it polled for ~6 minutes, on every launch. - No installer network call had a timeout, so one hung request wedged every update path with no expiry. - A live pid held an install lease forever, with no ceiling, so a recycled pid wedged updates permanently. - The background installer's outcome was written by the parent process, and the product tells the user to close that terminal — so failures went unrecorded, the attempt counter never advanced, and a version that could not succeed was retried on every launch. - `install.sh` rendered a progress bar only on a TTY; in the background it blocked in a single silent `curl` and reported nothing at all, which is why an update in flight looked identical to a wedged one. - Two foreground install paths ignored the lock entirely and could run while a detached installer was writing the same executable. - The banner chip and `/update` read different files, which is how they came to disagree. ## What changed Fourteen commits, each with tests, in the order they were verified. Grouped: **The channel tells the truth.** `latest.json` now carries the resolved per-platform artifact (`url` + `sha256`), copied from the release's own native manifest, and a native client requires an entry for its platform before it will advertise or install anything. npm-family sources are exempt — the published version *is* their artifact — and that exemption is the case the tests protect hardest. `minRequiredVersion` lets a release bypass the staged rollout when a client cannot skip it. **Progress is visible.** The installer emits newline-terminated machine progress on stderr — the stream the parent already pipes — and the parent records it on the install record. The footer status row under the prompt renders `↑ v0.11.0`, `↓ v0.11.0 ▰▰▰▱▱▱▱▱ 42%`, `↑ v0.11.0 restart to apply`, reusing the context gauge's own bar glyphs. An unknown download size drops the bar rather than inventing a percentage. **Nothing wedges.** Every installer fetch has a connect bound, a per-attempt ceiling and a stall guard (`--retry` is deliberately absent: it resets `--max-time`). One `lease.ts` states the lease rule once, with a ceiling on live pids. Startup reconciles an abandoned install into a recorded failure so a doomed version parks. Both foreground paths hold the lock and write their outcome. **It says what is happening.** `/update` reports the installing version *and* the newer target that follows, and reports a parked version's attempt count and recorded reason instead of a bare command. Deletions rather than additions where the shape allowed: the plain-text `/latest` fallback (it carried no platform data, so it reported an unverifiable target as verified), the duplicate `install.sh`/`install.ps1` under `apps/site/public/`, the duplicated `isProcessRunning` and four lease constants, the banner's update chip and its per-frame `readFileSync`, and one of the two update decisions per launch. Two scope decisions worth flagging: killing an in-flight installer to switch targets is **not** implemented — the lease ceiling and reconciliation make the wait finite, and honest reporting fixes what the user saw. And a writability precheck was dropped in favour of surfacing the installer's own recorded error, which covers EACCES, network faults and disk-full alike. Base is `fix/release-cdn-version-truth` (#37) because the manifest generator work stacks on it. ## Checklist - [x] I have read the [CONTRIBUTING](https://github.com/Pythoughts-labs/pythinker-code/blob/main/CONTRIBUTING.md) document. - [x] I have linked a related issue, or explained the problem above. - [x] I have added tests that prove my feature works. - [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 - **New Features** - Update status now appears beneath the prompt, including availability, download progress, required updates, waiting, and failures. - Updates can enforce a minimum supported version. - Platform-specific update availability is validated before installation. - **Bug Fixes** - Prevented conflicting or abandoned installations from blocking future updates. - Added clearer failure messages and retry information. - Installers now use connection, metadata, and download timeouts to avoid hanging. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
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 ## @pythoughts/pythinker-code@0.12.0 ### Minor Changes - [#35](#35) [`2ce6b5e`](2ce6b5e) - Show the plan before a Dynamic Workflow runs, and let a good one be saved as a command Manual mode used to approve every `DynamicWorkflow` call outright. That approval only ever fired in manual mode — auto and yolo approve earlier in the chain — so the one mode whose purpose is to ask was the one mode that never saw what it was agreeing to. A `DynamicWorkflow` call in manual mode now asks, and the approval carries the fan-out: how many subagents, the task list, the prompt template, the worker model, and the summed size of the prompts about to be sent. "Approve for this session" is keyed to that workflow's description rather than granting every future `DynamicWorkflow` call. `/workflow save <name>` writes the last run back out as a skill under `.pythinker-code/skills/`, so a fan-out that worked can be re-run by name. - [#38](#38) [`44efbc7`](44efbc7) - Let a release declare a minimum supported version, so a client below it is offered the update without waiting for its staged rollout batch. - [#38](#38) [`44efbc7`](44efbc7) - Show update availability and live download progress in the status row under the prompt, replacing the startup banner chip that was computed once and never refreshed. ### Patch Changes - [#37](#37) [`12069a8`](12069a8) - Stop offering updates to versions that were never published: the update channel now advertises only the release that is actually available for download. - [#38](#38) [`44efbc7`](44efbc7) - Stop offering an update with no build for the running platform, give every installer network call a timeout, expire a stale install lease instead of blocking updates forever, and say which version is installing and why a failed one stopped retrying. ## @pythoughts/pythinker-code-sdk@0.13.0 ### Minor Changes - [#35](#35) [`2ce6b5e`](2ce6b5e) - Show the plan before a Dynamic Workflow runs, and let a good one be saved as a command Manual mode used to approve every `DynamicWorkflow` call outright. That approval only ever fired in manual mode — auto and yolo approve earlier in the chain — so the one mode whose purpose is to ask was the one mode that never saw what it was agreeing to. A `DynamicWorkflow` call in manual mode now asks, and the approval carries the fan-out: how many subagents, the task list, the prompt template, the worker model, and the summed size of the prompts about to be sent. "Approve for this session" is keyed to that workflow's description rather than granting every future `DynamicWorkflow` call. `/workflow save <name>` writes the last run back out as a skill under `.pythinker-code/skills/`, so a fan-out that worked can be re-run by name. ## pythinker-code@0.8.6 ### Patch Changes - Updated dependencies [[`2ce6b5e`](2ce6b5e)]: - @pythoughts/pythinker-code-sdk@0.13.0 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added approval previews for Dynamic Workflow calls in manual mode, including fan-out details. - Added session-scoped workflow approvals. - Added `/workflow save <name>` to save the latest workflow run as a reusable skill. - Added live update availability and download progress indicators. - **Bug Fixes** - Improved update reliability with build checks, network timeouts, stale-install recovery, and clearer retry messages. - **Release Updates** - Released PyThinker Code 0.12.0, VS Code extension 0.8.6, and SDK 0.13.0. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Related Issue
No issue filed — the problem is described below.
Problem
Every installed client was being told to install a version that does not exist.
https://code.pythinker.com/pythinker-code/latest.jsonadvertised 0.11.0.dist-tags.latestwas 0.9.2; there is no 0.10.0 and no 0.11.0 on npm.Two independent causes:
apps/site/scripts/build-cdn.mjsderived the advertised version fromapps/pythinker-code/package.json. That file is bumped by theci: release packagesmerge, andthe site autodeploys on every push to main, so the CDN advertised the next version the moment the
version PR merged — before npm and the release assets had it, and permanently when the publish
never ran. The manifest also stamped
publishedAtwith the site build time, so every unrelateddeploy re-anchored the clients' rollout eligibility window.
Publishing is gated on
steps.changesets.outputs.published, and the changesets action onlypublishes when no changeset is pending. When a changeset lands on main while the version PR is
open, merging that PR bumps the version and opens the next version PR instead of publishing —
publishedstays false, all six publish jobs skip, and the run is green. This happened twicein a row (0.10.0, then 0.11.0).
scripts/release/verify-release-consistency.mjsalready comparesthe local version against the npm dist-tag and would have failed loudly, but its job is gated on
the same successful-publish condition, so it was skipped exactly when it mattered.
Client-side effect: the native installer polled GitHub for missing release assets for ~360 s on every
launch, and
/updatereported an install "already in progress" that could never finish.What changed
apps/site/scripts/build-cdn.mjs: resolve the advertised version from the npmdist-tags.latestof
@pythoughts/pythinker-codeinstead of the working tree, and takepublishedAtfrom npm's ownpublish timestamp for that version.
PYTHINKER_CDN_VERSIONpins it for local or offline builds.A registry read failure throws on purpose: a failed image build leaves the previous container
serving the last good manifest, which is the safe direction.
scripts/release/verify-release-consistency.mjs: also fail when the live CDN manifest advertises aversion newer than the npm dist-tag. Being behind npm is only deploy lag, so that logs and
passes.
.github/workflows/release.yml: run the consistency job onci: release packagesmerges too, notonly when a publish succeeded, so a version bump that published nothing turns the run red instead
of skipping the only guard.
Scope note: this makes the update channel truthful and makes the divergence loud. It does not change
the changesets publish path itself — a skipped version number is cosmetic, and surgery on the
irreversible npm publish step does not belong in the same PR.
github.event.head_commit.messageis used inside anif:expression viastartsWith(), evaluated bythe Actions expression engine; it is never interpolated into a
run:block.Verification
No test harness exists for
apps/site/scripts/*.mjsorscripts/release/*.mjs, so both scripts wererun directly:
node apps/site/scripts/build-cdn.mjs --out … --skip-rgnow emits{"version": "0.9.2", "publishedAt": "2026-08-05T23:15:55.254Z", "rollout": []}— the publishedversion, with npm's real publish time.
node scripts/release/verify-release-consistency.mjsfails withconsistency failed: local version 0.11.0 does not match latest 0.9.2, which is the alarm this PRun-gates.
0.10.0vs0.9.10and0.9.10vs0.9.9.pnpm lintpasses; the pre-push gates pass.Checklist
gen-changesetsskill, or this PR needs no changeset.gen-docsskill, or this PR needs no doc update.Summary by CodeRabbit