Skip to content

Release Python SDK

Release Python SDK #17

name: Release Python SDK
on:
# Triggered by agent-assembly's `notify-downstream` job after the upstream
# GitHub Release is created and aasm-* binaries are uploaded. Payload:
# { "release_tag": "v0.0.1-alpha.4" }
# See ai-agent-assembly/agent-assembly PR #842 for the dispatcher side.
repository_dispatch:
types: [agent-assembly-release-published]
workflow_dispatch:
inputs:
pypi_version:
description: "Version to publish to PyPI (e.g. 0.0.1a8.post1, 0.0.1a9). Required when dry-run is false."
required: false
type: string
binary_source_tag:
description: "agent-assembly tag whose aasm-* binaries to bundle into the wheel (e.g. v0.0.1-alpha.8). Defaults to latest agent-assembly Release."
required: false
type: string
dry-run:
description: "Skip PyPI upload, build wheels only (overrides pypi_version)"
type: boolean
default: false
permissions:
contents: read
id-token: write # PyPI Trusted Publisher OIDC
concurrency:
group: release-python-${{ github.ref }}
cancel-in-progress: false
env:
# Source of the prebuilt aasm sidecar binary, fetched per platform
# before each maturin build so it lands at agent_assembly/bin/aasm
# inside the wheel (matches runtime.py's WHEEL_BUNDLED_BIN search path).
AASM_BINARY_RELEASE_REPO: ai-agent-assembly/agent-assembly
PYTHON_VERSION: '3.12'
# protoc binary version + per-arch SHA256 sums (cross-verified against
# the GitHub release API's `digest:` field on the v32.1 release assets).
# Bump in one place when upgrading protoc.
PROTOC_VERSION: '32.1'
PROTOC_SHA256_X86_64: 'e9c129c176bb7df02546c4cd6185126ca53c89e7d2f09511e209319704b5dd7e'
PROTOC_SHA256_AARCH_64: '4a802ed23d70f7bad7eb19e5a3e724b3aa967250d572cadfd537c1ba939aee6a'
jobs:
resolve:
name: Resolve release tag and PyPI version
runs-on: ubuntu-latest
outputs:
binary_source_tag: ${{ steps.r.outputs.binary_source_tag }}
pypi_version: ${{ steps.r.outputs.pypi_version }}
dry_run: ${{ steps.r.outputs.dry_run }}
release_tag: ${{ steps.r.outputs.release_tag }}
steps:
# Need the working tree on disk so the resolve step can source the
# tag → PEP 440 conversion script (.github/scripts/tag-to-pep440.sh)
# and its inverse (.github/scripts/pep440-to-tag.sh). Those scripts
# are the single source of truth for the conversion, shared with the
# AAASM-2863 / AAASM-2956 unit tests.
- uses: actions/checkout@v6
- id: r
env:
EVENT_NAME: ${{ github.event_name }}
DISPATCH_BINARY_TAG: ${{ inputs.binary_source_tag }}
DISPATCH_PYPI_VERSION: ${{ inputs.pypi_version }}
DISPATCH_DRY_RUN: ${{ inputs.dry-run }}
DISPATCH_PAYLOAD_TAG: ${{ github.event.client_payload.release_tag }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then
# binary_source_tag is optional: when omitted, default to the
# latest agent-assembly Release. Mirrors today's default-latest
# behaviour of `gh release download` with no tag.
if [[ -n "${DISPATCH_BINARY_TAG:-}" ]]; then
binary_source_tag="$DISPATCH_BINARY_TAG"
else
binary_source_tag=$(gh release view --repo ai-agent-assembly/agent-assembly --json tagName --jq .tagName)
echo "::notice::binary_source_tag omitted; defaulting to latest agent-assembly release: $binary_source_tag"
fi
pypi_version="$DISPATCH_PYPI_VERSION"
dry_run="$DISPATCH_DRY_RUN"
elif [[ "$EVENT_NAME" == "repository_dispatch" ]]; then
binary_source_tag="$DISPATCH_PAYLOAD_TAG"
# Convert agent-assembly tag (e.g. v0.0.1-alpha.8) to PEP 440 form
# (0.0.1a8). Mirrors the sync-version-from-dispatch composite action
# so the repository_dispatch path produces identical wheel + version
# state to today. The conversion lives in a sourceable script so
# the AAASM-2863 unit tests exercise the same code path this
# workflow runs (instead of a copy of the regex).
# shellcheck source=.github/scripts/tag-to-pep440.sh
source "${GITHUB_WORKSPACE}/.github/scripts/tag-to-pep440.sh"
pypi_version="$(tag_to_pep440 "$DISPATCH_PAYLOAD_TAG")"
# repository_dispatch is always a real publish — it is fired by
# agent-assembly after a real upstream release.
dry_run="false"
else
echo "::error::unexpected event_name '$EVENT_NAME'"
exit 1
fi
# Fail fast: a real publish (dry_run != true) MUST have a non-empty
# pypi_version. Otherwise the wheel would be stamped with whatever
# version is checked in to master (lagging the bumper) and PyPI
# would reject the upload as a duplicate of the master version.
# See AAASM-2459 for the alpha-4 incident this guards against.
if [[ "$dry_run" != "true" && -z "${pypi_version:-}" ]]; then
echo "::error::dry-run is false but pypi_version is empty — supply pypi_version when dispatching for a real publish"
exit 1
fi
# Validate binary_source_tag against v*.*.* semver (allows -alpha.N,
# -beta.N, -rc.N suffixes). Catches typos like "v0.0.1.alpha.8"
# before we waste a 20-minute wheel-build run on a tag that
# gh release download will reject.
if [[ ! "$binary_source_tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+ ]]; then
echo "::error::binary_source_tag '$binary_source_tag' does not match v*.*.* (semver) pattern"
exit 1
fi
# Validate pypi_version against PEP 440 (basic but tight). Rejects
# hyphenated forms like "0.0.1-alpha.8.1" — PyPI accepts only
# "0.0.1a8.post1" style. Deliberately tight to fail fast on
# operator typos that would otherwise produce a wheel filename PyPI
# would reject at upload time (after the full build pipeline ran).
# https://peps.python.org/pep-0440/
if [[ -n "${pypi_version:-}" ]] && [[ ! "$pypi_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(a|b|rc)?[0-9]*(\.post[0-9]+)?(\.dev[0-9]+)?$ ]]; then
echo "::error::pypi_version '$pypi_version' is not valid PEP 440 (use 0.0.1a8.post1 not 0.0.1-alpha.8.1)"
exit 1
fi
# Resolve the canonical SemVer release tag the create-github-release
# job will cut at the published version (AAASM-2956). The
# repository_dispatch event already carries the canonical tag, so use
# it verbatim. The workflow_dispatch path only has the PEP 440
# pypi_version, so derive the tag from it via the single-source-of-
# truth inverse converter. .post/.dev republish forms have no own
# GitHub Release tag, so leave release_tag empty for those.
release_tag=""
if [[ "$dry_run" != "true" ]]; then
if [[ "$EVENT_NAME" == "repository_dispatch" ]]; then
release_tag="$DISPATCH_PAYLOAD_TAG"
elif [[ "$pypi_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(a|b|rc)?[0-9]*$ ]]; then
# shellcheck source=.github/scripts/pep440-to-tag.sh
source "${GITHUB_WORKSPACE}/.github/scripts/pep440-to-tag.sh"
release_tag="$(pep440_to_tag "$pypi_version")"
else
echo "::notice::pypi_version '$pypi_version' is a post/dev republish — no GitHub Release tag will be cut"
fi
fi
{
echo "binary_source_tag=${binary_source_tag}"
echo "pypi_version=${pypi_version}"
echo "dry_run=${dry_run}"
echo "release_tag=${release_tag}"
} >> "$GITHUB_OUTPUT"
echo "Resolved binary_source_tag=${binary_source_tag} pypi_version=${pypi_version} dry_run=${dry_run} release_tag=${release_tag}"
build-sdist:
name: Build sdist
needs: resolve
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Pin aa-ffi git deps to released core (binary_source_tag)
# Rewrite native/aa-ffi-python/Cargo.toml so the wheel compiles against
# the SAME core release whose aasm-* binaries it bundles. master's pins
# lag by one cycle (bumped only by the post-publish update PR), so
# without this the wheel would pin the PREVIOUS core. Ephemeral CI edit
# — not committed back. See AAASM-2959.
env:
BINARY_SOURCE_TAG: ${{ needs.resolve.outputs.binary_source_tag }}
run: .github/scripts/pin-ffi-to-tag.sh "$BINARY_SOURCE_TAG"
- name: Sync version
uses: ./.github/actions/sync-version
with:
pypi_version: ${{ needs.resolve.outputs.pypi_version }}
- name: Build source distribution
uses: PyO3/maturin-action@v1
with:
command: sdist
args: --out dist
- name: Upload sdist artifact
uses: actions/upload-artifact@v7
with:
name: wheels-sdist
path: dist/*.tar.gz
build-linux-x86_64:
name: Build manylinux_x86_64 wheel
needs: resolve
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Stage aasm sidecar binary
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
AASM_REPO: ${{ env.AASM_BINARY_RELEASE_REPO }}
AASM_TAG: ${{ needs.resolve.outputs.binary_source_tag }}
run: |
set -euo pipefail
mkdir -p agent_assembly/bin
# binary_source_tag is now guaranteed non-empty by the resolve job
# (either set explicitly via workflow_dispatch input, defaulted to
# the latest agent-assembly release, or derived from
# client_payload.release_tag for repository_dispatch).
# Hard error on missing binary: the repository_dispatch event
# guarantees the aasm-* assets exist on the upstream release
# at this point (see ai-agent-assembly/agent-assembly#842).
gh release download "$AASM_TAG" --repo "$AASM_REPO" --pattern 'aasm-x86_64-unknown-linux-gnu.tar.gz' --dir agent_assembly/bin/
# Tarball contains a single `aasm` binary at the root;
# extract in place, then drop the archive.
tar -xzf agent_assembly/bin/aasm-x86_64-unknown-linux-gnu.tar.gz -C agent_assembly/bin/
rm -f agent_assembly/bin/aasm-x86_64-unknown-linux-gnu.tar.gz
chmod +x agent_assembly/bin/aasm
echo "Bundled aasm binary into wheel"
- name: Pin aa-ffi git deps to released core (binary_source_tag)
# Rewrite native/aa-ffi-python/Cargo.toml so the wheel compiles against
# the SAME core release whose aasm-* binaries it bundles. master's pins
# lag by one cycle (bumped only by the post-publish update PR), so
# without this the wheel would pin the PREVIOUS core. Ephemeral CI edit
# — not committed back. See AAASM-2959.
env:
BINARY_SOURCE_TAG: ${{ needs.resolve.outputs.binary_source_tag }}
run: .github/scripts/pin-ffi-to-tag.sh "$BINARY_SOURCE_TAG"
- name: Sync version
uses: ./.github/actions/sync-version
with:
pypi_version: ${{ needs.resolve.outputs.pypi_version }}
- name: Build wheel
uses: PyO3/maturin-action@v1
with:
target: x86_64-unknown-linux-gnu
command: build
args: --release --out dist --interpreter ${{ env.PYTHON_VERSION }}
manylinux: auto
# The manylinux2014 image (CentOS 7-based) lacks protoc; aa-proto's
# build.rs needs it via prost-build for proto3 syntax. The yum/dnf
# `protobuf-compiler` package on CentOS 7 ships protoc 2.5.0 which
# ONLY understands proto2 ("Unrecognized syntax identifier 'proto3'"),
# so we download the official protoc binary release instead.
#
# SECURITY: the zip is downloaded over HTTPS from GitHub's release
# CDN AND verified against a hardcoded SHA256 cross-checked against
# the GitHub release API's `digest` field. Without the SHA check we'd
# be installing an arbitrary binary as root with no integrity gate.
before-script-linux: |
set -euo pipefail
(command -v unzip >/dev/null) || (yum install -y unzip 2>/dev/null || dnf install -y unzip 2>/dev/null || (apt-get update && apt-get install -y unzip))
ARCH=$(uname -m)
case "$ARCH" in
x86_64) PROTOC_ARCH="x86_64"; EXPECTED_SHA="${{ env.PROTOC_SHA256_X86_64 }}" ;;
aarch64) PROTOC_ARCH="aarch_64"; EXPECTED_SHA="${{ env.PROTOC_SHA256_AARCH_64 }}" ;;
*) echo "::error::unsupported manylinux arch: $ARCH"; exit 1 ;;
esac
curl -sSLf --retry 3 --retry-delay 5 \
"https://github.com/protocolbuffers/protobuf/releases/download/v${{ env.PROTOC_VERSION }}/protoc-${{ env.PROTOC_VERSION }}-linux-${PROTOC_ARCH}.zip" \
-o /tmp/protoc.zip
echo "${EXPECTED_SHA} /tmp/protoc.zip" | sha256sum --check --status \
|| { echo "::error::protoc-${{ env.PROTOC_VERSION }}-linux-${PROTOC_ARCH}.zip SHA256 mismatch — refusing to install"; sha256sum /tmp/protoc.zip; exit 1; }
unzip -o /tmp/protoc.zip -d /usr/local >/dev/null
protoc --version
- name: Upload wheel artifact
uses: actions/upload-artifact@v7
with:
name: wheels-linux-x86_64
path: dist/*.whl
build-linux-aarch64:
name: Build manylinux_aarch64 wheel
needs: resolve
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Stage aasm sidecar binary
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
AASM_REPO: ${{ env.AASM_BINARY_RELEASE_REPO }}
AASM_TAG: ${{ needs.resolve.outputs.binary_source_tag }}
run: |
set -euo pipefail
mkdir -p agent_assembly/bin
# See linux-x86_64 above for binary_source_tag resolution rationale.
# Hard error on missing binary: the repository_dispatch event
# guarantees the aasm-* assets exist on the upstream release
# at this point (see ai-agent-assembly/agent-assembly#842).
gh release download "$AASM_TAG" --repo "$AASM_REPO" --pattern 'aasm-aarch64-unknown-linux-gnu.tar.gz' --dir agent_assembly/bin/
# Tarball contains a single `aasm` binary at the root;
# extract in place, then drop the archive.
tar -xzf agent_assembly/bin/aasm-aarch64-unknown-linux-gnu.tar.gz -C agent_assembly/bin/
rm -f agent_assembly/bin/aasm-aarch64-unknown-linux-gnu.tar.gz
chmod +x agent_assembly/bin/aasm
echo "Bundled aasm binary into wheel"
- name: Pin aa-ffi git deps to released core (binary_source_tag)
# Rewrite native/aa-ffi-python/Cargo.toml so the wheel compiles against
# the SAME core release whose aasm-* binaries it bundles. master's pins
# lag by one cycle (bumped only by the post-publish update PR), so
# without this the wheel would pin the PREVIOUS core. Ephemeral CI edit
# — not committed back. See AAASM-2959.
env:
BINARY_SOURCE_TAG: ${{ needs.resolve.outputs.binary_source_tag }}
run: .github/scripts/pin-ffi-to-tag.sh "$BINARY_SOURCE_TAG"
- name: Sync version
uses: ./.github/actions/sync-version
with:
pypi_version: ${{ needs.resolve.outputs.pypi_version }}
- name: Build wheel
uses: PyO3/maturin-action@v1
with:
target: aarch64-unknown-linux-gnu
command: build
args: --release --out dist --interpreter ${{ env.PYTHON_VERSION }}
manylinux: auto
# See linux-x86_64 above for rationale + security model. Same
# SHA-verified protoc binary download.
before-script-linux: |
set -euo pipefail
(command -v unzip >/dev/null) || (yum install -y unzip 2>/dev/null || dnf install -y unzip 2>/dev/null || (apt-get update && apt-get install -y unzip))
ARCH=$(uname -m)
case "$ARCH" in
x86_64) PROTOC_ARCH="x86_64"; EXPECTED_SHA="${{ env.PROTOC_SHA256_X86_64 }}" ;;
aarch64) PROTOC_ARCH="aarch_64"; EXPECTED_SHA="${{ env.PROTOC_SHA256_AARCH_64 }}" ;;
*) echo "::error::unsupported manylinux arch: $ARCH"; exit 1 ;;
esac
curl -sSLf --retry 3 --retry-delay 5 \
"https://github.com/protocolbuffers/protobuf/releases/download/v${{ env.PROTOC_VERSION }}/protoc-${{ env.PROTOC_VERSION }}-linux-${PROTOC_ARCH}.zip" \
-o /tmp/protoc.zip
echo "${EXPECTED_SHA} /tmp/protoc.zip" | sha256sum --check --status \
|| { echo "::error::protoc-${{ env.PROTOC_VERSION }}-linux-${PROTOC_ARCH}.zip SHA256 mismatch — refusing to install"; sha256sum /tmp/protoc.zip; exit 1; }
unzip -o /tmp/protoc.zip -d /usr/local >/dev/null
protoc --version
- name: Upload wheel artifact
uses: actions/upload-artifact@v7
with:
name: wheels-linux-aarch64
path: dist/*.whl
build-macos-arm64:
name: Build macosx_arm64 wheel
needs: resolve
runs-on: macos-14 # Apple silicon runner
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Stage aasm sidecar binary
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
AASM_REPO: ${{ env.AASM_BINARY_RELEASE_REPO }}
AASM_TAG: ${{ needs.resolve.outputs.binary_source_tag }}
run: |
set -euo pipefail
mkdir -p agent_assembly/bin
# See linux-x86_64 above for binary_source_tag resolution rationale.
# Hard error on missing binary: the repository_dispatch event
# guarantees the aasm-* assets exist on the upstream release
# at this point (see ai-agent-assembly/agent-assembly#842).
gh release download "$AASM_TAG" --repo "$AASM_REPO" --pattern 'aasm-aarch64-apple-darwin.tar.gz' --dir agent_assembly/bin/
# Tarball contains a single `aasm` binary at the root;
# extract in place, then drop the archive.
tar -xzf agent_assembly/bin/aasm-aarch64-apple-darwin.tar.gz -C agent_assembly/bin/
rm -f agent_assembly/bin/aasm-aarch64-apple-darwin.tar.gz
chmod +x agent_assembly/bin/aasm
echo "Bundled aasm binary into wheel"
- name: Install protoc (macOS)
run: brew install protobuf
- name: Pin aa-ffi git deps to released core (binary_source_tag)
# Rewrite native/aa-ffi-python/Cargo.toml so the wheel compiles against
# the SAME core release whose aasm-* binaries it bundles. master's pins
# lag by one cycle (bumped only by the post-publish update PR), so
# without this the wheel would pin the PREVIOUS core. Ephemeral CI edit
# — not committed back. See AAASM-2959.
env:
BINARY_SOURCE_TAG: ${{ needs.resolve.outputs.binary_source_tag }}
run: .github/scripts/pin-ffi-to-tag.sh "$BINARY_SOURCE_TAG"
- name: Sync version
uses: ./.github/actions/sync-version
with:
pypi_version: ${{ needs.resolve.outputs.pypi_version }}
- name: Build wheel
uses: PyO3/maturin-action@v1
with:
target: aarch64-apple-darwin
command: build
args: --release --out dist --interpreter ${{ env.PYTHON_VERSION }}
- name: Upload wheel artifact
uses: actions/upload-artifact@v7
with:
name: wheels-macos-arm64
path: dist/*.whl
build-macos-x86_64:
name: Build macosx_x86_64 wheel
needs: resolve
runs-on: macos-15-intel # Intel runner (macos-13 sunset 2025-09-19)
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Stage aasm sidecar binary
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
AASM_REPO: ${{ env.AASM_BINARY_RELEASE_REPO }}
AASM_TAG: ${{ needs.resolve.outputs.binary_source_tag }}
run: |
set -euo pipefail
mkdir -p agent_assembly/bin
# See linux-x86_64 above for binary_source_tag resolution rationale.
# Hard error on missing binary: the repository_dispatch event
# guarantees the aasm-* assets exist on the upstream release
# at this point (see ai-agent-assembly/agent-assembly#842).
gh release download "$AASM_TAG" --repo "$AASM_REPO" --pattern 'aasm-x86_64-apple-darwin.tar.gz' --dir agent_assembly/bin/
# Tarball contains a single `aasm` binary at the root;
# extract in place, then drop the archive.
tar -xzf agent_assembly/bin/aasm-x86_64-apple-darwin.tar.gz -C agent_assembly/bin/
rm -f agent_assembly/bin/aasm-x86_64-apple-darwin.tar.gz
chmod +x agent_assembly/bin/aasm
echo "Bundled aasm binary into wheel"
- name: Install protoc (macOS)
run: brew install protobuf
- name: Pin aa-ffi git deps to released core (binary_source_tag)
# Rewrite native/aa-ffi-python/Cargo.toml so the wheel compiles against
# the SAME core release whose aasm-* binaries it bundles. master's pins
# lag by one cycle (bumped only by the post-publish update PR), so
# without this the wheel would pin the PREVIOUS core. Ephemeral CI edit
# — not committed back. See AAASM-2959.
env:
BINARY_SOURCE_TAG: ${{ needs.resolve.outputs.binary_source_tag }}
run: .github/scripts/pin-ffi-to-tag.sh "$BINARY_SOURCE_TAG"
- name: Sync version
uses: ./.github/actions/sync-version
with:
pypi_version: ${{ needs.resolve.outputs.pypi_version }}
- name: Build wheel
uses: PyO3/maturin-action@v1
with:
target: x86_64-apple-darwin
command: build
args: --release --out dist --interpreter ${{ env.PYTHON_VERSION }}
- name: Upload wheel artifact
uses: actions/upload-artifact@v7
with:
name: wheels-macos-x86_64
path: dist/*.whl
publish:
name: Publish to PyPI (Trusted Publisher)
needs:
- resolve
- build-sdist
- build-linux-x86_64
- build-linux-aarch64
- build-macos-arm64
- build-macos-x86_64
runs-on: ubuntu-latest
# Publish whenever the resolve job decided this is a real publish (not a
# dry-run). repository_dispatch is hardcoded to dry_run='false' in the
# resolve step; workflow_dispatch passes through its dry-run input (which
# defaults to false after AAASM-2856).
if: needs.resolve.outputs.dry_run != 'true'
environment:
name: pypi
url: https://pypi.org/p/agent-assembly
permissions:
id-token: write # OIDC token for Trusted Publisher
steps:
- name: Download all build artifacts
uses: actions/download-artifact@v8
with:
pattern: wheels-*
path: dist
merge-multiple: true
- name: Publish via PyPI Trusted Publisher
uses: pypa/gh-action-pypi-publish@release/v1
# No `with: password:` — Trusted Publisher uses OIDC, no token stored.
# Cut python-sdk's own GitHub Release at the just-published version so the
# repo's release line tracks PyPI (and the README `github/v/release` badge
# resolves) instead of drifting onto a separate source-of-truth tag like
# v0.0.2a1. Runs only on the real-publish path, gated on `publish` so the
# tag is never created for a wheel that failed to upload. See AAASM-2956.
create-github-release:
name: Cut GitHub Release at published version
needs:
- resolve
- publish
runs-on: ubuntu-latest
# Real publish only, and only when resolve produced a SemVer tag (empty
# for .post/.dev republishes, which do not get their own Release).
if: needs.resolve.outputs.dry_run != 'true' && needs.resolve.outputs.release_tag != ''
permissions:
contents: write # create the git tag + GitHub Release
steps:
- uses: actions/checkout@v6
- name: Create tag and GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# client_payload is attacker-controllable on repository_dispatch;
# RELEASE_TAG was validated as a SemVer tag by the resolve job and
# is only ever passed through env (never inlined into a run: body).
RELEASE_TAG: ${{ needs.resolve.outputs.release_tag }}
PYPI_VERSION: ${{ needs.resolve.outputs.pypi_version }}
run: |
set -euo pipefail
# Defence in depth: re-validate the tag shape here so a future
# change to resolve cannot smuggle an arbitrary string into the
# tag/gh commands below.
if [[ ! "$RELEASE_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta|rc)\.[0-9]+)?$ ]]; then
echo "::error::release_tag '$RELEASE_TAG' is not a valid SemVer release tag"
exit 1
fi
# Idempotent: skip if a Release already exists for this tag (e.g. a
# re-run after a partial failure), so the job never errors on retry.
if gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
echo "::notice::GitHub Release $RELEASE_TAG already exists — nothing to do"
exit 0
fi
prerelease_flag=""
if [[ "$RELEASE_TAG" =~ -(alpha|beta|rc)\. ]]; then
prerelease_flag="--prerelease"
fi
# `gh release create` creates the annotated tag at the current
# commit (the workflow ref) when the tag does not yet exist.
gh release create "$RELEASE_TAG" \
--repo "$GITHUB_REPOSITORY" \
$prerelease_flag \
--title "$RELEASE_TAG" \
--notes "python-sdk agent-assembly==${PYPI_VERSION} — coordinated with agent-assembly ${RELEASE_TAG}."
echo "Created GitHub Release $RELEASE_TAG (agent-assembly==${PYPI_VERSION})"
# Publish the *real* release tag as a tiny artifact so the documentation
# workflow (which is triggered by `workflow_run` after this workflow
# completes) can label the frozen docs snapshot with the human-facing tag
# and pick the right channel. The `workflow_run` event itself only exposes
# the PEP-440 pyproject version, which loses the canonical tag form — so we
# hand the tag across explicitly. See AAASM-2750.
publish-release-tag:
name: Publish release tag for docs
needs:
- publish
runs-on: ubuntu-latest
# Only the real release path (repository_dispatch) carries a tag; the
# workflow_dispatch dry-run has no tag and no docs snapshot to cut.
if: github.event_name == 'repository_dispatch'
steps:
- name: Write release tag to file
env:
RELEASE_TAG: ${{ github.event.client_payload.release_tag }}
run: |
set -euo pipefail
if [ -z "${RELEASE_TAG}" ]; then
echo "::error::client_payload.release_tag is empty on a release dispatch"
exit 1
fi
printf '%s\n' "${RELEASE_TAG}" > release-tag.txt
echo "Recorded release tag: ${RELEASE_TAG}"
- name: Upload release-tag artifact
uses: actions/upload-artifact@v7
with:
name: release-tag
path: release-tag.txt
if-no-files-found: error