Skip to content

[CI][AgentForge] Build and Push #1

[CI][AgentForge] Build and Push

[CI][AgentForge] Build and Push #1

name: "[CI][AgentForge] Build and Push"
description: "Build and push Agent Forge Plugin"
on:
# Trigger on push to main to build and push images
push:
branches:
- main
paths:
- 'ai_platform_engineering/**'
- 'build/**'
- 'pyproject.toml'
- 'uv.lock'
# Build on all manual new tags as well
tags:
- '**'
workflow_dispatch:
inputs:
tag_version:
description: 'Version tag to apply (e.g., 0.2.8-rc.1)'
required: false
type: string
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository_owner }}/backstage-plugin-agent-forge
jobs:
prepare:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
outputs:
should_build: ${{ steps.set-outputs.outputs.should_build }}
should_retag: ${{ steps.set-outputs.outputs.should_retag }}
tag_version: ${{ steps.version.outputs.tag_version }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Determine Tag
id: version
uses: ./.github/actions/determine-release-tag
with:
manual_tag: ${{ inputs.tag_version }}
token: ${{ secrets.GITHUB_TOKEN }}
- name: Detect changed paths
id: filter
uses: dorny/paths-filter@v3.0.2
with:
filters: |
forge:
- 'build/agent-forge/**'
- name: Set Outputs
id: set-outputs
env:
FORGE_CHANGED: ${{ steps.filter.outputs.forge }}
TAG_VERSION: ${{ steps.version.outputs.tag_version }}
run: |
SHOULD_BUILD="false"
SHOULD_RETAG="false"
TAG_VERSION="${{ env.TAG_VERSION }}"
if [[ "$TAG_VERSION" =~ -rc\.[0-9]+$ ]]; then
IS_RC="true"
else
IS_RC="false"
fi
# Determine build vs retag logic:
# 1. Final release (non-RC tag): Always build
# 2. RC tag with changes: Build
# 3. RC tag without changes: Retag from previous version
# 4. Push to main or workflow_dispatch without tag: Build
if [ -n "$TAG_VERSION" ]; then
# We have a tag version (from workflow_dispatch or tag push)
if [ "$IS_RC" == "true" ]; then
# RC tag: only build if forge files changed, otherwise retag
if [ "$FORGE_CHANGED" == "true" ]; then
SHOULD_BUILD="true"
echo "🔨 RC tag with forge changes: building"
else
SHOULD_RETAG="true"
echo "🏷️ RC tag without forge changes: retagging"
fi
else
# Final release (non-RC): always build
SHOULD_BUILD="true"
echo "🚀 Final release: building"
fi
elif [ "${{ github.ref }}" == "refs/heads/main" ] || [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
# Push to main or manual trigger without tag: always build
SHOULD_BUILD="true"
echo "📦 Push to main or manual trigger: building"
elif [ "$FORGE_CHANGED" == "true" ]; then
# PR with forge changes
SHOULD_BUILD="true"
echo "🔍 Forge files changed: building"
fi
echo "should_build=$SHOULD_BUILD" >> $GITHUB_OUTPUT
echo "should_retag=$SHOULD_RETAG" >> $GITHUB_OUTPUT
echo "Outputs: build=$SHOULD_BUILD, retag=$SHOULD_RETAG, is_rc=$IS_RC"
build-and-push:
needs: prepare
if: needs.prepare.outputs.should_build == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout main repository
uses: actions/checkout@v6
with:
path: main-repo
fetch-depth: 1
- name: Checkout community-plugins repository
uses: actions/checkout@v6
with:
repository: cnoe-io/community-plugins
ref: agent-forge-upstream-docker
token: ${{ secrets.GITHUB_TOKEN }}
path: community-plugins
fetch-depth: 1
- name: Copy custom Dockerfile
run: |
echo "Copying Dockerfile..."
cp main-repo/build/agent-forge/Dockerfile community-plugins/Dockerfile
echo "Ready to build!"
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=raw,value=latest,enable=${{ github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main' }}
type=raw,value=${{ needs.prepare.outputs.tag_version }},enable=${{ needs.prepare.outputs.tag_version != '' }}
type=sha,format=short
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: community-plugins
file: community-plugins/Dockerfile
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=min
platforms: linux/amd64,linux/arm64
# Retag unchanged agent forge from previous version when tag_version is provided
# If source image doesn't exist (still building), falls back to full build
retag-unchanged:
runs-on: ubuntu-latest
needs: prepare
if: needs.prepare.outputs.should_retag == 'true'
permissions:
contents: read
packages: write
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository_owner }}/backstage-plugin-agent-forge
steps:
- name: 🔒 Harden runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- name: 📦 Install crane
run: |
VERSION="v0.20.2"
curl -sL "https://github.com/google/go-containerregistry/releases/download/${VERSION}/go-containerregistry_Linux_x86_64.tar.gz" | tar xz crane
sudo mv crane /usr/local/bin/
- name: 🔐 Log in to registry
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
echo "$GH_TOKEN" | crane auth login ghcr.io -u ${{ github.actor }} --password-stdin
- name: 🔍 Check source image and retag or build
id: retag-or-build
env:
TAG_VERSION: ${{ needs.prepare.outputs.tag_version }}
run: |
FULL_IMAGE="${REGISTRY}/${IMAGE_NAME}"
echo "🏷️ Processing agent-forge..."
# Determine source tag (previous version)
if [[ "$TAG_VERSION" =~ ^(.+)-rc\.([0-9]+)$ ]]; then
BASE_VERSION="${BASH_REMATCH[1]}"
RC_NUM="${BASH_REMATCH[2]}"
if [[ "$RC_NUM" -gt 1 ]]; then
PREV_RC=$((RC_NUM - 1))
SOURCE_TAG="${BASE_VERSION}-rc.${PREV_RC}"
else
SOURCE_TAG="${BASE_VERSION}"
fi
else
SOURCE_TAG="latest"
fi
echo " Source: ${SOURCE_TAG}"
echo " Target: ${TAG_VERSION}"
# Check if source image exists
if crane manifest "${FULL_IMAGE}:${SOURCE_TAG}" >/dev/null 2>&1; then
echo " ✅ Source image exists, retagging..."
if crane tag "${FULL_IMAGE}:${SOURCE_TAG}" "${TAG_VERSION}"; then
echo " ✅ Successfully retagged from ${SOURCE_TAG}"
echo "needs_build=false" >> $GITHUB_OUTPUT
else
echo " ⚠️ Retag failed unexpectedly, will build instead"
echo "needs_build=true" >> $GITHUB_OUTPUT
fi
else
echo " ⚠️ Source image ${SOURCE_TAG} not found (may still be building)"
echo " 📦 Will build fresh instead of using stale fallback"
echo "needs_build=true" >> $GITHUB_OUTPUT
fi
# Fallback build if retag not possible
- name: Checkout main repository
if: steps.retag-or-build.outputs.needs_build == 'true'
uses: actions/checkout@v6
with:
path: main-repo
fetch-depth: 1
- name: Checkout community-plugins repository
if: steps.retag-or-build.outputs.needs_build == 'true'
uses: actions/checkout@v6
with:
repository: cnoe-io/community-plugins
ref: agent-forge-upstream-docker
token: ${{ secrets.GITHUB_TOKEN }}
path: community-plugins
fetch-depth: 1
- name: Copy custom Dockerfile
if: steps.retag-or-build.outputs.needs_build == 'true'
run: |
echo "Copying Dockerfile..."
cp main-repo/build/agent-forge/Dockerfile community-plugins/Dockerfile
echo "Ready to build!"
- name: Log in to GitHub Container Registry
if: steps.retag-or-build.outputs.needs_build == 'true'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU
if: steps.retag-or-build.outputs.needs_build == 'true'
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
if: steps.retag-or-build.outputs.needs_build == 'true'
uses: docker/setup-buildx-action@v3
- name: 🔨 Build and Push (fallback)
if: steps.retag-or-build.outputs.needs_build == 'true'
uses: docker/build-push-action@v6
with:
context: community-plugins
file: community-plugins/Dockerfile
push: true
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.prepare.outputs.tag_version }}
cache-from: type=gha
cache-to: type=gha,mode=min
platforms: linux/amd64,linux/arm64
# Notify release-finalize workflow when this CI completes (for final release builds only)
notify-release:
runs-on: ubuntu-latest
needs: [prepare, build-and-push, retag-unchanged]
# Only notify for final releases (non-RC tags) - RC builds don't need release finalization
if: |
always() &&
needs.prepare.outputs.tag_version != '' &&
!contains(needs.prepare.outputs.tag_version, '-rc.')
steps:
- name: 🔔 Notify release finalize
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG_VERSION: ${{ needs.prepare.outputs.tag_version }}
run: |
# Determine overall conclusion
BUILD_RESULT="${{ needs.build-and-push.result }}"
RETAG_RESULT="${{ needs.retag-unchanged.result }}"
# Consider skipped as success (job conditions not met)
if [[ "$BUILD_RESULT" == "skipped" ]]; then BUILD_RESULT="success"; fi
if [[ "$RETAG_RESULT" == "skipped" ]]; then RETAG_RESULT="success"; fi
if [[ "$BUILD_RESULT" == "success" && "$RETAG_RESULT" == "success" ]]; then
CONCLUSION="success"
else
CONCLUSION="failure"
fi
echo "🔔 Notifying release-finalize: tag=$TAG_VERSION, conclusion=$CONCLUSION"
gh api /repos/${{ github.repository }}/dispatches --input - <<EOF
{
"event_type": "ci-completed",
"client_payload": {
"tag": "$TAG_VERSION",
"workflow": "${{ github.workflow }}",
"conclusion": "$CONCLUSION"
}
}
EOF