Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 58 additions & 4 deletions .github/scripts/release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
#
# Shared release helpers for GitHub Actions workflows.
# Usage:
# release.sh parse-version <prefix> — extract SemVer from GITHUB_REF given a tag prefix
# release.sh prev-tag <prefix> — find the tag immediately before the current one
# release.sh create <artifacts_dir> — create or update a GitHub release with artifacts
# release.sh parse-version <prefix> — extract SemVer from GITHUB_REF given a tag prefix
# release.sh prev-tag <prefix> — find the tag immediately before the current one
# release.sh create <artifacts_dir> — create or update a GitHub release with artifacts
# release.sh read-version <pyproject.toml> — read `version = "x.y.z"` from a manifest
# release.sh pypi-exists <dist> <version> — check whether <dist>==<version> is already on PyPI
#
# Environment:
# GITHUB_REF — set by GitHub Actions (e.g. refs/tags/moq-relay-v1.2.3)
Expand Down Expand Up @@ -77,13 +79,65 @@ create_release() {
fi
}

# Read the static `version = "x.y.z"` from the [project] table of a
# pyproject.toml. Scoped to [project] so a `version` key in another table
# (e.g. a [tool.*] section) can't be picked up by mistake. Writes
# version=<ver> to $GITHUB_OUTPUT and stdout.
read_version() {
local manifest="$1"
local version
version=$(sed -n '/^\[project\]/,/^\[/{
s/^[[:space:]]*version[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p
}' "$manifest" | head -n1)
if [[ -z "$version" ]]; then
echo "Could not read version from [project] in $manifest" >&2
exit 1
fi
if [[ -n "${GITHUB_OUTPUT:-}" ]]; then
echo "version=${version}" >>"$GITHUB_OUTPUT"
fi
echo "$version"
}

# Check whether a distribution+version is already published on PyPI. This is the
# release gate (like release-plz checking the registry): the git tag is just a
# record, the registry is the source of truth. Writes exists=true|false to
# $GITHUB_OUTPUT. A non-200/404 response is treated as fatal rather than
# silently re-publishing.
pypi_exists() {
local dist="$1"
local version="$2"
local url="https://pypi.org/pypi/${dist}/${version}/json"

# Retry transient failures so a network blip doesn't fail the release gate.
local code
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 --retry 3 --retry-connrefused "$url" 2>/dev/null || true)

local exists
case "$code" in
200) exists=true ;;
404) exists=false ;;
*)
echo "Unexpected status $code querying $url" >&2
exit 1
;;
esac

if [[ -n "${GITHUB_OUTPUT:-}" ]]; then
echo "exists=${exists}" >>"$GITHUB_OUTPUT"
fi
echo "PyPI ${dist}==${version}: exists=${exists}"
}

# Dispatch subcommands
case "${1:-}" in
parse-version) parse_version "$2" ;;
prev-tag) prev_tag "$2" ;;
create) create_release "$2" ;;
read-version) read_version "$2" ;;
pypi-exists) pypi_exists "$2" "$3" ;;
*)
echo "Usage: $0 {parse-version|prev-tag|create} <args>" >&2
echo "Usage: $0 {parse-version|prev-tag|create|read-version|pypi-exists} <args>" >&2
exit 1
;;
esac
165 changes: 165 additions & 0 deletions .github/workflows/release-py-ffi.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
name: Release Py FFI

# Builds the `moq-ffi` wheel (raw uniffi bindings). Driven by the
# `moq-ffi-v*` tag that release-plz pushes when it bumps rs/moq-ffi/Cargo.toml.
# The wheel inherits its version from that same Cargo.toml (via maturin's
# dynamic version), so the moq-ffi package stays lockstep with the moq-ffi
# crate. The ergonomic `moq-rs` wrapper is released separately (release-py.yml).

on:
push:
tags:
- "moq-ffi-v*"
pull_request:
paths:
- ".github/workflows/release-py-ffi.yml"
- ".github/scripts/release.sh"

