Skip to content

Establish a release process #229

Description

@aaronschweig

Summary

Introduce a clear, self-contained release process for this repository. Today every push to main builds and publishes to npm automatically via the shared openmfp/gha workflows. This change separates validating a version from cutting a release: main builds and tests only, and releases become an explicit, manually triggered action owned entirely by this repo. It also adds a generated changelog, on-demand release branches for backports, and standard Conventional Commits versioning.

The everyday flow stays simple — open a PR, merge it — and cutting a release is a single manual workflow run.

Current state

  • .github/workflows/pipeline.yaml delegates to openmfp/gha (pipeline-node-module.yml, job-node-publish.yml), which builds, versions (via ietf-tools/semver-action), and publishes on every push to main.
  • npm publish auth (OIDC, npm environments) lives inside openmfp/gha, not here.
  • There is no CHANGELOG.md, no manual release path, and no release branches.

Goals

  1. main validates but never publishes.
  2. Releases are cut manually, on demand, from this repo alone.
  3. Versioning follows standard Conventional Commits / SemVer, documented and validated.
  4. A changelog is generated and committed on each release.
  5. Backports are supported via on-demand release branches.
  6. The next version and changelog can be previewed before releasing.

Design overview

Self-contained. All release logic lives in this repository. We do not depend on the shared openmfp/gha reusable workflows; build, test, and publish steps are defined here directly.

main builds and tests only. A push to main produces no publish, no version bump, and no tag — green CI simply confirms main is releasable. There is one version line and no prerelease/dev versions.

Releases are manual. A workflow_dispatch workflow performs the full release: compute version → build/test → generate changelog → publish both packages to npm → commit, tag, and push → publish a GitHub Release. Publishing happens before the git tag/commit is pushed so that a failed publish never leaves a tag pointing at an unreleased version; if the tag/push fails after a successful publish, re-running is safe because npm rejects a duplicate version. The GitHub Release is created against the new tag with the git-cliff-generated notes as its body. The workflow runs against whichever branch it is triggered on (github.ref_name), which is what enables backports.

One version, two packages. The repo publishes exactly two npm packages, in lockstep at the same version taken from the root package.json:

  • @openmfp/webcomponents — built to dist/webcomponents; includes the dashboard as its ./dashboard export.
  • @openmfp/ngx — built to dist/ngx.

The dashboard is not a separate package: scripts/bundle-wc.mjs bundles mfp-wc-dashboard.js into @openmfp/webcomponents and writes the computed version into dist/webcomponents/package.json. @openmfp/ngx needs the equivalent: projects/ngx/package.json carries a stale 0.1.0, so the workflow must write the computed version into dist/ngx/package.json before publishing (the shared workflow did this previously). A single release produces one git tag, one GitHub Release, and one CHANGELOG.md.

npm authentication. Publishing uses npm OIDC Trusted Publishing (no long-lived token). The pipeline already grants id-token: write.

Versioning

Standard Conventional Commits drive the version. Only feat and fix/perf cut a release on their own; breaking changes are signalled by ! or a BREAKING CHANGE: footer.

Commit (PR title) Increment Example (pre-1.0)
feat: … minor 0.18.7 → 0.19.0
fix: … / perf: … patch 0.18.7 → 0.18.8
feat!: … or BREAKING CHANGE: footer breaking (see below) 0.18.7 → 0.19.0
docs: chore: ci: build: refactor: test: style: revert: none

ietf-tools/semver-action remains the single source of truth for the computed version, configured as:

prefix: ""                    # tags are bare (0.19.0), matching existing history
majorList: ""                 # breaking handled via "!" / BREAKING CHANGE footer
minorList: "feat"
patchList: "fix, perf"
noVersionBumpBehavior: silent # housekeeping-only PRs cut no release
noNewCommitBehavior: silent
skipInvalidTags: true

Tags are bare (0.19.0), matching the existing 130 tags and the npm version exactly — one identical string across the git tag, GitHub Release, changelog heading, and package.json. No migration or seed tag is needed.

Pre-1.0 breaking changes. While on 0.y.z the public API is not considered stable (SemVer §4), so breaking changes bump the minor version rather than jumping to 1.0.0. semver-action has no native pre-1.0 handling — it calls semver.inc(version, 'major') unconditionally, which yields 1.0.0 from any 0.x breaking change. A small guard step in the workflow therefore downgrades a major result to the corresponding minor while the current version is 0.x. Promotion to 1.0.0 is a deliberate action via the release-as override, at which point this guard no longer applies.

Manual override. The release workflow accepts a release-as input to force an exact version (e.g. to ship 1.0.0).

Validation. Since the repo squash-merges, the PR title becomes the commit that lands on main and is the only message that drives versioning and the changelog. A required check lints the PR title against Conventional Commits and blocks merge if it does not conform (e.g. amannn/action-semantic-pull-request). Individual in-branch commits are not constrained. The accepted types and the table above are documented in CONTRIBUTING.md.

