Skip to content

Release

Release #25

Workflow file for this run

# ─────────────────────────────────────────────────────────────────────────────
# Release Workflow — agentomatic
#
# Semantic release → Bundle Studio UI → Build + Publish to PyPI → GitHub
# Release → Deploy docs. The bundle-studio step downloads the latest React
# UI build from agentomatic-studio releases and places it in
# src/agentomatic/studio/static/ so `pip install agentomatic[studio]` ships
# the frontend.
#
# Uses GitHub App (TECHNICAL_APP_APP_ID + TECHNICAL_APP_PEM) for cross-repo
# access to the private agentomatic-studio repository.
# ─────────────────────────────────────────────────────────────────────────────
name: Release
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
on:
workflow_dispatch:
inputs:
DRY_RUN:
required: false
default: false
type: boolean
description: "Run in dry-run mode (no actual release)"
# Allow agentomatic-studio to trigger a rebuild after a new UI release
repository_dispatch:
types: [studio-release]
permissions:
contents: write
pages: write
id-token: write
jobs:
# ─────────────────────────────────────────────────────────────
# 1. SEMANTIC RELEASE — Bump version, tag, create GitHub release
# ─────────────────────────────────────────────────────────────
semantic-release:
name: Semantic Release
runs-on: ubuntu-latest
concurrency: release
if: github.repository_owner == 'UnicoLab'
outputs:
new_version: ${{ steps.release.outputs.version }}
released: ${{ steps.release.outputs.released }}
steps:
- name: Generate App Token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.TECHNICAL_APP_APP_ID }}
private-key: ${{ secrets.TECHNICAL_APP_PEM }}
owner: UnicoLab
repositories: agentomatic,agentomatic-studio
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ steps.app-token.outputs.token }}
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install uv
uses: astral-sh/setup-uv@v5
- name: Install dependencies
run: uv sync --group dev
- name: Python Semantic Release
id: release
uses: python-semantic-release/python-semantic-release@v9
with:
github_token: ${{ steps.app-token.outputs.token }}
root_options: ${{ inputs.DRY_RUN && '--noop' || '' }}
- name: Show release info
run: |
echo "Released: ${{ steps.release.outputs.released }}"
echo "Version: ${{ steps.release.outputs.version }}"
- name: Save recovery version
if: steps.release.outputs.released == 'true'
run: |
mkdir -p .github/recovery
echo "${{ steps.release.outputs.version }}" > .github/recovery/last_release_version.txt
# ─────────────────────────────────────────────────────────────
# 2. BUNDLE STUDIO — Download latest Studio UI build from
# agentomatic-studio releases and stage for the wheel build.
# ─────────────────────────────────────────────────────────────
bundle-studio:
name: Bundle Studio UI
needs: [semantic-release]
if: needs.semantic-release.outputs.released == 'true'
runs-on: ubuntu-latest
steps:
- name: Generate App Token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.TECHNICAL_APP_APP_ID }}
private-key: ${{ secrets.TECHNICAL_APP_PEM }}
owner: UnicoLab
repositories: agentomatic-studio
- uses: actions/checkout@v4
with:
ref: main
fetch-depth: 1
# Download the latest Studio UI tarball from the private
# agentomatic-studio repo.
- name: Download latest studio release
id: download
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
echo "⬇️ Downloading latest Studio UI release..."
gh release download \
--repo UnicoLab/agentomatic-studio \
--pattern "studio-ui-*.tar.gz" \
--dir /tmp/studio-download
TARBALL=$(ls /tmp/studio-download/studio-ui-*.tar.gz)
echo "tarball=${TARBALL}" >> "$GITHUB_OUTPUT"
# Extract version from filename
STUDIO_VERSION=$(basename "$TARBALL" | sed 's/studio-ui-//; s/.tar.gz//')
echo "studio_version=${STUDIO_VERSION}" >> "$GITHUB_OUTPUT"
echo "📦 Studio UI version: ${STUDIO_VERSION}"
- name: Extract studio build
run: |
STATIC_DIR="src/agentomatic/studio/static"
# Clean previous files (keep .gitignore)
find "${STATIC_DIR}" -mindepth 1 ! -name '.gitignore' -delete 2>/dev/null || true
# Extract tarball — build contents are at root level
echo "📂 Extracting to ${STATIC_DIR}/"
tar -xzf "${{ steps.download.outputs.tarball }}" -C "${STATIC_DIR}"
# Verify
if [ ! -f "${STATIC_DIR}/index.html" ]; then
echo "❌ ERROR: index.html not found!"
exit 1
fi
echo "✅ Studio UI v${{ steps.download.outputs.studio_version }} extracted"
echo "📊 Files: $(find "${STATIC_DIR}" -type f | wc -l)"
# Upload as artifact so pypi-publish can access the files
- name: Upload studio-bundled source
uses: actions/upload-artifact@v4
with:
name: source-with-studio
path: .
retention-days: 1
# ─────────────────────────────────────────────────────────────
# 3. PYPI PUBLISH — Build and publish to PyPI
# (now includes bundled Studio UI static files)
# ─────────────────────────────────────────────────────────────
pypi-publish:
name: Publish to PyPI
needs: [semantic-release, bundle-studio]
if: needs.semantic-release.outputs.released == 'true'
runs-on: ubuntu-latest
steps:
# Download the source tree that already has studio files in place
- name: Download studio-bundled source
uses: actions/download-artifact@v4
with:
name: source-with-studio
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install uv
uses: astral-sh/setup-uv@v5
- name: Verify studio files are present
run: |
STATIC_DIR="src/agentomatic/studio/static"
if [ -f "${STATIC_DIR}/index.html" ]; then
echo "✅ Studio UI is bundled ($(find ${STATIC_DIR} -type f | wc -l) files)"
else
echo "⚠️ Studio UI not found — wheel will ship without frontend"
fi
- name: Build package
run: uv build
- name: Inspect wheel contents
run: |
echo "📦 Wheel contents (studio static files):"
python -m zipfile -l dist/*.whl | grep "studio/static" | head -20 || echo "(no studio files found)"
- name: Publish to PyPI
id: publish
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.PYPI_TOKEN }}
attestations: false
continue-on-error: true
- name: Retry PyPI publish on failure
if: steps.publish.outcome == 'failure'
run: |
echo "First attempt failed, retrying..."
sleep 10
rm -rf dist/
uv build
# Second attempt
- name: Retry publish
if: steps.publish.outcome == 'failure'
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.PYPI_TOKEN }}
attestations: false
# ─────────────────────────────────────────────────────────────
# 4. GITHUB RELEASE — Create GitHub release with assets
# ─────────────────────────────────────────────────────────────
github-release:
name: Publish GitHub Release
needs: [semantic-release]
if: needs.semantic-release.outputs.released == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: main
- name: Publish GitHub Release
uses: python-semantic-release/publish-action@v9
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
# ─────────────────────────────────────────────────────────────
# 5. DEPLOY DOCS — Build and deploy versioned documentation
# ─────────────────────────────────────────────────────────────
deploy-docs:
name: Deploy Release Docs
needs: [semantic-release]
if: needs.semantic-release.outputs.released == 'true'
runs-on: ubuntu-latest
concurrency:
group: deploy-docs
cancel-in-progress: false
steps:
- uses: actions/checkout@v4
with:
ref: main
fetch-depth: 0
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install uv
uses: astral-sh/setup-uv@v5
- name: Install docs dependencies
run: uv sync --group docs
- name: Configure git
run: |
git config user.name "${{ github.actor }}"
git config user.email "${{ github.actor }}@users.noreply.github.com"
- name: Deploy versioned docs
id: deploy_docs
run: |
VERSION="${{ needs.semantic-release.outputs.new_version }}"
echo "Deploying documentation for version $VERSION"
git fetch origin gh-pages:gh-pages --force || echo "No gh-pages branch exists yet — mike will create it"
uv run mike deploy --push --update-aliases --ignore-remote-status "$VERSION" latest
uv run mike set-default --push --ignore-remote-status latest
echo "✓ Deployed docs v${VERSION} (aliased as 'latest', set as default)"