Skip to content

Build and Push Tutorial Agent #489

Build and Push Tutorial Agent

Build and Push Tutorial Agent #489

name: Build and Push Tutorial Agent
on:
workflow_dispatch:
inputs:
rebuild_all:
description: "Rebuild all tutorial agents regardless of changes, this is reserved for maintainers only."
required: false
type: boolean
default: false
pull_request:
paths:
- "examples/tutorials/**"
push:
branches:
- main
paths:
- "examples/tutorials/**"
permissions:
contents: read
packages: write
jobs:
check-permissions:
runs-on: ubuntu-latest
steps:
- name: Check event type and permissions
run: |
if [ "${{ github.event_name }}" != "workflow_dispatch" ]; then
echo "Skipping permission check - not a workflow_dispatch event"
exit 0
fi
echo "Checking maintainer permissions for workflow_dispatch"
- name: Check if user is maintainer
if: ${{ github.event_name == 'workflow_dispatch' }}
uses: actions/github-script@v7
with:
script: |
const { data: permission } = await github.rest.repos.getCollaboratorPermissionLevel({
owner: context.repo.owner,
repo: context.repo.repo,
username: context.actor
});
const allowedRoles = ['admin', 'maintain'];
if (!allowedRoles.includes(permission.permission)) {
throw new Error(`❌ User ${context.actor} does not have sufficient permissions. Required: ${allowedRoles.join(', ')}. Current: ${permission.permission}`);
}
find-agents:
runs-on: ubuntu-latest
needs: [check-permissions]
outputs:
agents: ${{ steps.get-agents.outputs.agents }}
has_agents: ${{ steps.get-agents.outputs.has_agents }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch full history for git diff
- name: Find tutorial agents to build
id: get-agents
env:
REBUILD_ALL: ${{ inputs.rebuild_all }}
run: |
# Find all tutorial directories with manifest.yaml
all_agents=$(find examples/tutorials -name "manifest.yaml" -exec dirname {} \; | sort)
agents_to_build=()
if [ "$REBUILD_ALL" = "true" ]; then
echo "Rebuild all agents requested"
agents_to_build=($(echo "$all_agents"))
echo "### 🔄 Rebuilding All Tutorial Agents" >> $GITHUB_STEP_SUMMARY
else
# Determine the base branch for comparison
if [ "${{ github.event_name }}" = "pull_request" ]; then
BASE_BRANCH="origin/${{ github.base_ref }}"
echo "Comparing against PR base branch: $BASE_BRANCH"
else
# For pushes to main, compare against the first parent (pre-merge state)
BASE_BRANCH="HEAD^1"
echo "Comparing against previous commit: $BASE_BRANCH"
fi
# Check each agent directory for changes
for agent_dir in $all_agents; do
echo "Checking $agent_dir for changes..."
# Check if any files in this agent directory have changed
if git diff --name-only $BASE_BRANCH HEAD | grep -q "^$agent_dir/"; then
echo " ✅ Changes detected in $agent_dir"
agents_to_build+=("$agent_dir")
else
echo " ⏭️ No changes in $agent_dir - skipping build"
fi
done
echo "### 🔄 Changed Tutorial Agents" >> $GITHUB_STEP_SUMMARY
fi
# Convert array to JSON format and output summary
if [ ${#agents_to_build[@]} -eq 0 ]; then
echo "No agents to build"
echo "agents=[]" >> $GITHUB_OUTPUT
echo "has_agents=false" >> $GITHUB_OUTPUT
else
echo "Agents to build: ${#agents_to_build[@]}"
agents_json=$(printf '%s\n' "${agents_to_build[@]}" | jq -R -s -c 'split("\n") | map(select(length > 0))')
echo "agents=$agents_json" >> $GITHUB_OUTPUT
echo "has_agents=true" >> $GITHUB_OUTPUT
echo "" >> $GITHUB_STEP_SUMMARY
for agent in "${agents_to_build[@]}"; do
echo "- \`$agent\`" >> $GITHUB_STEP_SUMMARY
done
echo "" >> $GITHUB_STEP_SUMMARY
fi
build-agents:
needs: [find-agents]
if: ${{ needs.find-agents.outputs.has_agents == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
matrix:
agent_path: ${{ fromJson(needs.find-agents.outputs.agents) }}
fail-fast: false
name: build-${{ matrix.agent_path }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.12"
- name: Get latest agentex-sdk version from PyPI
id: get-version
run: |
LATEST_VERSION=$(curl -s https://pypi.org/pypi/agentex-sdk/json | jq -r '.info.version')
echo "Latest agentex-sdk version: $LATEST_VERSION"
echo "AGENTEX_SDK_VERSION=$LATEST_VERSION" >> $GITHUB_ENV
pip install agentex-sdk==$LATEST_VERSION
echo "Installed agentex-sdk version $LATEST_VERSION"
- name: Generate Image name
id: image-name
run: |
# Remove examples/tutorials/ prefix and replace / with -
AGENT_NAME=$(echo "${{ matrix.agent_path }}" | sed 's|^examples/tutorials/||' | sed 's|/|-|g')
echo "AGENT_NAME=$AGENT_NAME" >> $GITHUB_ENV
echo "agent_name=$AGENT_NAME" >> $GITHUB_OUTPUT
echo "Agent name set to $AGENT_NAME"
- name: Login to GitHub Container Registry
# Only login if we're going to push (main branch or rebuild_all)
if: ${{ github.event_name == 'push' || inputs.rebuild_all }}
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and Conditionally Push Agent Image
env:
REGISTRY: ghcr.io
run: |
AGENT_NAME="${{ steps.image-name.outputs.agent_name }}"
REPOSITORY_NAME="${{ github.repository }}/tutorial-agents/${AGENT_NAME}"
# Determine if we should push based on event type
if [ "${{ github.event_name }}" = "push" ] || [ "${{ inputs.rebuild_all }}" = "true" ]; then
SHOULD_PUSH=true
VERSION_TAG="latest"
echo "🚀 Building and pushing agent: ${{ matrix.agent_path }}"
else
SHOULD_PUSH=false
VERSION_TAG="${{ github.sha }}"
echo "🔍 Validating build for agent: ${{ matrix.agent_path }}"
fi
# Build command - add --push only if we should push
BUILD_ARGS="--manifest ${{ matrix.agent_path }}/manifest.yaml --registry ${REGISTRY} --tag ${VERSION_TAG} --platforms linux/amd64,linux/arm64 --repository-name ${REPOSITORY_NAME}"
if [ "$SHOULD_PUSH" = "true" ]; then
agentex agents build $BUILD_ARGS --push
echo "✅ Successfully built and pushed: ${REGISTRY}/${REPOSITORY_NAME}:${VERSION_TAG}"
# Set full image name for validation step
echo "FULL_IMAGE=${REGISTRY}/${REPOSITORY_NAME}:${VERSION_TAG}" >> $GITHUB_ENV
else
agentex agents build $BUILD_ARGS
echo "✅ Build validation successful for: ${{ matrix.agent_path }}"
# Set full image name for validation step (local build)
echo "FULL_IMAGE=${REGISTRY}/${REPOSITORY_NAME}:${VERSION_TAG}" >> $GITHUB_ENV
fi
- name: Validate agent image
run: |
set -e
FULL_IMAGE="${{ env.FULL_IMAGE }}"
AGENT_PATH="${{ matrix.agent_path }}"
echo "🔍 Validating agent image: $FULL_IMAGE"
# 1. Validate ACP entry point exists and is importable
echo "📦 Checking ACP entry point..."
docker run --rm "$FULL_IMAGE" python -c "from project.acp import acp; print('✅ ACP entry point validated')"
# 2. Check if tests/test_agent.py exists and run it
echo "🧪 Checking for tests/test_agent.py..."
if docker run --rm "$FULL_IMAGE" test -f /app/tests/test_agent.py; then
echo "Found tests/test_agent.py - running tests..."
docker run --rm "$FULL_IMAGE" python -m pytest /app/tests/test_agent.py -v --tb=short
echo "✅ Agent tests passed"
else
echo "⚠️ No tests/test_agent.py found at /app/tests/test_agent.py - skipping test validation"
echo " Consider adding tests/test_agent.py to validate agent functionality"
fi
# 3. Health check - start container and verify /api endpoint responds
echo "🏥 Running health check..."
CONTAINER_NAME="validate-agent-$$"
# Start container in background
docker run -d --name "$CONTAINER_NAME" -p 8000:8000 "$FULL_IMAGE"
# Wait for container to be ready (max 30 seconds)
echo "Waiting for agent to start..."
MAX_ATTEMPTS=30
ATTEMPT=0
HEALTH_OK=false
while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do
ATTEMPT=$((ATTEMPT + 1))
if curl -sf http://localhost:8000/api > /dev/null 2>&1; then
HEALTH_OK=true
break
fi
sleep 1
done
# Capture logs before cleanup
echo "📋 Container logs:"
docker logs "$CONTAINER_NAME" || true
# Cleanup container
docker stop "$CONTAINER_NAME" > /dev/null 2>&1 || true
docker rm "$CONTAINER_NAME" > /dev/null 2>&1 || true
if [ "$HEALTH_OK" = true ]; then
echo "✅ Health check passed - agent responds on /api"
else
echo "❌ Health check failed - agent did not respond on /api within 30 seconds"
exit 1
fi
echo "✅ All validations passed for: $FULL_IMAGE"