Changelog

git-cliff (via orhun/git-cliff-action) generates both the committed CHANGELOG.md and the GitHub Release notes in one step, with no npm dependency. It acts purely as a formatter: the workflow always passes the version computed by semver-action (--tag <version>), so git-cliff never decides the version itself.

Because versioning uses standard Conventional Commits, git-cliff's default parser (conventional_commits = true) classifies commits the same way semver-action does, with no separate wordlist to maintain. cliff.toml (scaffolded via git cliff --init github) groups feat → Features, fix → Bug Fixes, perf → Performance, breaking changes → Breaking Changes, and either groups or skips housekeeping types.

The release step passes an explicit commit range and version (git cliff <lastTag>..HEAD --tag <version>) rather than bare --unreleased, so changelogs on backport branches are scoped correctly. CHANGELOG.md is committed so history is browsable in-repo.

Preview

Maintainers can see the computed version and changelog before anything is tagged or published:

  • CI dry-run (primary): a dry-run input runs version computation and changelog generation, writes the result to the GitHub Actions step summary ($GITHUB_STEP_SUMMARY), and skips commit, tag, and publish.
  • Local (documented in RELEASING.md): git cliff --bumped-version prints the next version; git cliff --unreleased --bump prints the pending changelog. Both are read-only.

Backports

A normal release is just a tag on main; long-lived release branches are only created when a backport to an older line is actually needed. This on-demand model suits a library that ships forward from main, and avoids idle per-minor branches.

To backport a fix:

  1. Branch from the relevant tag: git checkout -b release/0.18 0.18.7.
  2. Cherry-pick or merge the fix onto that branch.
  3. Run the release workflow on release/0.18 to cut 0.18.x.

The workflow derives the baseline from the branch's own history (git describe --tags --abbrev=0) so it computes the next 0.18.x patch rather than picking up newer tags from main.

Tasks

  • Replace .github/workflows/pipeline.yaml with a self-contained build + test workflow for PRs and main pushes (no publish, no openmfp/gha dependency).
  • Add .github/workflows/release.yaml: manual workflow_dispatch release covering version → build/test → changelog → npm publish (latest, OIDC) → commit/tag/push → publish a GitHub Release (against the new tag, with the git-cliff notes as its body); publish before tagging so a failed publish leaves no tag; branch-aware via github.ref_name; dry-run and release-as inputs; concurrency group.
  • Publish @openmfp/webcomponents and @openmfp/ngx together at the computed version. Write the computed version into dist/ngx/package.json before publishing (overwriting the stale 0.1.0); fail the release if either publish fails.
  • Configure npm OIDC Trusted Publishing for both packages, reusing the existing npm environments (npmjs:@openmfp/webcomponents, npmjs:@openmfp/ngx) so backport branches publish too.
  • Configure semver-action for Conventional Commits SemVer (prefix: "" to match the existing bare tags) and add the pre-1.0 major→minor guard.
  • Add cliff.toml and bootstrap the initial CHANGELOG.md (most existing history predates Conventional Commits, so seed it manually).
  • Add a required PR-title Conventional Commits check.
  • Allow the release workflow to push to main/release/* if branch protection is enabled (github-actions[bot] bypass or a token).
  • Document commit conventions and the increment table in CONTRIBUTING.md.
  • Add RELEASING.md documenting the process: normal release, dry-run/preview, the backport workflow, and promotion to 1.0.0.

Out of scope

  • Prerelease or dev publishing from main.
  • Modifying the shared openmfp/gha workflows.
  • Pre-creating a release branch for every minor version.
  • Independent per-package versioning.

Implementation notes

  • Tags: bare (0.19.0), matching the existing 130 tags and the npm version — the git tag, GitHub Release, changelog heading, and package.json are all the same string. semver-action runs with prefix: ""; no migration needed.
  • npm OIDC: reuse the existing npm environments (npmjs:@openmfp/webcomponents, npmjs:@openmfp/ngx); the release job declares the same environment: so branch does not matter and backport branches publish too.
  • Publish before tag: publish both packages first, then commit/tag/push, so a failed publish never leaves a dangling tag and a re-run is safe (npm rejects duplicate versions).
  • ngx version: dist/ngx/package.json must be set to the computed version before publish; otherwise the stale 0.1.0 ships.
  • Pre-1.0 guard: semver.inc(major) on a 0.x version yields 1.0.0; downgrade to the corresponding minor until 1.0.0 is intentional.
  • PR-title check is load-bearing: with squash-merge the PR title is the message that reaches main and drives versioning — make the check required.
  • Housekeeping-only periods cut no release: a stretch of docs:/chore: merges intentionally produces no version.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    Status
    Ready

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions