This is a demonstration repo that proves out:
- a monorepo using pnpm
- a private NPM registry hosted on GitHub
- changesets using @atlassian/changesets
- CI through GitHub actions to tie this all together
There are scripts in the root package.json
as well as the scripts that a package defines in its package.json
.
Many of the scripts in the root package.json
can be applied to only packages which have changed since the most common ancestor with origin/master
(or a git ref supplied by the user).
As a utility, you can retrieve the common ancestor of HEAD
with origin/master
by running:
pnpm --silent echo:ancestor
Or, to find a common ancestor against any other commit, you can supply a git ref or commit id as the environment variable ANCESTOR_REF
:
ANCESTOR_REF=$(git rev-parse HEAD~20) pnpm echo:ancestor
hint: use
--silent
to supress additional NPM logging
ANCESTOR_REF
is also supported by other scripts.
For example, the following script will test only the packages which have changed since the common ancestor of HEAD
and twenty commits before HEAD
(that is, HEAD~20
).
ANCESTOR_REF=$(git rev-parse HEAD~20) pnpm test:ancestor
For example:
pnpm build:since
will build all packages that have changed since the divergence withorigin/master
SINCE=$(git rev-parse HEAD~2) pnpm build:since
will build all packages that have changed in the last two commits.
Create a new package, and version it to "0.0.0" in its package.json
, and apply a change (pnpm change
in the repo root).
Make a change, do not manually update the version, and apply a change (pnpm change
in the repo root).
Then open a PR and merge to master
.
There are two workflows:
This runs when a PR to master
is created.
It runs the following steps (grouped here for clarity):
- Setup
- Checkout the repo
- Setup node
- Cache the pnpm-store
- Downloads the pnpm self-installer
- Installs the workspace dependencies
- Fetches the master branch (so pnpm can build-test-lint only the changed packages)
- Runs build-test-lint
- Check formatting
- Builds all packages
- Tests all packages
- Lints all packages
- Ensures dependencies are correctly
This runs when a push to master
happens (e.g. a PR is merged into master).
It runs the following steps (grouped here for clarity):
- Setup
- Checkout the repo
- Setup node
- Cache the pnpm-store
- Downloads the pnpm self-installer
- Installs the workspace dependencies
- Check formatting (before any mutations in the repo)
- Version
- Updates the git config (adding committer details)
- Get the changeset status (determines if there was a change), and if there are changes:
- Run changeset version (updating changelogs,
package.json
s and auto-committing) - Reflect changeset version (update the
pnpm-lock.yml
, runs prettier against changes, and ammends the former commit)
- Run changeset version (updating changelogs,
- Runs build-test-lint
- Builds all packages
- Tests all packages
- Lints all packages
- Ensures dependencies are correctly
- Push version commit (if a changeset was found)
- Publishes (if a changeset was found)
- Checkout with monodist-worker's personal access token
- Sets the
.npmrc
authToken to publish to the GitHub NPM Registry - Run changeset publish (publishes to the registry, creates git tags)
- Push release git changes (the "version" commit from above) and git tags from the previous step
Because a push
to master
kicks off:
- integration workflow (e.g. build / test / lint)
pnpx changeset version
pnpx changeset publish
git push origin master
git push --follow-tags
And there is currently no support for preventing parallel workflows with GitHub Actions, there is a race condition when merging multiple branches into master
in rapid succession will cause the slower merges to conflict when running git push origin master
and push the wrong tags when running git push --follow-tags
.
This is because pnpx changeset version
and pnpx changeset publish
each produce a commit on top of master
, and won't reflect any intermediate commits. For example:
Consider the situation where 97dd2ae
is the master before a commit.
- merge of
feature/a
produces a newHEAD
formaster
(formerly97dd2ae
) as0657719
- the GitHub Action workflow for
push:master
begins on0657719
- merge of
feature/b
produces a newHEAD
formaster
(formerly97dd2ae
) as08ee9ae
- the GitHub Action workflow for
push:master
begins on0657719
- the commits from
version
andpublish
for0657719
(the resultantHEAD
of mergingfeature/a
) are pushed back tomaster
as0fa08cc
(correctly). - the commits from
version
andpublish
for08ee9ae
(the resultantHEAD
of mergingfeature/b
) are attempted to be pushed back tomaster
as0fa08cc
(incorrectly, causing a conflict and perhaps overwriting tags from0fa08cc
- the versioned and publishedHEAD
fromfeature/a
).
The solutions to this problem are:
- don't use GitHub Actions
- optimistically run
pnpx changeset version
andpnpx changeset publish
first, before any other integration workflow runs - acknowledge the problem, and don't merge to
master
if we're currently running a workflow on it. - use a solution like @softprops/turnstyle as a mutex for actions
The second approach simply narrows the span of time the race condition could present itself, it does not solve the race condition.
References:
- commitlint + git hook
- act upon a release event to distribute a release to (e.g.) a Docker Image, Netlify, Google Firebase, etc.