chore(repo): replace lerna+mklib with release-please + GH Actions#61
Merged
Conversation
Migrate release flow from local-only lerna/mklib to CI-driven release-please + GitHub Actions + `npm stage publish` (OIDC trusted publishing). Drops ~720 transitive deps (lerna, semantic-release, commitizen, hygen, all @babel/*); yarn.lock shrinks from 13k to 3k lines. Changes: - Add release-please config + manifest seeded with the actually-shipped versions (2.0.1 / 3.3.1). The non-shipped 2.0.2/3.3.2 tags and GitHub releases were deleted on remote prior to this commit. - Add three GH Actions workflows: ci.yml (lint+test+build+commitlint on PR), release-please.yml (Release-PR gate on master), publish.yml (release event → OIDC → `npm stage publish` in topological order). - Add commitlint with scope-enum (messenger, messenger-internal, repo, ci, deps) and scope-empty: never. Enforced locally via lefthook (commit-msg hook installed via postinstall) and in CI on PRs. - Switch messenger -> messenger-internal dep to `workspace:*` (exact pinning); yarn pack substitutes the concrete version on publish. - Drop mklib/lerna from devDeps; replace per-package scripts with direct tsc/eslint/jest/webpack calls. yarn workspaces foreach drives the topological build/lint/test from root. - Revert package.json versions to 2.0.1/3.3.1; remove CHANGELOG stubs for the never-shipped 2.0.2/3.3.2 entries. - Gitignore .npmrc (no longer needed in-tree; auth via OIDC in CI). - Delete lerna.json. Off-repo follow-ups (one-time setup not in this PR): - Configure Trusted Publishers on npmjs.com for both packages, pointing at publish.yml in this repo. - Enable 2FA on the npm account that approves staged publishes. - Enable "Allow GitHub Actions to create and approve pull requests" in the repo settings (required for release-please).
- webpack ^5.91.0 -> ^5.107.1 (minor)
- webpack-cli ^5.1.4 -> ^7.0.2 (major)
- jest ^29.7.0 -> ^30.4.2 (major)
- lefthook ^1.7.18 -> ^2.1.8 (major)
- @commitlint/{cli,config-conventional} ^19.5.0 -> ^21.0.1 (2 majors)
ESLint + @typescript-eslint + typescript are bumped in separate commits
because they require config migration.
Latest 5.x. Skipping TS 6 for now - its stricter defaults may flag existing code; that can be a follow-up.
ESLint 8.x and @typescript-eslint 5.x are EOL; this is the meaningful audit win. - eslint ^8.57.0 -> ^10.4.0 - Replace @typescript-eslint/eslint-plugin + @typescript-eslint/parser (both ^5.62.0) with the typescript-eslint meta-package ^8.59.4 (single install, flat-config helpers). - Add @eslint/js ^10.0.1 for flat-config recommended JS rules. - Replace .eslintrc.js (legacy config) with eslint.config.mjs (flat config). - Rename rule @typescript-eslint/no-empty-interface -> no-empty-object-type (the v5 rule was removed in v8; the new rule's allowInterfaces: with-single-extends mirrors the old allowSingleExtends behavior). - Update per-package lint scripts: drop the removed --ext flag, use glob pattern 'src/**/*.ts' instead.
Pinning by commit SHA (not tag) since GH Actions tags are mutable - a supply-chain attacker who compromises an action's repo can move the tag to a malicious commit. SHA pins are immutable. - actions/checkout v4 -> v6.0.2 (de0fac2) - actions/setup-node v4 -> v6.4.0 (48b55a0) - googleapis/release-please-action v4 -> v5.0.0 (45996ed) Inputs verified compatible: setup-node v6 still accepts cache: yarn and registry-url; release-please-action v5 still accepts config-file + manifest-file. Version comment after each SHA so dependabot/renovate (if ever added) and humans can read the intent.
buscape
requested changes
May 22, 2026
Contributor
buscape
left a comment
There was a problem hiding this comment.
nice lgtm in general, but check comments
`yarn pack --out '%s-%v.tgz'` substitutes scoped names by replacing '/' with '-', so the file becomes '@userlike-messenger-2.0.1.tgz' (not 'messenger-2.0.1.tgz' as my glob expected). The previous globs would never match. Drop the --out flag so yarn writes the default 'package.tgz' in each package dir, then reference those explicitly. Each package dir contains exactly one tarball, so no glob ambiguity. Caught by @buscape in PR review.
release-please's default PR title is 'chore: release ${branch}', which
fails our scope-empty: never rule. When CI runs commitlint on the Release
PR, it would block the release flow.
Two changes (per @buscape's review):
- Add 'release' to scope-enum in commitlint.config.mjs.
- Set group-pull-request-title-pattern in release-please-config.json to
'chore(release): release ${branch}', which has a valid scope.
Verified locally: 'chore(release): release master' now passes commitlint.
TS 6 deprecates 'target=ES5' and 'moduleResolution=Node' (= node10);
both will stop functioning in TS 7. Fixing them actually rather than
adding 'ignoreDeprecations':
config/tsconfig.es.json:
- target ES5 -> ES2015 (drops IE11 which has been EOL since 2022; all
evergreen browsers support ES2015 natively)
- lib ['ES5', 'dom', 'ES2015.Promise'] -> ['ES2015', 'DOM']
- module ES2015 -> ES2020 (matches the modern moduleResolution pairing)
- moduleResolution Node -> Bundler (recommended for npm-published
libraries; TS 6 allows pairing it with module: CommonJS too, which
is the surprise that made this fully fixable)
config/tsconfig.cjs.json:
- Inherits the new base. Override module: CommonJS, moduleResolution:
Bundler. Verified CJS output is real CommonJS ('use strict' +
__createBinding shim, no ESM imports).
Side effect: bundle dropped from 8.71 KiB to 4.62 KiB because TS no
longer polyfills classes/arrows/let-const for ES5. async/await still
shimmed because target stays at ES2015 (would need ES2017+ to natively
support). Open to bumping target higher later.
Drops the __awaiter polyfill since async/await is native in ES2017. Bundle: 4.62 KiB -> 3.63 KiB (was 8.71 KiB at original ES5 -> total ~58% cut from migration baseline). Browser support cut from ES2015: drops Safari <11 and pre-Chromium Android stock browser. caniuse shows async/await at ~98% global support; in 2026 this is essentially 'everyone except ~10-year-old devices'. IE was already dropped at the ES2015 bump.
The OIDC token GitHub issues to this workflow now includes the environment name in the 'sub' claim. npm's Trusted Publishers policy requires that match - so any workflow that doesn't declare 'environment: npm-publish' can't get a token usable for publishing, even if it sits in the same repo with id-token: write. Pairs with the npmjs.com Trusted Publisher config (which lists environment 'npm-publish') and the GitHub Environment of the same name.
release-please creates one GitHub release per package, so for any multi-package release publish.yml was firing twice in parallel. Both runs would build + pack everything, then both would try to stage both packages - the second run would fail with 'already staged' on whichever won the race. Now each run derives the target tarball from \$github.event.release.tag_name and stages only that one. Two parallel runs no longer collide; each stages its own package independently. Build + pack still run for all workspaces (topological) because yarn pack on messenger reads the workspace metadata for messenger-internal to substitute workspace:* into a concrete version. Building both is cheap and keeps the workflow simple. Promotion ordering on npmjs.com is unchanged: human approves messenger-internal first, then messenger. The new Summary step reminds of this.
When squash-merging, GitHub uses the PR title as the squash commit subject. Without this check, a PR with a non-conventional title would produce a master commit that doesn't follow our commit convention - and the commit-msg hook can't catch it because squash commits happen on GitHub, not locally. Lint the PR title using the same commitlint.config.mjs that gates local commits. Pass the title via env var to avoid shell-injection from arbitrary user input in the PR title. Pair this with repo setting: Settings -> General -> 'Default commit message for squash merging' -> 'Pull request title'.
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
Modernize the release flow: drop the local-only
lerna+@anilanar/mklibpipeline in favor of CI-driven release-please + GitHub Actions +npm stage publish(OIDC trusted publishing). Eliminates the localGH_TOKEN/~/.npmrcauth friction and improves audit/supply-chain posture.Dependency footprint: removes ~720 transitive deps (lerna, semantic-release, commitizen, hygen, all of
@babel/*); yarn.lock shrinks from 13k to ~3k lines.Commits (read in order — each is self-contained, lint+build+test green at every step)
chore(repo):— the main migration (lerna → release-please, workflows, commitlint, lefthook,workspace:*protocol).chore(deps):— bump webpack/jest/lefthook/commitlint/webpack-cli to latest.chore(deps):— TypeScript ^5.4.5 → ^5.9.3 (latest 5.x; deferred TS 6 since its stricter defaults may flag existing code).chore(deps):— ESLint 8 → 10 +@typescript-eslint/*5 → 8 (both EOL) with full flat-config migration (.eslintrc.js→eslint.config.mjs).ci(deps):— actions/checkout v4 → v6.2, actions/setup-node v4 → v6.4, release-please-action v4 → v5, all SHA-pinned (mutable tags are a supply-chain risk).Architecture (target)
Conventional-commit hygiene
Enforced via commitlint with
scope-enum: [messenger, messenger-internal, repo, ci, deps]andscope-empty: never. Two layers: lefthook commit-msg hook locally (auto-installed viapostinstall), and a CI check on PRs.Off-repo follow-ups (required before merge can publish)
@userlike/messengerand@userlike/messenger-internal, pointing atpublish.ymlin this repo. Lets the GH Action exchange its OIDC token for a short-lived npm publish token — noNPM_TOKENsecret needed.Test plan
yarn installworks (--immutablewill work in CI)yarn buildproduces ES + CJS + UMD (dist/browser/index.min.js) — same shape as pre-migrationyarn lintpasses with the new flat configyarn test(no tests in repo today;--passWithNoTestsso it exits 0)lefthook installruns inpostinstall; the commit-msg hook rejects bad scopes (verified during this PR's commits)release-please-actionopens a Release PRpublish.ymlruns and packages appear staged (not live) on npmjs.comnpm viewshows the new versionsRollback notes
The half-done 2.0.2 / 3.3.2 release attempt that motivated this work was cleaned up before this PR:
@userlike/[email protected]and@userlike/[email protected]deleted (remote + local)packages/*/package.jsonversions are reverted to 2.0.1 / 3.3.1 in this PR; CHANGELOG stubs for the never-shipped entries are removed;.release-please-manifest.jsonis seeded with 2.0.1 / 3.3.1 so release-please picks up from the actually-shipped baseline.