permissions:
contents: read

# Scope by event + ref so a PR dry-run can't queue ahead of a tag publish.
concurrency:
group: release-py-ffi-${{ github.event_name }}-${{ github.ref }}
cancel-in-progress: false

jobs:
parse-version:
name: Parse version
runs-on: ubuntu-latest
outputs:
version: ${{ steps.parse.outputs.version }}

steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false

- name: Parse version
id: parse
env:
EVENT_NAME: ${{ github.event_name }}
run: |
if [[ "$EVENT_NAME" == "pull_request" ]]; then
CARGO_VERSION=$(grep '^version' rs/moq-ffi/Cargo.toml | head -1 | sed 's/.*"\(.*\)".*/\1/')
echo "version=$CARGO_VERSION" >> "$GITHUB_OUTPUT"
echo "Using version from Cargo.toml (PR dry-run): $CARGO_VERSION"
else
.github/scripts/release.sh parse-version moq-ffi
fi

# Maturin reads the wheel version from rs/moq-ffi/Cargo.toml (via
# `dynamic = ["version"]` in pyproject.toml). If a tag is pushed by
# hand without bumping Cargo.toml first, the wheel would ship with
# the previous version and PyPI would reject the duplicate upload.
- name: Verify Cargo.toml matches tag
if: github.event_name == 'push'
env:
TAG_VERSION: ${{ steps.parse.outputs.version }}
run: |
CARGO_VERSION=$(grep '^version' rs/moq-ffi/Cargo.toml | head -1 | sed 's/.*"\(.*\)".*/\1/')
if [[ "$TAG_VERSION" != "$CARGO_VERSION" ]]; then
echo "::error::Tag version ($TAG_VERSION) does not match rs/moq-ffi/Cargo.toml version ($CARGO_VERSION). Bump Cargo.toml before tagging."
exit 1
fi
echo "Cargo.toml version matches tag: $TAG_VERSION"

build:
name: Build wheels (${{ matrix.target }})
needs: [parse-version]
runs-on: ${{ matrix.os }}

strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-gnu
os: ubuntu-latest
- target: aarch64-unknown-linux-gnu
os: ubuntu-latest
# Use manylinux_2_28 for aarch64-linux to get a newer GCC.
# manylinux2014 ships GCC 4.8.5 which can't compile aws-lc-sys.
manylinux: 2_28
# The cross image sets TARGET_CC/CXX which leaks into host builds.
before-script-linux: "unset TARGET_CC TARGET_CXX"
- target: x86_64-apple-darwin
os: macos-15-intel
- target: aarch64-apple-darwin
os: macos-latest
- target: x86_64-pc-windows-msvc
os: windows-latest

steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false

- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: "3.12"

- name: Build wheels
uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1
with:
# py/moq-ffi/pyproject.toml drives the build; its [tool.maturin]
# block points at rs/moq-ffi/Cargo.toml.
working-directory: py/moq-ffi
args: --release --out dist
target: ${{ matrix.target }}
manylinux: ${{ matrix.manylinux || 'auto' }}
before-script-linux: ${{ matrix.before-script-linux || '' }}

- name: Upload wheels
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: python-${{ matrix.target }}
path: py/moq-ffi/dist/*.whl

sdist:
name: Build sdist
needs: [parse-version]
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false

- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: "3.12"

- name: Build sdist
uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1
with:
working-directory: py/moq-ffi
command: sdist
args: --out dist

- name: Upload sdist
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: python-sdist
path: py/moq-ffi/dist/*.tar.gz

publish:
name: Publish to PyPI
needs: [build, sdist]
runs-on: ubuntu-latest
if: github.event_name == 'push'
environment:
name: pypi
url: https://pypi.org/p/moq-ffi
permissions:
id-token: write

steps:
- name: Download wheels
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
pattern: python-*
path: dist
merge-multiple: true

- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1
Loading
Loading