Skip to content

fix(runner): re-pin the authoring app to Handsontable 18.1.0 and derive the picker fallback (DEV-2735) - #298

Merged
demtario merged 1 commit into
masterfrom
fix/DEV-2735-authoring-ht-pins
Sep 2, 2026
Merged

fix(runner): re-pin the authoring app to Handsontable 18.1.0 and derive the picker fallback (DEV-2735)#298
demtario merged 1 commit into
masterfrom
fix/DEV-2735-authoring-ht-pins

Conversation

@demtario

@demtario demtario commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes DEV-2735. Found while doing DEV-2727.

The starter buckets re-pin themselves from npm on a weekly cron; the authoring app's own pins do not, and nothing failed when they drifted. All four sites still said 18.0.0 months after 18.1.0 became dist-tags.latest — while bucket 18, the 18.1 docs bucket and the twelve baked Tier-2 container recipes had already moved.

They did not self-correct because pipeline/theme-presets-version.test.mjs only compared the handsontable pin and BUNDLED_VERSION to each other. The two drift together and stay "consistent" while both go stale, which is exactly what happened: both were set once by feature work (0e46c1572, f35323151) and never bumped since.

What was stale, and what it cost

Site Was Effect while stale
apps/authoring/package.json:18 18.0.0 the Style panel's preset data
src/theme/presets.ts:76 BUNDLED_VERSION 18.0.0 names the version in the panel's fallback note, and gates the presetsFor.ts:110 short-circuit
src/catalog.ts:26 VERSION_OPTIONS ["18.0.0","17.1.0","17.0.1"] the picker's whole list until /api/versions answers
src/catalog.ts:27 DEFAULT_VERSION 18.0.0 what an unparameterised visit starts on

The picker one is worse than "until the API answers": App.tsx's versions-fetch fails open, so a registry hiccup or an API deploy leaves a visitor on that list for the whole session — offered no current release at all.

A fifth site the ticket did not list: App.tsx:621. FullMode seeds DEFAULT_VERSION independently of the main view (App.tsx:1006), so /d/:id had its own copy of the same staleness.

The fix

Bump the pin, BUNDLED_VERSION and the lockfile to 18.1.0 — and stop hand-maintaining the other two.

writeCatalogIndex now lifts each bucket's hotVersion into a bucketVersions map in catalog.json, and catalog.ts derives both constants from it through a new stableBucketVersions helper in the runtime package. The weekly re-pin already regenerates catalog.json (import-starters.yml runs import.mjs --index in its publish job), so the picker's fallback now moves with the buckets, with no workflow change and no human step.

That also makes the fallback strictly better than the list it replaces: every version it offers has a bucket behind it, so switching to one cannot land on a version whose <bucket>/<framework>.json does not exist. Today it derives ["18.1.0", "17.1.0", "16.2.0", "15.3.0"] — one bucket-backed release per major, where the old list held two 17.x and no 18.1.0.

Release buckets only. The map first included next too, on the reasoning that it should describe what is on disk and the consumer could filter. The first CI run disproved that: next is a nightly, so a committed file carrying it goes stale daily, and any open PR touching catalog.json goes red with it — to carry a value the picker filters back out anyway (it surfaces nightlies through its own next control). It is now filtered at generation time, by version shape rather than by the bucket key, so a future prerelease bucket cannot leak in either. stableBucketVersions keeps its own shape filter as belt-and-braces, since it is the last step before a version reaches the picker.

The helper lives in packages/runtime/src/version.ts, not next to its caller, because apps/authoring/src/catalog.ts uses bare attribute-less JSON imports and is Vite-only by design — node cannot load it, so the logic would have been untestable there.

What stops the next drift

The remaining two sites cannot be derived. BUNDLED_VERSION in particular must stay a literal: presetsFor.ts:10-15 records that nothing in theme/ may import outside theme/, because an import the harness cannot resolve turns all 20 cases of theme-wiring.test.mjs and theme-typecheck.test.mjs into skips that read as a green run. Importing package.json there would trade this bug for a worse one.

So they get a third assertion instead: the pin must equal the hotVersion of the bucket matching the pin's own major.

