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 6203c4ff0b..5c5310363c 100644 --- a/.github/workflows/alert.yml +++ b/.github/workflows/alert.yml @@ -23,11 +23,12 @@ on: # # Swift, OBS, and Platform are here despite also running on pull requests: # Swift warms its own Rust cache on `main`, and OBS and Platform build every - # merge to `main` and `dev`. Those push runs are nobody's check, so a break in them + # merge to `main` and `release`. Those push runs are nobody's check, so a break in them # would otherwise go unseen. The cost is a skipped Alert run per pull # 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/.github/workflows/obs.yml b/.github/workflows/obs.yml index 5364b00097..29e0e9dc88 100644 --- a/.github/workflows/obs.yml +++ b/.github/workflows/obs.yml @@ -39,7 +39,7 @@ name: OBS # .cargo/config.toml could do it too -- and `rs/**` would run this on nearly # every PR, which is the cost that removed the Windows and macOS gates (#2600). # So this filter covers the likely inputs at PR time, and pushes to `main` and -# `dev` rebuild on any Rust or plugin change to cover the rest. nightly.yml also +# `release` rebuild on any Rust or plugin change to cover the rest. nightly.yml also # runs the Linux recipe diff-independently. permissions: @@ -47,7 +47,7 @@ permissions: on: push: - branches: [main, dev] + branches: [main, release] paths: - "rs/**" - "cpp/obs/**" diff --git a/.github/workflows/platform.yml b/.github/workflows/platform.yml index 819d731bb6..268c27de30 100644 --- a/.github/workflows/platform.yml +++ b/.github/workflows/platform.yml @@ -9,9 +9,9 @@ name: Platform # # A pull request compiles only the crates it changed plus their dependents, # the same selection the Linux `check` job makes, because these are the -# slowest and scarcest runners in CI. Pushes to `main` and `dev` compile +# slowest and scarcest runners in CI. Pushes to `main` and `release` compile # everything, catching the break two individually green PRs make once both -# land. `dev` never ran the nightly at all. +# land. # # Outside Nix, like android.yml: neither runner has it, so each installs the # runner-native toolchain. rust-toolchain.toml still pins the compiler, and the @@ -26,7 +26,7 @@ permissions: on: push: - branches: [main, dev] + branches: [main, release] paths: - "rs/**" - ".cargo/**" diff --git a/.github/workflows/quest.yml b/.github/workflows/quest.yml index 6118d649dd..6243925a6c 100644 --- a/.github/workflows/quest.yml +++ b/.github/workflows/quest.yml @@ -2,7 +2,7 @@ name: Quest # Validates the quest tree on every branch that carries one. check.yml already # runs `quest check` on a pull request into any branch, but a direct push (a -# merge to `main` or `dev`, or `main` merged into a questline) is nobody's pull +# merge to `main` or `release`, or `main` merged into a questline) is nobody's pull # request, so a broken tree could land there unseen. # # Push only: a pull request is check.yml's job, and a pull_request trigger here @@ -18,7 +18,7 @@ on: push: branches: - main - - dev + - release - "quest/**" paths: - "quest/**" 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 bb817841ac..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,13 +20,13 @@ 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: push: branches: - - main + - release workflow_run: workflows: ["Release Go FFI"] types: @@ -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 422a685f12..81b8f36f72 100644 --- a/.github/workflows/release-js.yml +++ b/.github/workflows/release-js.yml @@ -6,12 +6,12 @@ permissions: on: push: - branches: [main] + branches: [release] paths: - '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-kt-lib.yml b/.github/workflows/release-kt-lib.yml index f3947ef2ac..32bc3eaf14 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" @@ -41,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"] @@ -144,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-py.yml b/.github/workflows/release-py.yml index beb00e7dad..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 @@ -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..de27458731 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. @@ -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/.github/workflows/release-swift-lib.yml b/.github/workflows/release-swift-lib.yml index 2684aa4f59..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 @@ -15,8 +15,7 @@ name: Release Swift Lib on: push: branches: - - main - - dev + - release paths: - "swift/VERSION" - "swift/Package.swift.template" @@ -46,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 @@ -68,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' }} @@ -149,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 @@ -196,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 diff --git a/AGENTS.md b/AGENTS.md index 0222c104d2..0512d65d63 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -58,7 +58,7 @@ A bad shape costs a breaking change in every language, and the surface is huge. - Report the public API and wire impact of every change, in the PR description and whenever asked. - Keep things private until a consumer needs them. Scrutinize every new exported item. -- Never add `foo_with_x`, `foo_checked`, or a compatibility shim. Make the breaking change to `foo` on `dev` instead. Additive changes stay on `main`. +- Never add `foo_with_x`, `foo_checked`, or a compatibility shim. Make the breaking change to `foo` instead. - Let the type system make misuse unrepresentable - A broadcast, track, or group name always means the same content. MoQ has no ETag-style invalidation, so a publisher that reuses a name for different content (such as restarting group numbers) is a bug; publish a new broadcast instead of working around it. - Avoid callback parameters. Return a handle, an event, or a Producer/Consumer split. @@ -67,19 +67,18 @@ A bad shape costs a breaking change in every language, and the surface is huge. - When a name or shape feels awkward, propose alternatives with a recommendation instead of shipping it. - Short names under a module namespace (`encode::Config`, not `EncoderConfig`). Mirror names across Rust, JS, and the bindings. - Document every exported symbol in one plain line, the way you'd say it out loud. -- Prefer refactoring and simplification when working on the `dev` branch; many APIs have have not been published yet. +- Prefer refactoring and simplification; many APIs have not been published yet. MoQ is split into many layers as part of the public API. The boundary between packages is extremely important to keep things modular and reusable. # Development -PRs target `main`. -`dev` is reserved for semver-breaking API changes, except for `0.0.x` and unpublished/private packages. +PRs target `main`, the trunk, where breaking API changes are allowed. +`release` is what ships; touch it only to cut a release or backport a fix (see `CONTRIBUTING.md`). Wire changes should be backwards compatible for any *published* drafts/versions. Before starting, `git fetch origin` and set the upstream to the base branch. -If a published API break requires `dev`, retarget the PR to `dev`, set the upstream to `origin/dev`, then rebase onto it. Write scratch files (PR bodies, logs, notes) to the worktree's gitignored `.scratch/`, never a directory other agents share. Use the Nix dev shell so tooling matches CI. @@ -94,7 +93,6 @@ These diff the branch against its base and only run the affected packages. Quests: when work mentions a quest, run `quest guide` and follow it. The `quest` binary comes from the kixelated/quest flake input and serves the quest skills; change them upstream and bump the input. -A quest deleted on `dev` is done, even while `main` still lists it. # Cross-Package Sync diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1974205cd8..c93c2efa09 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,7 +1,7 @@ # Commits PRs into `main` are squash-merged, so the PR title becomes the commit subject and the PR description becomes the body in `git log`. -PRs into any other branch (`dev`, a questline) use a merge commit, so their history survives until they land. +PRs into any other branch (`release`, a questline) use a merge commit, so their history survives until they land. - Use conventional-commit subjects (`feat(watch): ...`, `fix: ...`, `chore: ...`, `docs: ...`) - AI commit attribution goes in a `Co-Authored-By:` trailer, not the commit body. @@ -67,6 +67,14 @@ Its `moq-sync` workflow merges n0-computer/noq weekly as a PR; review it like an A carried change lists its upstream PR, or the reason it has none, in the fork PR. For an advisory against noq or Quinn, compare the pinned release's `PARENT` with the fixing upstream commit, then sync, release the fork, and bump the pin here. +# Releases + +`main` is the trunk; `release` is what ships, and release-plz and every branch-triggered publish run only there. + +- A release is cut by hand: a PR merging `main` into `release`, with a merge commit. +- An urgent fix between cuts lands on `main` first, then reaches `release` as a cherry-pick PR (a backport). +- After every push to `release`, the Back-merge workflow opens a PR merging `release` into `main`, so trunk carries the published versions and CHANGELOGs. It lands as a merge commit; never squash it, or the next back-merge conflicts. + # Versions Releases are cut separately; bump only when asked. Each package's version lives in one place: 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..616370aa2d 100644 --- a/doc/setup/install.md +++ b/doc/setup/install.md @@ -32,9 +32,9 @@ 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 run github:moq-dev/moq#moq-relay -- relay.toml -nix run github:moq-dev/moq#moq -- --help +# 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 # 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 b1cfb8013f..1888370eea 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/py/AGENTS.md b/py/AGENTS.md index 58bd95b8f0..b3177f075f 100644 --- a/py/AGENTS.md +++ b/py/AGENTS.md @@ -15,4 +15,4 @@ The `/py` uv workspace. Extends the root `AGENTS.md` and `rs/moq-ffi/AGENTS.md`. - Keyword-only args with defaults (`*, ...`) instead of an options object; that is how Python extends additively. - The package ships `py.typed`: types and docstrings are the API, so document public symbols. - `just py ` for everything. Tests live under each package's `tests/`. -- Releases: `moq-ffi` tracks `rs/moq-ffi` and publishes on `moq-ffi-v*` tags; `moq-rs` is versioned by hand and publishes on merge to `main` when the version isn't on PyPI yet. +- Releases: `moq-ffi` tracks `rs/moq-ffi` and publishes on `moq-ffi-v*` tags; `moq-rs` is versioned by hand and publishes on merge to `release` when the version isn't on PyPI yet. diff --git a/quest/README.md b/quest/README.md index 51892ea630..4e1f8ab493 100644 --- a/quest/README.md +++ b/quest/README.md @@ -12,9 +12,7 @@ Seattle, wildcard routing, and audio playout (jitter target and quality harness). m1 is the next wave across reliability, features, performance, and planning. m2 holds later features, design studies, and experiments. m3 is gated on the outside world: hardware, a partner, a consumer, or a provider's -offer. m4 waits on an upstream release. Priority is -separate from branch targeting: published API and wire breaks still land on dev -under the repository rules. +offer. m4 waits on an upstream release. A quest waiting on the outside world, in any milestone, requires a small quest beside it that names the condition. That condition quest stays ready, so diff --git a/quest/m0/README.md b/quest/m0/README.md index 222dcc4449..aedc341633 100644 --- a/quest/m0/README.md +++ b/quest/m0/README.md @@ -34,9 +34,7 @@ audio quality harness in `test/audio-quality/` has landed; it grades playout nightly and records the traces it replays. Its native lane is a standalone m1 quest, since nothing here waits on it. The [A/V clock](/quest/m1/av-clock.md) moved to m1 in the 2026-09-30 audit: it waits on the whole jitter line and is -a published `@moq/watch` break on dev. - -Published API or wire breaks still land on dev; each quest's Plan says so. +a published `@moq/watch` break. ## Required diff --git a/quest/m0/audio-jitter-target/README.md b/quest/m0/audio-jitter-target/README.md index 1056392c71..604b520987 100644 --- a/quest/m0/audio-jitter-target/README.md +++ b/quest/m0/audio-jitter-target/README.md @@ -25,7 +25,7 @@ additive and target `main`: the native knob is a new field on a `#[non_exhaustive]` struct, and the browser estimator is a new module plus a new `spread` observation. -Decided for landing: the line merges to `main`, not `dev`, with a changelog +Decided for landing: the line merges to `main` with a changelog note for two behavior changes treated as fixes. `@moq/watch` `Sync` takes a numeric delay literally instead of adding the rendition delay on top ([#3954](https://github.com/moq-dev/moq/pull/3954)), and `moq play --delay` @@ -41,7 +41,7 @@ asking the reporter; they replace the #3477 traces wherever the quests name them The algorithm is written down at `doc/concept/audio-jitter.md`, with a conformance corpus beside it that both implementations will read. -Neither `main` nor `dev` has a measured estimator yet. `js/watch/src/sync.ts:159` +Neither `main` nor `release` has a measured estimator yet. `js/watch/src/sync.ts:159` still computes `max(MIN_JITTER, minRtt * 1.25)` from the connection's PROBE, and `js/watch/src/audio/latency.ts` still exists. `sync.ts` also adds the advertised jitter to that term, where the document settles on a maximum. diff --git a/quest/m0/audio-jitter-target/watch.md b/quest/m0/audio-jitter-target/watch.md index ea2f8c97c5..782eed0ceb 100644 --- a/quest/m0/audio-jitter-target/watch.md +++ b/quest/m0/audio-jitter-target/watch.md @@ -17,9 +17,9 @@ them. ## Plan Start from `origin/quest/m0/3477-watch-auto-latency`, the branch of the closed -PR #3517, two commits ahead of `dev`. Rebase it onto the base branch first, and +PR #3517. Rebase it onto the base branch first, and expect conflicts: `sync.ts` on `main` has since replaced the `audio`/`video` -inputs with a `register(jitter)` list. Nothing on `main` or `dev` has any of +inputs with a `register(jitter)` list. Nothing on `main` or `release` has any of it: `js/watch/src/sync.ts:159` still sizes auto from `minRtt * 1.25`. What the branch already has, and this quest keeps: diff --git a/quest/m0/branch-flip.md b/quest/m0/branch-flip.md index 63c4df5900..716b1cc1bf 100644 --- a/quest/m0/branch-flip.md +++ b/quest/m0/branch-flip.md @@ -67,6 +67,19 @@ Decided (2026-10-02): second publish before the next cut conflicts; and a ruleset bypass for the workflow token. +Decided (2026-10-02, after #4737 and #4738 opened): + +- ✅ Releases stay patch-only for now, so no cut follows the flip: a cut + would carry trunk's breaking changes into `release` and force minor bumps. + The back-merge workflow, script, recipe, and alert entry ship in both PRs + as identical files, so `release` back-merges from its first publish. +- ✅ The Cloudflare docs and demo builds track `release`. The maintainer + switches them in the dashboard during step 4. +- ✅ #4605 (noq reassembly cap, part 1) is retargeted to trunk, then + backported to `release` once it merges. +- ✅ The `dev` PR merges first; the `main` PR merges immediately before + step 4, which needs the maintainer's explicit go-ahead. + Sequence: 1. dev-sync ([#4720](https://github.com/moq-dev/moq/pull/4720)) lands. @@ -74,15 +87,19 @@ Sequence: `platform`, and `quest` push filters name `main` and `release`, unpinned `workflow_run` checkouts pin the triggering `head_sha`, the back-merge workflow, docs pin `release`. -3. A small PR on `main`: publish triggers move to `release`; cache writers - keep `main`, so they stop running there. +3. A small PR on `main`: publish triggers move to `release`, and the + back-merge workflow lands identical to trunk's; cache writers keep + `main`, so they stop running there. 4. Admin, back to back, run by the agent only after the maintainer's go-ahead in chat: rename `main` to `release`, rename `dev` to `main`, set - the default branch, create the `release` ruleset, delete the `dev` - ruleset, retarget open PRs. Release-plz PRs (such as #4596) stay on - `release`. Dry-run first and show the PR list with each PR's class. + the default branch, create the `release` ruleset, drop linear history + from the default-branch ruleset, delete the `dev` ruleset, retarget open + PRs. Release-plz PRs (such as #4596) stay on + `release`. Dry-run first and show the PR list with each PR's class. The + maintainer points the Cloudflare docs and demo builds at `release`. 5. Verify: a no-op push to `main` publishes nothing; release-plz runs on `release`; a chained publish builds `release` while trunk differs; the - back-merges of two publishes without a cut between both land on `main`. + back-merges of two patch publishes on `release`, without a cut between, + both land on `main`. Public API: none. Wire: none. Contributors see the new branch model. diff --git a/quest/m0/noq-reassembly-cap.md b/quest/m0/noq-reassembly-cap.md index cc4a200a02..61356b07fd 100644 --- a/quest/m0/noq-reassembly-cap.md +++ b/quest/m0/noq-reassembly-cap.md @@ -4,8 +4,8 @@ The QUIC stacks the relay builds on bound how many out-of-order chunks a stream or CRYPTO buffer holds, as quinn-proto 0.11.15 does, and a connection's -receive window is finite by default. Today `moq-noq-proto` 1.3.2 (`main`) and -2.0.0 (`dev`), and upstream `noq-proto` 1.3.0 (pulled in by the relay's default `iroh` +receive window is finite by default. Today `moq-noq-proto` 1.3.2 (`release`) and +2.0.0 (`main`), and upstream `noq-proto` 1.3.0 (pulled in by the relay's default `iroh` feature), predate quinn's fix, and `cargo audit` cannot match them because the crates are renamed. @@ -35,7 +35,7 @@ the crates are renamed. The in-tree [fork](/quest/m1/quic/fork/README.md) inherits the cap from quinn main, so after it lands only the receive-window default and iroh's noq -pin remain relevant on `dev`. +pin remain relevant on `main`. Public API: none. Wire: a peer that exceeds the chunk cap is closed. diff --git a/quest/m0/qmux-credit.md b/quest/m0/qmux-credit.md index eaf72e9e07..1fcec9cb1c 100644 --- a/quest/m0/qmux-credit.md +++ b/quest/m0/qmux-credit.md @@ -8,7 +8,7 @@ STOP_SENDING, so a long-lived session never stalls on MAX_DATA. `close()` delivers its APPLICATION_CLOSE frame before the transport drops, so a TCP or WebSocket peer sees the close code whenever the transport stays writable within the close bound. Both hold on the 0.5 line that -`main` pins and the 0.6 line `dev` uses. +`release` pins and the 0.6 line `main` uses. ## Plan @@ -39,9 +39,8 @@ Work: delivering past its initial window, and a peer reads the close code after a close issued mid-write. - Release on both lines (0.5.x after 0.5.2, and 0.6.x), then bump - `main`'s pin to that 0.5.x. `main` stays on 0.5: 0.6 needs - web-transport-trait 0.5, a breaking change that belongs on `dev`, which - picks up 0.6.x. + `main` to that 0.6.x and backport `release`'s pin to that 0.5.x. `release` + stays on 0.5: 0.6 needs web-transport-trait 0.5, a breaking change. Why m0: the WebSocket fallback and the planned edge-to-core `tls://` links both run on qmux, and MoQ drops streams constantly. diff --git a/quest/m1/README.md b/quest/m1/README.md index 7bda75e93d..c9111dbf1b 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -26,12 +26,12 @@ QUIC studies there on that rule. - [Epoch primitive](/quest/m1/epoch.md) - one `Epoch` type in moq-net and @moq/net, carried as a trailing `@` path segment, shared by e2ee and broadcast epochs - [Broadcast epochs](/quest/m1/broadcast-epoch/README.md) - each publish of a name gets a fresh `@` epoch, viewers follow the newest live one at once, and bare names still resolve on every version - [Cluster routing](/quest/m1/cluster-routing/README.md) - any node routes toward a broadcast's origin over CDN and P2P links alike, with per-origin routes and path-less announces -- [Remove `--hop`](/quest/m1/hop-removal.md) - on `dev`, redundant publishers share an explicit `@`, and `--hop` and the publisher's Hop ID are gone -- [One route cost](/quest/m1/route-cost.md) - on `dev`, Warm and Cold collapse to one static route cost -- [Delete removed cluster flags](/quest/m1/cluster-shims.md) - on `dev`, once a release has carried their refusals, `mesh` and `linger` leave `cluster::Config` +- [Remove `--hop`](/quest/m1/hop-removal.md) - redundant publishers share an explicit `@`, and `--hop` and the publisher's Hop ID are gone +- [One route cost](/quest/m1/route-cost.md) - Warm and Cold collapse to one static route cost +- [Delete removed cluster flags](/quest/m1/cluster-shims.md) - once a release has carried their refusals, `mesh` and `linger` leave `cluster::Config` - [Track tail interop](/quest/m1/track-tail-interop.md) - a Rust publisher ending a track with a group in flight is read to its end by the JS subscriber, and the reverse, in `just test interop` - [moq-mux truncated group](/quest/m1/mux-truncated-group.md) - a test pins that moq-mux skips a truncated spliced group as aborted -- [Demand everywhere](/quest/m1/demand-everywhere.md) - on `dev`, requests and producers at every level watch subscribers through `demand()` alone, ahead of the bindings exposing it +- [Demand everywhere](/quest/m1/demand-everywhere.md) - requests and producers at every level watch subscribers through `demand()` alone, ahead of the bindings exposing it - [Merge queue](/quest/m1/merge-queue.md) - the required checks run on `merge_group`, so a stale green check can no longer break main - [Binding audio delay](/quest/m1/binding-surface.md) - moq-ffi and every wrapper configure and observe audio playout delay - [FFI shape](/quest/m1/ffi-shape/README.md) - the bindings mirror Rust's layers: net at the root, then media, json, flate, audio, and video namespaces built from the handle below @@ -46,7 +46,7 @@ QUIC studies there on that rule. - [Delete the live marker](/quest/m1/announce-live-removal.md) - announce streams yield only route events; the `Live` marker and its hold machinery are gone - [Browser close code](/quest/m1/browser-close-code.md) - a playwright case proves the page reads a relay's close code and reason, on every web-transport backend - [SUBSCRIBE_DROP](/quest/m1/subscribe-drop.md) - every stream group in a lite subscription arrives or is dropped by name, and lite-07 drops its stream count for it -- [Subscribe ranges](/quest/m1/subscribe-ranges/README.md) - on dev, a lite-07 SUBSCRIBE asks for past and live ranges in either order and replaces FETCH; relays fill misses by range, including over moq-transport +- [Subscribe ranges](/quest/m1/subscribe-ranges/README.md) - a lite-07 SUBSCRIBE asks for past and live ranges in either order and replaces FETCH; relays fill misses by range, including over moq-transport - [Cross-relay bursts](/quest/m1/cross-relay-bursts.md) - bursty small-group tracks cross two relays without lost groups, unanswered FETCHes, or stalls - [Late lower groups](/quest/m1/lite-late-lower-group.md) - a moq-lite subscriber with a floor receives a group created below the first served one, as moq-transport does - [Parked reads wake](/quest/m1/parked-read-wakes.md) - a read parked on an evicted or aborted group wakes and re-judges, for resume successors and plain tracks alike @@ -64,10 +64,10 @@ QUIC studies there on that rule. - [Plan: max-delay](/quest/m1/plan-max-delay.md) - decide whether `max_age` becomes `max_delay` across the CLI, APIs, and bindings - [Wire compatibility](/quest/m1/wire-compat.md) - a nightly run tests this checkout against the last published release for tokens, session wire, and catalog/container - [Same-hop importers](/quest/m1/hop-aligned-import.md) - importers fed one stream publish identical groups and timestamps, so failover between a redundant pair survives -- [Auth client CA](/quest/m1/relay-auth-client-ca.md) - on dev, `auth::Config::validate` and `init` take the client-CA flag, so no caller can skip the check +- [Auth client CA](/quest/m1/relay-auth-client-ca.md) - `auth::Config::validate` and `init` take the client-CA flag, so no caller can skip the check - [TS damaged units](/quest/m1/ts-damaged-units.md) - one malformed PES or access unit is dropped, counted as `damaged`, and resynced at the next keyframe instead of ending the import - [RTMP interleaving](/quest/m1/rtmp-interleaving.md) - isolate partial messages before optimizing assembly copies -- [TS stats module](/quest/m1/ts-stats-module.md) - on dev, the TS stats types move under `ts::stats` as `Snapshot` and `Stream`, with an owned `track` +- [TS stats module](/quest/m1/ts-stats-module.md) - the TS stats types move under `ts::stats` as `Snapshot` and `Stream`, with an owned `track` - [Audio capture without ALSA link](/quest/m1/capture-alsa-link.md) - moq-audio capture and playback build on Linux without linking libasound - [Capture by default](/quest/m1/capture-default.md) - moq-video and moq-audio build `capture` by default, so pre-merge checks test it and the capture gate goes away - [Ship capture and playback](/quest/m1/cli-packaging.md) - a released moq binary can capture and play, which no distribution currently enables @@ -93,20 +93,19 @@ QUIC studies there on that rule. - [C++ through moq-ffi](/quest/m1/cpp/README.md) - generated C++ over moq-ffi with futures and expected-style errors, shipped as a tarball, vcpkg, and Conan, and adopted by the OBS plugin - [Generated C bindings](/quest/m1/c/README.md) - C generated from moq-ffi ships as `moq-c` 0.8.0 and replaces the hand-written libmoq - [The final libmoq release is the stub](/quest/m1/libmoq-final-release.md) - the release that lets `rs/libmoq` go -- [Retire the libmoq stub](/quest/m1/libmoq-retire.md) - on dev, the published `libmoq` crate stops after its final release points users at `moq-c` -- [dev's decoded-frame surface reaches main](/quest/m1/obs-decoded-surface.md) - the surface the OBS source decodes into is on main +- [Retire the libmoq stub](/quest/m1/libmoq-retire.md) - the published `libmoq` crate stops after its final release points users at `moq-c` - [OBS native codecs](/quest/m1/obs-moq-video/README.md) - replace FFmpeg video and audio decoding with moq-video and moq-audio, deliver GPU frames, and use native audio/video encoders - [AudioToolbox decode](/quest/m1/audio-decode-audiotoolbox.md) - macOS and iOS decode HE-AAC, multichannel AAC, and what else the framework offers - [HE-AAC catalog output](/quest/m1/he-aac-catalog-output.md) - HE-AAC catalog entries name the output rate and layout, not the LC core - [TS Opus export refusals](/quest/m1/ts-opus-export-refusals.md) - the TS exporter refuses Opus heads its channel code cannot describe instead of mislabeling them - [TS Opus channel codes](/quest/m1/ts-opus-channel-codes.md) - TS Opus with a channel code of 0x81 or above imports with its real head or is refused, never guessed as stereo - [AudioToolbox encode](/quest/m1/audio-encode-audiotoolbox.md) - macOS and iOS encode AAC-LC -- [AAC encode refusals](/quest/m1/aac-encode-refusals.md) - on dev, `Config::encode` refuses channel counts it cannot name instead of writing stereo +- [AAC encode refusals](/quest/m1/aac-encode-refusals.md) - `Config::encode` refuses channel counts it cannot name instead of writing stereo - [AAC parse truncated SBR](/quest/m1/aac-parse-truncated-sbr.md) - `Config::parse` refuses SBR and PS configs cut off before their core - [GStreamer surround Opus](/quest/m1/gst-opus-surround.md) - the moq-gst sink publishes 3 to 8 channel Opus with the OpusHead its caps describe - [TS AAC PCE joins](/quest/m1/ts-aac-pce-join.md) - PCE-described AAC over TS plays after a mid-stream join or resume, and a missing PCE silences only its track -- [FFI frame duration default](/quest/m1/ffi-frame-duration-default.md) - on dev, the binding audio encoder takes the codec's own frame by default, so `aac()` needs no explicit 0 -- [Opus mapping family](/quest/m1/opus-mapping-family.md) - on dev, the Opus head config keeps its mapping family only in `mapping` +- [FFI frame duration default](/quest/m1/ffi-frame-duration-default.md) - the binding audio encoder takes the codec's own frame by default, so `aac()` needs no explicit 0 +- [Opus mapping family](/quest/m1/opus-mapping-family.md) - the Opus head config keeps its mapping family only in `mapping` - [mp4-atom dOps mapping](/quest/m1/mp4-atom-dops-mapping.md) - a released mp4-atom reads and writes any `dOps` channel mapping family and table - [CMAF surround Opus](/quest/m1/cmaf-opus-surround.md) - fMP4 import and export carry an Opus channel mapping table - [A self-hosted NVIDIA runner is registered](/quest/m1/gpu-runner.md) - the maintainer registers the host that runs the NVIDIA tests @@ -116,16 +115,16 @@ QUIC studies there on that rule. - [Rendition preference](/quest/m1/rendition-preference.md) - automatic selection by ``, `Video::ranked`, and WHEP keeps the highest `preference` that decodes, so a compatibility transcode is only picked when nothing preferred decodes - [JS rendition ranking](/quest/m1/js-ranked.md) - `@moq/hang` ranks video renditions like Rust, and `@moq/watch`'s fallback uses it - [Audio rendition pick](/quest/m1/audio-ranked.md) - single-track FLV/RTMP and WHEP serve the best audio rendition, not the first by name -- [FLV catalog stream](/quest/m1/flv-catalog-stream.md) - on dev, `flv::Export` takes a catalog stream like fmp4, replacing `with_select` +- [FLV catalog stream](/quest/m1/flv-catalog-stream.md) - `flv::Export` takes a catalog stream like fmp4, replacing `with_select` - [io_uring flow control](/quest/m1/uring-flow-control-windows.md) - the relay's io_uring workers honor the QUIC flow-control windows instead of refusing them - [Own the QUIC stack](/quest/m1/quic/README.md) - quinn hard-forked in-tree as `moq-quic`, carrying BBR, reliable reset, hierarchical scheduling, peer limits, and endpoint sharding -- [QoS](/quest/m1/qos/README.md) - broadcast health: relay starvation and timeliness histograms, on dev +- [QoS](/quest/m1/qos/README.md) - broadcast health: relay starvation and timeliness histograms - [Catalog track alias](/quest/m1/catalog-track-alias.md) - catalog rendition keys become aliases with an optional `track` name, so one catalog lists renditions from several broadcasts - [Media stats](/quest/m1/stats/README.md) - publishers announce a stats track in the catalog, viewers answer a soliciting catalog through a per-catalog `.echo` broadcast, and a Rust encoder adapts to them - [JS group-boundary handover](/quest/m1/js-group-handover.md) - a JS track subscription carries across a route swap at a group boundary, so `test/drain` passes at zero latency budget - [JS GOAWAY requests](/quest/m1/js-goaway-requests.md) - after GOAWAY the JS client opens no new request on the old session, like Rust - [Drain handshakes](/quest/m1/drain-handshakes.md) - a drain GOAWAYs and waits for sessions still in their handshake instead of exiting under them -- [Strict Redirect::resolve](/quest/m1/redirect-resolve.md) - on dev, `Redirect::resolve` can no longer quietly turn a refused redirect into a redial +- [Strict Redirect::resolve](/quest/m1/redirect-resolve.md) - `Redirect::resolve` can no longer quietly turn a refused redirect into a redial - [Transport upgrade](/quest/m1/transport-upgrade/README.md) - a session that came up over WebSocket moves to QUIC once the QUIC dial lands, handing over at a group boundary - [Scope track priority](/quest/m1/track-priority-scope.md) - priority orders one owner's streams, and a shared cluster session is fair across tenants - [IETF on the ring](/quest/m1/uring-ietf.md) - the io_uring workers serve moq-transport sessions too, so a uring relay drops no client protocol diff --git a/quest/m1/aac-encode-refusals.md b/quest/m1/aac-encode-refusals.md index 5f6dca0290..18f97da5a2 100644 --- a/quest/m1/aac-encode-refusals.md +++ b/quest/m1/aac-encode-refusals.md @@ -8,8 +8,8 @@ refused, not logged and written as stereo, the way the ADTS writer refuses it. ## Plan -- `encode` returns `Bytes` today, so refusing is a published API break: this - retargets to `dev`. Decide whether it returns a `Result` or whether a +- `encode` returns `Bytes` today, so refusing is a published API break. + Decide whether it returns a `Result` or whether a constructor validates the count up front so encoding cannot fail. - A count a program config element could describe (7, or more than 8) is still refused unless the caller supplies a layout; guessing speaker diff --git a/quest/m1/announce-live-removal.md b/quest/m1/announce-live-removal.md index c58d2c7d13..3a57b69c20 100644 --- a/quest/m1/announce-live-removal.md +++ b/quest/m1/announce-live-removal.md @@ -31,7 +31,7 @@ a Live/Offline toggle, and a per-session `live()`: Guidance: -- The marker lives on `dev` only (Rust since #4059, JS since #4261, bindings +- The marker is unreleased, on `main` only (Rust since #4059, JS since #4261, bindings since #4266), so this deletion breaks nothing published. - Look for code that only exists to produce the marker, and delete it rather than stubbing it: replay/landing counters in the origin, holds taken by the @@ -44,7 +44,7 @@ Guidance: - Run `just test interop --all`. Public API: removes `AnnounceEvent::Live` / `{ kind: "live" }` / -`MoqAnnounceEvent::Live` and the binding aliases, on `dev`. Wire: none. +`MoqAnnounceEvent::Live` and the binding aliases. Wire: none. ## Required diff --git a/quest/m1/archive/README.md b/quest/m1/archive/README.md index 28cf3ecec7..c5313b0b1c 100644 --- a/quest/m1/archive/README.md +++ b/quest/m1/archive/README.md @@ -20,10 +20,10 @@ landed on main; the line may still reshape both. ### Compatibility Decided (2026-09-28): the line may break the hang `archive` catalog entry and -the recording format in place on `main`, without a version bump or a `dev` -detour (for example the track-timeline rework in #4280). No archives exist +the recording format in place on `main`, without a version bump +(for example the track-timeline rework in #4280). No archives exist yet, so nothing recorded or published depends on either shape. This is an -exception to the main/dev rule for this line only; once a release ships +exception for this line only; once a release ships recordings, later format changes go through the entry's format version. ### Landed diff --git a/quest/m1/audio-encode-audiotoolbox.md b/quest/m1/audio-encode-audiotoolbox.md index 6892e275b1..a293f3b563 100644 --- a/quest/m1/audio-encode-audiotoolbox.md +++ b/quest/m1/audio-encode-audiotoolbox.md @@ -17,7 +17,7 @@ the encode seam as the platform candidate on macOS and iOS. converter allows. - The seam assumes one packet per frame. If the converter holds output back, the backend needs a `flush` and a zero-or-more return, which changes - `Encoder::encode` and so targets `dev`. + `Encoder::encode`. - Gate the seam's "AAC refused without a platform encoder" test to hosts without one. - Regression: a stereo and a 5.1 encode round-trip through the AudioToolbox @@ -29,8 +29,7 @@ the encode seam as the platform candidate on macOS and iOS. duplicates as holes and resets AAC between them. Shift the whole timeline by the deficit instead, including the terminal end (found in #4081 review). - Until [FFI frame duration default](/quest/m1/ffi-frame-duration-default.md) - lands on `dev`, binding callers pass `frame_duration_us: 0` with `aac()`. - Split out on 2026-09-30 so this quest stays on `main`. + lands, binding callers pass `frame_duration_us: 0` with `aac()`. ## Required diff --git a/quest/m1/av-clock.md b/quest/m1/av-clock.md index fe19a25091..46bdab9aeb 100644 --- a/quest/m1/av-clock.md +++ b/quest/m1/av-clock.md @@ -12,7 +12,7 @@ the next re-anchor. ## Plan Moved from m0 to m1 in the 2026-09-30 audit: it waits on the whole jitter -line and is a published `@moq/watch` break on dev, so it is not in flight. +line and is a published `@moq/watch` break, so it is not in flight. Settled: per-track handles, and this quest lands them. `sync.track("audio")` and `sync.track("video")` each report their advertised delay and measured @@ -23,8 +23,7 @@ already keeps one jitter entry per track and grows into the handles. The measured spread per track comes from the [Audio jitter target](/quest/m0/audio-jitter-target/README.md) line, whose watch branch replaces `probe` with per-track spread inputs that this quest folds into the -handles. `SyncInput` is a published `@moq/watch` shape, so the break lands on -dev. +handles. `SyncInput` is a published `@moq/watch` shape, so this is a break. Recommendations for the implementation: diff --git a/quest/m1/bbr-ack-cleanup.md b/quest/m1/bbr-ack-cleanup.md index a0b9db7614..5d55cc9b4b 100644 --- a/quest/m1/bbr-ack-cleanup.md +++ b/quest/m1/bbr-ack-cleanup.md @@ -55,7 +55,7 @@ is needed. ## Required -- [Hard fork](/quest/m1/quic/fork/README.md) - lands in `moq-quic` on `dev`, not the frozen fork +- [Hard fork](/quest/m1/quic/fork/README.md) - lands in `moq-quic`, not the frozen fork ## Related diff --git a/quest/m1/broadcast-epoch/README.md b/quest/m1/broadcast-epoch/README.md index 3f8b69c857..5bcb9a014b 100644 --- a/quest/m1/broadcast-epoch/README.md +++ b/quest/m1/broadcast-epoch/README.md @@ -62,6 +62,6 @@ This README owns: - [Origin](/quest/m1/broadcast-epoch/origin.md) - moq-net publish mints an epoch, consumers follow the newest live one, and bare requests resolve to it on every version - [Apps](/quest/m1/broadcast-epoch/apps.md) - moq-cli, the browser publish and watch components, and demo/web publish under epochs and play bare names - [Gateways](/quest/m1/broadcast-epoch/gateways.md) - RTMP, SRT, and WHIP ingest mint an epoch per incoming connection, so an encoder reconnect is a clean takeover -- [TS restart](/quest/m1/broadcast-epoch/ts-restart.md) - on dev, a signalled backward TS discontinuity finishes the broadcast and continues the same input under a fresh epoch +- [TS restart](/quest/m1/broadcast-epoch/ts-restart.md) - a signalled backward TS discontinuity finishes the broadcast and continues the same input under a fresh epoch - [Bindings](/quest/m1/broadcast-epoch/bindings.md) - moq-ffi and every wrapper expose the epoch and inherit the default - [GStreamer and OBS](/quest/m1/broadcast-epoch/gst-obs.md) - moqsink and the OBS plugin publish each run under a fresh epoch diff --git a/quest/m1/broadcast-epoch/bindings.md b/quest/m1/broadcast-epoch/bindings.md index e7bb2876fd..3bc0239146 100644 --- a/quest/m1/broadcast-epoch/bindings.md +++ b/quest/m1/broadcast-epoch/bindings.md @@ -13,12 +13,11 @@ reconnect counter `session.epoch()` is renamed so "epoch" has one meaning. - Expose the parsed epoch (text and time) and an explicit-epoch publish argument. Keep the surface to what a binding consumer needs. - Rename `session.epoch()` in every binding (for example to `connects()`). - That is a break, so it lands on `dev`. + That is a break. - Update `doc/lib/{py,swift,kt,go,dart}` per the cross-package sync table, and run `just test smoke --all`. -Decided in the 2026-09-30 audit: libmoq is frozen (renamed `rs/moq-c` on -`dev`), so C and C++ consumers get epochs from moq-ffi. +Decided in the 2026-09-30 audit: libmoq is frozen (renamed `rs/moq-c`), so C and C++ consumers get epochs from moq-ffi. ## Required diff --git a/quest/m1/broadcast-epoch/origin.md b/quest/m1/broadcast-epoch/origin.md index 0c02eab0ba..c37879cccc 100644 --- a/quest/m1/broadcast-epoch/origin.md +++ b/quest/m1/broadcast-epoch/origin.md @@ -41,7 +41,7 @@ any wire change. Public API: behavior change on publish (the announced path gains an epoch, and a 32-part bare path is refused), on bare-name consume, and on what an -exact grant admits. Decide at PR time whether that retargets to `dev`. +exact grant admits. Wire: none. ## Related diff --git a/quest/m1/broadcast-epoch/ts-restart.md b/quest/m1/broadcast-epoch/ts-restart.md index 2e438ff31b..baa7fa3d13 100644 --- a/quest/m1/broadcast-epoch/ts-restart.md +++ b/quest/m1/broadcast-epoch/ts-restart.md @@ -2,7 +2,7 @@ ## Goal -On dev, when a TS feed rewinds its time base and signals it +When a TS feed rewinds its time base and signals it (`discontinuity_indicator` on the PCR PID, ISO/IEC 13818-1 2.4.3.5), `moq import ts` and `moq-srt` finish the current broadcast cleanly and publish the rest of the same input as a new broadcast under a fresh epoch, in the same @@ -13,7 +13,7 @@ to turn into an epoch. An unsignalled rewind stays fatal, as #4543 decided. ## Plan -Since #4543 (on dev), every rewind ends the import with `TimestampRewind` from +Since #4543, every rewind ends the import with `TimestampRewind` from `Producer::write`; the importer already reads the flag in `timebase_break`, and a flagged forward jump publishes break markers and carries on. @@ -40,7 +40,7 @@ Tests: a fixture with a flagged rewind publishes two broadcasts, the second starting at the rewound PTS; the same rewind unflagged still errors; one SRT connection carries both epochs. Update `doc/bin/cli.md` and `doc/bin/srt.md`. -Public API: breaking in moq-mux on dev, `ts::Import::decode` reports a restart +Public API: breaking in moq-mux, `ts::Import::decode` reports a restart and `restart` is new. Wire: none. ## Required diff --git a/quest/m1/c/README.md b/quest/m1/c/README.md index 92aec50f5d..65f9deb971 100644 --- a/quest/m1/c/README.md +++ b/quest/m1/c/README.md @@ -27,8 +27,8 @@ Decided: (NULL on success) with `moq_error_message()`, results come through out-params, records are owned structs with `moq__free`, and names are the uniffi names in snake case. -- The line targets `dev`: #4288 already renamed the hand-written crate to - `moq-c` (`rs/moq-c`) there, and the generated package takes over that name +- #4288 already renamed the hand-written crate to + `moq-c` (`rs/moq-c`), and the generated package takes over that name and `moq::c` target, so C users migrate once. Its first release is 0.8.0, a minor bump over the hand-written 0.7.x. - Docs change inline: the consumer quest rewrites `doc/lib/c`, and retirement @@ -38,7 +38,7 @@ The line owns the end-to-end check: every `doc/lib/c` sample and the C interop client build and run against the released 0.8.0 archive, not only in-tree. The hand-written crate gets no more feature work: its shutdown, CMake library, -and fetch quests were abandoned for this line, and hidden is done on dev. +and fetch quests were abandoned for this line, and hidden is done. ## Required diff --git a/quest/m1/c/package.md b/quest/m1/c/package.md index 3a8a602f62..800a128758 100644 --- a/quest/m1/c/package.md +++ b/quest/m1/c/package.md @@ -5,7 +5,7 @@ The generated C ships as the `moq-c` package, version 0.8.0: a release archive with the header, the moq-ffi staticlib, a CMake config exporting `moq::c`, and `moq-c.pc`, built the same way `cpp/moq` builds the C++ package. It replaces the -hand-written `rs/moq-c` crate's artifacts (renamed from libmoq on dev by #4288) +hand-written `rs/moq-c` crate's artifacts (renamed from libmoq by #4288) under the same names. ## Plan diff --git a/quest/m1/c/retire.md b/quest/m1/c/retire.md index 5a821f75e3..d54db0c9f2 100644 --- a/quest/m1/c/retire.md +++ b/quest/m1/c/retire.md @@ -8,8 +8,8 @@ it are deleted. `doc/setup/upgrade.md` tells C users how to move. ## Plan -- #4288 renamed libmoq to `moq-c` on dev and left a code-free `rs/libmoq` stub - whose last release points at `moq-c`; dev's +- #4288 renamed libmoq to `moq-c` and left a code-free `rs/libmoq` stub + whose last release points at `moq-c`; `quest/m1/libmoq-retire.md` deletes that stub. This quest retires the hand-written `rs/moq-c` itself, so fold in or delete that quest, whichever is still open. diff --git a/quest/m1/catalog-track-alias.md b/quest/m1/catalog-track-alias.md index d2abff6177..a41e1b59fe 100644 --- a/quest/m1/catalog-track-alias.md +++ b/quest/m1/catalog-track-alias.md @@ -25,8 +25,8 @@ track name would collapse them. - Open: a released reader ignores `track` and subscribes by the key, so it cannot play a rendition whose `track` differs from its alias. Candidates: accept that, since such a listing was not expressible before and a - publisher sets `track` only when it must; or move the reader change to - `dev` behind a catalog version. The maintainer settled it as additive on + publisher sets `track` only when it must; or gate the reader change + behind a catalog version. The maintainer settled it as additive on main; confirm the forward-compatibility cost before starting. - The MSF conversion in `rs/moq-mux` names MSF tracks by the key today. It carries the wire name and keeps the alias through a round-trip test, or diff --git a/quest/m1/cluster-routing/README.md b/quest/m1/cluster-routing/README.md index 63a54c88c0..84dbc08cdd 100644 --- a/quest/m1/cluster-routing/README.md +++ b/quest/m1/cluster-routing/README.md @@ -85,8 +85,7 @@ Cold collapse to one cost ([One route cost](/quest/m1/route-cost.md)); epoch-qualified paths are a source's identity ([Selection](/quest/m1/cluster-routing/selection.md)); cluster links are moq-lite only ([moq-transport peers are plain clients](/quest/m1/ietf-cluster-off.md)); -`--hop` removal and One route cost land on `dev` on their own. Children -changing the wip lite version count as additive and land on `main`. +`--hop` removal and One route cost land on their own. Anything specific to moq.pro's deployment is planned in moq.pro. ### Simulator findings @@ -141,8 +140,8 @@ Once every child has landed: - [Wildcard](/quest/m0/wildcard/README.md) - the longest-prefix rule, pool spread, and reply identity Selection builds on - [P2P](/quest/m2/p2p/README.md) - browser and native peers that become routing nodes over this layer -- [Remove `--hop`](/quest/m1/hop-removal.md) - on `dev`: redundant publishers share an explicit `@` -- [One route cost](/quest/m1/route-cost.md) - on `dev`: Warm and Cold collapse to one static cost +- [Remove `--hop`](/quest/m1/hop-removal.md) - redundant publishers share an explicit `@` +- [One route cost](/quest/m1/route-cost.md) - Warm and Cold collapse to one static cost - [Same-hop importers](/quest/m1/hop-aligned-import.md) - the importer half of a redundant pair; `--hop` removal re-keys it to a shared epoch - [Broadcast epochs](/quest/m1/broadcast-epoch/README.md) - a redundant pair shares one epoch - [Cross-relay delivery under bursts](/quest/m1/cross-relay-bursts.md) - its #4349 report shows closed broadcasts announced for up to 229 s diff --git a/quest/m1/cluster-shims.md b/quest/m1/cluster-shims.md index 5713f838f0..86a09cdd00 100644 --- a/quest/m1/cluster-shims.md +++ b/quest/m1/cluster-shims.md @@ -12,7 +12,7 @@ pointer to the replacement (`deprecated()` in `rs/moq-relay/src/cluster.rs`, checked there and in `config.rs`). The linger refusal shipped in moq-relay 0.15.0; the mesh refusal (#4601) ships in the next release. Once a release has carried both, delete the fields, `deprecated()`, and its checks, -per the no-shim rule. Lands on `dev`: removing public fields is a break. +per the no-shim rule. Removing public fields is a break. Public API: removes two hidden `cluster::Config` fields. Wire: none. diff --git a/quest/m1/data-consumer-timestamps.md b/quest/m1/data-consumer-timestamps.md index ec755c545d..aeefb0ec23 100644 --- a/quest/m1/data-consumer-timestamps.md +++ b/quest/m1/data-consumer-timestamps.md @@ -43,7 +43,7 @@ moq-ffi's json/flate consumers return the timestamp too, and the py, swift, kt, go, and dart wrappers and `doc/lib/*` follow. JS is [JS data consumer timestamps](/quest/m1/js-data-consumer-timestamps.md). -Public API: breaking, on `dev`. Wire: none. +Public API: breaking. Wire: none. ## Required diff --git a/quest/m1/demand-everywhere.md b/quest/m1/demand-everywhere.md index 9db4a1b3aa..2505cea76f 100644 --- a/quest/m1/demand-everywhere.md +++ b/quest/m1/demand-everywhere.md @@ -2,7 +2,7 @@ ## Goal -On `dev`, every model handle that serves subscribers watches them through a +Every model handle that serves subscribers watches them through a `Demand` handle returned by `demand()`: `track::Request`, `group::Request`, `group::Producer`, and `broadcast::Producer`. Their own `used`/`unused`/`is_used`/`poll_unused` copies are gone, in Rust and `js/net`. @@ -14,7 +14,7 @@ handler can see a group request abandoned. Facts (2026-10-01): `track::Demand` and `broadcast::Demand` already exist. [#4528](https://github.com/moq-dev/moq/pull/4528) (the track-demand quest) moved -`track::Producer` to `demand()` on `dev`, and deliberately kept group and +`track::Producer` to `demand()`, and deliberately kept group and broadcast `used`/`unused`, since group demand drives fetch coalescing. `broadcast::Producer` is already `demand()`-only, with `used`/`unused` on `broadcast::Demand`; there is no `group::Demand` yet. @@ -39,7 +39,7 @@ Decided (2026-10-01): Keep fetch coalescing's group demand semantics, and keep `abort_unused` where its race still needs an owner. -Public API: breaking on `dev` in moq-net and `@moq/net`. Wire: none. +Public API: breaking in moq-net and `@moq/net`. Wire: none. ## Related diff --git a/quest/m1/epoch.md b/quest/m1/epoch.md index 1ebdeaacdd..eb59eebb63 100644 --- a/quest/m1/epoch.md +++ b/quest/m1/epoch.md @@ -19,8 +19,8 @@ line builds on, and it replaces the e2ee-local `moq_e2ee::Epoch`. counts as an epoch, so an app's own UUID segments never parse as one. - Path helpers split a path into name and epoch and join them back. A segment like `@alice` is valid today and stays valid: strict parsing already keeps it - from reading as an epoch. Rejecting it instead would break the path contract - and land on `dev`. Check how the split interacts with + from reading as an epoch. Rejecting it instead would break the path contract. + Check how the split interacts with path patterns (done on the [auth line](/quest/m1/auth/README.md)) and hidden broadcasts (a leading `.`, see `doc/concept/moq-lite.md`). - `moq-e2ee` uses the shared type. Update diff --git a/quest/m1/fetch-ok-properties.md b/quest/m1/fetch-ok-properties.md index 69271af05b..cd8b066c16 100644 --- a/quest/m1/fetch-ok-properties.md +++ b/quest/m1/fetch-ok-properties.md @@ -9,7 +9,7 @@ meets draft 16+'s rule to include all of a track's properties. ## Plan -Found (2026-10-01) while merging main into dev (#4647): a group FETCH with no +Found (2026-10-01) in #4647: a group FETCH with no prior SUBSCRIBE_OK now reads its max age from FETCH_OK, but our publisher sends an empty block. diff --git a/quest/m1/ffi-frame-duration-default.md b/quest/m1/ffi-frame-duration-default.md index 56496f8c79..f5295dbb4e 100644 --- a/quest/m1/ffi-frame-duration-default.md +++ b/quest/m1/ffi-frame-duration-default.md @@ -2,7 +2,7 @@ ## Goal -On `dev`, `MoqAudioEncoderOutput::frame_duration_us` defaults to 0, the +`MoqAudioEncoderOutput::frame_duration_us` defaults to 0, the codec's own frame, so `MoqAudioCodec::aac()` works without an explicit 0 in every binding. Opus still encodes 20 ms frames by default. @@ -10,8 +10,7 @@ every binding. Opus still encodes 20 ms frames by default. - Decided in [#4183](https://github.com/moq-dev/moq/pull/4183): the uniffi default moves from 20000 to 0. Changing a published binding default is a - break, so this targets `dev`. Split from AudioToolbox encode on 2026-09-30, - since a child of a `main`-bound line cannot land on `dev`. + break. Split from AudioToolbox encode on 2026-09-30. - `rs/moq-ffi/src/audio.rs` already treats 0 as the codec's frame; the `default_frame_duration_matches_moq_audio` test that pins 20000 to moq-audio's default goes away with the literal. diff --git a/quest/m1/ffi-shape/README.md b/quest/m1/ffi-shape/README.md index 776638077b..06db0e1b2d 100644 --- a/quest/m1/ffi-shape/README.md +++ b/quest/m1/ffi-shape/README.md @@ -11,7 +11,7 @@ and verb maps to a Rust one. A docs page shows the layers in each language. ## Plan Lands after the release, as one binding break in Python, Go, Swift, and -Kotlin (Dart is unpublished), so its PRs retarget to `dev`. +Kotlin (Dart is unpublished). Settled shape: diff --git a/quest/m1/ffi-shape/net.md b/quest/m1/ffi-shape/net.md index dd45e3ea40..f057703119 100644 --- a/quest/m1/ffi-shape/net.md +++ b/quest/m1/ffi-shape/net.md @@ -20,8 +20,8 @@ are renamed. today. - The cpp line's client-config quest (`quest/m1/cpp/client-config.md` on branch `quest/m1/cpp/README`) ships this same `MoqClientConfig` record - additively on `main`. Decided in the 2026-09-30 audit: this quest then only - removes the fallible setters on `dev`, rather than designing the record + additively. Decided in the 2026-09-30 audit: this quest then only + removes the fallible setters, rather than designing the record twice. - Objects that are only getters become records (`AnnounceUpdate` today). Handles with verbs (`Request`, `TrackRequest`, `GroupRequest`) stay objects. diff --git a/quest/m1/flv-catalog-stream.md b/quest/m1/flv-catalog-stream.md index 72d52b79d0..a0eec4cb76 100644 --- a/quest/m1/flv-catalog-stream.md +++ b/quest/m1/flv-catalog-stream.md @@ -2,12 +2,12 @@ ## Goal -On `dev`, `flv::Export` is built from a catalog stream like `fmp4::Export`, so +`flv::Export` is built from a catalog stream like `fmp4::Export`, so callers narrow renditions with `catalog::Stream::select` and the FLV-only `with_select` builder is gone. ## Plan -This is a published API break, so it targets `dev`. Consider whether the TS +This is a published API break. Consider whether the TS and Matroska exports should take the same shape in the same pass. RTMP play passes its client-capability selection through the stream instead. diff --git a/quest/m1/hop-aligned-import.md b/quest/m1/hop-aligned-import.md index 56ec73b95b..045f9a12c2 100644 --- a/quest/m1/hop-aligned-import.md +++ b/quest/m1/hop-aligned-import.md @@ -14,7 +14,7 @@ exits (#4354). Decided: same-hop publishers MUST publish the same broadcasts and tracks. Keep `--hop` for now: the users' bugs are on one relay today. Cluster -routing replaces it with a shared explicit epoch on `dev` (decided +routing replaces it with a shared explicit epoch (decided 2026-09-30), and its [`--hop` removal](/quest/m1/hop-removal.md) re-keys this quest's docs and tests. The importer work holds under either key. Make every container importer (ts, fmp4, flv, mkv, and the @@ -44,5 +44,5 @@ survives the standby joining and the incumbent stopping. ## Related -- [Remove `--hop`](/quest/m1/hop-removal.md) - on dev, a redundant pair shares an explicit epoch instead, and this quest's docs and tests are re-keyed to it +- [Remove `--hop`](/quest/m1/hop-removal.md) - a redundant pair shares an explicit epoch instead, and this quest's docs and tests are re-keyed to it - [Broadcast epochs](/quest/m1/broadcast-epoch/README.md) - a redundant pair shares one epoch diff --git a/quest/m1/hop-removal.md b/quest/m1/hop-removal.md index f6cf874015..0159d26f06 100644 --- a/quest/m1/hop-removal.md +++ b/quest/m1/hop-removal.md @@ -34,7 +34,7 @@ idle timeout (a failover service that retracts it, or active-active delivery to the relay). Neither is required to land this. Public API and wire: removes a CLI flag and the publisher's Hop setup -parameter; lands on `dev`, outside the cluster routing line (which lands on `main`). +parameter; lands outside the cluster routing line. ## Required diff --git a/quest/m1/js-data-consumer-timestamps.md b/quest/m1/js-data-consumer-timestamps.md index 1deaa229e5..bfd1b95fc4 100644 --- a/quest/m1/js-data-consumer-timestamps.md +++ b/quest/m1/js-data-consumer-timestamps.md @@ -22,7 +22,7 @@ order), and `latest()` skips to the newest state, today's behavior. A reader that falls behind still jumps to the newest group. Update `doc/lib/js/{json,flate}.md`. -Public API: breaking, on `dev`. Wire: none. +Public API: breaking. Wire: none. ## Required diff --git a/quest/m1/js-fetch.md b/quest/m1/js-fetch.md index 50ac6349eb..e029fe6a9d 100644 --- a/quest/m1/js-fetch.md +++ b/quest/m1/js-fetch.md @@ -35,7 +35,6 @@ Run the supported-draft matrix and `just test interop --all` through CI. Public API: a producer-side on-demand group request surface in `@moq/net`, matching Rust's lifecycle. Wire: implement the existing supported IETF FETCH formats; update relevant documentation and any MoQ draft claims that change. -A published API break, if the chosen shape requires one, goes through dev. ## Required diff --git a/quest/m1/js-publish-timestamp.md b/quest/m1/js-publish-timestamp.md index 4a485b6c91..c0b9667288 100644 --- a/quest/m1/js-publish-timestamp.md +++ b/quest/m1/js-publish-timestamp.md @@ -21,7 +21,7 @@ the same type in Callers in the repository (js/hang catalog, js/publish, js/room) pass their clock's now explicitly. Update `doc/lib/js/{json,flate,net}.md`. -Public API: breaking, on `dev`. Wire: none. +Public API: breaking. Wire: none. ## Required diff --git a/quest/m1/libmoq-final-release.md b/quest/m1/libmoq-final-release.md index 9ddcca1467..2792144099 100644 --- a/quest/m1/libmoq-final-release.md +++ b/quest/m1/libmoq-final-release.md @@ -2,7 +2,7 @@ ## Goal -The newest `libmoq` on crates.io is the code-free stub built from `dev`'s +The newest `libmoq` on crates.io is the code-free stub built from `main`'s `rs/libmoq`, whose README points at `moq-c`. This quest tracks a condition outside the repository. When it holds, delete @@ -11,5 +11,5 @@ this quest and every `Required` entry that links it. ## Plan As of 2026-10-02 the newest release is libmoq 0.6.9 (2026-09-30), cut from -`main`'s full crate, not the stub. Check with +`release`'s full crate, not the stub. Check with `curl -s https://crates.io/api/v1/crates/libmoq` for a newer `max_version`. diff --git a/quest/m1/mobile-ownership.md b/quest/m1/mobile-ownership.md index d16beae6f1..db1c5bf41c 100644 --- a/quest/m1/mobile-ownership.md +++ b/quest/m1/mobile-ownership.md @@ -13,7 +13,7 @@ Decided in the 2026-09-30 audit: option 2 from #700, Rust owns codecs. The tree already shipped it. `moq-ffi` defaults to the `audio` and `video` features (`rs/moq-ffi/Cargo.toml:27`), so the Kotlin and Swift packages already carry the Rust codecs, and #4094 added the `CVPixelBuffer` bridge -(`MoqVideoSurface::PixelBuffer` on `dev`). Option 1 would add a second media +(`MoqVideoSurface::PixelBuffer`). Option 1 would add a second media stack beside one that exists. What remains is recording the verdict in `rs/moq-ffi/AGENTS.md` (a maintainer diff --git a/quest/m1/obs-decoded-surface.md b/quest/m1/obs-decoded-surface.md deleted file mode 100644 index 0585da10b1..0000000000 --- a/quest/m1/obs-decoded-surface.md +++ /dev/null @@ -1,14 +0,0 @@ -# [XS] dev's decoded-frame surface reaches main - -## Goal - -`main` contains the moq-ffi decoded-frame surface from `dev` -([#4094](https://github.com/moq-dev/moq/pull/4094), renamed in `97575f002`). - -This quest tracks a condition outside the repository. When it holds, delete -this quest and every `Required` entry that links it. - -## Plan - -As of 2026-09-30 both are on `dev` only. Check with -`git merge-base --is-ancestor 97575f002 origin/main`. diff --git a/quest/m1/obs-moq-video/README.md b/quest/m1/obs-moq-video/README.md index f44a258241..44be96f105 100644 --- a/quest/m1/obs-moq-video/README.md +++ b/quest/m1/obs-moq-video/README.md @@ -14,7 +14,7 @@ Initial video decoding covers H.264, HEVC, and AV1 where moq-video has an availa Publishing remains opt-in, with one **Use MoQ encoders** choice for video and audio. Keep the existing OBS encoder mode. Internal OBS encoder adapters call moq-video/moq-audio, preserving OBS's A/V handling and the existing encoded MoQ output. The combined choice is enabled only when both adapters are present. Start with H.264, supported HEVC, and Opus; defer AV1/AAC encoding and PCM publishing UI. Keep bitrate separate from **Low latency**, **Balanced** (default), and **Quality** presets. Presets describe supported buffering/compression controls, not an end-to-end delay promise. -The quests separate portable decoding, audio, and publishing so each can land and be validated independently. moq-ffi's decoded frames own their surface and convert to CPU pixels only on request (#4094, on `dev`); a surface decode exposes the platform surface as a borrowed view, which each platform GPU quest extends to its own surface type. +The quests separate portable decoding, audio, and publishing so each can land and be validated independently. moq-ffi's decoded frames own their surface and convert to CPU pixels only on request (#4094); a surface decode exposes the platform surface as a borrowed view, which each platform GPU quest extends to its own surface type. Decided in the 2026-09-30 audit: the Windows and Linux GPU decode paths and the macOS and Windows GPU encoder inputs moved to m2 (listed under Related), because each needs physical-hardware proof, and Linux GPU input is already m2/m3. diff --git a/quest/m1/obs-moq-video/source.md b/quest/m1/obs-moq-video/source.md index 36161bb16d..e7b5e9a77a 100644 --- a/quest/m1/obs-moq-video/source.md +++ b/quest/m1/obs-moq-video/source.md @@ -7,7 +7,7 @@ The MoQ source loads and plays supported video without FFmpeg's video libraries ## Plan - Replace `cpp/obs/src/moq-source.cpp` video decode and conversion with moq-video through the generated C++ package: the moq-ffi video consumer for the CPU path, including frame ownership and cancellation semantics. Support H.264, HEVC, and available AV1 decoding; report unsupported VP8/VP9 explicitly until their follow-up lands. -- Decode with `MoqVideoDecoderOutput.surface` set: each `MoqVideoDecodedFrame` retains the decoder's surface, `surface()` borrows it (`MoqVideoSurface::PixelBuffer` on macOS) for as long as the frame lives, and `pixels(format)` is the CPU fallback. A platform with no `MoqVideoSurface` variant refuses the opt-in. Hold the frame until OBS's GPU work reading it completes; held frames hold decoder pool slots. This surface is on `dev` only (#4094, renamed in 97575f002). +- Decode with `MoqVideoDecoderOutput.surface` set: each `MoqVideoDecodedFrame` retains the decoder's surface, `surface()` borrows it (`MoqVideoSurface::PixelBuffer` on macOS) for as long as the frame lives, and `pixels(format)` is the CPU fallback. A platform with no `MoqVideoSurface` variant refuses the opt-in. Hold the frame until OBS's GPU work reading it completes; held frames hold decoder pool slots. This surface is unreleased (#4094, renamed in 97575f002). - Implement the macOS presentation probe immediately: retain VideoToolbox PixelBuffer/IOSurface storage, inspect OBS graphics import and rendering support, and convert to OBS's expected color format on the GPU if necessary. Adapt the source render path to import textures on the graphics thread; preserve source timing instead of simply drawing the newest frame. An asynchronous CPU source API alone does not prove native GPU delivery. - Bound decoded frames retained by the render thread. On import failure, switch to the existing I420 delivery path and show the reason in Stats. Keep fallback stable for the stream/device configuration rather than retrying every frame; re-probe on a relevant configuration change or restart. Device loss and resize must retire old surfaces only after rendering completes. - Preserve timestamps, range/primaries, stride and plane layout, catalog/rendition changes, reconnect, visibility/deactivation behavior, and existing source settings. Carry frame-generation identity so late callbacks cannot display frames from a replaced source. @@ -17,7 +17,6 @@ The MoQ source loads and plays supported video without FFmpeg's video libraries ## Required - [OBS migration](/quest/m1/cpp/obs.md) - the plugin is on the generated C++ before decode changes -- [dev's decoded-frame surface reaches main](/quest/m1/obs-decoded-surface.md) - the surface this decodes into ## Related diff --git a/quest/m1/open-gop-leading-pictures.md b/quest/m1/open-gop-leading-pictures.md index fe83d04f39..a932cd5101 100644 --- a/quest/m1/open-gop-leading-pictures.md +++ b/quest/m1/open-gop-leading-pictures.md @@ -62,7 +62,7 @@ decode, and the JS test should drive the software decoder. unproven delivered hole, or a latency skip, but not on the subscribe itself. Add the equivalent of JS `continuous`: false on the first frame after the subscribe and after every bump, true otherwise. It changes the moq-mux - consumer API, so pick main or dev by whether the shape is additive. + consumer API. - Tests: a synthetic group with a keyframe followed by two earlier-stamped deltas is trimmed on the first group and kept on the second; and a viewer that plays continuously, then latency-skips into a later open GOP, has that diff --git a/quest/m1/opus-mapping-family.md b/quest/m1/opus-mapping-family.md index 9bd243a1dd..29007429bc 100644 --- a/quest/m1/opus-mapping-family.md +++ b/quest/m1/opus-mapping-family.md @@ -2,10 +2,10 @@ ## Goal -On dev, `moq_mux::codec::opus::Config` stores the channel mapping family in one place: `mapping: Option`. The `mapping_family: u8` field that #4130 shipped in moq-mux 0.10.8 is removed, and every reader takes the family from `mapping`. +`moq_mux::codec::opus::Config` stores the channel mapping family in one place: `mapping: Option`. The `mapping_family: u8` field that #4130 shipped in moq-mux 0.10.8 is removed, and every reader takes the family from `mapping`. ## Plan -Decided 2026-09-28 while merging main into the audio-codecs line: that sync keeps both fields so the line stays additive on main, and `encode` refuses a `mapping_family` that disagrees with `mapping`. Removing the duplicate breaks the published moq-mux API, so it lands on dev. Update main's fMP4 and MSF paths that read `mapping_family`, plus the TS importer, which sets both fields. +Decided 2026-09-28 while merging main into the audio-codecs line: that sync keeps both fields so the line stays additive on main, and `encode` refuses a `mapping_family` that disagrees with `mapping`. Removing the duplicate breaks the published moq-mux API. Update the fMP4 and MSF paths that read `mapping_family`, plus the TS importer, which sets both fields. Public API: breaking in moq-mux (field removed). Wire: none. diff --git a/quest/m1/perf/3122-moq-uring-2-5-of-relay-cpu-is-vdso-clock-reads-the-drive.md b/quest/m1/perf/3122-moq-uring-2-5-of-relay-cpu-is-vdso-clock-reads-the-drive.md index 4d9317643d..8fa01db570 100644 --- a/quest/m1/perf/3122-moq-uring-2-5-of-relay-cpu-is-vdso-clock-reads-the-drive.md +++ b/quest/m1/perf/3122-moq-uring-2-5-of-relay-cpu-is-vdso-clock-reads-the-drive.md @@ -8,7 +8,7 @@ A worker drive turn reads the clock once and hands that instant down, so ## Plan -Profiling the io_uring relay (`dev` @ `fc57e0175`, `perf record -F 499`, +Profiling the io_uring relay (`fc57e0175`, `perf record -F 499`, relay process only) shows `[vdso]` as a top-5 DSO, at roughly 3x its share on the tokio worker path: diff --git a/quest/m1/perf/README.md b/quest/m1/perf/README.md index dc7aa2ce62..da0792c0dd 100644 --- a/quest/m1/perf/README.md +++ b/quest/m1/perf/README.md @@ -14,9 +14,7 @@ outcome that abandons the quest. ## Plan -Quests branch from main unless they say otherwise; -[Run to quiescence](/quest/m1/perf/uring-quiescence.md) needs dev, where -`kio`'s `Tasks::poll` changed (#4156). +Quests branch from main unless they say otherwise. Planning quests can settle their contracts independently. Facts from the 2026-09 hot-path survey, so quests don't re-litigate them: @@ -66,4 +64,4 @@ the rest. - [Ingest batch](/quest/m1/perf/ingest-batch.md) - relay ingest pays one lock, wake, and clock read per chunk burst instead of per chunk - [#3122](/quest/m1/perf/3122-moq-uring-2-5-of-relay-cpu-is-vdso-clock-reads-the-drive.md) - moq-uring: ~2.5% of relay CPU is vdso clock reads; the drive loop and its callers each re-read Instant::now() - [#3199](/quest/m1/perf/3199-moq-uring-remove-sq-indirection-and-per-enter-ring-fd.md) - moq-uring: remove SQ indirection and per-enter ring fd lookup -- [Remove moq-uring copies](/quest/m1/perf/uring-copies.md) - egress, stream send and receive, and datagram receive stop copying where the QUIC core already allows it, on `dev` after the fork +- [Remove moq-uring copies](/quest/m1/perf/uring-copies.md) - egress, stream send and receive, and datagram receive stop copying where the QUIC core already allows it, after the fork diff --git a/quest/m1/perf/uring-copies.md b/quest/m1/perf/uring-copies.md index 591596afc0..8fa7b0d290 100644 --- a/quest/m1/perf/uring-copies.md +++ b/quest/m1/perf/uring-copies.md @@ -23,8 +23,7 @@ Found in the 2026-09-30 fork planning; none needs a QUIC API change: The clock-read fix stays in [#3122](/quest/m1/perf/3122-moq-uring-2-5-of-relay-cpu-is-vdso-clock-reads-the-drive.md). -Measure with the echo benches and `just bench`. This branches from `dev`, -where the fork lands; paths above are pre-fork names. +Measure with the echo benches and `just bench`. Paths above are pre-fork names. ## Required diff --git a/quest/m1/perf/uring-quiescence.md b/quest/m1/perf/uring-quiescence.md index ba226bd729..35b6f0bef0 100644 --- a/quest/m1/perf/uring-quiescence.md +++ b/quest/m1/perf/uring-quiescence.md @@ -15,8 +15,8 @@ exhaustion, or to a quantum, before touching the ring. ## Plan -Branch from dev: `kio`'s `Tasks::poll` changed only there (#4156 merged -`Pollable` into `Task`), and the pass budget builds on that version. Keep the fairness the one-pass rule protects: a forward wake +The pass budget builds on `kio`'s current `Tasks::poll` (#4156 merged +`Pollable` into `Task`). Keep the fairness the one-pass rule protects: a forward wake chain must not starve the caller's other arms, and one connection's backlog must not starve the socket. diff --git a/quest/m1/plan-max-delay.md b/quest/m1/plan-max-delay.md index a8c700c497..28feaeeb16 100644 --- a/quest/m1/plan-max-delay.md +++ b/quest/m1/plan-max-delay.md @@ -23,5 +23,5 @@ To weigh: - The wire fields are `Publisher Max Age` and `Subscriber Max Age` in `drafts/draft-lcurley-moq-lite.md`. Renaming the draft's field is free on the wire, but it churns the spec. -- A rename breaks every published API and binding, so it lands on `dev`, +- A rename breaks every published API and binding, so it lands mirrored across Rust, JS, and the bindings in one release. diff --git a/quest/m1/publish-timestamp.md b/quest/m1/publish-timestamp.md index 0426aa75c5..86349bc552 100644 --- a/quest/m1/publish-timestamp.md +++ b/quest/m1/publish-timestamp.md @@ -61,7 +61,7 @@ fill `Timestamp::now()` (moq-net's clock, not the broadcast's), and moq-mux's The broadcast-clock input was requested by OneTooMany, who align KLV and MAVLink-derived telemetry with video. -Public API: breaking, on `dev`. Wire: none here; absence on the wire lands +Public API: breaking. Wire: none here; absence on the wire lands with the untimed implementation quests. ## Required diff --git a/quest/m1/qos/README.md b/quest/m1/qos/README.md index 96e59d38cd..0a0f2deed0 100644 --- a/quest/m1/qos/README.md +++ b/quest/m1/qos/README.md @@ -30,8 +30,8 @@ The counters and channels land here. The moq.pro (downstream) dashboard work, including the health badge, connection-health drill-down, and stream preflight, consumes them downstream. -Decided (2026-09-28): the whole line targets `dev`, because its moq-stats -changes break the published crate. Client stats left the line (2026-09-29) +Decided (2026-09-28): the line's moq-stats changes break the published +crate. Client stats left the line (2026-09-29) when media stats moved out of moq-stats onto hang tracks, which are additive on `main`. diff --git a/quest/m1/quic/README.md b/quest/m1/quic/README.md index 2cab9c3e8c..802190d376 100644 --- a/quest/m1/quic/README.md +++ b/quest/m1/quic/README.md @@ -18,9 +18,8 @@ gate this line. Decided in the 2026-09-30 plan: the moq-dev/noq soft fork is replaced by a hard fork of quinn in `rs/`, so every QUIC change here waits for -[the fork](/quest/m1/quic/fork/README.md) and lands on `dev`. Retarget this -line's PR (#3975) to `dev` when the fork starts. moq-dev/noq is frozen to -security patches for `main`. +[the fork](/quest/m1/quic/fork/README.md). moq-dev/noq is frozen to +security patches for `release`. Older quests say "the fork" or "noq"; read that as `moq-quic`. Their steps to publish a fork release, pin it, or offer a change upstream are superseded: diff --git a/quest/m1/quic/bbr-app-limited-edges.md b/quest/m1/quic/bbr-app-limited-edges.md index fdcc2b07b2..8884cf0aee 100644 --- a/quest/m1/quic/bbr-app-limited-edges.md +++ b/quest/m1/quic/bbr-app-limited-edges.md @@ -34,7 +34,7 @@ change is intended. ## Required -- [Hard fork](/quest/m1/quic/fork/README.md) - lands in `moq-quic` on `dev`, not the frozen fork +- [Hard fork](/quest/m1/quic/fork/README.md) - lands in `moq-quic`, not the frozen fork ## Related diff --git a/quest/m1/quic/bbr-app-limited.md b/quest/m1/quic/bbr-app-limited.md index 47c10053ad..90fc3e496c 100644 --- a/quest/m1/quic/bbr-app-limited.md +++ b/quest/m1/quic/bbr-app-limited.md @@ -31,7 +31,7 @@ quest. ## Required -- [Hard fork](/quest/m1/quic/fork/README.md) - lands in `moq-quic` on `dev`, not the frozen fork +- [Hard fork](/quest/m1/quic/fork/README.md) - lands in `moq-quic`, not the frozen fork ## Closes diff --git a/quest/m1/quic/fork/README.md b/quest/m1/quic/fork/README.md index c016b0574e..d1abf0cd89 100644 --- a/quest/m1/quic/fork/README.md +++ b/quest/m1/quic/fork/README.md @@ -33,22 +33,22 @@ them: moved to noq in the first place (#1706, #3342, #3789). - **Multipath is deleted**, and with it the multipath spike. - **In-tree**, so `just check` and CI cover the stack, one PR spans the core - and its consumers, and `main` and `dev` each carry their own copy instead of + and its consumers, and `release` and `main` each carry their own copy instead of double-landing fixes on the 1.3 and 2.0 fork lines. - **Named by role**: `moq-quic`, `moq-quic-udp`, `moq-quic-tokio`; `web-transport-moq` keeps its name. Each crate's README and license credit quinn and noq. -- **Lands on `dev`.** moq-tokio exposes `noq::Endpoint`, +- **A break.** moq-tokio exposes `noq::Endpoint`, `noq::TransportConfig`, and `noq::Incoming` publicly, so the crate swap is a break. - **Security folds into the fork.** Rebasing on quinn picks up the 2026 - advisories noq is missing; `main` stays on `moq-noq` until `dev` releases. + advisories noq is missing; `release` stays on `moq-noq` until the next cut. [noq reassembly cap](/quest/m0/noq-reassembly-cap.md) still finishes on the - old fork for `main`. + old fork for `release`. - **moq-dev/noq is frozen**: security patches for `main` only, archived once no released MoQ crate depends on it. The other [QUIC quests](/quest/m1/quic/README.md) wait for this line and land - in-tree on `dev`. + in-tree. This README's own work: delete moq-dev/noq's `moq-sync.yml`, mark its README frozen, and document advisory triage in `rs/moq-quic/README.md`. `cargo audit` diff --git a/quest/m1/quic/peer-limits.md b/quest/m1/quic/peer-limits.md index 2646f5a6ff..3cff0b7630 100644 --- a/quest/m1/quic/peer-limits.md +++ b/quest/m1/quic/peer-limits.md @@ -32,7 +32,7 @@ viewer session does not; the io_uring path applies the same values; a ## Required -- [Hard fork](/quest/m1/quic/fork/README.md) - lands in `moq-quic` on `dev`, not the frozen fork +- [Hard fork](/quest/m1/quic/fork/README.md) - lands in `moq-quic`, not the frozen fork - [io_uring flow control](/quest/m1/uring-flow-control-windows.md) - the io_uring workers hardcode their windows today, so the peer values have nothing to raise there until the static windows reach them diff --git a/quest/m1/quic/reliable-reset.md b/quest/m1/quic/reliable-reset.md index eca1feab38..7ec7024ba8 100644 --- a/quest/m1/quic/reliable-reset.md +++ b/quest/m1/quic/reliable-reset.md @@ -66,7 +66,7 @@ provisional codepoints if the document changes before release. ## Required -- [Hard fork](/quest/m1/quic/fork/README.md) - lands in `moq-quic` on `dev`, not the frozen fork +- [Hard fork](/quest/m1/quic/fork/README.md) - lands in `moq-quic`, not the frozen fork ## Related diff --git a/quest/m1/quic/scheduler.md b/quest/m1/quic/scheduler.md index e6f995919f..6cad87bb3a 100644 --- a/quest/m1/quic/scheduler.md +++ b/quest/m1/quic/scheduler.md @@ -81,8 +81,7 @@ Record queue/lock work, CPU, throughput, latency, and byte fairness on the same congested workloads. Both equal-priority subscriptions must progress while a higher-priority subscription preempts them and each subscription sheds its own old backlog. Measure the full scope of trait and adapter changes -before publishing the API; any published break targets dev under the normal -release policy. +before publishing the API. Retransmissions follow the same hierarchy. noq already re-queues a lost range through the stream's priority (`StreamsState::retransmit`), so a lost @@ -104,7 +103,7 @@ where the new implementation makes it redundant. ## Required -- [Hard fork](/quest/m1/quic/fork/README.md) - lands in `moq-quic` on `dev`, not the frozen fork +- [Hard fork](/quest/m1/quic/fork/README.md) - lands in `moq-quic`, not the frozen fork ## Closes diff --git a/quest/m1/raw-stream-codes.md b/quest/m1/raw-stream-codes.md index 7ddf9891e3..4ba1a415bc 100644 --- a/quest/m1/raw-stream-codes.md +++ b/quest/m1/raw-stream-codes.md @@ -15,9 +15,9 @@ on stream errors. WebTransport sessions keep the mapping they need. `web-transport-iroh` and `web-transport-quinn` (moq-dev/web-transport) do the same. A raw peer's code 5 reads as `None` or another value, and ours reaches it as a large HTTP/3 code. -- Close codes (#4262, merged on `dev` 09-28) fixed the same mix-up for - `ApplicationClosed` and brought `web-transport-trait` 0.5, which exists - only on `dev`, so this targets `dev`. Let each stream know whether its +- Close codes (#4262, merged 09-28) fixed the same mix-up for + `ApplicationClosed` and brought `web-transport-trait` 0.5, which is + unreleased. Let each stream know whether its session is raw and skip the mapping there, in all three adapters, with a round-trip test per adapter against a plain QUIC peer. - Release the fixed crates and bump the pins here in the same quest; published diff --git a/quest/m1/redirect-resolve.md b/quest/m1/redirect-resolve.md index 8e31590936..6280dea19d 100644 --- a/quest/m1/redirect-resolve.md +++ b/quest/m1/redirect-resolve.md @@ -19,7 +19,6 @@ the connection it documents. Recommendation: make it private. Nothing outside `moq-tokio` calls it (only its own unit tests), and the repo keeps things private until a consumer needs them. If a consumer turns up, the alternative is returning the same -`Result>` as the internal `target` does, so empty and refused stay distinct. Removing or changing a published method is a break, so this -targets `dev`; update `doc/lib/rs` if it mentions the method. +`Result>` as the internal `target` does, so empty and refused stay distinct. Removing or changing a published method is a break; +update `doc/lib/rs` if it mentions the method. -Start by merging `main` into `dev` if `dev` does not have the drain line yet. diff --git a/quest/m1/relay-auth-client-ca.md b/quest/m1/relay-auth-client-ca.md index 3dec129961..d020ee67f9 100644 --- a/quest/m1/relay-auth-client-ca.md +++ b/quest/m1/relay-auth-client-ca.md @@ -2,7 +2,7 @@ ## Goal -On dev, no caller of `moq_relay::auth::Config` can start with `--auth-public` +No caller of `moq_relay::auth::Config` can start with `--auth-public` rules alongside a listener TLS client CA by forgetting a check. `Config::validate` takes the client-CA flag, `init` requires it too, and `validate_client_ca` is gone. The `moq` CLI fails loud on an invalid auth @@ -11,7 +11,7 @@ config instead of quietly refusing every session. ## Plan - [#4364](https://github.com/moq-dev/moq/pull/4364) (merged 09-28, and - `dev` has merged `main` since) added an additive + now on `main`) added an additive `Config::validate_client_ca(&self, client_ca: bool)` that `Relay::load` and the CLI must each remember to call; the CLI missing the original check is the bug it fixes. Fold it into `validate(&self, client_ca: bool)` so every @@ -34,4 +34,4 @@ config instead of quietly refusing every session. the methods. Public API: breaks `moq_relay::auth::Config::validate` and `init`, removes -`validate_client_ca`, so this targets `dev`. Wire: none. +`validate_client_ca`. Wire: none. diff --git a/quest/m1/relay-iroh-opt-in.md b/quest/m1/relay-iroh-opt-in.md index 522796d96f..1a3270e8d0 100644 --- a/quest/m1/relay-iroh-opt-in.md +++ b/quest/m1/relay-iroh-opt-in.md @@ -14,7 +14,7 @@ Decided in planning: questline dials native peers over it, which mDNS's LAN discovery doesn't replace. `moq relay` (the planned verb) keeps iroh through moq-cli's features. -- Target `dev`. Removing a default feature from a published crate, and +- Removing a default feature from a published crate, and flags from a shipped binary, is a published break. Guidance: diff --git a/quest/m1/route-cost.md b/quest/m1/route-cost.md index b5bddafe6e..19476c1110 100644 --- a/quest/m1/route-cost.md +++ b/quest/m1/route-cost.md @@ -28,5 +28,5 @@ state, so Warm has no job. single cost stays on ANNOUNCE as the origin's per-prefix seed and stops accumulating per hop; link costs move to the ROUTE metric. -Public API: `Cost` changes shape (a break, so it lands on `dev`). Wire: the +Public API: `Cost` changes shape (a break). Wire: the wip version drops a field; lite-06 is unchanged. diff --git a/quest/m1/rs2ts/README.md b/quest/m1/rs2ts/README.md index b89bcd59c4..c315b490c1 100644 --- a/quest/m1/rs2ts/README.md +++ b/quest/m1/rs2ts/README.md @@ -42,9 +42,8 @@ Decided in planning (2026-09-27), with the spike data in change. - Parity: `just test interop --all`, plus moq-net's own tests translated with the code once they run on a mock clock instead of tokio. -- The line lands on `dev`: the Rust refactors break moq-net's published API, - and the translator and generated code build on them. Only the additive - JS `U64` (`js/net/src/util/u64.ts`) is on `main`, package-internal. +- The Rust refactors break moq-net's published API, and the translator and + generated code build on them. - Hand-written js/net fixes keep landing until the generated path replaces them; it is months out. diff --git a/quest/m1/rs2ts/lite.md b/quest/m1/rs2ts/lite.md index f42bccd742..b038a40a8e 100644 --- a/quest/m1/rs2ts/lite.md +++ b/quest/m1/rs2ts/lite.md @@ -21,7 +21,7 @@ first-frame latency are no worse than the hand-written js/net. - Size budget: js/net's `lite/*` is 15 KB gzip today; keep generated output near it. Watch for std shims and fmt/tracing pulling in weight. -Public API: breaks `@moq/net`; retargets to `dev`. Wire: none. +Public API: breaks `@moq/net`. Wire: none. ## Required diff --git a/quest/m1/rs2ts/mock-clock.md b/quest/m1/rs2ts/mock-clock.md index 3f1ea344c9..cf53c4653e 100644 --- a/quest/m1/rs2ts/mock-clock.md +++ b/quest/m1/rs2ts/mock-clock.md @@ -9,7 +9,7 @@ TypeScript runs the same tests under bun. ## Plan tokio is in moq-net's tests only for paused, advanceable time -(`start_paused`, `advance`): 591 `tokio::test`s on dev, 86 of them paused. +(`start_paused`, `advance`): 591 `tokio::test`s, 86 of them paused. Drive them from the model's injectable clock and a small synchronous executor instead. diff --git a/quest/m1/rs2ts/sans-io/README.md b/quest/m1/rs2ts/sans-io/README.md index a74c9af8bb..a580aa4920 100644 --- a/quest/m1/rs2ts/sans-io/README.md +++ b/quest/m1/rs2ts/sans-io/README.md @@ -10,8 +10,8 @@ cargo feature, and a CI lane builds and tests the crate without it. Decided in planning: moq-net itself is the core, not a second crate. JS reimplements the async helpers natively with Promises, so the translator -reads the crate without the `async` feature. Split by layer so each lands on -`dev` independently. +reads the crate without the `async` feature. Split by layer so each lands +independently. The line has no work of its own beyond its children. Decided in the 2026-09-30 audit: the IETF session moved to m2 with the rest of the IETF diff --git a/quest/m1/rs2ts/sans-io/async-feature.md b/quest/m1/rs2ts/sans-io/async-feature.md index 586cc5309e..4cc3832a15 100644 --- a/quest/m1/rs2ts/sans-io/async-feature.md +++ b/quest/m1/rs2ts/sans-io/async-feature.md @@ -18,7 +18,7 @@ crate that [Generated lite](/quest/m1/rs2ts/lite.md) translates. that do stay behind the feature. Public API: moq-net's async helpers move behind a default feature, so a -`default-features = false` caller loses them; lands on `dev` with the line. +`default-features = false` caller loses them; lands with the line. Wire: none. ## Required diff --git a/quest/m1/rs2ts/sans-io/lite.md b/quest/m1/rs2ts/sans-io/lite.md index 51e426c403..5626d332cb 100644 --- a/quest/m1/rs2ts/sans-io/lite.md +++ b/quest/m1/rs2ts/sans-io/lite.md @@ -23,4 +23,4 @@ Guidance: or return values the driver acts on. - Keep maps as Vec slabs where the key space is small. -Public API: breaks moq-net's session API; retargets to `dev`. Wire: none. +Public API: breaks moq-net's session API. Wire: none. diff --git a/quest/m1/rs2ts/sans-io/model.md b/quest/m1/rs2ts/sans-io/model.md index 9060397847..84941215c7 100644 --- a/quest/m1/rs2ts/sans-io/model.md +++ b/quest/m1/rs2ts/sans-io/model.md @@ -14,5 +14,5 @@ that reaches a runtime or wall clock directly (`runtime::Deadline`, the cache pool's expiry, stats timers). Route time through one injectable clock, the seam the [mock-clock tests](/quest/m1/rs2ts/mock-clock.md) use. -Public API: may break moq-net's model constructors; retargets to `dev`. +Public API: may break moq-net's model constructors. Wire: none. diff --git a/quest/m1/rs2ts/translator.md b/quest/m1/rs2ts/translator.md index 2f9f222427..f34d6b3d44 100644 --- a/quest/m1/rs2ts/translator.md +++ b/quest/m1/rs2ts/translator.md @@ -52,7 +52,7 @@ Guidance: - Charon stalled for 40+ minutes on the whole crate; extract only the modules being generated. -Public API: none (internal tool). Lands on `dev` with the codec it +Public API: none (internal tool). Lands with the codec it translates. Wire: none. ## Required diff --git a/quest/m1/rs2ts/varint-codec.md b/quest/m1/rs2ts/varint-codec.md index cda246b8ba..fc2e6cf778 100644 --- a/quest/m1/rs2ts/varint-codec.md +++ b/quest/m1/rs2ts/varint-codec.md @@ -33,4 +33,4 @@ Guidance: - Benchmark the codec before and after (Criterion); it is on every message. Public API: breaks moq-net's `coding` module (Encode/Decode on primitives -go away), so this retargets to `dev`. Wire: none. +go away). Wire: none. diff --git a/quest/m1/shaper-step-clock.md b/quest/m1/shaper-step-clock.md index dca86b4b88..24218e78d3 100644 --- a/quest/m1/shaper-step-clock.md +++ b/quest/m1/shaper-step-clock.md @@ -2,7 +2,7 @@ ## Goal -On `dev`, `moq-shaper`'s `a_step_gets_worse_part_way_through` asserts on a +`moq-shaper`'s `a_step_gets_worse_part_way_through` asserts on a paused or mock clock, not wall-clock latency, so a loaded runner cannot fail it. ## Plan diff --git a/quest/m1/stats/README.md b/quest/m1/stats/README.md index 3bd008a5b2..460fb24dcd 100644 --- a/quest/m1/stats/README.md +++ b/quest/m1/stats/README.md @@ -85,8 +85,8 @@ kind. Gauges are carried but never summed. - **On main.** Every change is additive: optional sections on `#[non_exhaustive]` catalog types, and new types. The line left the - [QoS](/quest/m1/qos/README.md) line, which stays on `dev` for the relay's - moq-stats changes. + [QoS](/quest/m1/qos/README.md) line, which keeps the relay's moq-stats + changes. - **No `@moq/stats` package.** Media types live in `@moq/hang`, and the demo dashboard's relay-stats reader stays where it is. - Docs stay inline: `doc/concept/hang.md` documents both catalog sections, diff --git a/quest/m1/subscribe-ranges/README.md b/quest/m1/subscribe-ranges/README.md index 7da5d132bb..4a8a160150 100644 --- a/quest/m1/subscribe-ranges/README.md +++ b/quest/m1/subscribe-ranges/README.md @@ -30,7 +30,7 @@ relay bounds a sparse FETCH only if the model can request ranges: `request_groups` as a separate API was rejected. A publisher's `track::Dynamic` receives range requests for misses, replacing per-group `requested_group`, and `fetch_group` becomes a one-range subscription or is - removed. This breaks published moq-net APIs, so the line lands on `dev`. + removed. This breaks published moq-net APIs. - **moq-transport.** Some deployments use non-contiguous group numbers, so a relay never probes upstream one group at a time. Upstream, each run of locally missing groups becomes one standalone range FETCH, capped below the diff --git a/quest/m1/timestamp-rewind-detail.md b/quest/m1/timestamp-rewind-detail.md index d10328975d..5e0da051df 100644 --- a/quest/m1/timestamp-rewind-detail.md +++ b/quest/m1/timestamp-rewind-detail.md @@ -22,7 +22,7 @@ frame a group start, so it refuses any backward step, while a dip inside an open group is tolerated. Test: a FLAC `import::Track` with `cut(None)` per frame and one frame 1 µs back reports both values. -Public API: a unit struct gains fields, so breaking, on `dev`. Wire: none. +Public API: a unit struct gains fields, so breaking. Wire: none. ## Related diff --git a/quest/m1/transport-seam.md b/quest/m1/transport-seam.md index 2b2794778f..28e757d907 100644 --- a/quest/m1/transport-seam.md +++ b/quest/m1/transport-seam.md @@ -11,8 +11,8 @@ then a patch for moq-net: only the adapters outside moq-net change. Today `rs/moq-net/src/lib.rs` re-exports `web_transport_trait`, and `Client::connect` / `Server::accept` (via `transport::poll::Boxable`) and the `*_lite` entry points are bounded by `transport::poll::Session`, which on both -`main` and `dev` is a supertrait of `web_transport_trait::poll::Session` with a -blanket impl. So the upstream trait is public either way, and moving `main` from +`release` and `main` is a supertrait of `web_transport_trait::poll::Session` with a +blanket impl. So the upstream trait is public either way, and moving `release` from qmux 0.5 to 0.6 (which needs `web-transport-trait` 0.5) would break moq-net. That kept qmux 0.5.2 a hand-published backport on 2026-10-01. @@ -29,7 +29,7 @@ Decisions (2026-10-01): moq-tokio's `Client`/`Server` see no change; direct moq-net users wrap explicitly. Rejected: a blanket impl behind a feature in moq-net (versioned or not) and a new adapter crate. -- ✅ Standalone m1 quest on `dev`, related to sans-io rather than a child of it. +- ✅ Standalone m1 quest, related to sans-io rather than a child of it. Work: @@ -42,7 +42,7 @@ Work: branch. Add an adapter test each. - Update `doc/` and the rustdoc on `transport`. -Public API: breaking (moq-net's transport bounds and re-export), so `dev`. +Public API: breaking (moq-net's transport bounds and re-export). Wire: none. ## Related diff --git a/quest/m1/ts-stats-module.md b/quest/m1/ts-stats-module.md index 93ce37348a..96cbdb6563 100644 --- a/quest/m1/ts-stats-module.md +++ b/quest/m1/ts-stats-module.md @@ -2,7 +2,7 @@ ## Goal -On `dev`, every moq-mux TS stats type lives in the `container::ts::stats` +Every moq-mux TS stats type lives in the `container::ts::stats` module beside `stats::Log`: `ts::Stats` becomes `ts::stats::Snapshot` and `ts::StreamStats` becomes `ts::stats::Stream`. `StreamStats.track` becomes an owned `String`, and `ts::MultipleProgramsError` becomes `#[non_exhaustive]`. @@ -14,8 +14,8 @@ Decided while planning the follow-ups of [#4505](https://github.com/moq-dev/moq/pull/4505) and [#4506](https://github.com/moq-dev/moq/pull/4506): -- **Breaking, so `dev`.** `Stats` and `StreamStats` have been published since - moq-mux 0.9.14. #4506 kept them at `ts::` on `main` for that reason. +- **Breaking.** `Stats` and `StreamStats` have been published since + moq-mux 0.9.14. #4506 kept them at `ts::` on `release` for that reason. - **Names: `stats::Snapshot` and `stats::Stream`.** `Snapshot` matches `moq_net::stats::Snapshot`. `stats::Import` was rejected because it reads like `ts::Import`, and `stats::Pid` because the row is a stream's liveness, diff --git a/quest/m1/tstd/delay.md b/quest/m1/tstd/delay.md index e07d26616e..0526a3ff8c 100644 --- a/quest/m1/tstd/delay.md +++ b/quest/m1/tstd/delay.md @@ -63,7 +63,7 @@ compare with #4618's numbers. Update `doc/bin/cli.md` and the `moq export ts` examples. Public API: `ts::Export` takes the delay in place of its max age and loses the -hold; breaking, on `dev`. Wire: +hold; breaking. Wire: none. ## Related diff --git a/quest/m1/watch-worker.md b/quest/m1/watch-worker.md index 527a11c747..1c9a6a18c2 100644 --- a/quest/m1/watch-worker.md +++ b/quest/m1/watch-worker.md @@ -23,7 +23,7 @@ requirement. Public API: removing `renderer.out.frame`, moving `Sync`, and changing what `Player` and the composable classes accept are breaks to the published -`@moq/watch`, so the PR targets `dev`. The `@moq/signals` bridge is additive. +`@moq/watch`. The `@moq/signals` bridge is additive. Wire: none. Gate on the plan's jank harness and N-player sweep, both nightly. diff --git a/quest/m1/wire-compat.md b/quest/m1/wire-compat.md index 98cef18c3e..3d5c5945c6 100644 --- a/quest/m1/wire-compat.md +++ b/quest/m1/wire-compat.md @@ -50,7 +50,7 @@ Guidance: the published crates if the build time threatens the nightly budget. - Subscribers must decode the catalog and frames, not only see a non-empty frame, or the container lane proves nothing. -- A deliberate break on `dev` is expected to fail against `main`'s release. +- A deliberate break on `main` is expected to fail against the last release. Run against `main` only, and document in the harness how a planned break is acknowledged (for example a skip list that the next release clears). - Wire the job into the existing nightly (`interop.yml` already has a diff --git a/quest/m1/wt-close-upstream.md b/quest/m1/wt-close-upstream.md index 168d8748a7..9914a94806 100644 --- a/quest/m1/wt-close-upstream.md +++ b/quest/m1/wt-close-upstream.md @@ -29,8 +29,8 @@ session right after `close()` against a hand-rolled HTTP/3 peer and fails on Remaining, once the releases carrying them are published: -- Bump the `web-transport-moq` pin on `main` (and on `dev` if 2.0.x lands - first). +- Bump the `web-transport-moq` pin on `main`, and backport it to `release` + if the fix lands on its line too. - Delete `CLOSE_LINGER`, its task, and its mock-session tests from moq-tokio. They are exactly what #4429 added, so reverting it is enough. diff --git a/quest/m2/README.md b/quest/m2/README.md index b47fafec37..58a5f4b264 100644 --- a/quest/m2/README.md +++ b/quest/m2/README.md @@ -65,7 +65,7 @@ waiting on an upstream release waits in [m4](/quest/m4/README.md). - [Text availability](/quest/m2/text-schema.md) - a text track publishes its own coverage index instead of copying the media timeline - [Closure counters](/quest/m2/closure-counters.md) - a departed node's return never regresses the closure counters a consumer already saw - [Bench coverage](/quest/m2/bench-coverage.md) - Criterion targets for moq-pattern matching first, then the stats producer, moq-mux containers, the hang catalog, and moq-auth -- [Signed priority](/quest/m2/signed-priority.md) - on dev, every API priority is an `i8` with 0 as the unset midpoint, and hang's built-ins sit above it +- [Signed priority](/quest/m2/signed-priority.md) - every API priority is an `i8` with 0 as the unset midpoint, and hang's built-ins sit above it - [AV1 metadata separation](/quest/m2/av1-metadata.md) - retain metadata OBUs inline while evaluating separate delivery - [Catalog track identity](/quest/m2/catalog-tracks.md) - a changed track configuration becomes a new track name or epoch, never a mutated definition - [Catalog colour model](/quest/m2/color-catalog.md) - the catalog describes a rendition's colour and HDR properties once a renderer consumes them diff --git a/quest/m2/audio-encode-mediacodec.md b/quest/m2/audio-encode-mediacodec.md index c5d5d3a9e6..55e581bce2 100644 --- a/quest/m2/audio-encode-mediacodec.md +++ b/quest/m2/audio-encode-mediacodec.md @@ -19,7 +19,7 @@ behind the `mediacodec` feature and the encode seam. with the first output buffer; assert the two match. - MediaCodec pipelines output, which the one-packet-per-frame seam does not allow yet: add a `flush` and a zero-or-more return, a change to - `Encoder::encode` that targets `dev`, unless the AudioToolbox quest already did. + `Encoder::encode`, unless the AudioToolbox quest already did. - Multichannel is device-dependent; probe the encoder's capabilities at open and refuse a layout it does not list. - Round-trip regression through the MediaCodec decoder; runtime proof on a diff --git a/quest/m2/catalog-tracks.md b/quest/m2/catalog-tracks.md index d301e2dd90..e3c039ea0e 100644 --- a/quest/m2/catalog-tracks.md +++ b/quest/m2/catalog-tracks.md @@ -24,7 +24,7 @@ when the whole broadcast restarts. The catalog may still add and remove tracks; a removed name is never reused for different content. Cover codec changes, rendition switches, reconnects, and late joiners in Rust, -JS, and HLS/watch tests. Target any published API break at `dev`. +JS, and HLS/watch tests. ## Related diff --git a/quest/m2/ladder/README.md b/quest/m2/ladder/README.md index ebb9027691..4f3e8c4052 100644 --- a/quest/m2/ladder/README.md +++ b/quest/m2/ladder/README.md @@ -41,7 +41,6 @@ priorities, what to send first; ranking means on the first mile versus a cluster session before the controller depends on it. -`moq-transcode` is 0.1.x, so a published break in this line targets dev. ### Adaptive bands diff --git a/quest/m2/processor/grant-lease.md b/quest/m2/processor/grant-lease.md index b28462b6c4..d7691c03b6 100644 --- a/quest/m2/processor/grant-lease.md +++ b/quest/m2/processor/grant-lease.md @@ -20,7 +20,7 @@ expired grant is denied as it is today. Keep deadline enforcement in the relay authorization owner rather than a cooperative worker timer. Build on what exists: `Grant::deadline` (`rs/moq-auth/src/grant.rs`, #4237) already pins an accepted grant to a fixed -deadline, and dev's `moq_auth::lease` (#3943) re-checks a session on cadence +deadline, and `moq_auth::lease` (#3943) re-checks a session on cadence and reports why it ended. Extend those to the handles a grant opened rather than adding a second timer. No clock-skew grace: #4368 (merged) made expiry exact and dropped `CLOCK_SKEW`, so a deadline in the past is expired. diff --git a/quest/m2/quic-keep-alive.md b/quest/m2/quic-keep-alive.md index 063277f616..8bb1d76428 100644 --- a/quest/m2/quic-keep-alive.md +++ b/quest/m2/quic-keep-alive.md @@ -6,8 +6,7 @@ A connection sends a PING only when its idle deadline is near, never on a fixed clock. An idle connection with a 10 s idle timeout costs one packet per roughly 10 s minus a few PTOs; a busy connection costs none. The `keep_alive` setting on `quic::Client` and `quic::Server` becomes an optional -maximum, default none. Redefining it is a config and CLI break, so this -targets `dev`. +maximum, default none. Redefining it is a config and CLI break. ## Plan diff --git a/quest/m2/quic-qmux.md b/quest/m2/quic-qmux.md index a131cdfb01..5e1cf97bfc 100644 --- a/quest/m2/quic-qmux.md +++ b/quest/m2/quic-qmux.md @@ -61,7 +61,7 @@ bidirectional interoperability against the last web-transport `qmux` release (0. the TypeScript qmux/WebSocket peer used by `js/net`. Preserve rejection of prohibited QUIC frames, params-first setup, record-size validation, close and reset semantics (the first recorded close wins, as close codes #4262 -settled on dev), keep-alive behavior, and bounded flow-control tests. +settled), keep-alive behavior, and bounded flow-control tests. There must be one stream state machine in the dependency graph. diff --git a/quest/m2/rs2ts-ietf.md b/quest/m2/rs2ts-ietf.md index 75390d156e..e7bafa6345 100644 --- a/quest/m2/rs2ts-ietf.md +++ b/quest/m2/rs2ts-ietf.md @@ -12,7 +12,7 @@ Values above 2^53 are legal on the IETF wire (request ids, track aliases); they stay exact as `U64` and only fail where code converts them to `number`. -Public API: breaks `@moq/net`; retargets to `dev`. Wire: none. +Public API: breaks `@moq/net`. Wire: none. Decided in the 2026-09-30 audit: deferred to m2 until generated lite passes its no-downgrade go/no-go in the [rs2ts line](/quest/m1/rs2ts/README.md). diff --git a/quest/m2/rs2ts-sans-io-ietf.md b/quest/m2/rs2ts-sans-io-ietf.md index a6d764f4e3..af2c58fc90 100644 --- a/quest/m2/rs2ts-sans-io-ietf.md +++ b/quest/m2/rs2ts-sans-io-ietf.md @@ -14,7 +14,7 @@ here. If the [async feature](/quest/m1/rs2ts/sans-io/async-feature.md) landed first with the IETF session behind it, move the session out. -Public API: breaks moq-net's IETF session API; retargets to `dev`. Wire: none. +Public API: breaks moq-net's IETF session API. Wire: none. Decided in the 2026-09-30 audit: deferred to m2 with generated IETF, its only consumer, until generated lite passes its go/no-go. diff --git a/quest/m2/shared-clock.md b/quest/m2/shared-clock.md index 1eb45c0949..80800597cb 100644 --- a/quest/m2/shared-clock.md +++ b/quest/m2/shared-clock.md @@ -14,8 +14,7 @@ backwards, and `container::Producer::write` refuses a group below the last one ## Plan Follow-up of [#4668](https://github.com/moq-dev/moq/pull/4668), which moved -data tracks onto the anchored clock. The anchor exists only on `dev`, so this -targets `dev`. +data tracks onto the anchored clock. The anchor is unreleased. Decided (2026-10-01): diff --git a/quest/m2/signed-priority.md b/quest/m2/signed-priority.md index fd520e1960..1dd13fb15c 100644 --- a/quest/m2/signed-priority.md +++ b/quest/m2/signed-priority.md @@ -29,8 +29,7 @@ Decided with the maintainer: - A zeroed moq-c `moq_track_info` then means the default, which retires the need for a `priority_present` flag. -Changing published `u8` fields to `i8` is an API break in every language, so -this lands on `dev`. Look for anything that does arithmetic on priority +Changing published `u8` fields to `i8` is an API break in every language. Look for anything that does arithmetic on priority (the lite send queue, JS send-order packing, the bandwidth allocator, the relay's max-of-subscribers) and keep its ordering, not just its type. @@ -44,7 +43,7 @@ Report the wire impact in the PR: none in format, but the default byte moves again, from 127 to 128 on moq-lite and from 128 to 127 on IETF. Decided in the 2026-09-30 audit: moved to m2. It stays deferred unless it -ships in the same `dev` release as the moxygen default change, so the default +ships in the same release as the moxygen default change, so the default byte moves once instead of twice. ## Related diff --git a/quest/m2/stats-delta.md b/quest/m2/stats-delta.md index 04e757a8af..9c97471f0e 100644 --- a/quest/m2/stats-delta.md +++ b/quest/m2/stats-delta.md @@ -88,7 +88,7 @@ Design points left open for the implementer: `requested_track_shape` and the track-name helpers for every name that takes `.json.z`, created only when requested rather than with the plain/compressed pair. Weigh replacing `compressed: bool` in the helpers with a flavor enum, -which is a published API break and goes to `dev` unless additive. +which is a published API break. **Readers.** `Consumer` and the aggregate read either flavor into `TrafficFrame` / `SessionsFrame`. The JS decoder is about 120 lines and diff --git a/quest/m2/ts-stopped-av-only.md b/quest/m2/ts-stopped-av-only.md index e5d97c52dc..77ddca64ff 100644 --- a/quest/m2/ts-stopped-av-only.md +++ b/quest/m2/ts-stopped-av-only.md @@ -30,5 +30,5 @@ none. ## Related -- [TS stats module](/quest/m1/ts-stats-module.md) - renames these types on dev +- [TS stats module](/quest/m1/ts-stats-module.md) - renames these types - [TS health stats](/quest/m2/ts-health-stats.md) - publishes the same per-PID liveness 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/rs/moq-video/DESIGN-native-codecs.md b/rs/moq-video/DESIGN-native-codecs.md index 5a16e36b0b..c32cc03a37 100644 --- a/rs/moq-video/DESIGN-native-codecs.md +++ b/rs/moq-video/DESIGN-native-codecs.md @@ -290,9 +290,9 @@ A backend "fails to open" (driver missing, no device) the same way an ffmpeg 6. **VAAPI backend** (cros-codecs) last, behind its feature -- the GBM/DMA-buf plumbing is isolated and non-blocking once openh264 covers the fallback. -This work (including the capture swap and ffmpeg removal) ships to `dev`, since -it's a breaking change to `moq-video`'s public API and a dependency overhaul. -It reaches `main` on the next `dev` -> `main` merge. +This work (including the capture swap and ffmpeg removal) is a breaking change +to `moq-video`'s public API and a dependency overhaul, so it ships in the next +release cut. ## Risks / open questions diff --git a/sh/gh/back-merge.sh b/sh/gh/back-merge.sh new file mode 100755 index 0000000000..3021f3e318 --- /dev/null +++ b/sh/gh/back-merge.sh @@ -0,0 +1,69 @@ +#!/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 + +# 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=$(find_pr) +fi +if [[ -z "$pr" ]]; then + echo "error: no pull request from $repo:$branch into main" >&2 + exit 1 +fi + +# 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" diff --git a/swift/README.md b/swift/README.md index 5436721937..8bcbaa1ee4 100644 --- a/swift/README.md +++ b/swift/README.md @@ -111,7 +111,7 @@ Edit the templates when changing a released manifest; never copy the monolithic Two workflows, mirroring the two packages: - **`release-swift-ffi.yml`** fires on each `moq-ffi-v*` tag (pushed by release-plz). It builds the per-target libs + bindings, assembles the `MoqFFI` package via `package-ffi.sh`, attaches `MoqFFI.xcframework.zip` to the `moq-ffi-v*` GitHub Release, verifies the staged package resolves (`verify-ffi.sh`), and mirrors it to [moq-dev/moq-swift-ffi](https://github.com/moq-dev/moq-swift-ffi) on a bare-semver tag (`publish-ffi.sh`). -- **`release-swift-lib.yml`** fires on push to `main`/`dev` when `swift/VERSION` (or the wrapper sources) change. It reads `swift/VERSION`, checks whether that tag already exists on the mirror (the release gate, the same model release-plz uses for crates), assembles the wrapper via `package.sh` (substituting the `moq-ffi` pin from `rs/moq-ffi/Cargo.toml`), verifies it resolves against the published `MoqFFI` (`verify.sh`), and publishes to [moq-dev/moq-swift](https://github.com/moq-dev/moq-swift) only when the version is new (`publish.sh`). +- **`release-swift-lib.yml`** fires on push to `release` when `swift/VERSION` (or the wrapper sources) change. It reads `swift/VERSION`, checks whether that tag already exists on the mirror (the release gate, the same model release-plz uses for crates), assembles the wrapper via `package.sh` (substituting the `moq-ffi` pin from `rs/moq-ffi/Cargo.toml`), verifies it resolves against the published `MoqFFI` (`verify.sh`), and publishes to [moq-dev/moq-swift](https://github.com/moq-dev/moq-swift) only when the version is new (`publish.sh`). Both `verify` jobs build a throwaway SPM consumer against the staged package before any mirror push, so a manifest SPM cannot resolve never reaches consumers. The `moq-bot` GitHub App mints a fresh installation token per run, scoped to the relevant mirror.