From 72890f8bf8d771fd536cf13fe47302dc79850cdc Mon Sep 17 00:00:00 2001 From: Robert M1 <50460704+githubrobbi@users.noreply.github.com> Date: Tue, 7 Jul 2026 14:16:45 -0700 Subject: [PATCH] ci(release): dispatch crates.io publish from release.yml, not just tag push MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The crates-io-publish OIDC job was gated only on `push: tags: v*`, but the release tag is created by release.yml via softprops/action-gh-release using GITHUB_TOKEN — and GitHub's anti-loop policy means a GITHUB_TOKEN tag does NOT trigger `push: tags`. So the automated release path (release-auto-trigger → release.yml) never fired the crates publish; only a manual `just release-tag` (real credential) would have. Fix by mirroring the submit-winget pattern: * release-plz.yml: add a `publish_tag` workflow_dispatch input; the crates-io-publish job now also runs on workflow_dispatch when publish_tag is set, and checks out that tag to publish the exact released version. release-pr / release jobs stay off for such dispatches (inputs.publish_tag == ''). The OIDC trusted-publisher registration (workflow release-plz.yml + environment crates.io-publish) is unchanged. * release.yml: add a submit-crates job that dispatches release-plz.yml with publish_tag= after the release lands — exactly like submit-winget dispatches winget-publish.yml. Now every release publishes uffs-time/uffs-text via trusted publishing, no long-lived token. actionlint clean. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/release-plz.yml | 34 +++++++++++++++++++---------- .github/workflows/release.yml | 36 +++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 11 deletions(-) diff --git a/.github/workflows/release-plz.yml b/.github/workflows/release-plz.yml index 62e5a993d..9dd0ca36d 100644 --- a/.github/workflows/release-plz.yml +++ b/.github/workflows/release-plz.yml @@ -271,18 +271,26 @@ on: # `just/build.just` `release-pr` / `release-tag`. # # push.branches:[main] stays disabled by design (Path B — manual - # releases; see the trigger-history comment above). Instead a narrow - # tag trigger drives the OIDC `crates-io-publish` job below: it fires - # exactly when a release tag is cut (the same `v*` tags `release.yml` - # builds binaries for), carrying the crates.io publish along with - # every `just ship` release — no long-lived token, no manual step. - # The release-pr / release jobs stay workflow_dispatch-only (they are - # gated to `github.event_name == 'workflow_dispatch'`), so a tag push - # only runs the publish job. + # releases; see the trigger-history comment above). The OIDC + # `crates-io-publish` job below runs in two ways: + # * a real `v*` tag push (e.g. `just release-tag`), and + # * a `workflow_dispatch` with `publish_tag` set — how `release.yml`'s + # `submit-crates` job fires it after a release. The release tag is + # created by GITHUB_TOKEN, which by GitHub anti-loop policy does NOT + # trigger the `push: tags` event, so the automated path must dispatch — + # exactly like `submit-winget` dispatches winget-publish.yml. + # The release-pr / release jobs stay workflow_dispatch-only AND only run + # when `publish_tag` is empty (manual release-pr mode), so a crates-publish + # dispatch runs only the publish job. push: tags: - 'v*' workflow_dispatch: + inputs: + publish_tag: + description: 'Release tag to publish to crates.io (set by release.yml submit-crates; empty = manual release-pr mode).' + required: false + default: '' # Default to ZERO permissions; each job grants only what it needs. # Matches the release-plz repo's own workflow shape. @@ -316,7 +324,7 @@ jobs: # release-plz docs recommend. Restricted to workflow_dispatch: the # `v*` tag trigger is reserved for the crates-io-publish job, so the # release-pr job must not run on a tag push. - if: github.repository_owner == 'skyllc-ai' && github.event_name == 'workflow_dispatch' + if: github.repository_owner == 'skyllc-ai' && github.event_name == 'workflow_dispatch' && inputs.publish_tag == '' # Push branch + open/update PR. Granted at job level so the # release job below can run with a tighter (no pull-requests) @@ -390,7 +398,7 @@ jobs: # workflow_dispatch only — the `v*` tag trigger is reserved for the # crates-io-publish job; this release job must not fire on a tag push # (release.yml already builds binaries from the tag). - if: github.repository_owner == 'skyllc-ai' && github.event_name == 'workflow_dispatch' + if: github.repository_owner == 'skyllc-ai' && github.event_name == 'workflow_dispatch' && inputs.publish_tag == '' # Tag push only — no PR operations from this job. permissions: @@ -497,7 +505,7 @@ jobs: # checkout below resolves to the tag ref, so cargo publishes the # freshly-released version. Re-running against an already-live # version is a harmless no-op-error from crates.io. - if: ${{ vars.ENABLE_CRATES_IO_PUBLISH == 'true' && startsWith(github.ref, 'refs/tags/v') }} + if: ${{ vars.ENABLE_CRATES_IO_PUBLISH == 'true' && (startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch' && inputs.publish_tag != '')) }} environment: crates.io-publish permissions: @@ -509,6 +517,10 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 + # On a submit-crates dispatch, resolve to the release tag so we + # publish exactly that version; on a real `v*` tag push github.ref + # is already the tag. + ref: ${{ inputs.publish_tag || github.ref }} - name: Install Rust (nightly toolchain) uses: dtolnay/rust-toolchain@nightly diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6cb14f9f8..e4d4c6666 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1253,6 +1253,42 @@ jobs: -f "release-tag=${RELEASE_TAG}" echo "✅ winget-publish.yml dispatched for ${RELEASE_TAG} on ${default_branch} (runs independently; check the Actions tab)." + # ═══════════════════════════════════════════════════════════════════════════ + # Publish crates.io — dispatch release-plz.yml's OIDC crates-io-publish job + # ═══════════════════════════════════════════════════════════════════════════ + # + # The release tag is created by GITHUB_TOKEN, which (GitHub anti-loop policy) + # does NOT trigger release-plz.yml's `push: tags: v*` event — so the crates + # publish must be dispatched explicitly, exactly like submit-winget above. + # Dispatching against the default branch keeps the dispatch ref stable; the + # tag is passed as `publish_tag`, and release-plz.yml checks out that tag and + # publishes it via OIDC trusted publishing (registration: workflow + # release-plz.yml + environment crates.io-publish — unchanged by this job). + submit-crates: + name: 📦 Publish crates.io (OIDC) + runs-on: ubuntu-latest + needs: [release-preparation, create-github-release] + if: github.repository_owner == 'skyllc-ai' + continue-on-error: true + permissions: + actions: write # required to dispatch release-plz.yml via the API + timeout-minutes: 5 + steps: + - name: Dispatch release-plz crates-io-publish for the release tag + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + RELEASE_TAG: ${{ needs.release-preparation.outputs.tag }} + shell: bash + run: | + set -euo pipefail + default_branch="$(gh repo view "${GH_REPO}" --json defaultBranchRef -q .defaultBranchRef.name)" + echo "Dispatching release-plz.yml crates-io-publish for ${RELEASE_TAG}" + gh workflow run release-plz.yml \ + --ref "${default_branch}" \ + -f "publish_tag=${RELEASE_TAG}" + echo "✅ release-plz.yml dispatched for ${RELEASE_TAG} (crates publish runs independently; check the Actions tab)." + # ═══════════════════════════════════════════════════════════════════════════ # Failure Notification — opens a GitHub Issue on release failure # ═══════════════════════════════════════════════════════════════════════════