Release frontend module
release-frontend-module.yml publishes a Yarn Berry frontend module to npm. It publishes a pre-release to the next
dist-tag on every push to the default branch, and cuts versioned releases either from a GitHub Release created by hand
(legacy mode) or entirely from a workflow run (tagged-release mode). Builds and tests for pull requests are done by
Build frontend module.
Usage
on:
push:
branches: [main]
workflow_dispatch:
jobs:
release:
permissions:
contents: write
id-token: write
uses: openmrs/openmrs-contrib-gha-workflows/.github/workflows/release-frontend-module.yml@mainThis publishes with npm trusted publishing, the recommended setup for npmjs: no token to store or
rotate. See Modes for the callers that cut releases, and Permissions for why contents: write is
needed even for pre-releases.
Modes
Which mode runs depends on the triggering event and the release_type / release_version inputs.
| Mode | Runs when | Publishes |
|---|---|---|
| Pre-release | push or workflow_dispatch on the default branch (or patch/* with support_patch_branches), with no release_type / release_version | <next patch>-pre.<run number> to next |
| Legacy release | release event (the caller triggers on release: created) | The Release’s tag version, to latest (or next for a prerelease) |
| Tagged release | release_type or release_version is set | A new version, to latest (or next for a pre-release version), then a GitHub Release |
Pre-release
On each push to the default branch the workflow:
- Reads the current version and computes
<current version with patch + 1>-pre.<run number>, e.g.1.2.4-pre.42. - Sets that version, builds, and commits the change locally (the commit is never pushed).
- Publishes with the pre-release publish command, which targets the
nextdist-tag. - Uploads the build output as
<repository name>-packages. - Triggers the downstream build, if one is configured.
Pre-releases are serialized per branch, so an older run cannot overwrite next after a newer one. Add [skip-pre] to
the head commit message to skip the pre-release for a push.
With support_patch_branches: true, pushes to patch/* branches produce <version>-patch.<run number> and publish
with pre_release_patch_publish_command. Without a patch publish command, nothing is published from those branches.
Without support_patch_branches, pushes to patch/* do not run the workflow’s jobs at all.
The run number restarts when the caller workflow file is renamed, so keep the caller’s file name stable. If versions
such as 1.2.4-pre.1 already exist on npm, no new pre-release is published until the run number passes them. The
inferred publish commands pass --tolerate-republish, so Yarn skips those versions and the job still succeeds: next
does not move, but the downstream build is still triggered. A custom publish command without that flag fails instead.
Legacy release
Kept for callers that still trigger on release: created. When a GitHub Release is created:
- The tag, minus
tag_prefix, must be a semver version, otherwise the job fails. Build metadata is allowed but not published, since npm drops it:v1.2.0+build.5publishes1.2.0. - If the package version does not match the tag, the workflow sets it with the release version command.
- It builds and publishes with the release publish command. A Release marked as a prerelease is published with the
pre-release publish command instead, so it goes to
nextand never becomeslatest.
on:
push:
branches: [main]
release:
types: [created]
jobs:
release:
permissions:
contents: write
id-token: write
uses: openmrs/openmrs-contrib-gha-workflows/.github/workflows/release-frontend-module.yml@mainLegacy mode does not push anything and has no rollback: if the publish fails, fix the problem and re-run the job.
Tagged release
Setting release_type (patch, minor or major) or release_version (an exact version, e.g. 12.4.0) cuts a
release from the default branch:
- Bumps the version. On a major release, internal
peerDependencieswritten as<major>.xare moved to the new major. - Commits
(chore) Release <tag_prefix><version>and tags it<tag_prefix><version>locally. - Builds, runs the verify command on the release tree, and uploads the build output.
- Pushes the commit and tag together (
git push --atomic). - Publishes, retrying up to three times.
- Creates a GitHub Release with notes generated from merged pull requests, grouped by PR title type. Disable with
create_github_release: false.
A release_version with a pre-release suffix, such as 12.4.0-beta.1, is published to next with the pre-release
publish command, and its GitHub Release is marked as a prerelease and not made Latest. A typical caller is a manually dispatched workflow:
on:
workflow_dispatch:
inputs:
release_type:
type: choice
options: [patch, minor, major]
release_version:
description: Exact version, overrides release_type
required: false
publish_only:
description: Finish a release whose tag is already pushed
type: boolean
default: false
jobs:
release:
permissions:
contents: write
id-token: write
uses: openmrs/openmrs-contrib-gha-workflows/.github/workflows/release-frontend-module.yml@main
with:
release_type: ${{ inputs.release_type }}
release_version: ${{ inputs.release_version }}
publish_only: ${{ inputs.publish_only }}
secrets:
APP_ID: ${{ secrets.RELEASE_APP_ID }}
APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}Releases can only be cut from the default branch. The push needs a token that can bypass branch protection; see GitHub App or PAT.
Failed releases
A tagged release pushes before it publishes, because npm versions cannot be replaced. If the run fails after the push, the workflow works out what reached npm and acts on it:
| What reached npm | Result |
|---|---|
| Nothing | The tag is deleted and a revert of the release commit is pushed, in one atomic push. Fix the problem and run the release again. |
| Some or all packages | The tag and commit stay. The job summary lists what is live and what is missing. |
| Unknown (the registry could not be reached) | Nothing is rolled back. |
To decide, it uses two sources:
- Publish log. The inferred publish commands record each package as it is published, so those count as live without asking the registry.
- Registry probes. Every other publishable workspace is checked at its own version on the registry Yarn publishes it to, using the registry’s per-version endpoint rather than a cached package document.
To finish a release whose tag is already pushed but whose packages are partly or not published, run the workflow again
with release_version set to that version and publish_only: true. It checks out the existing tag, builds it and
publishes without bumping, committing or pushing.
Inferred commands
The workflow reads package.json to choose its commands. Any input you pass overrides the inferred value.
| Command | Inferred value |
|---|---|
| Set pre-release version | ci:version script, else yarn workspaces foreach ... version (monorepo) or yarn version |
| Set release version | ci:version script, else yarn workspaces foreach --all --topological version (monorepo) or yarn version |
| Read current version | ci:current-version script, else version in the root package.json |
| Pre-release publish | ci:prepublish or ci:publish-next script, else a recorded yarn npm publish --tag next per non-private package |
| Patch pre-release publish | ci:prepublish-patch script, else none |
| Release publish | ci:publish script, else a recorded yarn npm publish --tolerate-republish per non-private package |
| Build, verify, artifacts, Node.js | As in Build frontend module |
Version scripts
Repositories whose version does not live in the root package.json, for example lerna-managed monorepos, can define
two scripts in the root package.json:
ci:version <version>sets the version. It receives either an exact version or, in tagged-release mode, a strategy (patch,minorormajor), asyarn versiondoes.ci:current-versionprints the current version and nothing else.
Without ci:current-version, the root package.json must have a version; the job fails with a clear message
otherwise.
A repository ci:publish script is used as is. Tagged releases retry a failed publish, so a monorepo script should pass
--tolerate-republish to yarn npm publish, otherwise the retry stops at the first package that already went out.
Credentials
Trusted publishing (recommended)
For packages on npmjs, use npm trusted publishing . The workflow exchanges a short-lived GitHub OIDC token for publish rights, so there is no long-lived npm token to store, leak or rotate. It needs:
id-token: writegranted on the calling job;- Yarn 4.10.3 or later;
- a trusted publisher configured on npmjs for each package.
Whether npm accepts the OIDC token is only known at publish time, so check the trusted-publisher setup:
- The npm package’s trusted publisher must name the caller’s workflow file (e.g.
ci.yml), not this one. - If the trusted publisher names a GitHub environment, set
publish_environmentto the same name. - npm provenance checks
repository.urlinpackage.jsonagainst the repository running the workflow.
Do not also pass NPM_AUTH_TOKEN: when a token is present, Yarn uses it instead of trusted publishing.
A trusted publisher can only be configured for a package that already exists on npmjs, so publish a brand-new package once by hand (or with a token) first.
npm token
Pass an npm token as NPM_AUTH_TOKEN when trusted publishing is not an option: for the first publish of a new
package, or for a registry other than npmjs that does not support it. Prefer a granular token scoped to the packages
being published.
The workflow hands the token to Yarn. A custom publish command that uses the npm CLI instead also receives it as
NODE_AUTH_TOKEN, but must configure npm to use it, e.g. with an .npmrc containing
//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}.
The job fails early when neither a token nor trusted publishing is available, or when trusted publishing is the only option and Yarn is older than 4.10.3.
publish_environment also makes environment secrets available to the publishing jobs. The environment secret must
be named NPM_AUTH_TOKEN, the name this workflow reads, and the caller must still pass it (NPM_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} or secrets: inherit); GitHub then resolves it to the environment’s value. An
environment secret under any other name is not seen, even if the caller maps it to NPM_AUTH_TOKEN.
Registry
Packages publish wherever Yarn’s configuration sends them: publishConfig.registry, then the scope’s
npmPublishRegistry, then the top-level npmPublishRegistry, then npmRegistryServer. npm_registry_url overrides
the top-level npmPublishRegistry. The workflow is tested against npmjs only; please report problems with other
registries.
Publish guard
Outside the openmrs GitHub organization, publish defaults to false, so a fork that inherits a caller workflow
skips pre-releases, and a release run by hand fails. Turn it on from the repository owner, so that
forks of your repository do not publish either:
with:
publish: ${{ github.repository_owner == 'my-org' }}Once it is on, publishing @openmrs/* packages or the openmrs package to the public npm registry is not supported,
and the run fails before anything is built. The same names on any other registry, such as a private feed, are
allowed.
Downstream build
After a pre-release from the default branch, the workflow can dispatch a workflow in another repository, for example
to rebuild a distribution that consumes next:
with:
downstream_repository: my-org/my-distro
downstream_workflow: build-frontend.yml
secrets:
APP_ID: ${{ secrets.DISTRO_APP_ID }}
APP_PRIVATE_KEY: ${{ secrets.DISTRO_APP_PRIVATE_KEY }}The dispatch runs gh workflow run with no inputs, so the target workflow needs a workflow_dispatch trigger. The
token needs actions: write on the downstream repository. Set trigger_downstream_build: false to turn it off;
trigger_refapp_build is a deprecated alias and either set to false disables the dispatch.
GitHub App or PAT
Two operations need more than the job’s github.token:
- Tagged-release push. Pushes the release commit and tag to the default branch. The token must be allowed past branch protection or rulesets.
- Downstream dispatch. Needs
actions: writeon another repository.
Pass APP_ID and APP_PRIVATE_KEY for a GitHub App (preferred), or BOT_PAT. An App does not bypass branch
protection implicitly: add it to the ruleset bypass list. A PAT works only if its owner may bypass the rules. Supply
both App secrets or neither; one without the other fails the run. Without either, the tagged-release push falls back
to github.token, which fails on a protected branch, and the downstream dispatch fails.
OpenMRS defaults
- O3 repositories call the workflow from
ci.ymlon pushes tomain, publish with trusted publishing (noNPM_AUTH_TOKEN), and pass theOMRS_ESM_RELEASEApp credentials. - With no
downstream_repository, repositories in theopenmrsorganization triggerbuild-frontend.ymlinopenmrs/openmrs-distro-referenceapplicationafter each pre-release, which rebuilds the Reference Application frontend image. The App must be installed there withactions: write. - Tags are
v<version>, and release commits are authored byopenmrs-bot/infrastructure@openmrs.org.
Outside OpenMRS
- Repositories must use Yarn Berry, with the project at the repository root.
- Set
publishto turn publishing on; see Publish guard. - Set
git_user_nameandgit_user_emailfor tagged releases; the defaults are OpenMRS’s bot identity. - Publish to npmjs with trusted publishing. If you use a token, map your own secret
name in the caller, e.g.
NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }};secrets: inheritdoes not rename secrets. - If
lateston npm points at a pre-release, it was most likely set by the first publish of the package, which npm always tagslatest. Fix it once withnpm dist-tag add <package>@<version> latest.
Permissions
A called workflow’s jobs cannot have more permissions than the caller grants, and GitHub checks this when the run starts, for every job, including jobs that end up skipped. Grant these on the calling job:
| Permission | Why |
|---|---|
contents: write | The GitHub Release job requests it, so every caller needs it, even for pre-releases only. |
id-token: write | Needed for npm trusted publishing, the recommended setup. Not needed with NPM_AUTH_TOKEN. |
The tagged-release job inherits the caller’s permissions, so it can use id-token: write when granted.
Inputs
| Name | Type | Default | Description |
|---|---|---|---|
node_version | string | Node.js version to use. If omitted, inferred from engines.node in package.json. | |
build_command | string | Command to build the project. If omitted, inferred from package.json. | |
enable_turborepo_cache | string | Deprecated and ignored. Publishing builds always run uncached so that what reaches the registry is built from the release tree. Retained so existing callers that pass it keep validating; remove it from your workflow at your convenience. | |
pre_release_version_command | string | Command to set the pre-release version. Available env vars: PRERELEASE_VERSION (e.g., 1.2.4-pre.42), GITHUB_RUN_NUMBER. If omitted, inferred from package.json. | |
pre_release_publish_command | string | Command to publish a pre-release from the default branch. If omitted, inferred from package.json. | |
pre_release_patch_publish_command | string | Command to publish a pre-release from a patch branch. If empty, patch branch publishing is skipped. | |
release_publish_command | string | Command to publish a release. If omitted, inferred from package.json. In tagged-release mode a failed publish is retried, so a monorepo command should pass --tolerate-republish to yarn npm publish; otherwise a retry stops at the first package that already went out. | |
verify_command | string | Command run on the release tree before the release commit and tag are pushed (tagged-release mode). If omitted, inferred from package.json. | |
support_patch_branches | boolean | false | Whether to support patch/* branches for pre-releases |
publish | boolean | ${{ github.repository_owner == 'openmrs' }} | Whether to publish to npm. Defaults to true in the openmrs org and false elsewhere, so forks that inherit a caller workflow do not publish; outside the openmrs org, set it from an expression such as github.repository_owner == 'my-org'. When false, pre-releases are skipped and releases refuse to run. When true, publishing fails unless NPM_AUTH_TOKEN is set or the caller grants id-token: write for npm trusted publishing. |
npm_registry_url | string | Registry to publish to, overriding npmPublishRegistry in .yarnrc.yml. Scoped registries (npmScopes) and publishConfig.registry still take precedence. If omitted, Yarn's configuration decides. Tested against npmjs only. | |
turbo_concurrency | string | 5 | Turborepo --concurrency for the inferred build and verify commands. Not added where the repo's own script already sets it. Empty for none. |
trigger_downstream_build | boolean | true | Whether to dispatch downstream_workflow in downstream_repository after a pre-release from the default branch. Without downstream_repository, repos in the openmrs org trigger the Reference Application frontend build and other repos trigger nothing. |
trigger_refapp_build | boolean | true | Deprecated alias for trigger_downstream_build. Setting either to false disables the trigger. |
downstream_repository | string | Repository (owner/name) whose workflow is dispatched after a pre-release from the default branch. | |
downstream_workflow | string | Workflow file name or ID to dispatch in downstream_repository. Required when downstream_repository is set. | |
artifact_path | string | Path(s) to build artifacts to upload (glob supported). If omitted, inferred from package.json. | |
release_type | string | Version bump: patch, minor or major, applied to the version in the root package.json. Ignored when release_version is set. Setting either input cuts a tagged release, mirroring release-backend-module.yml: bump versions, commit, tag, push, publish to npm and create a GitHub Release with generated notes. When both are empty the workflow pre-releases on push and, for callers still triggered by release: created, publishes the GitHub Release's tag (legacy mode). | |
release_version | string | Exact version to release (e.g., 12.4.0, or 12.4.0-beta.1 to publish to next). When set, overrides release_type. | |
publish_only | boolean | false | Finish a release whose publish failed after the tag was already pushed and could not be rolled back automatically (e.g. some packages were published). Skips the bump, commit and push entirely: checks out the existing tag for release_version, builds it and publishes to npm. Requires release_version; release_type is ignored. |
release_version_command | string | Command to set the release version. Available env var: RELEASE_VERSION, which holds either a strategy (patch/minor/major) or an exact version; the command must accept both, as yarn version does. If omitted, inferred from package.json. | |
tag_prefix | string | v | Prefix for release tags, stripped to get the version in legacy mode. O3 repos tag as v<version>. |
create_github_release | boolean | true | Whether to create a GitHub Release with release notes after publishing (tagged-release mode) |
git_user_name | string | openmrs-bot | Git committer name for release commits (tagged-release mode) |
git_user_email | string | infrastructure@openmrs.org | Git committer email for release commits (tagged-release mode) |
publish_environment | string | GitHub environment for the jobs that publish to npm. Set this to match the environment configured on the package's npm trusted publisher, if any. |
Secrets
| Name | Description |
|---|---|
NPM_AUTH_TOKEN | NPM authentication token for publishing |
APP_ID | GitHub App ID. Used for triggering the downstream build (needs actions:write on the downstream repository) and, in tagged-release mode, for pushing the release commit and tag (needs contents:write on this repo and a place on the ruleset bypass list). When set with APP_PRIVATE_KEY, a short-lived App token is preferred over BOT_PAT. |
APP_PRIVATE_KEY | Private key for the release bot GitHub App. |
BOT_PAT | Legacy GitHub PAT, fallback when App credentials are not provided. Needs actions:write on the downstream repository for the downstream build and, in tagged-release mode, permission to push past branch protection on this repo. |