From 8f0d8f64daeb96e999f7925f2aad169ddd238054 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Thu, 23 Jul 2026 13:03:56 -0700 Subject: [PATCH 1/2] ci(release-go): chain off Release Go FFI instead of polling the mirror release-go and release-go-ffi both fired on the shared moq-ffi-v* tag and raced: release-go polled `git ls-remote` for the moq-go-ffi mirror tag with a ~5 min budget (30x10s), but release-go-ffi builds a 5-target matrix for ~25 min before it publishes that tag, so the wait always timed out and the wrapper never republished against a new ffi. Replace the poll with a workflow_run trigger on "Release Go FFI" completion: the upstream run only concludes success after publish-ffi.sh pushes the mirror tag, so by the time this workflow starts the tag is guaranteed present and there is nothing to wait for. The ffi version comes from the upstream tag ref (workflow_run.head_branch), gated on conclusion == success and a moq-ffi-v* head so PR/failed upstream runs are ignored. The main-push path (wrapper API / VERSION changes) keeps a single existence check instead of a loop: in steady state the current crate's ffi mirror tag already exists; if it doesn't, a release is mid-flight and the job skips cleanly (green no-op) since the workflow_run chain will republish once the ffi lands. Serialize all runs via a constant concurrency group so two runs can't race to compute the same patch tag. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/release-go.yml | 90 ++++++++++++++++++++------------ 1 file changed, 57 insertions(+), 33 deletions(-) diff --git a/.github/workflows/release-go.yml b/.github/workflows/release-go.yml index 04889529d7..63ae9ed91a 100644 --- a/.github/workflows/release-go.yml +++ b/.github/workflows/release-go.yml @@ -8,35 +8,51 @@ name: Release Go # (registry is the gate, like release-plz / the PyPI check in release.sh). # # Triggers: -# * push to main — a wrapper API or VERSION change. -# * moq-ffi-v* tags — auto-bump the moq-go-ffi require to the new ffi and -# re-publish, so `moq-go@latest` always pulls the -# newest native core (Go MVS won't float on its own). +# * push to main — a wrapper API or VERSION change. The ffi require is +# pinned to the current crate version, whose mirror tag +# already exists in steady state. If it's missing (a +# release is mid-flight), the job skips: the +# workflow_run below republishes once the ffi lands. +# * Release Go FFI done — a new ffi just published to the moq-go-ffi mirror. +# Re-cut the wrapper so `moq-go@latest` floats to the +# newest native core (Go MVS won't float on its own). +# Chaining off the upstream run guarantees the mirror +# tag is already present, so there is nothing to poll. # -# No `paths` filter on push: GitHub drops tag pushes when paths is set (a tag on -# an existing commit has no file diff), and we must react to moq-ffi-v* tags. -# Stray main pushes are harmless — publish-wrapper.sh skips when the staged tree +# Stray main pushes are harmless: publish-wrapper.sh skips when the staged tree # matches the mirror, so unchanged content never mints an empty patch release. on: push: branches: - main - tags: - - "moq-ffi-v*" + workflow_run: + workflows: ["Release Go FFI"] + types: + - completed permissions: contents: read +# One at a time: publish-wrapper.sh derives the next patch from the mirror's +# tags, so two concurrent runs could race to the same tag. concurrency: - group: release-go-${{ github.event_name }}-${{ github.ref }} + group: release-go cancel-in-progress: false jobs: build: name: Build wrapper module runs-on: ubuntu-latest - if: ${{ github.repository_owner == 'moq-dev' }} + # Owner gate, plus: only react to a successful Release Go FFI run for a + # moq-ffi-v* tag (ignore its PR/failed runs). Pushes are unconditional. + if: >- + github.repository_owner == 'moq-dev' && + (github.event_name == 'push' || + (github.event.workflow_run.conclusion == 'success' && + startsWith(github.event.workflow_run.head_branch, 'moq-ffi-v'))) + outputs: + skip: ${{ steps.gate.outputs.skip }} steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 @@ -58,38 +74,44 @@ jobs: echo "line=$LINE" >> "$GITHUB_OUTPUT" echo "wrapper line: $LINE" - # The ffi version the wrapper should require: the triggering tag's version, - # or (on main push) the current crate version. + # The ffi version the wrapper should require: the version of the ffi that + # just published (workflow_run), or the current crate version (main push). - name: Resolve ffi version id: ffi + env: + HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }} run: | - if [[ "$GITHUB_REF" == refs/tags/moq-ffi-v* ]]; then - .github/scripts/release.sh parse-version moq-ffi + if [[ "$GITHUB_EVENT_NAME" == "workflow_run" ]]; then + # Upstream tag ref, e.g. moq-ffi-v0.3.1. + VERSION="${HEAD_BRANCH#moq-ffi-v}" + echo "Triggered by Release Go FFI: ffi v$VERSION" else - CARGO_VERSION=$(grep '^version' rs/moq-ffi/Cargo.toml | head -1 | sed 's/.*"\(.*\)".*/\1/') - echo "version=$CARGO_VERSION" >> "$GITHUB_OUTPUT" - echo "Using ffi version from Cargo.toml: $CARGO_VERSION" + VERSION=$(grep '^version' rs/moq-ffi/Cargo.toml | head -1 | sed 's/.*"\(.*\)".*/\1/') + echo "Using ffi version from Cargo.toml: $VERSION" fi - - # On a moq-ffi-v* tag the ffi mirror is published concurrently by - # release-go-ffi.yml; the wrapper can't resolve moq-go-ffi@vX.Y.Z (for - # go mod tidy) until that tag lands. Poll briefly before packaging. - - name: Wait for moq-go-ffi tag + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + # On a main push the wrapper pins the current crate's ffi, whose mirror tag + # already exists in steady state. If it's missing, a release is mid-flight + # (release-plz merged but Release Go FFI hasn't published yet): skip, since + # the workflow_run trigger republishes once the ffi lands. One existence + # check, no polling. workflow_run runs are chained after that publish, so + # the tag is guaranteed present and this gate doesn't run for them. + - name: Gate on published ffi (push only) + id: gate + if: github.event_name == 'push' env: FFI_VERSION: ${{ steps.ffi.outputs.version }} run: | - for i in $(seq 1 30); do - if git ls-remote --tags https://github.com/moq-dev/moq-go-ffi "refs/tags/v${FFI_VERSION}" | grep -q .; then - echo "moq-go-ffi v${FFI_VERSION} is published." - exit 0 - fi - echo "Waiting for moq-go-ffi v${FFI_VERSION} to publish ($i/30)..." - sleep 10 - done - echo "::error::moq-go-ffi v${FFI_VERSION} not published after timeout" - exit 1 + if git ls-remote --tags https://github.com/moq-dev/moq-go-ffi "refs/tags/v${FFI_VERSION}" | grep -q .; then + echo "moq-go-ffi v${FFI_VERSION} is published; proceeding." + else + echo "::warning::moq-go-ffi v${FFI_VERSION} not on the mirror yet; deferring to the Release Go FFI chain." + echo "skip=true" >> "$GITHUB_OUTPUT" + fi - name: Package wrapper + if: steps.gate.outputs.skip != 'true' env: LINE: ${{ steps.line.outputs.line }} FFI_VERSION: ${{ steps.ffi.outputs.version }} @@ -97,6 +119,7 @@ jobs: ./go/scripts/package-wrapper.sh --line "$LINE" --ffi-version "$FFI_VERSION" --output release-out - name: Upload wrapper module + if: steps.gate.outputs.skip != 'true' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: go-wrapper-package @@ -105,6 +128,7 @@ jobs: publish: name: Publish to Go module mirror needs: [build] + if: needs.build.outputs.skip != 'true' runs-on: ubuntu-latest steps: From d776630c7673c49a926298ef5349311fe43a8cfc Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Thu, 23 Jul 2026 14:06:09 -0700 Subject: [PATCH 2/2] ci(release-go): harden workflow_run guard and mirror lookup Address CodeRabbit review on #2463: - Require github.event.workflow_run.event == 'push' before trusting workflow_run.head_branch. Release Go FFI also runs on pull_request, so a fork PR from a branch named `moq-ffi-v*` could otherwise satisfy the head_branch check and reach the publish job's secrets in the base repo's privileged workflow_run context. - Fail loudly when the moq-go-ffi mirror lookup errors instead of treating a failed `git ls-remote` as "tag absent" and silently deferring. Capture the command status explicitly and skip only on a successful empty result; a wrapper-only push has no later trigger to retry, so a silent skip would drop the release. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/release-go.yml | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-go.yml b/.github/workflows/release-go.yml index 63ae9ed91a..e2fedb1be4 100644 --- a/.github/workflows/release-go.yml +++ b/.github/workflows/release-go.yml @@ -45,11 +45,15 @@ jobs: name: Build wrapper module runs-on: ubuntu-latest # Owner gate, plus: only react to a successful Release Go FFI run for a - # moq-ffi-v* tag (ignore its PR/failed runs). Pushes are unconditional. + # moq-ffi-v* tag. Require event == 'push' so a fork PR whose head branch is + # named `moq-ffi-v*` can't satisfy head_branch and reach the publish secrets + # (workflow_run runs in the base repo's privileged context). Pushes here are + # unconditional. if: >- github.repository_owner == 'moq-dev' && (github.event_name == 'push' || (github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.event == 'push' && startsWith(github.event.workflow_run.head_branch, 'moq-ffi-v'))) outputs: skip: ${{ steps.gate.outputs.skip }} @@ -103,7 +107,14 @@ jobs: env: FFI_VERSION: ${{ steps.ffi.outputs.version }} run: | - if git ls-remote --tags https://github.com/moq-dev/moq-go-ffi "refs/tags/v${FFI_VERSION}" | grep -q .; then + # Distinguish "tag absent" from a lookup failure: a network error must + # fail loudly, not masquerade as "defer" (nothing retries a wrapper-only + # push, so a silent skip would drop the release). + if ! TAGS=$(git ls-remote --tags https://github.com/moq-dev/moq-go-ffi "refs/tags/v${FFI_VERSION}"); then + echo "::error::failed to query the moq-go-ffi mirror for tags" + exit 1 + fi + if [[ -n "$TAGS" ]]; then echo "moq-go-ffi v${FFI_VERSION} is published; proceeding." else echo "::warning::moq-go-ffi v${FFI_VERSION} not on the mirror yet; deferring to the Release Go FFI chain."