Skip to content

Release

Release #500

Workflow file for this run

name: Release
on:
# Automatic staging releases on push to main
push:
branches:
- main
paths:
- 'control-plane/**'
- 'sdk/**'
- 'VERSION'
- '.github/workflows/release.yml'
# Manual releases (production or staging)
workflow_dispatch:
inputs:
release_environment:
description: 'Target environment (staging=prerelease, production=stable release)'
type: choice
options:
- staging
- production
default: production
release_component:
description: 'Which part of the SemVer to bump (for new release lines)'
type: choice
options:
- patch
- minor
- major
default: patch
prerelease_label:
description: 'Label for staging releases (e.g., rc, beta, alpha)'
type: string
default: 'rc'
existing_version:
description: 'Optional explicit version to reuse (skips bumping/pushing)'
type: string
required: false
publish_pypi:
description: 'Publish Python SDK to PyPI'
type: boolean
default: true
publish_npm:
description: 'Publish TypeScript SDK (staging=@next, production=@latest)'
type: boolean
default: true
publish_docker:
description: 'Push Docker image (staging=staging-X.Y.Z, production=vX.Y.Z+latest)'
type: boolean
default: true
permissions:
contents: write
packages: write
id-token: write
env:
GO_VERSION: '1.25.x'
PYTHON_VERSION: '3.11'
NODE_VERSION: '20'
jobs:
prepare:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
tag_name: ${{ steps.version.outputs.tag_name }}
release_sha: ${{ steps.version.outputs.release_sha }}
environment: ${{ steps.version.outputs.environment }}
publish_pypi: ${{ steps.flags.outputs.publish_pypi }}
publish_npm: ${{ steps.flags.outputs.publish_npm }}
publish_docker: ${{ steps.flags.outputs.publish_docker }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
ssh-key: ${{ secrets.RELEASE_DEPLOY_KEY }}
- name: Set up Python for tooling
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install git-cliff
uses: taiki-e/install-action@v2
with:
tool: git-cliff
- name: Determine publish flags
id: flags
env:
# For push events, default to publishing everything
# For workflow_dispatch, use the provided inputs
INPUT_PUBLISH_PYPI: ${{ github.event.inputs.publish_pypi }}
INPUT_PUBLISH_NPM: ${{ github.event.inputs.publish_npm }}
INPUT_PUBLISH_DOCKER: ${{ github.event.inputs.publish_docker }}
EVENT_NAME: ${{ github.event_name }}
run: |
set -euo pipefail
# Default to true for push events (automatic staging releases)
if [ "${EVENT_NAME}" = "push" ]; then
echo "publish_pypi=true" >> "$GITHUB_OUTPUT"
echo "publish_npm=true" >> "$GITHUB_OUTPUT"
echo "publish_docker=true" >> "$GITHUB_OUTPUT"
else
# Use inputs for workflow_dispatch
if [ "${INPUT_PUBLISH_PYPI}" = "true" ]; then
echo "publish_pypi=true" >> "$GITHUB_OUTPUT"
else
echo "publish_pypi=false" >> "$GITHUB_OUTPUT"
fi
if [ "${INPUT_PUBLISH_NPM}" = "true" ]; then
echo "publish_npm=true" >> "$GITHUB_OUTPUT"
else
echo "publish_npm=false" >> "$GITHUB_OUTPUT"
fi
if [ "${INPUT_PUBLISH_DOCKER}" = "true" ]; then
echo "publish_docker=true" >> "$GITHUB_OUTPUT"
else
echo "publish_docker=false" >> "$GITHUB_OUTPUT"
fi
fi
- name: Prepare release version
id: version
env:
# For push events: default to staging with patch bump
# For workflow_dispatch: use provided inputs
INPUT_ENVIRONMENT: ${{ github.event.inputs.release_environment }}
INPUT_COMPONENT: ${{ github.event.inputs.release_component }}
INPUT_PRERELEASE_LABEL: ${{ github.event.inputs.prerelease_label }}
INPUT_EXISTING_VERSION: ${{ github.event.inputs.existing_version }}
EVENT_NAME: ${{ github.event_name }}
REF_NAME: ${{ github.ref_name }}
run: |
set -euo pipefail
# Determine environment and component based on trigger type
if [ "${EVENT_NAME}" = "push" ]; then
# Automatic staging release on push to main
ENVIRONMENT="staging"
COMPONENT="patch"
PRERELEASE_LABEL="rc"
EXISTING_VERSION=""
echo "Automatic staging release triggered by push to main"
else
# Manual workflow_dispatch
ENVIRONMENT="${INPUT_ENVIRONMENT:-production}"
COMPONENT="${INPUT_COMPONENT:-patch}"
PRERELEASE_LABEL="${INPUT_PRERELEASE_LABEL:-rc}"
EXISTING_VERSION="${INPUT_EXISTING_VERSION:-}"
fi
if [ -n "${EXISTING_VERSION}" ]; then
version="${EXISTING_VERSION}"
tag="v${version}"
git fetch --tags origin "refs/tags/${tag}:refs/tags/${tag}" --depth=1
release_sha="$(git rev-parse "${tag}")"
else
if [ "${ENVIRONMENT}" = "staging" ]; then
# Staging: Always create prerelease version
CHANNEL="prerelease"
LABEL="${PRERELEASE_LABEL:-rc}"
cmd=(python scripts/bump_version.py --channel "${CHANNEL}" --component "${COMPONENT}" --prerelease-label "${LABEL}")
else
# Production: Create stable version (finalizes from prerelease if exists)
CHANNEL="stable"
cmd=(python scripts/bump_version.py --channel "${CHANNEL}" --component "${COMPONENT}")
fi
version="$("${cmd[@]}")"
tag="v${version}"
python scripts/update_changelog.py --version "${version}"
git status --short
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add -A
git commit -m "chore(release): ${tag} [skip ci]"
git tag -a "${tag}" -m "Release ${tag}"
git push origin "HEAD:${REF_NAME}"
git push origin "${tag}"
release_sha="$(git rev-parse HEAD)"
fi
echo "version=${version}" >> "$GITHUB_OUTPUT"
echo "tag_name=${tag}" >> "$GITHUB_OUTPUT"
echo "release_sha=${release_sha}" >> "$GITHUB_OUTPUT"
echo "environment=${ENVIRONMENT}" >> "$GITHUB_OUTPUT"
build-binaries:
needs: prepare
strategy:
matrix:
include:
- os: ubuntu-latest
label: linux
goreleaser_ids: '--id agentfield-linux-amd64 --id agentfield-linux-arm64'
- os: macos-14
label: darwin
goreleaser_ids: '--id agentfield-darwin-amd64 --id agentfield-darwin-arm64 --id agentfield-tray-darwin-amd64 --id agentfield-tray-darwin-arm64'
runs-on: ${{ matrix.os }}
env:
GORELEASER_CURRENT_TAG: ${{ needs.prepare.outputs.tag_name }}
GORELEASER_CURRENT_COMMIT: ${{ needs.prepare.outputs.release_sha }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ needs.prepare.outputs.release_sha }}
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
cache: true
cache-dependency-path: control-plane/go.sum
- name: Install cross-compilers
if: matrix.label == 'linux'
shell: bash
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get install -y gcc-aarch64-linux-gnu
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
cache-dependency-path: control-plane/web/client/package-lock.json
- name: Build Web UI
working-directory: control-plane/web/client
run: |
npm install
npm run build
- name: Run GoReleaser build
uses: goreleaser/goreleaser-action@v6
with:
version: '~> v2'
args: build --clean --skip=validate --skip=before ${{ matrix.goreleaser_ids }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Flatten binaries to dist root
shell: bash
run: |
set -euo pipefail
echo "=== Original dist/ structure ==="
ls -lah dist/
# Move binaries from subdirectories to dist root
find dist/ -type f -name "agentfield-*" -exec mv {} dist/ \; 2>/dev/null || true
# Remove empty subdirectories
find dist/ -mindepth 1 -type d -empty -delete 2>/dev/null || true
echo ""
echo "=== Flattened dist/ directory ==="
ls -lah dist/agentfield-* 2>/dev/null || echo "No binaries found"
- name: Rename artifact metadata
shell: bash
run: |
set -euo pipefail
if [ -f dist/artifacts.json ]; then mv dist/artifacts.json dist/artifacts-${{ matrix.label }}.json; fi
if [ -f dist/metadata.json ]; then mv dist/metadata.json dist/metadata-${{ matrix.label }}.json; fi
if [ -f dist/config.yaml ]; then mv dist/config.yaml dist/config-${{ matrix.label }}.yaml; fi
- name: Upload dist artifacts
uses: actions/upload-artifact@v4
with:
name: dist-${{ matrix.label }}
path: dist
publish:
needs:
- prepare
- build-binaries
runs-on: ubuntu-latest
permissions:
contents: write
packages: write
id-token: write
env:
GORELEASER_CURRENT_TAG: ${{ needs.prepare.outputs.tag_name }}
GORELEASER_CURRENT_COMMIT: ${{ needs.prepare.outputs.release_sha }}
steps:
- name: Checkout release commit
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ needs.prepare.outputs.release_sha }}
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
cache-dependency-path: control-plane/web/client/package-lock.json
- name: Build Web UI
working-directory: control-plane/web/client
run: |
npm install
npm run build
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Download linux artifacts
uses: actions/download-artifact@v4
with:
name: dist-linux
path: dist
- name: Download darwin artifacts
uses: actions/download-artifact@v4
with:
name: dist-darwin
path: dist
- name: Merge binaries and create checksums
shell: bash
run: |
set -euo pipefail
echo "Downloaded binaries:"
ls -lah dist/
cd dist
sha256sum agentfield-* > checksums.txt
cat checksums.txt
cd ..
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.prepare.outputs.tag_name }}
draft: false
prerelease: ${{ needs.prepare.outputs.environment == 'staging' }}
generate_release_notes: true
body: |
${{ needs.prepare.outputs.environment == 'staging' && '> ⚠️ **This is a staging/pre-release version for testing. Not recommended for production use.**' || '' }}
## AgentField ${{ needs.prepare.outputs.version }}
### Installation
${{ needs.prepare.outputs.environment == 'staging' && '```bash
# Staging binary (use --staging flag)
curl -fsSL https://agentfield.ai/install.sh | bash -s -- --staging
# Python SDK (prerelease - requires --pre flag)
pip install --pre agentfield
# TypeScript SDK
npm install @agentfield/sdk@next
```' || '```bash
curl -fsSL https://agentfield.ai/install.sh | bash
```' }}
### Version Pinning
```bash
VERSION=${{ needs.prepare.outputs.tag_name }} curl -fsSL https://agentfield.ai/install.sh | bash
```
### Manual Download
Download the binary for your platform below, make it executable, and move it to your PATH.
### Supported Platforms
- **macOS Intel:** `agentfield-darwin-amd64`
- **macOS Apple Silicon:** `agentfield-darwin-arm64`
- **Linux x86_64:** `agentfield-linux-amd64`
- **Linux ARM64:** `agentfield-linux-arm64`
files: |
dist/agentfield-*
dist/checksums.txt
- name: Upload binary artifacts
uses: actions/upload-artifact@v4
with:
name: binaries-dist
path: dist/
- name: Build Python package
working-directory: sdk/python
run: |
python -m pip install --upgrade pip build
python -m build
- name: Upload Python package artifacts
uses: actions/upload-artifact@v4
with:
name: python-sdk-dist
path: sdk/python/dist
- name: Publish to PyPI
if: needs.prepare.outputs.publish_pypi == 'true'
working-directory: sdk/python
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
run: |
python -m pip install --upgrade twine
twine upload --skip-existing dist/*
- name: Set up Node.js for npm publish
uses: actions/setup-node@v4
with:
node-version: '24'
registry-url: 'https://registry.npmjs.org'
- name: Build TypeScript SDK
working-directory: sdk/typescript
run: |
npm ci
npm run build
- name: Upload TypeScript SDK artifacts
uses: actions/upload-artifact@v4
with:
name: typescript-sdk-dist
path: sdk/typescript/dist
- name: Publish to npm (staging with @next tag)
if: |
needs.prepare.outputs.environment == 'staging' &&
needs.prepare.outputs.publish_npm == 'true'
working-directory: sdk/typescript
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: npm publish --provenance --access public --tag next
- name: Publish to npm (production with @latest tag)
if: |
needs.prepare.outputs.environment == 'production' &&
needs.prepare.outputs.publish_npm == 'true'
working-directory: sdk/typescript
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: npm publish --provenance --access public
- name: Compute Docker metadata
if: needs.prepare.outputs.publish_docker == 'true'
id: docker_meta
env:
TAG_NAME: ${{ needs.prepare.outputs.tag_name }}
VERSION: ${{ needs.prepare.outputs.version }}
ENVIRONMENT: ${{ needs.prepare.outputs.environment }}
run: |
set -euo pipefail
image="agentfield/control-plane"
echo "image=${image}" >> "$GITHUB_OUTPUT"
if [ "${ENVIRONMENT}" = "staging" ]; then
# Staging: use staging-X.Y.Z-rc.N prefix
echo "tags=${image}:staging-${VERSION}" >> "$GITHUB_OUTPUT"
else
# Production: version tag + latest
echo "tags=${image}:${TAG_NAME},${image}:latest" >> "$GITHUB_OUTPUT"
fi
- name: Set up Docker Buildx
if: needs.prepare.outputs.publish_docker == 'true'
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
if: needs.prepare.outputs.publish_docker == 'true'
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push control plane image
if: needs.prepare.outputs.publish_docker == 'true'
uses: docker/build-push-action@v5
with:
context: .
file: deployments/docker/Dockerfile.control-plane
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ steps.docker_meta.outputs.tags }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Attach Python artifacts to GitHub release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.prepare.outputs.tag_name }}
files: |
sdk/python/dist/*.whl
sdk/python/dist/*.tar.gz
# Desktop installers — unsigned for now (signing/notarization is a known
# follow-up): DMG + zip on macOS (Apple Silicon), NSIS .exe on Windows
# (x64), attached to the same release as the CLI binaries so users download
# whichever they want. Runs for staging (RC prereleases) and production
# alike. The bundled af CLI is built from the release commit with the
# embedded web UI + sqlite FTS (parity with build-single-binary.sh), so the
# installed app is fully self-contained on a fresh machine.
desktop-installers:
needs:
- prepare
- publish
strategy:
fail-fast: false
matrix:
include:
- os: macos-14
assets: desktop/release/*.dmg desktop/release/*.zip
- os: windows-latest
assets: desktop/release/*.exe
runs-on: ${{ matrix.os }}
permissions:
contents: write
env:
# No signing identities in CI yet; package unsigned.
CSC_IDENTITY_AUTO_DISCOVERY: 'false'
steps:
- name: Checkout release commit
uses: actions/checkout@v4
with:
ref: ${{ needs.prepare.outputs.release_sha }}
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
cache-dependency-path: desktop/package-lock.json
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
cache-dependency-path: control-plane/go.sum
- name: Ensure C toolchain for CGO (sqlite FTS in the bundled CLI)
if: runner.os == 'Windows'
shell: bash
run: gcc --version || choco install mingw -y --no-progress
- name: Build Web UI (embedded into the bundled CLI)
working-directory: control-plane/web/client
run: |
npm install
npm run build
- name: Install desktop dependencies
working-directory: desktop
run: npm ci
- name: Stamp release version into the app
working-directory: desktop
shell: bash
# Installer filenames and the app's About version follow the release,
# not the static version in package.json.
run: npm version --no-git-tag-version "${{ needs.prepare.outputs.version }}"
- name: Bundle af CLI (embedded UI + FTS)
working-directory: desktop
env:
CGO_ENABLED: '1'
# Stamp the bundled CLI with the release version (goreleaser
# parity). Without it the binary answers `Version: dev` and the
# app can neither min-version-gate it nor ever update over it.
AF_CLI_VERSION: ${{ needs.prepare.outputs.tag_name }}
run: npm run bundle-cli -- full
- name: Build installers
working-directory: desktop
run: npm run dist
- name: Attach installers to the release
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ needs.prepare.outputs.tag_name }}
run: |
set -euo pipefail
shopt -s nullglob
files=(${{ matrix.assets }})
if [ ${#files[@]} -eq 0 ]; then
echo "no installer artifacts found under desktop/release" >&2
ls -la desktop/release || true
exit 1
fi
ls -lah "${files[@]}"
gh release upload "$TAG" "${files[@]}" --clobber