Skip to content

feat!: rename to @variablelab/theme-toggle 2.0.0 and move CI to withvariablelab - #8

Merged
rqbazan merged 8 commits into
mainfrom
chore/migrate-to-withvariablelab
Oct 13, 2026
Merged

rqbazan merged 8 commits into
mainfrom
chore/migrate-to-withvariablelab

Conversation

@rqbazan

@rqbazan rqbazan commented Oct 12, 2026 •

Copy link
Copy Markdown
Member

@vlandoss/theme-toggle becomes @variablelab/theme-toggle 2.0.0, and CI moves to withvariablelab (npm OIDC, vland-bot via create-github-app-token@v3, two-job previews, zizmor).

Release stays red until preview-comment.yml is on gh-actions main. The workflows reference withvariablelab/gh-actions@main: setup-pnpm, preview-release with mode: publish, and the reusable .github/workflows/preview-comment.yml. The repo now exists, and actions/setup-pnpm is on its main. preview-comment.yml is not there yet: it arrives with withvariablelab/gh-actions#1, then #2 (chore/two-job-previews).

State on e986ef7:

  • CI › test and CI › zizmor: green. That includes pnpm install --frozen-lockfile via setup-pnpm with the new .npmrc.
  • Release: "workflow file issue". GitHub won't start the workflow while the reusable preview-comment.yml@main can't be resolved. That also blocks the push-to-main release job, so merge this only after preview-comment.yml is on gh-actions main.

What to review

1. Key/event rename + automatic migration (src/const.ts, src/migrate.ts, src/initial.ts, src/index.ts)

  • Storage key @variablelab/theme-toggle.colorMode, event @variablelab/theme-toggle.colorModeChange.
  • migrateLegacyColorMode() runs on init in both the inline script and the useColorMode initializer (apps that skip the inline script still migrate). If the new key is absent and @vlandoss/theme-toggle.colorMode exists, the value is copied and the old key removed.
  • When both keys exist, the new key wins and the old key is removed as well (e986ef7), so no stale 1.x entry is left in localStorage.
  • The inline IIFE inlines the migration: dist/initial.js is 756 B (gzip 0.39 kB).
  • Tests: 6 new cases (migration in both entry points, both-keys-present, invalid legacy value) plus a contract test that pins the public key/event strings. 25/25 pass. With the previous both-keys behavior, the two both-keys tests fail.

2. Changeset .changeset/renamed-to-variablelab.md (major): explains the rename, the new key/event, and the automatic migration. README gets the unpkg URL, the table, and an "Upgrading from 1.x" section. The CHANGELOG heading and its old links point to withvariablelab.

