Skip to main content

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

FileFor
.github/workflows/publish.ymlthe canary and release jobs; the release's notes come from the changelog, and main moves on straight after
.github/workflows/changelog.ymlthe pull request check for a changelog line
.github/workflows/verify.ymlthe pull request check that what fg-dist writes is current — see verify
CHANGELOG.mdwhere each pull request's line goes, when the module has none
.github/actions/publish-prepare/action.ymlinstall and build, shared by both
package.jsonthe 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:

  1. npm publish once from a workstation.
  2. 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.