Skip to Content
WorkflowsRelease frontend module

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

.github/workflows/ci.yml
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@main

This 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.

ModeRuns whenPublishes
Pre-releasepush 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 releaserelease event (the caller triggers on release: created)The Release’s tag version, to latest (or next for a prerelease)
Tagged releaserelease_type or release_version is setA 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:

  1. Reads the current version and computes <current version with patch + 1>-pre.<run number>, e.g. 1.2.4-pre.42.
  2. Sets that version, builds, and commits the change locally (the commit is never pushed).
  3. Publishes with the pre-release publish command, which targets the next dist-tag.
  4. Uploads the build output as <repository name>-packages.
  5. 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:

  1. 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.5 publishes 1.2.0.
  2. If the package version does not match the tag, the workflow sets it with the release version command.
  3. 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 next and never becomes latest.
.github/workflows/ci.yml
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@main

Legacy 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:

  1. Bumps the version. On a major release, internal peerDependencies written as <major>.x are moved to the new major.
  2. Commits (chore) Release <tag_prefix><version> and tags it <tag_prefix><version> locally.
  3. Builds, runs the verify command on the release tree, and uploads the build output.
  4. Pushes the commit and tag together (git push --atomic).
  5. Publishes, retrying up to three times.
  6. 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:

.github/workflows/release.yml
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 npmResult
NothingThe 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 packagesThe 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.

CommandInferred value
Set pre-release versionci:version script, else yarn workspaces foreach ... version (monorepo) or yarn version
Set release versionci:version script, else yarn workspaces foreach --all --topological version (monorepo) or yarn version
Read current versionci:current-version script, else version in the root package.json
Pre-release publishci:prepublish or ci:publish-next script, else a recorded yarn npm publish --tag next per non-private package
Patch pre-release publishci:prepublish-patch script, else none
Release publishci:publish script, else a recorded yarn npm publish --tolerate-republish per non-private package
Build, verify, artifacts, Node.jsAs 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, minor or major), as yarn version does.
  • ci:current-version prints 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

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: write granted 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_environment to the same name.
  • npm provenance checks repository.url in package.json against 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: write on 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.yml on pushes to main, publish with trusted publishing (no NPM_AUTH_TOKEN), and pass the OMRS_ESM_RELEASE App credentials.
  • With no downstream_repository, repositories in the openmrs organization trigger build-frontend.yml in openmrs/openmrs-distro-referenceapplication after each pre-release, which rebuilds the Reference Application frontend image. The App must be installed there with actions: write.
  • Tags are v<version>, and release commits are authored by openmrs-bot / infrastructure@openmrs.org.

Outside OpenMRS

  • Repositories must use Yarn Berry, with the project at the repository root.
  • Set publish to turn publishing on; see Publish guard.
  • Set git_user_name and git_user_email for 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: inherit does not rename secrets.
  • If latest on npm points at a pre-release, it was most likely set by the first publish of the package, which npm always tags latest. Fix it once with npm 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:

PermissionWhy
contents: writeThe GitHub Release job requests it, so every caller needs it, even for pre-releases only.
id-token: writeNeeded 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

NameTypeDefaultDescription
node_versionstringNode.js version to use. If omitted, inferred from engines.node in package.json.
build_commandstringCommand to build the project. If omitted, inferred from package.json.
enable_turborepo_cachestringDeprecated 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_commandstringCommand 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_commandstringCommand to publish a pre-release from the default branch. If omitted, inferred from package.json.
pre_release_patch_publish_commandstringCommand to publish a pre-release from a patch branch. If empty, patch branch publishing is skipped.
release_publish_commandstringCommand 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_commandstringCommand run on the release tree before the release commit and tag are pushed (tagged-release mode). If omitted, inferred from package.json.
support_patch_branchesbooleanfalseWhether to support patch/* branches for pre-releases
publishboolean${{ 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_urlstringRegistry 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_concurrencystring5Turborepo --concurrency for the inferred build and verify commands. Not added where the repo's own script already sets it. Empty for none.
trigger_downstream_buildbooleantrueWhether 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_buildbooleantrueDeprecated alias for trigger_downstream_build. Setting either to false disables the trigger.
downstream_repositorystringRepository (owner/name) whose workflow is dispatched after a pre-release from the default branch.
downstream_workflowstringWorkflow file name or ID to dispatch in downstream_repository. Required when downstream_repository is set.
artifact_pathstringPath(s) to build artifacts to upload (glob supported). If omitted, inferred from package.json.
release_typestringVersion 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_versionstringExact version to release (e.g., 12.4.0, or 12.4.0-beta.1 to publish to next). When set, overrides release_type.
publish_onlybooleanfalseFinish 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_commandstringCommand 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_prefixstringvPrefix for release tags, stripped to get the version in legacy mode. O3 repos tag as v<version>.
create_github_releasebooleantrueWhether to create a GitHub Release with release notes after publishing (tagged-release mode)
git_user_namestringopenmrs-botGit committer name for release commits (tagged-release mode)
git_user_emailstringinfrastructure@openmrs.orgGit committer email for release commits (tagged-release mode)
publish_environmentstringGitHub 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

NameDescription
NPM_AUTH_TOKENNPM authentication token for publishing
APP_IDGitHub 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_KEYPrivate key for the release bot GitHub App.
BOT_PATLegacy 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.
Last updated on