3. Release-age cooldown: in .npmrc, not pnpm-workspace.yaml, and without the exclude list

  • .npmrc holds only minimum-release-age=1440. pnpm → 10.30.3 (packageManager + mise.toml). The lockfile is unchanged and --frozen-lockfile passes.
  • Why .npmrc: @rrlab/cli 1.3.0 (via @vlandoss/clibuddy 0.7.1, Pkg.isMonorepo()) treats any pnpm-workspace.yaml as a monorepo. rr tsc then throws Invalid workspace patterns, which breaks rr check and the pre-commit hook. With packages: ["."] it silently skips the typecheck instead. Tracked in clibuddy: treat pnpm-workspace.yaml as a workspace only when it declares packages dx#249.
  • Why there are no excludes (4724d0c): minimum-release-age-exclude[] lines in .npmrc disable the cooldown for every package when pnpm is launched through npm (npx pnpm, npm exec, npm run). npm re-exports the array as one npm_config_minimum_release_age_exclude="@variablelab/*\n\n@rrlab/*" string, and pnpm 10.30.3 then exempts everything, even with a single unrelated @foo/* entry. When pnpm runs directly (I tested mise and a nested pnpm run), the array was parsed and enforced correctly. CI's pnpm/action-setup also runs pnpm directly, and CI installs use --frozen-lockfile, which doesn't re-resolve versions, so the cooldown matters mostly for local pnpm add/update. The setting still must not depend on how pnpm is launched.
    • Cost: @variablelab/* and @rrlab/* wait 24 h too. To adopt a fresh own release right away: pnpm add <pkg> --config.minimum-release-age=0.
  • Verified on pnpm 10.30.3 with the age raised to 2 years (1051200): pnpm add typescript@5.9.2 --lockfile-only fails with ERR_PNPM_NO_MATURE_MATCHING_VERSION both directly and via npx pnpm. @rrlab/ts-config@0.0.2 is now rejected too, and --config.minimum-release-age=0 overrides it. With the previous two exclude lines, the npx pnpm run succeeded (bypass reproduced).
  • Remaining cost of .npmrc: npm warns Unknown project config "minimum-release-age" on every command, and pnpm 11 stops reading it: with minimum-release-age=1051200 in .npmrc, pnpm 11.28.5 accepts typescript@5.9.2. preview-release also treats any project .npmrc as token-only, so publish_mode: oidc-only must stay explicit in release.yml; a comment there now says so (45512fe).
  • Once dx#249 ships in @rrlab/cli, move the setting plus minimumReleaseAgeExclude: ["@variablelab/*", "@rrlab/*"] to pnpm-workspace.yaml (honored there regardless of launcher, verified) and delete .npmrc.

4. release.yml

  • permissions: {} at the top; each job grants only what it needs.
  • release: id-token: write + contents: read. No NPM_TOKEN: changesets/action v1.9.0 detects OIDC and changeset publish uses trusted publishing, the same pattern as withvariablelab/env (its 0.5.0/0.5.1 show trustedPublisher on npm).
    • vland-bot token: create-github-app-token@v3 (SHA-pinned) with client-id: vars.VLAND_BOT_CLIENT_ID, private-key: secrets.VLAND_BOT_PRIVATE_KEY, permission-contents: write, permission-pull-requests: write.
    • The git identity comes from app-slug plus the <slug>[bot] user id, passed through env: (not interpolated into run:). The 282456776 is no longer hardcoded.
  • changesets now gets the app token again (reverts the workaround from fix(ci): use default GITHUB_TOKEN for changesets changelog step #3), so the bot opens the version PR and CI runs on it. fix(ci): use default GITHUB_TOKEN for changesets changelog step #3's GraphQL "Premature close" was the Node 24.17 node-fetch break that fix(ci): pin Node to 22 LTS — 24.17.0 breaks node-fetch #4/fix(ci): pin Node to exact 24.16.0 to dodge node-fetch break #5 fixed with the 24.16.0 pin. env uses the app token with changelog-github and its version PRs are authored by app/vland-bot.
  • The checkout uses persist-credentials: false: changesets/action writes ~/.netrc with its GITHUB_TOKEN (the bot token) and pushes through it. Validated by reading the action's source, not by a CI run.
  • preview (same trigger conditions): id-token: write + contents: read, checkout without persisted credentials, preview-release with mode: publish + publish_mode: oidc-only. It only exposes outputs.packages.
  • preview-comment: needs: preview, if: needs.preview.result == 'success', permissions: {}. It calls the reusable preview-comment.yml@main with action: preview-release, packages and secrets: VLAND_BOT_PRIVATE_KEY. It never checks out or runs PR code.
  • Checked against the workflow_call contract on gh-actions chore/two-job-previews at 8113f3e. That commit was local and not yet pushed when I checked. actionlint ran on a scratch copy with the reusable workflow made local: inputs and secrets match, and a negative control confirmed it rejects a missing action. If that branch changes the interface before merging, this block must follow.

5. ci.yml: contents: read, gh-actions → withvariablelab, SHA pins, plus a new zizmor job (pipx run --spec zizmor==1.30.1, with a read-only GH_TOKEN for the online audits).

zizmor: 0 findings locally (offline, default persona). The 5 @main references to first-party gh-actions are ignored inline (tracked at @main on purpose). gh-actions gets the same result with a .github/zizmor.yml policy (withvariablelab/*: ref-pin) instead; switching to that is easy if you prefer it. The auditor persona also flags secrets-outside-env (the bot key outside a GitHub environment). That is optional hardening: it needs a repo environment, so it is not done here. actionlint is clean too.

Before merging (your side)

  • withvariablelab/gh-actions created and both stacked PRs merged
  • vland-bot public and installed in withvariablelab; org variable VLAND_BOT_CLIENT_ID and secret VLAND_BOT_PRIVATE_KEY available to this repo

After merging, keeping 2.0.0 as the first version

The package version stays 1.0.0 until changesets bumps it, so don't seed-publish from this branch: that would publish the breaking 2.0 code as @variablelab/theme-toggle@1.0.0. Suggested order:

  1. Merge → the bot opens chore: update versions (2.0.0).
  2. From that branch: pnpm rr pack && pnpm publish --access public --otp=<2FA>, then npm trust github @variablelab/theme-toggle --repo withvariablelab/theme-toggle --file release.yml --allow-publish (npm refuses a trust config without a permission flag, and --allow-publish defaults to false).
  3. Merge the version PR. changeset publish skips 2.0.0 because it is already published, so CI won't create the @variablelab/theme-toggle@2.0.0 git tag or the GitHub release. If you want them, run pnpm changeset tag && git push origin --tags from the merged main, then create the release by hand.
  4. Within 48 h, run one OIDC publish from CI to validate the trust config: a PR with the preview label, or a patch changeset.
  5. Delete NPM_TOKEN if nothing else uses it; npm deprecate @vlandoss/theme-toggle "Moved to @variablelab/theme-toggle"; update control/apps/panel.

Verified locally

pnpm install --frozen-lockfile (pnpm 10.30.3), pnpm rr check (biome + oxlint type-check), pnpm test (25/25, Chromium via Playwright), pnpm rr pack, zizmor 1.30.1 (offline), actionlint 1.7.12. All re-run on e986ef7. zizmor: 0 findings (5 ignored, 5 suppressed). dist/initial.js: still 756 B.

Not verified

Anything that only runs in CI: the OIDC publish, minting the app token, pushes through .netrc, the preview → preview-comment handoff, and zizmor's online audits.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MrWogBto4w6eFAaVEkczQf

rqbazan and others added 3 commits October 12, 2026 18:22
…e key

- package name, repository/homepage/bugs URLs and changelog-github repo
  now point at @variablelab/theme-toggle and withvariablelab/theme-toggle
- the localStorage key and the change event move to the
  @variablelab/theme-toggle.* namespace
- on init, the inline script and useColorMode copy a value stored under
  the old @vlandoss/theme-toggle.colorMode key to the new key (only when
  the new key is absent) and remove the old key, so upgrading keeps the
  user's choice
- README (including the unpkg URL and an upgrade section) and CHANGELOG
  heading renamed; major changeset describing the breaking change

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…an 24 h

minimumReleaseAge needs pnpm >= 10.16. The setting lives in .npmrc rather
than pnpm-workspace.yaml because @rrlab/cli (via @vlandoss/clibuddy) treats
any pnpm-workspace.yaml as a monorepo marker, which breaks `rr tsc` in this
single-package repo. Our own scopes (@variablelab/*, @rrlab/*) are exempt.

The lockfile is unchanged: `pnpm install --frozen-lockfile` passes on
10.30.3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…b previews

- every workflow declares its permissions (the org default token is
  read-only); checkouts use persist-credentials: false
- release: drop NPM_TOKEN, publish through npm trusted publishing (OIDC);
  mint the vland-bot token with create-github-app-token v3 (client id,
  scoped to contents and pull-requests write) and pass it to changesets,
  so the bot opens the version PR and CI runs on it; derive the git
  identity from the app slug instead of hardcoding the bot user id
- previews in two jobs: `preview` runs the PR code and publishes via OIDC
  (mode: publish, publish_mode: oidc-only), exposing only the package
  list; `preview-comment` calls the gh-actions reusable workflow, which
  never checks out PR code and is the only place the bot key is used
- actions from withvariablelab/gh-actions (tracked at @main), every other
  action pinned by full SHA
- CI audits the workflows with zizmor 1.30.1

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a9eb3a2

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@variablelab/theme-toggle Major

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

preview-comment.yml (withvariablelab/gh-actions, chore/two-job-previews)
requires an `action` input naming the action that renders the comment;
this repo publishes with preview-release.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rqbazan and others added 3 commits October 12, 2026 19:17
When pnpm is launched through npm (`npx pnpm`, `npm exec`, `npm run`), npm
re-exports `minimum-release-age-exclude[]` as one
`npm_config_minimum_release_age_exclude="@variablelab/*\n\n@rrlab/*"`
string. pnpm 10.30.3 then exempts every package, so the 24 h cooldown has no
effect. A single unrelated entry (`@foo/*`) does the same. When pnpm is run
directly (mise, pnpm/action-setup, nested `pnpm run`), it parses the array
correctly.

Keep only `minimum-release-age=1440`, so own scopes wait 24 h too. Checked on
pnpm 10.30.3 with the age raised to 2 years: `pnpm add typescript@5.9.2
--lockfile-only` fails with ERR_PNPM_NO_MATURE_MATCHING_VERSION both directly
and via `npx pnpm`, and `--config.minimum-release-age=0` overrides it.
`pnpm install --frozen-lockfile` still passes.

The excludes come back once the setting can move to pnpm-workspace.yaml
(withvariablelab/dx#249).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
preview-release core.ts treats any project .npmrc as token-only when
publish_mode is auto, and this repo has no NPM_TOKEN. The explicit
`oidc-only` must stay until .npmrc can be deleted (withvariablelab/dx#249).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
migrateLegacyColorMode() used to return early when both keys existed, so
`@vlandoss/theme-toggle.colorMode` stayed in localStorage forever. D2 says
the old key is copied and then deleted. Now, whenever the legacy key exists,
its value is copied only if the new key is absent (a stored new value still
wins), and the legacy key is then removed either way.

The two "both present" tests now expect the legacy key to be gone. With the
previous implementation both fail, and all 25 pass with this one. The
changeset, README and JSDoc describe the new behavior. dist/initial.js stays
at 756 B.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@rqbazan rqbazan closed this Oct 13, 2026
@rqbazan rqbazan reopened this Oct 13, 2026
Comments and docs now explain each policy on their own; the decision
history lives in git.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrWogBto4w6eFAaVEkczQf
@rqbazan
rqbazan marked this pull request as ready for review October 13, 2026 00:44
@rqbazan
rqbazan merged commit 48299ee into main Oct 13, 2026
5 checks passed
@rqbazan
rqbazan deleted the chore/migrate-to-withvariablelab branch October 13, 2026 00:44
@vland-bot vland-bot Bot mentioned this pull request Oct 13, 2026
@rqbazan rqbazan changed the title feat!: rename to @variablelab/theme-toggle 2.0.0 and move CI to withvariablelab feat!: rename to @variablelab/theme-toggle 2.0.0 and move CI to withvariablelab Oct 13, 2026
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