ci: migrate release pipeline to Changesets (+ Linear SDK sync, .claude tooling)#388
Open
chybisov wants to merge 24 commits into
Open
ci: migrate release pipeline to Changesets (+ Linear SDK sync, .claude tooling)#388chybisov wants to merge 24 commits into
chybisov wants to merge 24 commits into
Conversation
- Add @changesets/cli + @svitejs/changesets-changelog-github-compact devDeps
- Add .changeset/{config.json,README.md,pre.json} (pre-mode beta, independent versioning)
- Replace lockstep release:* scripts with changeset:{version,prepublish,publish}
- Fix per-package build:prerelease cpy glob (README.md only, no CHANGELOG clobber)
- Delete lerna.json, standard-version config block, orphaned scripts/prepublishOnly.js
- Freeze root CHANGELOG.md as pre-cutover archive (per-package changelogs going forward)
- Rewrite publish.yaml to the Changesets model: verify -> changesets (version PR) -> release (changeset publish + GitHub Releases, OIDC id-token) -> linear sync. Keep filename for npmjs trusted-publisher OIDC binding. Drop tag trigger and softprops/action-gh-release; push:main + concurrency(cancel-in-progress:false). - Add reusable linear-release.yaml (release_name, version, channel, access_key); single static anchor @lifi/sdk -> 'SDK'; sync/update/complete per channel. - Add changeset-check.yaml: fail-closed PR gate requiring a changeset when a publishable package source changes; docs/chore-only exempt. - tests.yaml: add workflow_call so publish.yaml can reuse it as the verify gate.
- 6 packages (was 5; add tron); document hub-and-spoke workspace:* deps - Per-PR changeset rule (feat/fix/breaking -> minor/patch/major; skip docs/chore; don't author cascade-only dependents) - Pre-mode (beta) blocker: never 'pre exit' until cutting stable 4.0.0 - Pipeline, root scripts, publish transform, Linear anchor SKIP policy - @lifi/types + @lifi/data-types are external pinned deps -> manual bumps
- adopt widget's generic reusable linear-release.yaml (release_name, version, channel, release_tag) - linear-meta now outputs full + stripped version + channel; Linear release is keyed on the marketing X.Y.Z and deep-links the @lifi/sdk@<full> GitHub tag - include CHANGELOG.md in each package's npm tarball (files array)
Add a contributor 'changeset' skill (+ /changeset command) so every PR that touches a publishable package ships with a changeset, and a maintainer 'release' skill documenting the Changesets + Linear pipeline, channels, and dist-tag safety. Read-only command allowlist in settings.json. Uniform structure across all repos; references tailored per repo. Also narrow the .gitignore .claude/ rule to keep settings.local.json + worktrees/ ignored while tracking the shared skills/commands/settings.json.
This PR is infrastructure-only (Changesets adoption, CI rewrite, .claude tooling, package `files`/prerelease tweaks) and intentionally ships no package release, so it carries an empty changeset. This satisfies the new fail-closed changeset-check gate that this same PR introduces. The published-tarball improvements (CHANGELOG.md inclusion, stripped dev fields) take effect on the next real release. Swap this for a patch changeset before merge if the team prefers those tarball changes to ship immediately.
- changeset-check.yaml: use three-dot (`$BASE_SHA...$HEAD_SHA`) diff so an out-of-date PR doesn't pick up unrelated commits from main. Two-dot can mis-attribute mainline changes to the PR diff. - publish.yaml: drop redundant `needs.release.result == 'success' &&` from the linear-meta job's `if`. A skipped/failed release already skips downstream jobs by default; the explicit check was inconsistent with the other three repos.
The release skill duplicated material already in CLAUDE.md ## Release. Under
the Changesets flow the maintainer's job is to merge the always-open
'chore: version packages' PR — there is no recurring maintainer task that
benefits from a skill triggering. The high-stakes operational rules (never
'pre exit' casually, anchor + skip policy, OIDC) already live in CLAUDE.md;
one-time op docs live in docs/release/.
Keeps:
- .claude/skills/changeset/ (contributor skill — actively prevents the
'forgot a changeset' failure mode on every PR)
- .claude/commands/changeset.md
- .claude/settings.json
…github
@svitejs/changesets-changelog-github-compact is marked deprecated
('unmaintained') on npm. The first-party @changesets/changelog-github is
actively maintained, ~10x the usage, and a drop-in replacement (same config
interface). The format is slightly more verbose ('Thanks @user!' credit per
entry) but adds proper contributor recognition in every release note.
No CHANGELOG re-render needed — we haven't shipped any generated entries yet.
Copy the repository-root CHANGELOG.md (the lerna + standard-version era archive) into packages/sdk/CHANGELOG.md as the historical baseline for @lifi/sdk. H1 swapped to the package name; archive note rewritten to explain that pre-Changesets history is repo-wide and references multiple packages. Changesets prepends new release entries between the H1 and the first historical entry, so future releases stack cleanly on top while the historical record is preserved at the bottom of the file (and remains visible on the npm page). The root CHANGELOG.md stays frozen as the in-tree historical archive (unchanged by this commit).
Supply-chain hygiene pass over every external 'uses:' reference in the workflows this PR creates or touches. Every action is now pinned to its commit SHA with a '# vX.Y.Z' comment naming the resolved version. Bumps where a newer release was available. - changesets/action: v1.5.3 → v1.8.0 (commit-SHA-pinned) - linear/linear-release-action: comment fixed from '# v0' to '# v0.14.0' - pnpm-install composite: bump pnpm/action-setup v5.0.0 → v6.0.8 + actions/setup-node v6.3.0 → v6.4.0 (both SHA-pinned) Verified each SHA against the upstream repo's tag → commit mapping (using the commit SHA, not the annotated-tag-object SHA — both forms resolve in Actions, but pinning to the commit is immutable even if a tag is force-moved).
Was ^2.29.7 (caret already resolved to 2.31.0); update the declared range to match the latest release explicitly. No behavior change — lockfile already installed 2.31.0.
Add a `canary` job to publish.yaml: applying the `release-canary` label to a PR publishes a throwaway 0.0.0-canary-<timestamp> build of the changed packages to npm under the `canary` dist-tag, for sharing PR builds with other teams / externally. The label is auto-removed after publish (one-shot; re-add to repeat). This restores the pre-migration ability to publish prereleases from a PR branch (previously `pnpm release:beta` + a pushed `v*-beta.N` tag) and keeps the SAME trust boundary that flow had — a maintainer publishing unreviewed branch code via OIDC — while being strictly safer (0.0.0-canary can never become `latest`). Security controls (the job builds+publishes PR code with OIDC publish rights): * trigger is `pull_request: types:[labeled]` (never pull_request_target) * same-repo branches only — forks/external PRs can't trigger it * fail-closed check that the label-applier has write+ permission * isolated job: only id-token/contents/pull-requests perms; no AWS/CF/Linear secrets The main-release chain is gated to non-PR events so it never runs on label events. sdk: sdk is in pre mode, so the job exits pre mode in the throwaway CI checkout (never committed/pushed) before snapshotting.
Document the release-canary label flow (0.0.0-canary-<ts> to the canary dist-tag), the install command, and the trust-boundary guardrails for maintainers.
The canary trigger is intentionally open to anyone with write access to the repo
— the same trust population that could publish via the old 'v*-beta.N' tag-push
flow. Clarify the in-workflow check and docs accordingly:
- permission gate now reads as admin|write (.permission maps maintain->write,
triage->read), dropping the dead 'maintain' arm
- step/comment/error wording: 'maintainer (write+)' -> 'write access'
- CLAUDE.md canary note reworded to 'someone with write access'
No behavior change (the prior check already allowed admin+write); this is a
correctness-of-intent + messaging cleanup.
Switch the canary authorization check from the legacy .permission base-role field to the granular .role_name (admin|maintain|write). Same endpoint, same fully-automatic in-workflow check (no manual approval) — just the modern role field. Custom write-granting roles would be added to the allowlist if introduced.
Simplify the canary job: remove the gh-api role check and the redundant author_association clause. Applying a label already requires Triage+ on the repo (external people / fork-PR authors cannot label), and the same-repo guard means the published code was pushed by someone with Write access. GitHub has no per-label permission control, so 'collaborators with Triage+' is the trigger population — matching, and only slightly broader than, the old v*-beta.N tag-push flow. Versions remain throwaway 0.0.0-canary. The job stays isolated (id-token/contents/pull-requests only; no deploy/Linear secrets).
The install hint hardcoded the anchor package (npm i @anchor@<ver>), which was wrong whenever a change didn't cascade up to the anchor — e.g. a provider-only sdk PR (providers depend on @lifi/sdk, not vice-versa) or a widget-light-only widget PR (widget-light is standalone). The named package@version was never published in those cases. Now the detect step records every non-private package bumped to a 0.0.0-canary version (the exact publish set) to $RUNNER_TEMP/canary-pkgs.txt, and the comment emits one 'npm i <name>@<version>' per published package via --body-file. The detect + comment steps are now identical across all four repos.
Move the canary snapshot/publish/comment/label-removal steps into a local composite action (.github/actions/canary-publish) for readability. The canary job in publish.yaml is now thin (checkout → install → run the action). A `pre_mode` input handles the ephemeral `changeset pre exit` for the pre-mode repos (widget/sdk), so the action file is byte-identical across all four repos. Composite — NOT a reusable workflow — so its steps run inside the calling job and the npm OIDC trusted-publisher identity stays bound to publish.yaml; no extra trusted-publisher registration needed. The release/version/Linear flow is unchanged and still gated to non-PR events.
Adopt two further workflow hardenings we didn't already have
(the version-PR/publish split was already in place):
- permissions: {} at the workflow top level — deny-by-default; every job
declares only what it needs. This also fixes sdk, which had no top-level
permissions block (it inherited the repo default token scope). Added explicit
contents: read to the verify jobs that relied on the old top-level default
(sdk, bigmi).
- skip the pnpm store cache in privileged jobs (changesets / release / canary,
plus explorer's deploy-prod) via a new cache input on the pnpm-install
composite (default true). Prevents restoring a poisoned dependency cache into
a context that holds publish (id-token) or write permissions. verify keeps the
cache (read-only, runs on every push). Marginal under our write-access trust
model, but it's the defense-in-depth best practice.
cancel-in-progress: false and per-job least-privilege were already in place.
…ect) Two cleanups from a simplification review (no behavior change): - Drop the pre_mode input; detect Changesets pre mode at runtime from .changeset/pre.json instead. Removes a dual source of truth (callers no longer assert pre_mode that could drift from the committed pre.json) and makes the composite self-configuring + every caller identical (no 'with:'). - Replace the per-file 3x-jq detect loop with a single jq pass over packages/*/package.json (verified equivalent output).
Trim verbose prose added during the migration (no logic change): - canary trust-boundary comment block in publish.yaml: ~19 -> ~11 lines, keeping the Triage+/same-repo/isolated/throwaway rationale and the 'NEVER pull_request_target' warning. - canary-publish action description: drop step re-narration + the OIDC line (the OIDC-binding rationale stays at the publish.yaml call site); pre-mode comment 3 -> 2. - pnpm-install cache input description trimmed to 2 lines. - CLAUDE.md canary guardrails: drop the 'mirroring the old tag flow' clause. - sdk linear-meta: trim filler + fix a stale CLAUDE.md cross-ref; explorer deploy-prod gating comment 5 -> 2 lines.
Describe the pipeline on its own terms (Changesets) rather than by reference to another project's adoption of it. No behavior change.
bb4da5a to
b010425
Compare
Replace the bespoke changeset-check.yaml with the canonical changeset-bot GitHub App (comments a changeset reminder on PRs). Hard CI enforcement was never the real gate — nothing publishes until the maintainer-reviewed 'version packages' PR merges. CLAUDE.md + the changeset skill are updated to describe the bot (a reminder, not a block).
🦋 Changeset detectedLatest commit: a5e46b3 The changes in this PR will be included in the next version bump. This PR includes changesets to release 0 packagesWhen changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Migrate the release pipeline from Lerna + standard-version to Changesets, driven by
push: main(per-package npm publishes + GitHub Releases in one run, OIDC provenance). Adds Linear release sync, a label-triggered canary preview flow, changeset reminders viachangeset-bot, and a.claude/changeset helper.What changed
@changesets/cli@^2.31.0+@changesets/changelog-github@^0.7.0;.changeset/config.json(independent versioning across@lifi/sdk+ the 5 providers;updateInternalDependencies: minorso providers cascade automatically when the sdk range moves)..changeset/pre.json(tag: beta) so the 4.x line publishes under@betawhilelateststays on v3 (3.16.3). Neverpre exitexcept to deliberately cut stable 4.0.0.publish.yamlrewritten —verify(reusestests.yaml)→ changesets(the "version packages" PR)→ release(publish + GitHub Releases)→ linear-release. Top-levelpermissions: {}with per-job least privilege; privileged jobs skip the pnpm cache; all third-party actions SHA-pinned.linear-release.yaml; single static anchor on@lifi/sdk→ "SDK". A provider-only cycle (no sdk bump) is intentionally skipped — no fallback anchor.changeset-botapp comments on PRs missing a changeset (a nudge, not a hard CI block); the maintainer-reviewed Version PR is the publish gate.release-canarylabel to a PR to publish0.0.0-canary-<timestamp>of the changed packages to thecanarydist-tag (via thecanary-publishcomposite action); it comments the exact install command and removes the label (one-shot). In pre-mode--snapshotis disallowed, so the job runschangeset pre exitin the throwaway CI checkout only (never committed). Gated to same-repo branches; applying the label requires Triage+.0.0.0-canarycan never becomelatest/beta..claude/—changesetskill +/changesetcommand + read-only allowlist; CLAUDE.md## Releaserewritten.OIDC
Publishing stays in
publish.yaml(jobrelease) via OIDC trusted publishing —mainalready publishes from this file, so no npmjs re-point is needed. Confirm provenance on the first real publish.Before merge
LINEAR_RELEASE_ACCESS_KEYsecret + the "SDK" Linear pipeline exist.On merge
The first
push: mainopens a "version packages" PR that only deletes the empty changeset; merging it runsreleaseas a no-op (nothing to publish). The next feature PR carrying a real changeset produces the first real release.