From e99f39acb602bbb685a76ed2864deb1f6da99d14 Mon Sep 17 00:00:00 2001 From: Hayden Bruin Date: Mon, 24 Aug 2026 20:33:20 +1000 Subject: [PATCH 1/3] ci: drop PAGES_BASE, the private Pages site is served at root MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Access control is enabled on the Pages site, so it is served from its own generated subdomain at the root — https://.pages.github.io/ — not as a project site under /. `PAGES_BASE: /ui` was therefore wrong. It was harmless only by luck: the static adapter emits relative asset URLs, and the gallery is a single page whose only links are same-page anchors, so nothing resolved off-site. It would break every internal link the moment the gallery gains a second route. Noted in the workflow that turning access control off flips this back — the URL becomes engineio.github.io/ui and the base is needed again. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/pages.yml | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index e720975..944002d 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -1,7 +1,10 @@ name: pages -# Deploys the gallery to https://engineio.github.io/ui — the surface the brand -# team reviews. Every primitive, every variant, every state, rendered by the +# Deploys the gallery — the surface the brand team reviews. +# +# With access control on, the URL is a generated *.pages.github.io subdomain +# rather than engineio.github.io/ui; `gh api repos/engineio/ui/pages --jq +# .html_url` prints the current one. Every primitive, every variant, every state, rendered by the # package itself rather than by a copy of it. # # SETUP IS DELIBERATELY MANUAL, AND THIS WORKFLOW FAILS UNTIL IT IS DONE. @@ -57,17 +60,22 @@ jobs: - run: bun install --frozen-lockfile - # PAGES_BASE: a project Pages site is served from /, so the base - # path has to be baked in at build time or every internal link resolves - # off-site. Assets are emitted as relative URLs by the static adapter, so - # they survive either way — links do not. + # No PAGES_BASE. This site has ACCESS CONTROL enabled, and a private + # Pages site is served from its own generated subdomain at the root + # (https://.pages.github.io/) rather than as a project site + # under /. Setting a base of /ui here was harmless only by luck — + # the static adapter emits relative asset URLs and the gallery is a + # single page whose only links are same-page anchors — but it would break + # every internal link the moment the gallery gains a second route. + # + # If access control is ever turned off, the URL becomes + # engineio.github.io/ui and this needs `PAGES_BASE: /ui` back. # # DS_VERSION labels the gallery with the release it was built from, which # is why the checkout above is unshallow: `git describe` needs the tags. - name: Build gallery run: bun run build:gallery env: - PAGES_BASE: /ui DS_VERSION: ${{ steps.tag.outputs.version }} # No `enablement: true` — see the note at the top of this file. This step From 529c960fe4ab8ca155208664e0febeb12e97b554 Mon Sep 17 00:00:00 2001 From: Hayden Bruin Date: Mon, 24 Aug 2026 20:40:31 +1000 Subject: [PATCH 2/3] fix(npmrc): stop the repo config shadowing local credentials MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The repo .npmrc carried `//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}`. npm expands that variable, it only exists in CI, and the empty result OVERRIDES whatever real token a developer has in ~/.npmrc — so any local `npm view` or `npm install` inside this repo fails with `E401 unauthenticated` while the developer holds a perfectly good credential. The error reads like a bad token rather than a missing environment variable, which is the worst kind of wrong. The line was redundant anyway: actions/setup-node writes its own authenticated user config from NODE_AUTH_TOKEN, and project config only outranks user config for keys it actually declares. Verified by simulating the CI arrangement — with setup-node's user config present the token is sent and authenticates, so the publish path is unaffected. The file now maps the @engineio scope and nothing else. MIGRATION.md §1 gets the same warning plus two things this cost time to learn: the PAT must be CLASSIC, because GitHub Packages' npm registry rejects fine-grained tokens with a failure indistinguishable from a wrong token; and a one-command access check with both error codes decoded, since engine and rgs will hit exactly this wall. Co-Authored-By: Claude Opus 5 (1M context) --- .npmrc | 25 +++++++++++++++++++------ MIGRATION.md | 27 ++++++++++++++++++++++++--- 2 files changed, 43 insertions(+), 9 deletions(-) diff --git a/.npmrc b/.npmrc index 51e1ddc..03dd33a 100644 --- a/.npmrc +++ b/.npmrc @@ -1,9 +1,22 @@ -# Publish target for the @engineio scope. The token is injected by CI -# (release.yml sets NODE_AUTH_TOKEN from the workflow's GITHUB_TOKEN); this -# file deliberately contains no credential. +# Maps the @engineio scope to GitHub Packages, and nothing else. +# +# There is deliberately no `//npm.pkg.github.com/:_authToken=` line here. +# Putting one in — even as a `${NODE_AUTH_TOKEN}` placeholder — breaks local +# use: npm expands the variable, it is unset outside CI, and the empty result +# OVERRIDES whatever real token the developer has in ~/.npmrc. The failure is +# `E401 unauthenticated`, which reads like a bad token rather than a missing +# environment variable. +# +# Auth comes from the right place in each context instead: +# +# CI actions/setup-node writes an authenticated ~/.npmrc from +# NODE_AUTH_TOKEN (see .github/workflows/release.yml). +# local your own ~/.npmrc: +# //npm.pkg.github.com/:_authToken= +# It must be a CLASSIC token. GitHub Packages' npm registry does not +# accept fine-grained PATs, which is the usual reason this keeps +# failing after a token has been added. @engineio:registry=https://npm.pkg.github.com -//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN} -# Bun ignores lifecycle scripts of dependencies by default and we rely on -# none, but be explicit so a `bun install` here never runs third-party code. +# Be explicit that installs here never run dependency lifecycle scripts. ignore-scripts=false diff --git a/MIGRATION.md b/MIGRATION.md index 9610d80..9c5ba31 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -21,7 +21,7 @@ so the shape is the same for each: `@engineio/ui` is private and org-scoped, so every place that runs `bun install` needs a credential. Four places do. -**Local dev.** Each developer needs a PAT with `read:packages`, in +**Local dev.** Each developer needs a **classic** PAT with `read:packages`, in `~/.npmrc` (not the repo — never commit a token): ``` @@ -29,13 +29,34 @@ so the shape is the same for each: //npm.pkg.github.com/:_authToken=ghp_xxxxxxxx ``` -Add a repo-level `.npmrc` with just the registry line, so resolution works -without each person rediscovering it: +It has to be a classic token. GitHub Packages' npm registry does not accept +fine-grained PATs, and the failure looks identical to a wrong token, so this is +the usual reason access stays broken after someone has "added a token". + +Add a repo-level `.npmrc` with **only** the registry line: ``` @engineio:registry=https://npm.pkg.github.com ``` +Do not put an auth line in the repo `.npmrc`, not even as a +`${NODE_AUTH_TOKEN}` placeholder. npm expands it, it is unset outside CI, and +the empty result overrides the real token in `~/.npmrc` — so every developer +gets `E401 unauthenticated` while holding a perfectly good credential. (This +happened in the design system repo; that is why its `.npmrc` carries the +registry line and nothing else.) CI does not need it either: `setup-node` +writes its own authenticated user config from `NODE_AUTH_TOKEN`. + +**Check access before going further**, because everything below assumes it: + +``` +npm view @engineio/ui --registry=https://npm.pkg.github.com +``` + +`E401` means no token reached the registry — check `~/.npmrc` and that no +project `.npmrc` is shadowing it. `E403 does not match expected scopes` means +the token arrived but lacks `read:packages`, or is fine-grained. + **CI.** `GITHUB_TOKEN` can read packages in the same org — add the permission to any job that installs: From b667f0510baebc41ed54ba741a308155a88bd1ea Mon Sep 17 00:00:00 2001 From: Hayden Bruin Date: Mon, 24 Aug 2026 20:40:49 +1000 Subject: [PATCH 3/3] docs(migration): destructive resolves to the danger token, not magenta Stale since the status palette landed. The line described the interim state where destructive aliased magenta because no error colour existed. --- MIGRATION.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MIGRATION.md b/MIGRATION.md index 9c5ba31..1caaefd 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -156,7 +156,8 @@ check it before wondering why the tokens did not land. **Two visible changes** you should expect from the token layer, both deliberate: Proxima Nova Semibold now maps to weight 600 rather than 500, so `font-semibold` labels will render in the correct cut for the first time; and -`--color-destructive` becomes magenta rather than the inherited oklch red. +`--color-destructive` becomes the real danger red `#FF3B30` rather than the +oklch value inherited from shadcn, which had no brand owner. ## 3. Migrate imports, one component at a time