workflows
Gives every module a publishing workflow, a changelog check and verify.
pnpm dist workflows [name...] [--force] [--distribution]
Name modules — pnpm dist workflows id members — to give only those theirs,
leaving the rest, and the distribution's own check, alone.
Gives every module a publishing workflow, so each one can release itself, and a changelog check on its pull requests.
Run it in a distribution. Each module is a separate repository, so the files
land in each submodule's working tree and are committed and pushed there —
workflows writes them and stops.
apps/widget .github/actions/publish-prepare/action.yml, .github/workflows/publish.yml, package.json
packages/design has one already
A file a module already has is left alone — it may have been changed on purpose,
and overwriting it would be the kind of help nobody asked for — so a module that
publishes already just gains what it lacks. Pass --force to replace them. A
module without a CHANGELOG.md gets one, opened at the version it carries; one
it has is never written over.
In a distribution, the distribution gets the changelog check too — for pull
requests changing its policy — with its own CHANGELOG.md. That makes nothing
public; --distribution, below, is what publishes it.
The distribution itself
--distribution gives the distribution its own publishing workflow instead, so
others can extend it. Asked for
rather than done with the modules, because it takes private off the
distribution and makes it public on npm.
It is the same shape — a canary on every push to main, a release by hand — with
two differences: the checkout includes the submodules, since what is published
records every module's version, place, repository and commit; and nothing is
built, since what a distribution publishes is its manifest and its policy.
files is set to policies.
What it writes
| File | For |
|---|---|
.github/workflows/publish.yml | the canary and release jobs; the release's notes come from the changelog, and main moves on straight after |
.github/workflows/changelog.yml | the pull request check for a changelog line |
.github/workflows/verify.yml | the pull request check that what fg-dist writes is current — see verify |
CHANGELOG.md | where each pull request's line goes, when the module has none |
.github/actions/publish-prepare/action.yml | install and build, shared by both |
package.json | the release scripts, @fairgarden/distribution, packageManager |
The workflow is two files and no logic of its own. Every step that has to decide
something calls fg-dist, through a named script:
{
"scripts": {
"dist": "fg-dist",
"canary": "fg-dist canary",
"next-version": "fg-dist next-version",
"release:check": "fg-dist release --check",
"changelog": "fg-dist changelog"
}
}
So the workflow is byte-for-byte the same in every module — only the package
name it publishes differs — and pnpm next-version is the answer to "how do I
release this" wherever you happen to be standing. A script the module already
defines is left alone; it may call the tool differently on purpose.
@fairgarden/distribution is added as a devDependency at the version of the CLI
that wrote the files — or moved up to it, when a module has an older one, since
the workflows call commands only this version has and a module's own CI
resolves it from npm — so a module is pinned to the tool that set it up rather
than to whatever was current when the template was last edited.
Run from a checkout of fg-dist rather than an install — a distribution
developing it, as @fairgarden/core does — its version is the one its main is
working towards, which npm does not have. A module is given the newest tagged
release instead, and workflows says so: anything the workflows run that is
newer than that release fails in the modules' CI until it is published. Publish
fg-dist first, then run workflows again to move them up. The one
exception is @fairgarden/distribution itself, which cannot depend on being
published to release itself: its scripts call node dist/cli.js.
The packageManager field is copied from the distribution — it is the pnpm the
module is developed against, whether or not the module said so. pnpm/action-setup
reads it, and will not run without it. A module that pins its own is left as it is.
There is no token
Publishing is npm trusted publishing over OIDC. The workflow asks for
id-token: write and npm checks the claim against the repository and the
workflow file; there is nothing to store, rotate or leak.
It has to be configured once per package, and it cannot be configured for a package that does not exist yet:
npm publishonce from a workstation.- On npmjs.com, add a trusted publisher for the package, pointing at the
repository and
.github/workflows/publish.yml.
After that the workflow publishes on its own. The file path is part of what npm checks, which is why both jobs live in one file rather than two.
The canary job
Runs on every push to main, and nightly. One step — canary
— works out the version and whether there is anything to publish at all, so the
nightly run is a no-op when nothing has landed. Pushes authored by Renovate are
skipped too; a lockfile bump does not warrant a canary.
The release job
Dispatched by hand, with a dist-tag and a dry-run switch. It publishes the
version that is already in package.json — it does not bump anything.
release:check refuses if that version is already on npm, which is the common
mistake: main is meant to carry the next unreleased version, so a release that
finds its own version published means pnpm next-version has
not run yet.
It runs in the npm-publish environment, so a review or a wait can be attached
to it in repository settings without touching this file. On success it tags the
commit and opens a GitHub release with generated notes.
Pinning the actions
actions/checkout is pinned to a commit, because it handles credentials. The
rest are on tags with a note to pin them once Renovate is watching the
repository — a pin nobody updates is worse than a tag.
See Releasing a module for what the versions mean, and
next-version for what happens after a release ships.
Holding pull requests
Once it has published and tagged, the release job holds every open pull request
into the branch it released: changelog hold runs
each one's changelog check again, which finds the version the branch is at
released and refuses it. pnpm next-version opens the pull
request that starts the next version; merging it lets the others through, once
each has caught up with it. The job needs nothing but its own token — actions: write to run the checks again — and never pushes to main.
For the hold to stop anything merging, main's branch rules need:
- Require status checks to pass, with the changelog check among them.
- Require branches to be up to date before merging, or a merge queue, so a pull request runs the check against main's next version before it merges.
A distribution's release job does the same. Its version is not moved for it
either: when a month has turned before the next release, pnpm next-version
dates it — in a pull request, like any other — and the publish check refuses a
release of a month that is over until then.