Skip to content

chore(repo): replace lerna+mklib with release-please + GH Actions#61

Merged
anilanar merged 12 commits into
masterfrom
auto-publish
May 22, 2026
Merged

chore(repo): replace lerna+mklib with release-please + GH Actions#61
anilanar merged 12 commits into
masterfrom
auto-publish

Conversation

@anilanar
Copy link
Copy Markdown
Member

Summary

Modernize the release flow: drop the local-only lerna + @anilanar/mklib pipeline in favor of CI-driven release-please + GitHub Actions + npm stage publish (OIDC trusted publishing). Eliminates the local GH_TOKEN / ~/.npmrc auth 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)

  1. chore(repo): — the main migration (lerna → release-please, workflows, commitlint, lefthook, workspace:* protocol).
  2. chore(deps): — bump webpack/jest/lefthook/commitlint/webpack-cli to latest.
  3. chore(deps): — TypeScript ^5.4.5 → ^5.9.3 (latest 5.x; deferred TS 6 since its stricter defaults may flag existing code).
  4. chore(deps): — ESLint 8 → 10 + @typescript-eslint/* 5 → 8 (both EOL) with full flat-config migration (.eslintrc.jseslint.config.mjs).
  5. 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)

push to master
   └─► release-please-action ── opens/updates Release PR
                                  │   - bumps versions per conventional-commit scope
                                  │   - regenerates CHANGELOGs
                                  ▼
                              maintainer merges Release PR
                                  ▼
                              GitHub Release(s) created
                                  ▼
                          publish.yml (release event)
                                  │   - yarn install + build
                                  │   - yarn pack (workspace:* → concrete version)
                                  │   - npm stage publish (OIDC, no token)
                                  ▼
                          staged on npmjs.com (NOT live)
                                  ▼
                          maintainer approves via npmjs.com (2FA)
                                  ▼
                              package goes live

Conventional-commit hygiene

Enforced via commitlint with scope-enum: [messenger, messenger-internal, repo, ci, deps] and scope-empty: never. Two layers: lefthook commit-msg hook locally (auto-installed via postinstall), and a CI check on PRs.

Off-repo follow-ups (required before merge can publish)

  • Configure Trusted Publishers on npmjs.com for @userlike/messenger and @userlike/messenger-internal, pointing at publish.yml in this repo. Lets the GH Action exchange its OIDC token for a short-lived npm publish token — no NPM_TOKEN secret needed.
  • Confirm 2FA enabled on the npm account that approves staged publishes (this is the human gate).
  • In repo settings, enable "Allow GitHub Actions to create and approve pull requests" (release-please needs this to open the Release PR).

Test plan

  • yarn install works (--immutable will work in CI)
  • yarn build produces ES + CJS + UMD (dist/browser/index.min.js) — same shape as pre-migration
  • yarn lint passes with the new flat config
  • yarn test (no tests in repo today; --passWithNoTests so it exits 0)
  • lefthook install runs in postinstall; the commit-msg hook rejects bad scopes (verified during this PR's commits)
  • After the off-repo setup is done, merge → confirm release-please-action opens a Release PR
  • Merge that Release PR → confirm publish.yml runs and packages appear staged (not live) on npmjs.com
  • Approve staged publishes on npmjs.com (messenger-internal first, then messenger) → confirm npm view shows the new versions

Rollback notes

The half-done 2.0.2 / 3.3.2 release attempt that motivated this work was cleaned up before this PR:

  • GitHub releases for 2.0.2/3.3.2 deleted
  • Tags @userlike/[email protected] and @userlike/[email protected] deleted (remote + local)
  • npm registry was untouched — neither version was ever published

packages/*/package.json versions are reverted to 2.0.1 / 3.3.1 in this PR; CHANGELOG stubs for the never-shipped entries are removed; .release-please-manifest.json is seeded with 2.0.1 / 3.3.1 so release-please picks up from the actually-shipped baseline.

anilanar added 5 commits May 22, 2026 13:17
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.
Copy link
Copy Markdown
Contributor

@buscape buscape left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice lgtm in general, but check comments

Comment thread .github/workflows/publish.yml Outdated
Comment thread commitlint.config.mjs
anilanar added 4 commits May 22, 2026 17:34
`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.
@anilanar anilanar requested a review from buscape May 22, 2026 16:10
@anilanar anilanar assigned buscape and unassigned anilanar May 22, 2026
Copy link
Copy Markdown
Contributor

@buscape buscape left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm, thanks

@buscape buscape assigned anilanar and unassigned buscape May 22, 2026
anilanar added 3 commits May 22, 2026 19:02
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'.
@anilanar anilanar changed the title Replace lerna+mklib with release-please + GH Actions chore(repo): replace lerna+mklib with release-please + GH Actions May 22, 2026
@anilanar anilanar merged commit 41459eb into master May 22, 2026
1 check passed
@anilanar anilanar deleted the auto-publish branch May 22, 2026 17:30
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