From ff3b69a4317c12301fc8ed8e8024fbe333aa7e4e Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 2 Oct 2026 11:16:38 -0700 Subject: [PATCH 1/6] ci: publish only from release Moves every branch-triggered publish (release-plz, JS, Py, Go, Kotlin and Swift libs) from main (and dev) to release, ahead of renaming main to release. The trigger lines match the trunk PR's, so the first cut merges them cleanly. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/release-go.yml | 2 +- .github/workflows/release-js.yml | 2 +- .github/workflows/release-kt-lib.yml | 3 +-- .github/workflows/release-py.yml | 2 +- .github/workflows/release-rs.yml | 2 +- .github/workflows/release-swift-lib.yml | 3 +-- 6 files changed, 6 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release-go.yml b/.github/workflows/release-go.yml index bb817841ac..303811fa01 100644 --- a/.github/workflows/release-go.yml +++ b/.github/workflows/release-go.yml @@ -26,7 +26,7 @@ name: Release Go on: push: branches: - - main + - release workflow_run: workflows: ["Release Go FFI"] types: diff --git a/.github/workflows/release-js.yml b/.github/workflows/release-js.yml index 422a685f12..56c6346006 100644 --- a/.github/workflows/release-js.yml +++ b/.github/workflows/release-js.yml @@ -6,7 +6,7 @@ permissions: on: push: - branches: [main] + branches: [release] paths: - 'js/**' - '.github/workflows/release-js.yml' diff --git a/.github/workflows/release-kt-lib.yml b/.github/workflows/release-kt-lib.yml index f3947ef2ac..e17ccd274c 100644 --- a/.github/workflows/release-kt-lib.yml +++ b/.github/workflows/release-kt-lib.yml @@ -22,8 +22,7 @@ name: Release Kotlin lib on: push: branches: - - main - - dev + - release paths: - "kt/moq/**" - "kt/gradle.properties" diff --git a/.github/workflows/release-py.yml b/.github/workflows/release-py.yml index beb00e7dad..1a92d28ebb 100644 --- a/.github/workflows/release-py.yml +++ b/.github/workflows/release-py.yml @@ -15,7 +15,7 @@ name: Release Py on: push: branches: - - main + - release paths: - "py/moq-rs/**" - ".github/workflows/release-py.yml" diff --git a/.github/workflows/release-rs.yml b/.github/workflows/release-rs.yml index bc111c9d81..d3b5eb13b2 100644 --- a/.github/workflows/release-rs.yml +++ b/.github/workflows/release-rs.yml @@ -12,7 +12,7 @@ concurrency: on: push: branches: - - main + - release # A run costs the better part of an hour and they serialize on the # concurrency group above, so a push that can't change a crate's version # or its API shouldn't queue one. diff --git a/.github/workflows/release-swift-lib.yml b/.github/workflows/release-swift-lib.yml index 2684aa4f59..cd10542078 100644 --- a/.github/workflows/release-swift-lib.yml +++ b/.github/workflows/release-swift-lib.yml @@ -15,8 +15,7 @@ name: Release Swift Lib on: push: branches: - - main - - dev + - release paths: - "swift/VERSION" - "swift/Package.swift.template" From 0a19a97aa478162876442f786e834ff6b42c7ad8 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 2 Oct 2026 12:36:23 -0700 Subject: [PATCH 2/6] ci: back-merge release into main from day one Releases stay patch-only, so no cut follows the flip right away. The Back-merge workflow, script, recipe, and alert entry are identical to the trunk PR's, so release back-merges from its first publish. Co-Authored-By: Claude Opus 5.5 --- .github/justfile | 4 +++ .github/workflows/alert.yml | 1 + .github/workflows/back-merge.yml | 51 +++++++++++++++++++++++++++++ sh/gh/back-merge.sh | 56 ++++++++++++++++++++++++++++++++ 4 files changed, 112 insertions(+) create mode 100644 .github/workflows/back-merge.yml create mode 100755 sh/gh/back-merge.sh diff --git a/.github/justfile b/.github/justfile index 1bccfc5338..f73be6168b 100644 --- a/.github/justfile +++ b/.github/justfile @@ -34,6 +34,10 @@ formula *args: install-formula *args: python3 sh/gh/install-formula.py "$@" +# Open the pull request that merges `release` back into `main`, queued to land as a merge commit. +back-merge: + sh/gh/back-merge.sh + # Trigger the apt-repo and rpm-repo workflows for a release tag. [positional-arguments] trigger-repo-publish *args: diff --git a/.github/workflows/alert.yml b/.github/workflows/alert.yml index 2c4084d32e..d0134bcb4d 100644 --- a/.github/workflows/alert.yml +++ b/.github/workflows/alert.yml @@ -28,6 +28,7 @@ on: # request. workflows: - apt-repo + - Back-merge - Cache - Cachix - Docker diff --git a/.github/workflows/back-merge.yml b/.github/workflows/back-merge.yml new file mode 100644 index 0000000000..e03ac6a2b3 --- /dev/null +++ b/.github/workflows/back-merge.yml @@ -0,0 +1,51 @@ +name: Back-merge + +# Every push to `release` (a cut, a backport, or release-plz's version bump) +# merges `release` back into `main`, so trunk carries the published versions +# and CHANGELOGs and the next cut starts from an advanced merge base. +# +# It lands through a pull request as a merge commit, never a squash: a squash +# leaves the merge base at the last cut, so a second publish before the next +# cut would conflict. + +on: + push: + branches: [release] + # Re-running by hand, e.g. after resolving a conflict that failed a run. + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: back-merge + cancel-in-progress: false + +jobs: + back-merge: + name: Back-merge + runs-on: ubuntu-latest + if: github.repository_owner == 'moq-dev' + + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - uses: ./.github/actions/just + + # The app token, not GITHUB_TOKEN: a pull request GITHUB_TOKEN opens + # triggers no workflows, so Check and Test would never report. + - name: Generate moq-bot token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3 + id: token + with: + client-id: ${{ secrets.APP_CLIENT_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + permission-contents: write + permission-pull-requests: write + + - name: Merge release into main + env: + GH_TOKEN: ${{ steps.token.outputs.token }} + run: just gh back-merge diff --git a/sh/gh/back-merge.sh b/sh/gh/back-merge.sh new file mode 100755 index 0000000000..4356e9d830 --- /dev/null +++ b/sh/gh/back-merge.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# +# Open (or refresh) the pull request that merges `release` back into `main`, +# and queue it to land as a merge commit once Check and Test pass. +# +# The pull request's head is its own branch rather than `release` itself: the +# repository deletes head branches on merge, and a conflict is resolved by +# merging `main` into that branch, which must never touch `release`. +# +# Required env: +# GH_TOKEN A token whose pull requests trigger CI (not GITHUB_TOKEN). +# GITHUB_REPOSITORY owner/repo, set by GitHub Actions. + +set -euo pipefail + +repo="${GITHUB_REPOSITORY:?GITHUB_REPOSITORY must be set}" +branch=merge/release-into-main + +ahead=$(gh api "repos/$repo/compare/main...release" --jq .ahead_by) +if [[ "$ahead" == 0 ]]; then + echo "main already contains release" + exit 0 +fi + +sha=$(gh api "repos/$repo/git/ref/heads/release" --jq .object.sha) + +# --exit-code: 2 means the branch is absent; anything else nonzero is a failure. +status=0 +git ls-remote --exit-code --heads "https://github.com/$repo" "$branch" >/dev/null || status=$? +case "$status" in + 0) + # An open pull request may carry a hand-resolved conflict, so merge into the + # branch instead of resetting it. A conflict fails here, loudly. + gh api -X POST "repos/$repo/merges" -f base="$branch" -f head="$sha" \ + -f commit_message="Merge release into $branch" >/dev/null + ;; + 2) + gh api -X POST "repos/$repo/git/refs" -f ref="refs/heads/$branch" -f sha="$sha" >/dev/null + ;; + *) + echo "error: cannot query $branch on $repo" >&2 + exit 1 + ;; +esac + +pr=$(gh pr list --repo "$repo" --head "$branch" --base main --state open --json number --jq '.[0].number // empty') +if [[ -z "$pr" ]]; then + gh pr create --repo "$repo" --head "$branch" --base main \ + --title "chore: merge release into main" \ + --body "Carries what \`release\` published (versions, CHANGELOGs, backports) back to trunk. Opened by the Back-merge workflow. + +**Merge with a merge commit. Never squash.** A squash leaves the merge base at the last cut, so the next back-merge conflicts. On a conflict, merge \`main\` into \`$branch\` and resolve it there." + pr=$(gh pr list --repo "$repo" --head "$branch" --base main --state open --json number --jq '.[0].number') +fi + +gh pr merge --repo "$repo" "$pr" --auto --merge From 178d8c6b0b139485044237f8d4ec550152ece4bc Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 2 Oct 2026 13:15:33 -0700 Subject: [PATCH 3/6] ci: refuse dispatch publishing off release, pin installs to release Matches the trunk PR: release-rs and release-kt-lib publish by hand only on release, the back-merge script only queues its own branch at the commit it produced, and the docs this branch serves after the flip pin user-facing nix installs to release. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/release-kt-lib.yml | 8 ++++++-- .github/workflows/release-rs.yml | 9 ++++++--- doc/bin/gstreamer.md | 4 ++-- doc/setup/install.md | 8 ++++---- flake.nix | 2 +- rs/moq-gst/README.md | 8 ++++---- sh/gh/back-merge.sh | 19 ++++++++++++++++--- 7 files changed, 39 insertions(+), 19 deletions(-) diff --git a/.github/workflows/release-kt-lib.yml b/.github/workflows/release-kt-lib.yml index e17ccd274c..32bc3eaf14 100644 --- a/.github/workflows/release-kt-lib.yml +++ b/.github/workflows/release-kt-lib.yml @@ -40,7 +40,8 @@ on: - "sh/gh/release.sh" - "rs/moq-ffi/build.sh" # Manual trigger so a wrapper bump can be published once the bindings it - # depends on land on Maven Central (the guard below skips otherwise). + # depends on land on Maven Central (the guard below skips otherwise). Only a + # dispatch on `release` publishes. workflow_dispatch: workflow_run: workflows: ["Release Kotlin FFI"] @@ -143,7 +144,10 @@ jobs: name: Publish wrapper needs: [check-version] # PRs always run (dry-run). Pushes publish only when the version is new. - if: github.event_name == 'pull_request' || needs.check-version.outputs.should_publish == 'true' + if: >- + github.event_name == 'pull_request' || + (needs.check-version.outputs.should_publish == 'true' && + (github.event_name != 'workflow_dispatch' || github.ref == 'refs/heads/release')) # Only publishing runs share the serialized group. GitHub keeps a single # pending run per group and drops the older one, so a PR or a no-op push # sharing it silently cancelled a queued release. Scoped to this job so runs diff --git a/.github/workflows/release-rs.yml b/.github/workflows/release-rs.yml index d3b5eb13b2..de27458731 100644 --- a/.github/workflows/release-rs.yml +++ b/.github/workflows/release-rs.yml @@ -26,7 +26,8 @@ on: - "flake.lock" - ".release-plz.toml" - ".github/workflows/release-rs.yml" - # Releasing by hand, e.g. after a skipped push or a cold cache. + # Releasing by hand, e.g. after a skipped push or a cold cache. Dispatch it + # on `release`; the job refuses any other ref. workflow_dispatch: jobs: @@ -34,7 +35,7 @@ jobs: name: Plz runs-on: ubuntu-latest - if: ${{ github.repository_owner == 'moq-dev' }} + if: ${{ github.repository_owner == 'moq-dev' && github.ref == 'refs/heads/release' }} env: # release-plz copies each crate into its own temp directory to compare it @@ -83,7 +84,9 @@ jobs: # Its own scope rather than the shared store cache.yml warms: this holds # cargo-semver-checks rustdoc, not check and test artifacts, and mixing # the two would have each restoring a target tree built for the other. - # This workflow only runs on `main`, so it is the sole writer of its scope. + # This workflow only runs on `release`, so it is the sole writer of its scope. + # The cache action saves push runs only on the default branch, so pushes + # here restore without saving; a dispatch on `release` refreshes it. # # The action's shim is what gets mbx in here at all. release-plz spawns # Cargo and cargo-semver-checks itself, so no environment variable reaches diff --git a/doc/bin/gstreamer.md b/doc/bin/gstreamer.md index 117064a59a..e51a5fc504 100644 --- a/doc/bin/gstreamer.md +++ b/doc/bin/gstreamer.md @@ -10,10 +10,10 @@ subscribes to a broadcast and exposes one source pad per rendition. ```bash # Inspect (Nix bundles the plugin with gst-launch) -nix shell github:moq-dev/moq#moq-gst --command gst-inspect-1.0 moq +nix shell github:moq-dev/moq/release#moq-gst --command gst-inspect-1.0 moq # Play the public test broadcast -nix shell github:moq-dev/moq#moq-gst --command gst-launch-1.0 -e \ +nix shell github:moq-dev/moq/release#moq-gst --command gst-launch-1.0 -e \ moqsrc name=s url=https://cdn.moq.dev/demo broadcast=bbb.hang \ s.video_0 ! queue ! decodebin3 ! videoconvert ! autovideosink \ s.audio_0 ! queue ! decodebin3 ! audioconvert ! autoaudiosink diff --git a/doc/setup/install.md b/doc/setup/install.md index 83a5e9ce0f..c596ec9f35 100644 --- a/doc/setup/install.md +++ b/doc/setup/install.md @@ -33,8 +33,8 @@ cargo install moq-relay moq-cli brew install moq-dev/tap/moq-relay moq-dev/tap/moq # Nix (pin a release tag to use the binary cache) -nix run github:moq-dev/moq#moq-relay -- relay.toml -nix run github:moq-dev/moq#moq -- --help +nix run github:moq-dev/moq/release#moq-relay -- relay.toml +nix run github:moq-dev/moq/release#moq -- --help # Docker (linux/amd64 and linux/arm64) docker run -p 4443:4443/udp -p 4443:4443/tcp -v "$PWD/relay.toml:/app/relay.toml:ro" moqdev/moq-relay /app/relay.toml @@ -44,8 +44,8 @@ docker run -i moqdev/moq --help Static binaries for Linux (x86\_64, aarch64), macOS (Apple Silicon), and Windows (x64) are attached to every [GitHub release](https://github.com/moq-dev/moq/releases). The Nix cache at -`kixelated.cachix.org` only holds tagged releases, so an unpinned -`github:moq-dev/moq` builds from source. +`kixelated.cachix.org` only holds tagged releases, so the `release` branch +builds from source. ## Debian and Ubuntu diff --git a/flake.nix b/flake.nix index c537b778b2..e0ac5182e2 100644 --- a/flake.nix +++ b/flake.nix @@ -447,7 +447,7 @@ # The package was `moq-cli` through 0.12.2. Refuse with the new name # so `nix run` and `nix profile upgrade` break instead of going stale. moq-cli = pkgs.writeShellScriptBin "moq" '' - echo "error: the moq-cli package is now moq: nix run github:moq-dev/moq#moq" >&2 + echo "error: the moq-cli package is now moq: nix run github:moq-dev/moq/release#moq" >&2 exit 1 ''; diff --git a/rs/moq-gst/README.md b/rs/moq-gst/README.md index ffd42f952c..6e1ec211a7 100644 --- a/rs/moq-gst/README.md +++ b/rs/moq-gst/README.md @@ -36,21 +36,21 @@ sudo dnf install gstreamer1-moq The `moq-gst` flake output bundles the plugin with wrappers around `gst-inspect-1.0` / `gst-launch-1.0` that preload moq + the standard `gst-plugins-{base,good,bad}` set, so no `GST_PLUGIN_PATH` setup is needed. ```bash -# Inspect: list moqsink + moqsrc. (Or one-shot: `nix run github:moq-dev/moq#moq-gst -- moq`.) -nix shell github:moq-dev/moq#moq-gst --command gst-inspect-1.0 moq +# Inspect: list moqsink + moqsrc. (Or one-shot: `nix run github:moq-dev/moq/release#moq-gst -- moq`.) +nix shell github:moq-dev/moq/release#moq-gst --command gst-inspect-1.0 moq # Subscribe to the always-on public test broadcast and render to a window. # moqsrc emits one pad per rendition (video_0, audio_0, ...); link the one(s) you # want by name. A bare `moqsrc ! decodebin3 ! ...` only links the first pad offered, # which on a video+audio broadcast may be the audio pad (so a video sink shows nothing). -nix shell github:moq-dev/moq#moq-gst --command gst-launch-1.0 -v -e \ +nix shell github:moq-dev/moq/release#moq-gst --command gst-launch-1.0 -v -e \ moqsrc name=s url=https://cdn.moq.dev/demo broadcast=bbb.hang \ s.video_0 ! queue ! decodebin3 ! videoconvert ! autovideosink \ s.audio_0 ! queue ! decodebin3 ! audioconvert ! autoaudiosink # Publish your own broadcast on the public anon relay (then sub to it from anywhere). curl -fsSL https://vid.moq.dev/bbb.mp4 -o bbb.mp4 -nix shell github:moq-dev/moq#moq-gst --command gst-launch-1.0 -v -e \ +nix shell github:moq-dev/moq/release#moq-gst --command gst-launch-1.0 -v -e \ multifilesrc location=bbb.mp4 loop=true ! parsebin name=parse \ parse. ! queue ! identity sync=true ! mux.sink_0 \ parse. ! queue ! identity sync=true ! mux.sink_1 \ diff --git a/sh/gh/back-merge.sh b/sh/gh/back-merge.sh index 4356e9d830..3021f3e318 100755 --- a/sh/gh/back-merge.sh +++ b/sh/gh/back-merge.sh @@ -43,14 +43,27 @@ case "$status" in ;; esac -pr=$(gh pr list --repo "$repo" --head "$branch" --base main --state open --json number --jq '.[0].number // empty') +# Only a pull request from this repository's own branch: `gh pr list --head` +# matches the branch name alone, so a fork's branch of the same name would pass. +find_pr() { + gh api "repos/$repo/pulls?state=open&base=main&head=${repo%%/*}:$branch" \ + --jq "[.[] | select(.head.repo.full_name == \"$repo\")][0].number // empty" +} + +pr=$(find_pr) if [[ -z "$pr" ]]; then gh pr create --repo "$repo" --head "$branch" --base main \ --title "chore: merge release into main" \ --body "Carries what \`release\` published (versions, CHANGELOGs, backports) back to trunk. Opened by the Back-merge workflow. **Merge with a merge commit. Never squash.** A squash leaves the merge base at the last cut, so the next back-merge conflicts. On a conflict, merge \`main\` into \`$branch\` and resolve it there." - pr=$(gh pr list --repo "$repo" --head "$branch" --base main --state open --json number --jq '.[0].number') + pr=$(find_pr) +fi +if [[ -z "$pr" ]]; then + echo "error: no pull request from $repo:$branch into main" >&2 + exit 1 fi -gh pr merge --repo "$repo" "$pr" --auto --merge +# Queue exactly the commit this run produced, so a later push cannot ride along. +head=$(gh api "repos/$repo/git/ref/heads/$branch" --jq .object.sha) +gh pr merge --repo "$repo" "$pr" --auto --merge --match-head-commit "$head" From 7a53070206cef827cb7e4c97880b7d41170fdb19 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 2 Oct 2026 13:39:21 -0700 Subject: [PATCH 4/6] docs: install header matches the release pin Co-Authored-By: Claude Opus 5.5 --- doc/setup/install.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/setup/install.md b/doc/setup/install.md index c596ec9f35..616370aa2d 100644 --- a/doc/setup/install.md +++ b/doc/setup/install.md @@ -32,7 +32,7 @@ cargo install moq-relay moq-cli # Homebrew (macOS and Linux) brew install moq-dev/tap/moq-relay moq-dev/tap/moq -# Nix (pin a release tag to use the binary cache) +# Nix (`release` builds from source; a release tag such as moq-relay-v0.12.4 uses the binary cache) nix run github:moq-dev/moq/release#moq-relay -- relay.toml nix run github:moq-dev/moq/release#moq -- --help From bc7ffc6c65eec86c2345b42d8032a6947c219fe6 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 2 Oct 2026 14:09:29 -0700 Subject: [PATCH 5/6] ci: release publishers match trunk's Takes trunk's release-go, release-js, release-py, release-swift-lib, and release-brew whole: comments name release, and chained runs build the triggering head_sha. Identical files keep the first back-merge and any cut conflict-free. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/release-brew.yml | 3 +++ .github/workflows/release-go.yml | 10 ++++++---- .github/workflows/release-js.yml | 2 +- .github/workflows/release-py.yml | 2 +- .github/workflows/release-swift-lib.yml | 10 ++++++---- 5 files changed, 17 insertions(+), 10 deletions(-) diff --git a/.github/workflows/release-brew.yml b/.github/workflows/release-brew.yml index 5e8594de05..c04f0b4e6e 100644 --- a/.github/workflows/release-brew.yml +++ b/.github/workflows/release-brew.yml @@ -39,6 +39,9 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + # workflow_run runs the default branch's file, whose checkout would + # otherwise take trunk rather than the tag that was released. + ref: ${{ github.event.workflow_run.head_sha }} persist-credentials: false - uses: ./.github/actions/just diff --git a/.github/workflows/release-go.yml b/.github/workflows/release-go.yml index 303811fa01..759d4ad19c 100644 --- a/.github/workflows/release-go.yml +++ b/.github/workflows/release-go.yml @@ -8,7 +8,7 @@ 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. The ffi require is +# * push to release — 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) or rs/moq-ffi changed since @@ -20,7 +20,7 @@ name: Release Go # Chaining off the upstream run guarantees the mirror # tag is already present, so there is nothing to poll. # -# Stray main pushes are harmless: publish-wrapper.sh skips when the staged tree +# Stray release pushes are harmless: publish-wrapper.sh skips when the staged tree # matches the mirror, so unchanged content never mints an empty patch release. on: @@ -62,6 +62,7 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.sha }} # Tags, for the unreleased ffi check. fetch-depth: 0 persist-credentials: false @@ -84,7 +85,7 @@ jobs: echo "wrapper line: $LINE" # The ffi version the wrapper should require: the version of the ffi that - # just published (workflow_run), or the current crate version (main push). + # just published (workflow_run), or the current crate version (release push). - name: Resolve ffi version id: ffi env: @@ -100,7 +101,7 @@ jobs: fi echo "version=$VERSION" >> "$GITHUB_OUTPUT" - # On a main push the wrapper pins the current crate's ffi version, but it + # On a release push the wrapper pins the current crate's ffi version, but it # may call API added to rs/moq-ffi since that version was tagged. - name: Check for unreleased ffi changes (push only) id: ffi-diff @@ -180,6 +181,7 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.sha }} persist-credentials: false - uses: ./.github/actions/just diff --git a/.github/workflows/release-js.yml b/.github/workflows/release-js.yml index 56c6346006..81b8f36f72 100644 --- a/.github/workflows/release-js.yml +++ b/.github/workflows/release-js.yml @@ -11,7 +11,7 @@ on: - 'js/**' - '.github/workflows/release-js.yml' # Dry-run: builds, packs, and runs `deno publish --dry-run`, which catches - # JSR-only failures before they break a release on main. + # JSR-only failures before they break a release on `release`. pull_request: paths: - 'js/**' diff --git a/.github/workflows/release-py.yml b/.github/workflows/release-py.yml index 1a92d28ebb..51fb6f5bd5 100644 --- a/.github/workflows/release-py.yml +++ b/.github/workflows/release-py.yml @@ -5,7 +5,7 @@ name: Release Py # depends on are built separately (release-py-ffi.yml). # # Versioned independently of the moq-ffi crate: bump `version` in -# py/moq-rs/pyproject.toml by hand in a normal PR. On merge to main this reads +# py/moq-rs/pyproject.toml by hand in a normal PR. On merge to `release` this reads # that version and publishes only if it isn't already on PyPI. That registry # check is the release gate (the same model release-plz uses for crates), so # there's no separate tag to remember. When wrapper and FFI changes land diff --git a/.github/workflows/release-swift-lib.yml b/.github/workflows/release-swift-lib.yml index cd10542078..7283b8dfc1 100644 --- a/.github/workflows/release-swift-lib.yml +++ b/.github/workflows/release-swift-lib.yml @@ -5,7 +5,7 @@ name: Release Swift Lib # on are built separately (release-swift-ffi.yml). # # Versioned independently of the moq-ffi crate: bump `swift/VERSION` by hand in a -# normal PR. On merge to main/dev this reads that version and publishes only if +# normal PR. On merge to `release` this reads that version and publishes only if # it isn't already tagged on the mirror. That tag check is the release gate (the # same model release-plz uses for crates), so there's no separate tag to # remember. When wrapper and FFI changes land together, publication waits for @@ -45,8 +45,8 @@ on: permissions: contents: read -# Separate PR builds from mainline publishes so a PR run can't queue ahead of -# (and delay) a release on main. +# Separate PR builds from publishes so a PR run can't queue ahead of (and +# delay) a release. concurrency: group: release-swift-lib-${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || 'publish' }} cancel-in-progress: false @@ -67,7 +67,7 @@ jobs: version: ${{ steps.version.outputs.version }} ffi_version: ${{ steps.version.outputs.ffi_version }} ffi_unreleased: ${{ steps.ffi-diff.outputs.unreleased }} - # On PRs we only build (dry-run). On main/dev we publish when the version + # On PRs we only build (dry-run). On `release` we publish when the version # isn't already tagged on the mirror. publish: ${{ (github.event_name == 'push' || github.event_name == 'workflow_run') && steps.ffi-diff.outputs.unreleased != 'true' && steps.ffi-gate.outputs.exists == 'true' && steps.gate.outputs.exists == 'false' }} @@ -148,6 +148,7 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 with: + ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.sha }} persist-credentials: false - uses: ./.github/actions/just @@ -195,6 +196,7 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 with: + ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.sha }} persist-credentials: false - uses: ./.github/actions/just From 69c595b8748def1ac5941a89afd01ef6b44f48f3 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 2 Oct 2026 14:24:53 -0700 Subject: [PATCH 6/6] ci: release-brew accepts only this repository's tag pushes It checks out the triggering commit, so a successful fork PR run with a matching workflow and branch name must not reach the tap credentials. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/release-brew.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release-brew.yml b/.github/workflows/release-brew.yml index c04f0b4e6e..d31ca30f36 100644 --- a/.github/workflows/release-brew.yml +++ b/.github/workflows/release-brew.yml @@ -33,7 +33,14 @@ permissions: jobs: publish: name: Publish formula to homebrew-tap - if: ${{ github.event.workflow_run.conclusion == 'success' && startsWith(github.event.workflow_run.head_branch, 'moq-') }} + # workflow_run has the base repository's privileges and this job checks out + # the triggering commit, so accept only a successful tag push in this + # repository: a fork PR can rename a workflow and its branch to match. + if: >- + github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.event == 'push' && + github.event.workflow_run.head_repository.full_name == github.repository && + startsWith(github.event.workflow_run.head_branch, 'moq-') runs-on: ubuntu-latest steps: