Skip to content

fix(release): advertise only published versions on the update channel - #37

Merged
elkaix merged 2 commits into
mainfrom
fix/release-cdn-version-truth
Aug 7, 2026
Merged

fix(release): advertise only published versions on the update channel#37
elkaix merged 2 commits into
mainfrom
fix/release-cdn-version-truth

Conversation

@elkaix

@elkaix elkaix commented Aug 7, 2026

Copy link
Copy Markdown
Member

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.json advertised 0.11.0.
  • npm dist-tags.latest was 0.9.2; there is no 0.10.0 and no 0.11.0 on npm.
  • The latest GitHub release was 0.9.2; there are no 0.10.0 or 0.11.0 release assets.

Two independent causes:

  1. apps/site/scripts/build-cdn.mjs derived the advertised version from
    apps/pythinker-code/package.json. That file is bumped by the ci: release packages merge, and
    the 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 publishedAt with the site build time, so every unrelated
    deploy re-anchored the clients' rollout eligibility window.

  2. Publishing is gated on steps.changesets.outputs.published, and the changesets action only
    publishes 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 —
    published stays false, all six publish jobs skip, and the run is green. This happened twice
    in a row (0.10.0, then 0.11.0). scripts/release/verify-release-consistency.mjs already compares
    the 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 /update reported an install "already in progress" that could never finish.

What changed

  • apps/site/scripts/build-cdn.mjs: resolve the advertised version from the npm dist-tags.latest
    of @pythoughts/pythinker-code instead of the working tree, and take publishedAt from npm's own
    publish timestamp for that version. PYTHINKER_CDN_VERSION pins 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 a
    version 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 on ci: release packages merges too, not
    only 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.message is used inside an if: expression via startsWith(), evaluated by
the Actions expression engine; it is never interpolated into a run: block.

Verification

No test harness exists for apps/site/scripts/*.mjs or scripts/release/*.mjs, so both scripts were
run directly:

  • node apps/site/scripts/build-cdn.mjs --out … --skip-rg now emits
    {"version": "0.9.2", "publishedAt": "2026-08-05T23:15:55.254Z", "rollout": []} — the published
    version, with npm's real publish time.
  • node scripts/release/verify-release-consistency.mjs fails with
    consistency failed: local version 0.11.0 does not match latest 0.9.2, which is the alarm this PR
    un-gates.
  • The version comparator was checked against ordering cases including 0.10.0 vs 0.9.10 and
    0.9.10 vs 0.9.9.
  • pnpm lint passes; the pre-push gates pass.

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked a related issue, or explained the problem above.
  • I have added tests that prove my feature works. — no test harness covers these two scripts; see Verification above. The un-gated consistency job now acts as the CI check for the manifest generator.
  • 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
    • Update notifications now advertise only published versions available for download.
    • Release metadata now reflects the actual publication time.
    • Invalid, unavailable, or prematurely advertised releases are rejected.
  • Reliability
    • Improved consistency checks between downloads, CDN data, and the latest published version.
    • Release verification now handles delayed CDN updates safely.
    • Release information remains accurate when package publication and CDN deployment occur at different times.

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.
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e61d9249-2f8f-4ac8-96ae-ad8cedd07281

📥 Commits

Reviewing files that changed from the base of the PR and between d825318 and 3becbb0.

📒 Files selected for processing (1)
  • apps/site/scripts/build-cdn.mjs
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/site/scripts/build-cdn.mjs

📝 Walkthrough

Walkthrough

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

Changes

Published CDN release handling

Layer / File(s) Summary
Resolve published release metadata
.changeset/update-channel-published-versions-only.md, apps/site/scripts/build-cdn.mjs
The CDN build resolves the published npm latest version and timestamp, supports PYTHINKER_CDN_VERSION, validates metadata, and writes the resolved timestamp to latest.json.
Verify CDN release consistency
scripts/release/verify-release-consistency.mjs, .github/workflows/release.yml
Release verification fetches the CDN manifest, compares semver values with npm latest, handles unavailable manifests, and runs for package publications or release-package commits.

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
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title uses the fix prefix, stays within 72 characters, and clearly states the release-channel correction in imperative form.
Description check ✅ Passed The description includes all required sections, explains the problem and changes, documents verification, and addresses the unchecked test item.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.

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

@pkg-pr-new

pkg-pr-new Bot commented Aug 7, 2026

Copy link
Copy Markdown
pnpm dlx https://pkg.pr.new/@pythoughts/pythinker-code@3becbb0
npx https://pkg.pr.new/@pythoughts/pythinker-code@3becbb0

commit: 3becbb0

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between db8ef5a and d825318.

📒 Files selected for processing (4)
  • .changeset/update-channel-published-versions-only.md
  • .github/workflows/release.yml
  • apps/site/scripts/build-cdn.mjs
  • scripts/release/verify-release-consistency.mjs

Comment thread apps/site/scripts/build-cdn.mjs Outdated
Comment thread apps/site/scripts/build-cdn.mjs Outdated
Comment thread scripts/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.
@elkaix
elkaix merged commit 12069a8 into main Aug 7, 2026
12 checks passed
@elkaix
elkaix deleted the fix/release-cdn-version-truth branch August 7, 2026 19:48
@elkaix
elkaix restored the fix/release-cdn-version-truth branch August 7, 2026 19:49
@elkaix
elkaix deleted the fix/release-cdn-version-truth branch August 7, 2026 19:50
elkaix added a commit that referenced this pull request Aug 7, 2026
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.
elkaix added a commit that referenced this pull request Aug 7, 2026
)

## 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 -->
elkaix pushed a commit that referenced this pull request Aug 7, 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
## @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>
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