Skip to content

Hotfix PRs to main: enforce changeset presence via targeted CI check #48

Description

@martyy-code

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
          fi

Update 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 main would 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 the hotfix label 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 hotfix label is already in CONTRIBUTING.md as 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.yml exists and runs on PRs to main with label hotfix.
  • The workflow fails when a labelled hotfix PR is opened/reopened against main without a .changeset/*.md in its diff.
  • The workflow does not run (or runs as a no-op) on regular release PRs without the hotfix label.
  • CONTRIBUTING.md documents the hotfix label as a required gate for hotfix PRs, alongside the existing changeset requirement.
  • The hotfix path remains a valid release path: a hotfix PR with hotfix label + changeset merges cleanly through the existing release.yml on main.

Related

  • Release system plan: docs/internal/engineering/plans/release-system.md (Section 1 Rules 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.1 documented in the same plan Appendix A.

Metadata

Metadata

Assignees

No one assigned

    Labels

    documentationImprovements or additions to documentationeffort: xsA few minutesenhancementNew feature or requestp3: lowNice to have

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions