From 5004ccfb6e11e99def927e125a9fe6e6c85584e1 Mon Sep 17 00:00:00 2001 From: martyy-code Date: Mon, 3 Aug 2026 13:41:43 +0200 Subject: [PATCH] ci(publish): skip publish paths when no changeset pending MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .github/workflows/publish.yml | 66 +++++++++++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 477fe238..5f52fb7f 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -35,10 +35,64 @@ concurrency: cancel-in-progress: false jobs: + # Detect whether there is anything to publish. For push events + # to main, this corresponds to a Changesets "Version Packages" + # PR having been merged with pending changesets. For pull + # requests to staging, this corresponds to the PR containing a + # changeset file under .changeset/. Tag pushes are always + # considered publishable (a tag is an explicit signal). + # + # When no changesets are pending, this job exits 0 and the + # downstream jobs are skipped — the workflow run reports green + # with an explicit "nothing to publish" log line. This keeps + # the CI history clean: pushes that should not trigger a + # release do not show up as failures. + check-release: + name: Detect pending changesets + runs-on: ubuntu-latest + outputs: + has_changeset: ${{ steps.detect.outputs.found }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + fetch-depth: 0 + + - name: Detect pending changesets + id: detect + run: | + # Tag pushes are always publishable: a tag is an + # explicit signal that the maintainer wants to + # publish. + if [ "$GITHUB_EVENT_NAME" = "push" ] && \ + case "$GITHUB_REF" in refs/tags/v*) true ;; *) false ;; esac; then + echo "found=true" >> "$GITHUB_OUTPUT" + echo "Tag push detected — proceeding with hotfix publish." + exit 0 + fi + + # Otherwise, count pending changesets under .changeset/. + # README.md and config.json are bookkeeping, not + # actual changesets. + count=$(ls .changeset/*.md 2>/dev/null \ + | grep -v 'README.md$' \ + | grep -v 'config.json$' \ + | wc -l) + if [ "$count" -gt 0 ]; then + echo "found=true" >> "$GITHUB_OUTPUT" + echo "$count changeset(s) detected." + else + echo "found=false" >> "$GITHUB_OUTPUT" + echo "No pending changesets — nothing to publish." + fi + # Stable release: triggered by push to main (the canonical # "Version Packages" PR merge path). release: - if: github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/') + needs: check-release + if: >- + needs.check-release.outputs.has_changeset == 'true' + && github.event_name == 'push' + && !startsWith(github.ref, 'refs/tags/') uses: ./.github/workflows/_publish-release.yml permissions: id-token: write @@ -48,7 +102,10 @@ jobs: # a maintainer as part of the hotfix procedure (see release # pipeline docs). hotfix: - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') + needs: check-release + if: >- + github.event_name == 'push' + && startsWith(github.ref, 'refs/tags/v') uses: ./.github/workflows/_publish-hotfix.yml permissions: id-token: write @@ -57,7 +114,10 @@ jobs: # Canary: triggered by a PR targeting staging. Skips cleanly when # the PR has no changeset. canary: - if: github.event_name == 'pull_request' + needs: check-release + if: >- + needs.check-release.outputs.has_changeset == 'true' + && github.event_name == 'pull_request' uses: ./.github/workflows/_publish-canary.yml permissions: id-token: write