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