Skip to content

ci(publish): skip publish paths when no changeset pending - #369

Merged
codewizdave merged 1 commit into
mainfrom
fix/skip-publish-without-changesets
Aug 3, 2026
Merged

ci(publish): skip publish paths when no changeset pending#369
codewizdave merged 1 commit into
mainfrom
fix/skip-publish-without-changesets

Conversation

@martyy-code

Copy link
Copy Markdown
Contributor

Summary

When publish.yml is triggered by a push to main that does not contain a pending changeset (infra-only changes, doc edits, back-merges), the workflow currently proceeds to the _publish-release reusable workflow, which then fails the anti-republish guard when the local version is already on npm. This produces a red CI run for what is essentially a no-op.

This PR adds a check-release job at the top of publish.yml that detects pending changesets and exposes a has_changeset output. The three downstream jobs (release, hotfix, canary) are gated on this output. When the workflow is triggered with no pending changesets, only the check-release job runs and exits 0 with a "nothing to publish" log line — the overall workflow run reports green.

Why

Observed after #368 merged: the merge of stagingmain (which contained no changeset files) triggered publish.yml, which routed to _publish-release, which failed the anti-republish guard because @deessejs/fp@1.0.1 was already on npm. The CI history showed a red run on the activation merge commit, which is misleading.

The senior fix is to make the publish entrypoint decide first whether there is anything to publish, and only then dispatch. This mirrors the pattern already in place in _publish-canary.yml (the "Detect pending changesets" step), but lifted to the entrypoint level so all three publish paths share the same decision.

Changes

.github/workflows/publish.yml:

  • New job check-release runs first:
    • For tag pushes (e.g. refs/tags/v1.2.3): always emits has_changeset=true. A tag is an explicit publish signal, not derived from changesets.
    • Otherwise: counts .changeset/*.md files (excluding README.md and config.json). Emits has_changeset=true if any exist, false otherwise.
  • release job: gated on needs.check-release.outputs.has_changeset == 'true' && push to main && not a tag.
  • hotfix job: gated on push tag v* (independent of has_changeset, since tag push is the trigger).
  • canary job: gated on needs.check-release.outputs.has_changeset == 'true' && pull_request.

Behavior matrix

Trigger has_changeset Result
push to main with changesets true release job runs, publishes via Trusted Publishing
push to main without changesets false Only check-release runs, exits 0, run is green
push tag vX.Y.Z true (forced) hotfix job runs, publishes via Trusted Publishing
pull_request to staging with changeset true canary job runs, publishes snapshot to canary dist-tag
pull_request to staging without changeset false Only check-release runs, exits 0, run is green

Defense in depth preserved

The anti-republish guard in _publish-release.yml is kept. It should never fire under normal operation (the changeset detection is what gates the dispatch), but it catches any edge case where:

  • A changeset is malformed but still consumed by changeset version.
  • The check-release job's count is off-by-one due to a globbing quirk.
  • A future code change accidentally bypasses the gate.

Removing the guard would lose the last line of defense. Keeping it costs nothing.

Test plan

  • pnpm turbo type-check passes.
  • pnpm turbo lint passes.
  • Post-merge: a push to main with no changeset files results in a green run with "nothing to publish" log.
  • Post-merge: a push to main with changeset files routes to _publish-release and publishes.
  • Post-merge: a tag push to main (vX.Y.Z) routes to _publish-hotfix and publishes.
  • Post-merge: a PR to staging with a changeset routes to _publish-canary and publishes the snapshot.

Risk

Very low. No published version changes. The fix changes only the dispatch logic; the reusable workflows are unchanged. A push to main that previously failed the anti-republish guard now produces a green run instead — strictly better.

Rollback

Revert the merge commit. The previous behavior (red run on no-changeset pushes) returns.

🤖 Generated with Claude Code

When the publish.yml entrypoint is triggered by a push to main
that does not contain a pending changeset (e.g. infra-only
changes, doc edits, or back-merges), the workflow currently
proceeds to the reusable workflow, which then fails the
anti-republish guard when the local version is already on npm.
This produces a red CI run for what is essentially a no-op.

Add a check-release job at the top of publish.yml that detects
pending changesets and exposes a 'has_changeset' output. The
three downstream jobs (release, hotfix, canary) are gated on
this output:

- release:  runs only when has_changeset=true AND push to main
  (not a tag).
- hotfix:   runs only when push tag v*. Always publishes
  regardless of has_changeset (a tag is an explicit signal).
- canary:   runs only when has_changeset=true AND pull_request.

When the workflow is triggered with no pending changesets, only
the check-release job runs and exits 0 with a 'nothing to
publish' log. The downstream jobs are skipped and the overall
workflow run reports green. This keeps the CI history clean
without losing the audit trail.

The anti-republish guard in _publish-release.yml is preserved
as defense in depth — it should never fire under normal
operation, but it catches any edge case where the changeset
detection lies (e.g. a malformed changeset that is still
consumed by the snapshot).

Tag push handling:
- check-release always emits has_changeset=true for tag pushes
  (tags are explicit publish signals, not derived from
  changesets).
- hotfix job's 'if' uses ref matching, not the has_changeset
  output, since tag push semantics are independent.

Concurrency:
- The check-release job runs first; release/hotfix/canary run
  after it (needs: check-release). Sequential, but the cost is
  a few seconds — negligible compared to npm publish latency.

All third-party actions remain SHA-pinned.
@codewizdave
codewizdave merged commit 7b004b7 into main Aug 3, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants