Repository navigation
Hotfix PRs to main: enforce changeset presence via targeted CI check #48
Description
Context
The release workflow (release.yml) runs on every merge to main with no label gate (we dropped the version bump label in PR #41). It then walks the merge diff for has_changesets — a non-empty changesets diff triggers the version + publish flow, an empty one short-circuits the whole job into a no-op (no publish, no tag, no GitHub Release).
This is correct for normal release PRs (the release engineer cherry-picks a batch that already includes the changesets in the diff), but fragile for hotfix PRs: a hotfix directly to main — typically branched from main, often under time pressure — can be merged without a .changeset/*.md file. The workflow runs, sees no changeset, becomes a silent no-op, and the fix never reaches npm. Downstream consumers discover the missing fix only when their issue is not resolved.
docs/internal/engineering/plans/release-system.md and CONTRIBUTING.md already document hotfix conventions, but neither is enforced. We have observed this failure mode in practice: PR #44 → #47 of the @deessejs/errors release stack hit several related issues, including a tag drift on @deessejs/errors@1.1.1 where the workflow silently shipped without a tag at the version-bump commit. The fixes hardened the release side, but the hotfix side is still a contract by convention only.
Proposed approach
Add a targeted CI check, separate from the existing lint on staging:
# .github/workflows/hotfix-ci.yml (new)
name: Hotfix CI
on:
pull_request:
branches: [main]
types: [opened, synchronize]
jobs:
require-changeset-on-hotfix:
if: contains(github.event.pull_request.labels.*.name, 'hotfix')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Require changeset
run: |
if ! git diff --name-only origin/main...HEAD | grep -q '^\.changeset/.*\.md$'; then
echo "::error::Hotfix PRs require a .changeset/*.md."
exit 1
fiUpdate CONTRIBUTING.md to document the contract: hotfix PRs must carry the hotfix label AND a .changeset/*.md file.
Why this scope
- Targeted, not general. A general lint on every PR to
mainwould block the release engineer cherry-pick release PRs, which often have changesets committed in upstream PRs rather than the cherry-pick diff. Tying the check to thehotfixlabel preserves the label-less release flow while gating the case where fail-fast is most needed. - Tiny footprint. One small workflow file (~25 lines) and one documentation update.
- Label as intent. The
hotfixlabel is already inCONTRIBUTING.mdas the convention for urgent fixes. Anchoring the CI on it makes the contract explicit at the GitHub UI level (a maintainer will see the failing check on the PR).
Acceptance criteria
-
.github/workflows/hotfix-ci.ymlexists and runs on PRs tomainwith labelhotfix. - The workflow fails when a labelled
hotfixPR is opened/reopened againstmainwithout a.changeset/*.mdin its diff. - The workflow does not run (or runs as a no-op) on regular release PRs without the
hotfixlabel. -
CONTRIBUTING.mddocuments thehotfixlabel as a required gate for hotfix PRs, alongside the existing changeset requirement. - The hotfix path remains a valid release path: a hotfix PR with
hotfixlabel + changeset merges cleanly through the existingrelease.ymlonmain.
Related
- Release system plan:
docs/internal/engineering/plans/release-system.md(Section 1Rules of the road, Section 7 post-plan additions). - Release process runbook:
docs/internal/engineering/process/releasing-a-new-version.md(Failure modes and recoveries). - Past incident: tag drift on
@deessejs/errors@1.1.1documented in the same plan Appendix A.
Activity