Offline, against committed state, rather than the ticket's suggested live-registry check. No pipeline/*.test.mjs touches the network — eleven of them stub globalThis.fetch specifically to keep it that way — and docs/TESTING.md rules out an env gate to hide a red test behind. The bucket manifests are cron-refreshed truth already in the repo, so the check costs nothing.

Deliberately per-major, not "must equal the newest bucket". A highest-bucket rule would go red the week HT 19 ships and demand the app move major — THEME_API_MIN_MAJOR, preset shape, wrapper peers — which is a different and much riskier job that must not be forced by a red suite. Within its own major it still catches the reported drift. There is a comment saying not to tighten it.

Operational consequence, worth knowing before it happens

From the next Handsontable release onward, the automated chore/starter-example-buckets PR arrives red, because that PR is what carries the new catalog.json. ci.yml runs on pull_request, so its unit job fails until someone edits the pin, BUNDLED_VERSION and the lockfile into it.

That is the intended notification channel — a release forces the app forward — not a surprise. Bumping the pin into that PR is the fix; loosening the test is not. (check-test-presence.mjs is unaffected: generated public/ is excluded as source.)

I considered auto-bumping it inside the publish job instead, and did not: it would need pnpm update running under that job's contents: write + pull-requests: write permissions to make a two-line edit that a human should see.

