zmux 1.0.2

Rust implementation of the ZMux v1 stream multiplexing protocol
Documentation
name: Release

on:
  push:
    tags:
      - 'v*.*.*'

permissions:
  contents: read

env:
  CARGO_TERM_COLOR: always

concurrency:
  group: release-${{ github.ref }}
  cancel-in-progress: false

jobs:
  release:
    name: Release
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v6
        with:
          fetch-depth: 0

      - name: Install Rust
        shell: bash
        run: |
          rustup toolchain install stable --profile minimal
          rustup default stable
          rustup component add rustfmt clippy

      - name: Cache Cargo
        uses: actions/cache@v4
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/git
            target
          key: release-rust-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
          restore-keys: |
            release-rust-${{ runner.os }}-
            rust-${{ runner.os }}-

      - name: Validate crates.io token
        shell: bash
        env:
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
        run: |
          if [[ -z "${CARGO_REGISTRY_TOKEN}" ]]; then
            echo "Missing CARGO_REGISTRY_TOKEN secret." >&2
            exit 1
          fi

      - name: Validate release tag and crate versions
        shell: bash
        run: |
          python3 - <<'PY'
          import json
          import os
          import re
          import subprocess
          import sys

          metadata = json.loads(
              subprocess.check_output(
                  ["cargo", "metadata", "--locked", "--no-deps", "--format-version", "1"],
                  text=True,
              )
          )
          packages = {package["name"]: package for package in metadata["packages"]}
          core = packages.get("zmux")
          adapter = packages.get("zmux-quinn")
          if core is None or adapter is None:
              sys.exit("workspace must contain both zmux and zmux-quinn packages")

          core_version = core["version"]
          adapter_version = adapter["version"]
          if not re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+", core_version):
              sys.exit(f"zmux version {core_version!r} is not a plain X.Y.Z release")
          if adapter_version != core_version:
              sys.exit(
                  f"zmux-quinn version {adapter_version!r} must match zmux {core_version!r}"
              )

          dep = next(
              (
                  dependency
                  for dependency in adapter["dependencies"]
                  if dependency["name"] == "zmux"
              ),
              None,
          )
          if dep is None:
              sys.exit("zmux-quinn must depend on zmux")
          allowed = {core_version, f"^{core_version}", f"={core_version}"}
          if dep["req"] not in allowed:
              sys.exit(
                  f"zmux-quinn dependency on zmux is {dep['req']!r}, expected {core_version!r}"
              )

          tag = os.environ.get("GITHUB_REF_NAME", "")
          expected_tag = f"v{core_version}"
          if tag != expected_tag:
              sys.exit(f"tag {tag!r} does not match crate version {expected_tag!r}")

          with open(os.environ["GITHUB_ENV"], "a", encoding="utf-8") as env:
              env.write(f"ZMUX_VERSION={core_version}\n")
          PY

      - name: Check formatting
        run: cargo fmt --all -- --check

      - name: Run clippy
        run: cargo clippy --locked --workspace --all-targets -- -D warnings

      - name: Run clippy with optional async I/O features
        run: cargo clippy --locked -p zmux --features tokio-io,futures-io --all-targets -- -D warnings

      - name: Run tests
        run: cargo test --locked --workspace

      - name: Build docs
        run: cargo doc --locked --workspace --no-deps

      - name: Check existing crates.io versions
        id: published
        shell: bash
        run: |
          crate_version_exists() {
            local crate="$1"
            local version="$2"
            python3 - "${crate}" "${version}" <<'PY'
          import sys
          import urllib.error
          import urllib.request

          crate, version = sys.argv[1], sys.argv[2]
          request = urllib.request.Request(
              f"https://crates.io/api/v1/crates/{crate}/{version}",
              headers={"User-Agent": "zmux-rs-release-workflow"},
          )
          try:
              with urllib.request.urlopen(request, timeout=20) as response:
                  sys.exit(0 if response.status == 200 else 1)
          except urllib.error.HTTPError as exc:
              if exc.code == 404:
                  sys.exit(1)
              print(f"crates.io returned HTTP {exc.code} for {crate} {version}", file=sys.stderr)
              sys.exit(2)
          except Exception as exc:
              print(f"could not query crates.io for {crate} {version}: {exc}", file=sys.stderr)
              sys.exit(2)
          PY
          }

          record_version_status() {
            local crate="$1"
            local output="$2"
            if crate_version_exists "${crate}" "${ZMUX_VERSION}"; then
              echo "${output}=true" >> "${GITHUB_OUTPUT}"
              echo "${crate} ${ZMUX_VERSION} is already published; publish will be skipped."
              return
            fi
            local status=$?
            if [[ "${status}" -eq 1 ]]; then
              echo "${output}=false" >> "${GITHUB_OUTPUT}"
              return
            fi
            exit "${status}"
          }

          record_version_status zmux core
          record_version_status zmux-quinn quinn

      - name: Dry-run core publish
        if: steps.published.outputs.core != 'true'
        env:
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
        run: cargo publish --locked -p zmux --dry-run

      - name: Publish core crate
        if: steps.published.outputs.core != 'true'
        env:
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
        run: cargo publish --locked -p zmux

      - name: Wait for core crate indexing
        if: steps.published.outputs.quinn != 'true'
        shell: bash
        run: |
          crate_version_exists() {
            local crate="$1"
            local version="$2"
            python3 - "${crate}" "${version}" <<'PY'
          import sys
          import urllib.error
          import urllib.request

          crate, version = sys.argv[1], sys.argv[2]
          request = urllib.request.Request(
              f"https://crates.io/api/v1/crates/{crate}/{version}",
              headers={"User-Agent": "zmux-rs-release-workflow"},
          )
          try:
              with urllib.request.urlopen(request, timeout=20) as response:
                  sys.exit(0 if response.status == 200 else 1)
          except urllib.error.HTTPError as exc:
              if exc.code == 404:
                  sys.exit(1)
              print(f"crates.io returned HTTP {exc.code} for {crate} {version}", file=sys.stderr)
              sys.exit(2)
          except Exception as exc:
              print(f"could not query crates.io for {crate} {version}: {exc}", file=sys.stderr)
              sys.exit(2)
          PY
          }

          for attempt in {1..30}; do
            echo "Checking whether zmux ${ZMUX_VERSION} is available for adapter verification, attempt ${attempt}/30"
            if crate_version_exists zmux "${ZMUX_VERSION}"; then
              exit 0
            fi
            status=$?
            if [[ "${status}" -ne 1 ]]; then
              echo "crates.io version check failed; retrying." >&2
            fi
            sleep 20
          done

          echo "zmux ${ZMUX_VERSION} was not visible to crates.io dependency resolution in time." >&2
          exit 1

      - name: Dry-run Quinn adapter publish
        if: steps.published.outputs.quinn != 'true'
        env:
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
        run: cargo publish --locked -p zmux-quinn --dry-run

      - name: Publish Quinn adapter crate
        if: steps.published.outputs.quinn != 'true'
        env:
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
        run: cargo publish --locked -p zmux-quinn