Build and Push Tutorial Agent #489
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" |