This already happened once, on this PR. chore(runner): update versioned starter buckets (#297) merged while the PR was open and moved the next nightly, so the merge commit had master's manifests against my catalog.json — and the new consistency assertion caught it rather than letting an inconsistent index merge. Rebased, and the next entry is gone for the reason above, which removes the daily-churn half of that failure mode for good.

Verification

Raw pnpm throughout — rtk's filters have fabricated pass summaries on this repo before.

Check Result
pnpm test 954 tests, 0 fail, 0 skipped (master baseline 949 — the +5 are exactly the tests added)
theme harnesses alone 20/20 pass, 0 skip — no silent-skip regression from touching theme/
pnpm build + pnpm typecheck clean, all 4 projects
app build + check-compiler-chunk pass
pnpm e2e (deterministic) 241 passed / 151 skipped / 0 failed, vs. a clean-env master baseline of 240 / 151 / 0

Both new checks were verified to fail without the fix, not just pass with it:

  • reverting the pin to 18.0.0 gives the handsontable pin (18.0.0) trails bucket 18 (18.1.0) — bump apps/authoring/package.json, BUNDLED_VERSION and the lockfile together
  • setting bucketVersions.18 back to 18.0.0 and rebuilding makes e2e/version-fallback.spec.ts fail on getByText('Handsontable 18.1.0') not found

Two notes on how the numbers were obtained, because both nearly went wrong:

The master e2e baseline needed VITE_DEV_USER=. A local apps/authoring/.env.local short-circuits currentUser(), so 8 anonymous/auth specs fail in that tree — nothing to do with this change. With the override the baseline is a clean 240/151/0.

A preview server from a deleted worktree was squatting port 4173. It had been started with --port 4183 --strictPort, but pnpm ate both flags, so it bound 4173 and reuseExistingServer: !CI would have adopted it — the whole suite silently run against a two-day-old build of another branch. Killed it, and ran with CI=1 so a busy port fails loudly instead.

Incidental

pnpm update handsontable --lockfile-only also dragged in an unrelated @codemirror/commands 6.10.4 → 6.11.0. Reverted and used install --lockfile-only: the changed spec forces re-resolution on its own, so the lock diff is 5 lines, handsontable only.

stableBucketVersions returns [string, ...string[]] rather than string[]. DEFAULT_VERSION = VERSION_OPTIONS[0] was two real TS errors under the app's noUncheckedIndexedAccess; the tuple encodes the throw-on-empty guarantee instead of hiding it behind a !. It throws rather than returning [] on a map with nothing usable in it — an empty dropdown and a version-less first visit is the same silent failure class this PR removes.

Token-count prose refreshed: tokens/main is 285 keys at 18.1.0, up from 279 at 18.0.0. colors/*, density and sizing are byte-identical between the two (checked against jsDelivr), so the existing "stable either way" claim still holds.

Out of scope

runner/e2e/docs-frameworks.spec.ts:18-19 still hardcodes &v=18.0.0 — tracked separately per the ticket.

pipeline/theme-ramp.test.mjs:120 says "38 of the 279 tokens". The 279 is now stale, but it pairs with a DEV-2497 measurement I could not reproduce (18.1.0 has 12 primary-referencing tokens in tokens/main, not 38), and guessing at prose in an unrelated test seemed worse than leaving it.

🤖 Generated with Claude Code


Note

Low Risk
Low risk: dependency and catalog-index changes with strong offline/e2e guards; main operational note is future weekly bucket PRs may fail CI until the authoring pin is bumped within the same major.

Overview
Re-pins the authoring app to Handsontable 18.1.0 (package.json, lockfile, BUNDLED_VERSION) so the Style panel’s bundled theme presets match the current 18.x bucket.

Version picker fallback no longer uses a hand-maintained list that had lagged npm latest. writeCatalogIndex now writes bucketVersions in catalog.json (each release bucket’s manifest hotVersion; next nightly omitted). VERSION_OPTIONS and DEFAULT_VERSION come from new runtime helper stableBucketVersions — stable exact releases only, newest first, every choice backed by a starter bucket — so the weekly bucket re-pin updates the fallback automatically.

Adds pipeline smoke/unit coverage for bucketVersions and stableBucketVersions, a per-major check that the app pin matches catalog.bucketVersions[major], and Playwright coverage that the UI shows the newest bucket version when /api/versions fails.

Reviewed by Cursor Bugbot for commit 21cfec9. Bugbot is set up for automated code reviews on this repo. Configure here.

@demtario demtario self-assigned this Sep 2, 2026
…ve the picker fallback (DEV-2735)

The starter buckets re-pin themselves from npm on a weekly cron; the authoring
app's own pins did not, and nothing failed when they drifted. All four sites
still said 18.0.0 months after 18.1.0 became `dist-tags.latest` — while bucket
18 and the twelve baked Tier-2 container recipes had already moved.

They did not self-correct because `theme-presets-version.test.mjs` only compared
the `handsontable` pin and `BUNDLED_VERSION` to each other, so the two drifted
together and stayed "consistent" while both went stale.

Bump the pin, `BUNDLED_VERSION` and the lockfile to 18.1.0, and stop
hand-maintaining the other two. `writeCatalogIndex` now lifts each release
bucket's `hotVersion` into a `bucketVersions` map in `catalog.json`, and
`catalog.ts` derives `VERSION_OPTIONS` / `DEFAULT_VERSION` from it via a new
`stableBucketVersions` helper — so the weekly re-pin moves the picker's fallback
too, and every version it offers has a bucket behind it. That also fixes the
fifth site the ticket did not list: `FullMode` seeds `DEFAULT_VERSION`
independently of the main view.

Release buckets only. The `next` bucket is a nightly, so putting it in the index
would make a committed file — and every open PR that touches it — go stale
daily, to carry a value the picker filters back out anyway. Filtered by version
shape rather than by the bucket key, so a future prerelease bucket cannot leak
in either.

For the two that must stay literal — `BUNDLED_VERSION` cannot import
`package.json`, because an import reaching outside `theme/` turns both theme
harnesses into silent skips — a third assertion checks the pin against the
bucket for its own major. Offline, against committed state: no pipeline test
touches the registry, and `docs/TESTING.md` rules out an env gate to hide one.
Deliberately per-major rather than "newest bucket", so the week HT 19 ships does
not turn a red suite into a demand for a major bump.

Note the operational consequence: from the next release onward the automated
`chore/starter-example-buckets` PR arrives red, because that PR carries the new
`catalog.json`. Bumping the pin into it is the intended fix, not loosening the
test.

Also refresh the token-count prose — `tokens/main` is 285 keys at 18.1.0, up
from 279 at 18.0.0; `colors/*`, `density` and `sizing` are unchanged between
the two, so the byte-identical claim still holds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@demtario
demtario force-pushed the fix/DEV-2735-authoring-ht-pins branch from 703d2a6 to 21cfec9 Compare September 2, 2026 11:19
@demtario
demtario merged commit 1ab5c24 into master Sep 2, 2026
6 checks passed
@demtario
demtario deleted the fix/DEV-2735-authoring-ht-pins branch September 2, 2026 11:31
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.

2 participants