Release Pipeline #38
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: Release Pipeline | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| bump_type: | |
| description: 'Version bump type' | |
| required: true | |
| default: 'patch' | |
| type: choice | |
| options: | |
| - major | |
| - minor | |
| - patch | |
| force_build_all: | |
| description: 'Force rebuild all components' | |
| required: false | |
| type: boolean | |
| default: true | |
| components: | |
| description: 'Components to build (comma-separated: frontend,backend,operator,ambient-runner,state-sync,public-api,ambient-api-server) - leave empty for all' | |
| required: false | |
| type: string | |
| default: '' | |
| concurrency: | |
| group: prod-release-deploy | |
| cancel-in-progress: false | |
| jobs: | |
| release: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| outputs: | |
| new_tag: ${{ steps.next_version.outputs.new_tag }} | |
| build-matrix: ${{ steps.matrix.outputs.build-matrix }} | |
| merge-matrix: ${{ steps.matrix.outputs.merge-matrix }} | |
| steps: | |
| - name: Checkout Repository | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 # Fetch all history for changelog generation | |
| - name: Get Latest Tag | |
| id: get_latest_tag | |
| run: | | |
| # List all existing tags for debugging | |
| echo "All existing tags:" | |
| git tag --list 'v*.*.*' --sort=-version:refname | |
| # Get the latest tag using version sort, or use v0.0.0 if no tags exist | |
| LATEST_TAG=$(git tag --list 'v*.*.*' --sort=-version:refname | head -n 1) | |
| if [ -z "$LATEST_TAG" ]; then | |
| exit 1 | |
| fi | |
| echo "latest_tag=$LATEST_TAG" >> $GITHUB_OUTPUT | |
| echo "Latest tag: $LATEST_TAG" | |
| - name: Calculate Next Version | |
| id: next_version | |
| run: | | |
| LATEST_TAG="${{ steps.get_latest_tag.outputs.latest_tag }}" | |
| # Remove 'v' prefix for calculation | |
| VERSION=${LATEST_TAG#v} | |
| # Split version into components | |
| IFS='.' read -r MAJOR MINOR PATCH <<< "$VERSION" | |
| # Bump version based on input | |
| case "${{ github.event.inputs.bump_type }}" in | |
| major) | |
| MAJOR=$((MAJOR + 1)) | |
| MINOR=0 | |
| PATCH=0 | |
| ;; | |
| minor) | |
| MINOR=$((MINOR + 1)) | |
| PATCH=0 | |
| ;; | |
| patch) | |
| PATCH=$((PATCH + 1)) | |
| ;; | |
| esac | |
| NEW_VERSION="v${MAJOR}.${MINOR}.${PATCH}" | |
| echo "new_tag=$NEW_VERSION" >> $GITHUB_OUTPUT | |
| echo "New version: $NEW_VERSION" | |
| - name: Generate Changelog | |
| id: changelog | |
| run: | | |
| LATEST_TAG="${{ steps.get_latest_tag.outputs.latest_tag }}" | |
| NEW_TAG="${{ steps.next_version.outputs.new_tag }}" | |
| echo "# Release $NEW_TAG" > RELEASE_CHANGELOG.md | |
| echo "" >> RELEASE_CHANGELOG.md | |
| echo "## Changes since $LATEST_TAG" >> RELEASE_CHANGELOG.md | |
| echo "" >> RELEASE_CHANGELOG.md | |
| # Generate changelog from commits | |
| if [ "$LATEST_TAG" = "v0.0.0" ]; then | |
| # First release - include all commits | |
| git log --pretty=format:"- %s (%h)" >> RELEASE_CHANGELOG.md | |
| else | |
| # Get commits since last tag | |
| git log ${LATEST_TAG}..HEAD --pretty=format:"- %s (%h)" >> RELEASE_CHANGELOG.md | |
| fi | |
| echo "" >> RELEASE_CHANGELOG.md | |
| echo "" >> RELEASE_CHANGELOG.md | |
| echo "**Full Changelog**: https://github.com/${{ github.repository }}/compare/${LATEST_TAG}...${NEW_TAG}" >> RELEASE_CHANGELOG.md | |
| cat RELEASE_CHANGELOG.md | |
| - name: Create Tag | |
| id: create_tag | |
| uses: rickstaa/action-create-tag@v1 | |
| with: | |
| tag: ${{ steps.next_version.outputs.new_tag }} | |
| message: "Release ${{ steps.next_version.outputs.new_tag }}" | |
| force_push_tag: false | |
| github_token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Create Release Archive | |
| id: create_archive | |
| run: | | |
| NEW_TAG="${{ steps.next_version.outputs.new_tag }}" | |
| ARCHIVE_NAME="vteam-${NEW_TAG}.tar.gz" | |
| # Create archive of entire repository at this tag | |
| git archive --format=tar.gz --prefix=vteam-${NEW_TAG}/ HEAD > $ARCHIVE_NAME | |
| echo "archive_name=$ARCHIVE_NAME" >> $GITHUB_OUTPUT | |
| - name: Create Release | |
| id: create_release | |
| uses: softprops/action-gh-release@v2 | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| with: | |
| tag_name: ${{ steps.next_version.outputs.new_tag }} | |
| name: "Release ${{ steps.next_version.outputs.new_tag }}" | |
| body_path: RELEASE_CHANGELOG.md | |
| draft: false | |
| prerelease: false | |
| files: | | |
| ${{ steps.create_archive.outputs.archive_name }} | |
| RELEASE_CHANGELOG.md | |
| - name: Build component matrices | |
| id: matrix | |
| run: | | |
| ALL_COMPONENTS='[ | |
| {"name":"frontend","context":"./components/frontend","image":"quay.io/ambient_code/vteam_frontend","dockerfile":"./components/frontend/Dockerfile"}, | |
| {"name":"backend","context":"./components/backend","image":"quay.io/ambient_code/vteam_backend","dockerfile":"./components/backend/Dockerfile"}, | |
| {"name":"operator","context":"./components/operator","image":"quay.io/ambient_code/vteam_operator","dockerfile":"./components/operator/Dockerfile"}, | |
| {"name":"ambient-runner","context":"./components/runners","image":"quay.io/ambient_code/vteam_claude_runner","dockerfile":"./components/runners/ambient-runner/Dockerfile"}, | |
| {"name":"state-sync","context":"./components/runners/state-sync","image":"quay.io/ambient_code/vteam_state_sync","dockerfile":"./components/runners/state-sync/Dockerfile"}, | |
| {"name":"public-api","context":"./components/public-api","image":"quay.io/ambient_code/vteam_public_api","dockerfile":"./components/public-api/Dockerfile"}, | |
| {"name":"ambient-api-server","context":"./components/ambient-api-server","image":"quay.io/ambient_code/vteam_api_server","dockerfile":"./components/ambient-api-server/Dockerfile"} | |
| ]' | |
| FORCE_ALL="${{ github.event.inputs.force_build_all }}" | |
| SELECTED="${{ github.event.inputs.components }}" | |
| if [ "$FORCE_ALL" == "true" ] || [ -z "$SELECTED" ]; then | |
| FILTERED="$ALL_COMPONENTS" | |
| else | |
| FILTERED=$(echo "$ALL_COMPONENTS" | jq -c --arg sel "$SELECTED" '[.[] | select(.name as $n | $sel | split(",") | map(gsub("^\\s+|\\s+$";"")) | index($n))]') | |
| fi | |
| if [ "$(echo "$FILTERED" | jq 'length')" -eq 0 ]; then | |
| echo "::error::No components matched the selection. Aborting release." | |
| exit 1 | |
| fi | |
| BUILD_MATRIX=$(echo "$FILTERED" | jq -c '.') | |
| MERGE_MATRIX=$(echo "$FILTERED" | jq -c '[.[] | {name, image}]') | |
| echo "build-matrix=$BUILD_MATRIX" >> $GITHUB_OUTPUT | |
| echo "merge-matrix=$MERGE_MATRIX" >> $GITHUB_OUTPUT | |
| echo "Components to build:" | |
| echo "$FILTERED" | jq -r '.[].name' | |
| build: | |
| needs: release | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| issues: read | |
| id-token: write | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| # IMPORTANT: suffix values must match the hardcoded suffixes in | |
| # merge-manifests. Update both together if arches change. | |
| arch: | |
| - runner: ubuntu-latest | |
| platform: linux/amd64 | |
| suffix: amd64 | |
| - runner: ubuntu-24.04-arm | |
| platform: linux/arm64 | |
| suffix: arm64 | |
| component: ${{ fromJSON(needs.release.outputs.build-matrix) }} | |
| runs-on: ${{ matrix.arch.runner }} | |
| steps: | |
| - name: Checkout code from the tag generated above | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ needs.release.outputs.new_tag }} | |
| fetch-depth: 0 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Log in to Quay.io | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: quay.io | |
| username: ${{ secrets.QUAY_USERNAME }} | |
| password: ${{ secrets.QUAY_PASSWORD }} | |
| - name: Log in to Red Hat Container Registry | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: registry.redhat.io | |
| username: ${{ secrets.REDHAT_USERNAME }} | |
| password: ${{ secrets.REDHAT_PASSWORD }} | |
| - name: Build and push ${{ matrix.component.name }} (${{ matrix.arch.suffix }}) | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: ${{ matrix.component.context }} | |
| file: ${{ matrix.component.dockerfile }} | |
| platforms: ${{ matrix.arch.platform }} | |
| push: true | |
| tags: ${{ matrix.component.image }}:${{ needs.release.outputs.new_tag }}-${{ matrix.arch.suffix }} | |
| build-args: AMBIENT_VERSION=${{ needs.release.outputs.new_tag }} | |
| cache-from: type=gha,scope=${{ matrix.component.name }}-${{ matrix.arch.suffix }} | |
| cache-to: type=gha,mode=max,scope=${{ matrix.component.name }}-${{ matrix.arch.suffix }} | |
| merge-manifests: | |
| needs: [release, build] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| id-token: write | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| component: ${{ fromJSON(needs.release.outputs.merge-matrix) }} | |
| steps: | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Log in to Quay.io | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: quay.io | |
| username: ${{ secrets.QUAY_USERNAME }} | |
| password: ${{ secrets.QUAY_PASSWORD }} | |
| - name: Create multi-arch manifest for ${{ matrix.component.name }} | |
| # Suffixes (-amd64, -arm64) must match the arch matrix in the build job above. | |
| # Arch-suffixed tags remain in the registry after merging. Clean these up | |
| # via Quay tag expiration policies or a periodic job. | |
| run: | | |
| docker buildx imagetools create \ | |
| -t ${{ matrix.component.image }}:${{ needs.release.outputs.new_tag }} \ | |
| ${{ matrix.component.image }}:${{ needs.release.outputs.new_tag }}-amd64 \ | |
| ${{ matrix.component.image }}:${{ needs.release.outputs.new_tag }}-arm64 | |
| deploy-to-openshift: | |
| runs-on: ubuntu-latest | |
| needs: [release, merge-manifests] | |
| steps: | |
| - name: Checkout code from release tag | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ needs.release.outputs.new_tag }} | |
| - name: Install oc | |
| uses: redhat-actions/oc-installer@v1 | |
| with: | |
| oc_version: 'latest' | |
| - name: Install kustomize | |
| uses: imranismail/setup-kustomize@v2 | |
| with: | |
| kustomize-version: '5.4.3' | |
| - name: Log in to OpenShift Cluster | |
| run: | | |
| oc login ${{ secrets.PROD_OPENSHIFT_SERVER }} --token=${{ secrets.PROD_OPENSHIFT_TOKEN }} --insecure-skip-tls-verify | |
| - name: Deploy observability stack | |
| run: | | |
| oc apply -k components/manifests/observability/ | |
| - name: Determine which components were built | |
| id: built | |
| run: | | |
| MATRIX='${{ needs.release.outputs.build-matrix }}' | |
| NAMES=$(echo "$MATRIX" | jq -r '[.[].name] | join(",")') | |
| echo "names=$NAMES" >> $GITHUB_OUTPUT | |
| echo "Built components: $NAMES" | |
| - name: Update kustomization with release image tags | |
| working-directory: components/manifests/overlays/production | |
| run: | | |
| RELEASE_TAG="${{ needs.release.outputs.new_tag }}" | |
| BUILT="${{ steps.built.outputs.names }}" | |
| # Map component names to their deployment names and container names | |
| # runner and state-sync are Jobs (no deployment); their tags come from the operator env | |
| declare -A DEPLOY_MAP=( | |
| ["frontend"]="frontend:frontend" | |
| ["backend"]="backend-api:backend-api" | |
| ["operator"]="agentic-operator:agentic-operator" | |
| ["public-api"]="public-api:public-api" | |
| ["ambient-api-server"]="ambient-api-server:ambient-api-server" | |
| ) | |
| for comp_image in \ | |
| "frontend:quay.io/ambient_code/vteam_frontend" \ | |
| "backend:quay.io/ambient_code/vteam_backend" \ | |
| "operator:quay.io/ambient_code/vteam_operator" \ | |
| "ambient-runner:quay.io/ambient_code/vteam_claude_runner" \ | |
| "state-sync:quay.io/ambient_code/vteam_state_sync" \ | |
| "public-api:quay.io/ambient_code/vteam_public_api" \ | |
| "ambient-api-server:quay.io/ambient_code/vteam_api_server"; do | |
| COMP="${comp_image%%:*}" | |
| IMAGE="${comp_image#*:}" | |
| # Seed kustomize with the currently deployed tag so unbuilt components | |
| # don't fall back to the repo's ":latest" placeholder | |
| DEPLOY_INFO="${DEPLOY_MAP[$COMP]:-}" | |
| if [ -n "$DEPLOY_INFO" ]; then | |
| DEPLOY_NAME="${DEPLOY_INFO%%:*}" | |
| CONTAINER_NAME="${DEPLOY_INFO#*:}" | |
| CURRENT_TAG=$(oc get deployment "$DEPLOY_NAME" -n ambient-code \ | |
| -o jsonpath="{.spec.template.spec.containers[?(@.name=='${CONTAINER_NAME}')].image}" 2>/dev/null \ | |
| | grep -oP ':\K[^"]+$' || true) | |
| if [ -n "$CURRENT_TAG" ]; then | |
| kustomize edit set image ${IMAGE}:latest=${IMAGE}:${CURRENT_TAG} | |
| fi | |
| elif [ "$COMP" = "ambient-runner" ]; then | |
| CURRENT_TAG=$(oc get deployment agentic-operator -n ambient-code \ | |
| -o jsonpath='{.spec.template.spec.containers[?(@.name=="agentic-operator")].env[?(@.name=="AMBIENT_CODE_RUNNER_IMAGE")].value}' 2>/dev/null \ | |
| | grep -oP ':\K[^"]+$' || true) | |
| if [ -n "$CURRENT_TAG" ]; then | |
| kustomize edit set image ${IMAGE}:latest=${IMAGE}:${CURRENT_TAG} | |
| fi | |
| elif [ "$COMP" = "state-sync" ]; then | |
| CURRENT_TAG=$(oc get deployment agentic-operator -n ambient-code \ | |
| -o jsonpath='{.spec.template.spec.containers[?(@.name=="agentic-operator")].env[?(@.name=="STATE_SYNC_IMAGE")].value}' 2>/dev/null \ | |
| | grep -oP ':\K[^"]+$' || true) | |
| if [ -n "$CURRENT_TAG" ]; then | |
| kustomize edit set image ${IMAGE}:latest=${IMAGE}:${CURRENT_TAG} | |
| fi | |
| fi | |
| # Override with the new release tag if this component was built | |
| if echo ",$BUILT," | grep -q ",$COMP,"; then | |
| kustomize edit set image ${IMAGE}:latest=${IMAGE}:${RELEASE_TAG} | |
| fi | |
| done | |
| - name: Validate kustomization | |
| working-directory: components/manifests/overlays/production | |
| run: | | |
| kustomize build . > /dev/null | |
| echo "✅ Kustomization validation passed" | |
| - name: Apply production overlay with kustomize | |
| working-directory: components/manifests/overlays/production | |
| run: | | |
| oc apply -k . -n ambient-code | |
| - name: Update frontend environment variables | |
| run: | | |
| oc set env deployment/frontend -n ambient-code -c frontend \ | |
| GITHUB_APP_SLUG="ambient-code" \ | |
| FEEDBACK_URL="https://forms.gle/7XiWrvo6No922DUz6" | |
| - name: Update operator environment variables | |
| run: | | |
| RELEASE_TAG="${{ needs.release.outputs.new_tag }}" | |
| BUILT="${{ steps.built.outputs.names }}" | |
| ARGS="" | |
| if echo ",$BUILT," | grep -q ",ambient-runner,"; then | |
| ARGS="$ARGS AMBIENT_CODE_RUNNER_IMAGE=quay.io/ambient_code/vteam_claude_runner:${RELEASE_TAG}" | |
| fi | |
| if echo ",$BUILT," | grep -q ",state-sync,"; then | |
| ARGS="$ARGS STATE_SYNC_IMAGE=quay.io/ambient_code/vteam_state_sync:${RELEASE_TAG}" | |
| fi | |
| if [ -n "$ARGS" ]; then | |
| oc set env deployment/agentic-operator -n ambient-code -c agentic-operator $ARGS | |
| fi | |
| - name: Update agent registry ConfigMap with release image tags | |
| run: | | |
| RELEASE_TAG="${{ needs.release.outputs.new_tag }}" | |
| BUILT="${{ steps.built.outputs.names }}" | |
| REGISTRY=$(oc get configmap ambient-agent-registry -n ambient-code \ | |
| -o jsonpath='{.data.agent-registry\.json}') | |
| if echo ",$BUILT," | grep -q ",ambient-runner,"; then | |
| REGISTRY=$(echo "$REGISTRY" | sed \ | |
| "s|quay.io/ambient_code/vteam_claude_runner[@:][^\"]*|quay.io/ambient_code/vteam_claude_runner:${RELEASE_TAG}|g") | |
| fi | |
| if echo ",$BUILT," | grep -q ",state-sync,"; then | |
| REGISTRY=$(echo "$REGISTRY" | sed \ | |
| "s|quay.io/ambient_code/vteam_state_sync[@:][^\"]*|quay.io/ambient_code/vteam_state_sync:${RELEASE_TAG}|g") | |
| fi | |
| oc patch configmap ambient-agent-registry -n ambient-code --type=merge \ | |
| -p "{\"data\":{\"agent-registry.json\":$(echo "$REGISTRY" | jq -Rs .)}}" |