Skip to content

Release Pipeline

Release Pipeline #49

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,ambient-control-plane,ambient-mcp) - 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 }}"
# Use Python for reliable changelog generation with author grouping
python3 -c "
import subprocess, sys
latest_tag = sys.argv[1]
new_tag = sys.argv[2]
repo = sys.argv[3]
commit_range = 'HEAD' if latest_tag == 'v0.0.0' else f'{latest_tag}..HEAD'
result = subprocess.run(
['git', 'log', commit_range, '--format=%an<<<<DELIM>>>>%s (%h)'],
capture_output=True, text=True
)
if result.returncode != 0:
print(f'Error: git log failed: {result.stderr}', file=sys.stderr)
sys.exit(1)
commits_by_author = {}
count_by_author = {}
for line in result.stdout.strip().split('\n'):
if line and '<<<<DELIM>>>>' in line:
author, commit = line.split('<<<<DELIM>>>>', 1)
if author not in commits_by_author:
commits_by_author[author] = []
count_by_author[author] = 0
commits_by_author[author].append(commit)
count_by_author[author] += 1
sorted_authors = sorted(count_by_author.items(), key=lambda x: x[1], reverse=True)
# Detect first-time contributors
first_timers = []
if latest_tag != 'v0.0.0':
# Resolve tag to ISO date — --before requires a date, not a ref name
tag_date_result = subprocess.run(
['git', 'log', '-1', '--format=%ci', latest_tag],
capture_output=True, text=True
)
tag_date = tag_date_result.stdout.strip()
if tag_date_result.returncode == 0 and tag_date:
# Get all unique author names before the tag date in one call
prior = subprocess.run(
['git', 'log', '--all', f'--before={tag_date}', '--format=%an'],
capture_output=True, text=True
)
prior_authors = set()
if prior.returncode == 0 and prior.stdout.strip():
prior_authors = set(prior.stdout.strip().split('\n'))
for author, _ in sorted_authors:
if author not in prior_authors:
first_timers.append(author)
print(f'# Release {new_tag}')
print()
print(f'## Changes since {latest_tag}')
print()
if first_timers:
print('## 🎉 First-Time Contributors')
print()
for author in sorted(first_timers):
print(f'- {author}')
print()
for author, count in sorted_authors:
print(f'### {author} ({count})')
for commit in commits_by_author[author]:
print(f'- {commit}')
print()
print(f'**Full Changelog**: https://github.com/{repo}/compare/{latest_tag}...{new_tag}')
" "$LATEST_TAG" "$NEW_TAG" "${{ github.repository }}" > RELEASE_CHANGELOG.md
cat RELEASE_CHANGELOG.md
- name: Generate Loading Tips
run: |
LATEST_TAG="${{ steps.get_latest_tag.outputs.latest_tag }}"
NEW_TAG="${{ steps.next_version.outputs.new_tag }}"
REPO="${{ github.repository }}"
OUTPUT="components/frontend/src/lib/loading-tips.ts"
python3 scripts/generate-loading-tips.py "$NEW_TAG" "$LATEST_TAG" "$REPO" "$OUTPUT"
# Commit the updated loading tips so the frontend build includes them
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add "$OUTPUT"
if git diff --cached --quiet; then
echo "No loading tips changes to commit"
else
git commit -m "chore(frontend): update loading tips for ${NEW_TAG}
Auto-generated release tips highlighting contributors and changes."
fi
- 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/ambient-runner","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"},
{"name":"ambient-control-plane","context":"./components","image":"quay.io/ambient_code/vteam_control_plane","dockerfile":"./components/ambient-control-plane/Dockerfile"},
{"name":"ambient-mcp","context":"./components/ambient-mcp","image":"quay.io/ambient_code/vteam_mcp","dockerfile":"./components/ambient-mcp/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@v7
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 }}
GIT_COMMIT=${{ github.sha }}
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
pre-pull-images:
runs-on: ubuntu-latest
needs: [release, merge-manifests]
permissions:
contents: read
steps:
- name: Checkout code from release tag
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ needs.release.outputs.new_tag }}
- name: Install oc
uses: redhat-actions/openshift-tools-installer@144527c7d98999f2652264c048c7a9bd103f8a82 # v1
with:
oc: latest
- name: Log in to OpenShift Cluster
run: |
oc login ${{ secrets.PROD_OPENSHIFT_SERVER }} --token=${{ secrets.PROD_OPENSHIFT_TOKEN }}
- name: Deploy image puller infrastructure
run: |
oc apply -f components/manifests/components/image-puller/namespace.yaml
oc apply -f components/manifests/components/image-puller/rbac.yaml
oc apply -f components/manifests/components/image-puller/configmap.yaml
oc apply -f components/manifests/components/image-puller/deployment.yaml
oc apply -f components/manifests/components/image-puller/prometheusrule.yaml
- name: Ensure registry pull secrets exist
run: |
# Secret for pulling the image puller itself from registry.redhat.io
oc create secret docker-registry redhat-registry-pull-secret \
--docker-server=registry.redhat.io \
--docker-username="${{ secrets.REDHAT_USERNAME }}" \
--docker-password="${{ secrets.REDHAT_PASSWORD }}" \
-n acp-image-puller --dry-run=client -o yaml | oc apply -f -
# Secret for pulling ACP component images from quay.io
oc create secret docker-registry quay-pull-secret \
--docker-server=quay.io \
--docker-username="${{ secrets.QUAY_USERNAME }}" \
--docker-password="${{ secrets.QUAY_PASSWORD }}" \
-n acp-image-puller --dry-run=client -o yaml | oc apply -f -
- name: Build image list from release components
id: image-list
run: |
RELEASE_TAG="${{ needs.release.outputs.new_tag }}"
MERGE_MATRIX='${{ needs.release.outputs.merge-matrix }}'
# Build semicolon-separated name=image:tag list from the merge matrix
IMAGES=$(echo "$MERGE_MATRIX" | jq -r \
--arg tag "$RELEASE_TAG" \
'[.[] | "\(.name)=\(.image):\($tag)"] | join(";")')
echo "images=${IMAGES}" >> $GITHUB_OUTPUT
echo "Image list for pre-pull:"
echo "$IMAGES" | tr ';' '\n'
- name: Update image puller ConfigMap with new images
run: |
oc patch configmap acp-image-puller-config \
-n acp-image-puller \
--type merge \
-p "{\"data\":{\"IMAGES\":\"${{ steps.image-list.outputs.images }}\"}}"
- name: Restart image puller to pick up new image list
run: |
oc rollout restart deployment/acp-image-puller -n acp-image-puller
oc rollout status deployment/acp-image-puller -n acp-image-puller --timeout=120s
- name: Wait for DaemonSet to pull images on all nodes
run: |
echo "Waiting for image puller DaemonSet to schedule on all nodes..."
for i in $(seq 1 60); do
# The puller needs a moment to create the DaemonSet
DS_STATUS=$(oc get daemonset acp-image-puller-ds -n acp-image-puller \
-o jsonpath='{.status.desiredNumberScheduled},{.status.numberReady}' 2>/dev/null || echo "0,0")
DESIRED=$(echo "$DS_STATUS" | cut -d, -f1)
READY=$(echo "$DS_STATUS" | cut -d, -f2)
if [ "$DESIRED" -gt 0 ] && [ "$DESIRED" -eq "$READY" ]; then
echo "All $READY/$DESIRED nodes have images pre-pulled."
break
fi
if [ "$i" -eq 60 ]; then
echo "::warning::Image pre-pull did not complete on all nodes within timeout ($READY/$DESIRED ready). Proceeding with deploy."
break
fi
echo "Attempt $i/60 - DaemonSet $READY/$DESIRED ready, waiting 10s..."
sleep 10
done
echo "DaemonSet status:"
oc get daemonset -n acp-image-puller || true
deploy-rhoai-mlflow:
runs-on: ubuntu-latest
needs: [release]
steps:
- name: Checkout code from release tag
uses: actions/checkout@v6
with:
ref: ${{ needs.release.outputs.new_tag }}
- name: Install oc
uses: redhat-actions/openshift-tools-installer@v1
with:
oc: latest
- name: Log in to OpenShift Cluster
run: |
oc login ${{ secrets.PROD_OPENSHIFT_SERVER }} --token=${{ secrets.PROD_OPENSHIFT_TOKEN }}
- name: Deploy Service Mesh operator via OLM
run: |
oc apply -f components/manifests/components/openshift-ai/service-mesh-namespace.yaml
oc apply -f components/manifests/components/openshift-ai/service-mesh-operatorgroup.yaml
oc apply -f components/manifests/components/openshift-ai/service-mesh-subscription.yaml
- name: Deploy RHOAI operator via OLM
run: |
oc apply -f components/manifests/components/openshift-ai/namespace.yaml
oc apply -f components/manifests/components/openshift-ai/operatorgroup.yaml
oc apply -f components/manifests/components/openshift-ai/subscription.yaml
- name: Wait for RHOAI operator to be ready
run: |
echo "Waiting for RHOAI operator CSV to appear..."
for i in $(seq 1 60); do
CSV=$(oc get subscription rhods-operator -n redhat-ods-operator \
-o jsonpath='{.status.installedCSV}' 2>/dev/null)
if [ -n "$CSV" ]; then
echo "Found CSV: $CSV"
break
fi
if [ "$i" -eq 60 ]; then
echo "::error::RHOAI operator CSV did not appear within timeout"
exit 1
fi
echo "Attempt $i/60 - CSV not yet available, waiting 10s..."
sleep 10
done
echo "Waiting for CSV $CSV to succeed..."
oc wait csv "$CSV" -n redhat-ods-operator \
--for=jsonpath='{.status.phase}'=Succeeded --timeout=600s
- name: Wait for DataScienceCluster v2 API to be available
run: |
echo "Waiting for DataScienceCluster v2 API to be served..."
for i in $(seq 1 60); do
if oc api-resources --api-group=datasciencecluster.opendatahub.io 2>/dev/null | grep -q v2; then
echo "DataScienceCluster v2 API is available"
break
fi
if [ "$i" -eq 60 ]; then
echo "::error::DataScienceCluster v2 API did not become available within timeout"
exit 1
fi
echo "Attempt $i/60 - v2 API not yet available, waiting 10s..."
sleep 10
done
- name: Apply DSCInitialization and DataScienceCluster
run: |
oc apply -f components/manifests/components/openshift-ai/dsci.yaml
oc apply -f components/manifests/components/openshift-ai/datasciencecluster.yaml
- name: Wait for MLflow Operator CRD to be available
run: |
echo "Waiting for MLflow CRD to be registered..."
for i in $(seq 1 60); do
if oc get crd mlflows.mlflow.opendatahub.io &>/dev/null; then
echo "MLflow CRD is available"
break
fi
if [ "$i" -eq 60 ]; then
echo "::error::MLflow CRD did not become available within timeout"
exit 1
fi
echo "Attempt $i/60 - MLflow CRD not yet available, waiting 10s..."
sleep 10
done
- name: Ensure mlflow database exists in PostgreSQL
run: |
oc exec -n ambient-code deploy/postgresql -- \
psql -U postgres -tAc \
"SELECT 1 FROM pg_database WHERE datname = 'mlflow'" | grep -q 1 \
|| oc exec -n ambient-code deploy/postgresql -- \
psql -U postgres -c "CREATE DATABASE mlflow"
- name: Ensure mlflow-db-credentials secret exists
run: |
echo "Reconciling mlflow-db-credentials from postgresql-credentials..."
DB_USER=$(oc get secret postgresql-credentials -n ambient-code -o jsonpath='{.data.db\.user}' | base64 -d)
DB_PASS=$(oc get secret postgresql-credentials -n ambient-code -o jsonpath='{.data.db\.password}' | base64 -d)
DB_HOST=$(oc get secret postgresql-credentials -n ambient-code -o jsonpath='{.data.db\.host}' | base64 -d)
DB_PORT=$(oc get secret postgresql-credentials -n ambient-code -o jsonpath='{.data.db\.port}' | base64 -d)
ENC_USER=$(python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=""))' "$DB_USER")
ENC_PASS=$(python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=""))' "$DB_PASS")
ENCODED_URI="postgresql://${ENC_USER}:${ENC_PASS}@${DB_HOST}.ambient-code.svc.cluster.local:${DB_PORT}/mlflow?sslmode=disable"
oc create namespace redhat-ods-applications --dry-run=client -o yaml | oc apply -f -
oc apply -f - <<EOF
apiVersion: v1
kind: Secret
metadata:
name: mlflow-db-credentials
namespace: redhat-ods-applications
type: Opaque
stringData:
uri: "${ENCODED_URI}"
EOF
- name: Deploy MLflow instance
run: |
oc apply -f components/manifests/components/openshift-ai/mlflow.yaml
deploy-to-openshift:
runs-on: ubuntu-latest
needs: [release, merge-manifests, pre-pull-images]
steps:
- name: Checkout code from release tag
uses: actions/checkout@v6
with:
ref: ${{ needs.release.outputs.new_tag }}
- name: Install oc
uses: redhat-actions/openshift-tools-installer@v1
with:
oc: 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 }}
- 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"
["ambient-control-plane"]="ambient-control-plane:ambient-control-plane"
)
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" \
"ambient-control-plane:quay.io/ambient_code/vteam_control_plane" \
"ambient-mcp:quay.io/ambient_code/vteam_mcp"; 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" \
GITHUB_CALLBACK_URL="https://ambient-code.apps.rosa.vteam-uat.0ksl.p3.openshiftapps.com/api/auth/github/user/callback" \
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: Pin OPERATOR_IMAGE in operator-config ConfigMap
run: |
RELEASE_TAG="${{ needs.release.outputs.new_tag }}"
BUILT="${{ steps.built.outputs.names }}"
if echo ",$BUILT," | grep -q ",operator,"; then
oc patch configmap operator-config -n ambient-code --type=merge \
-p "{\"data\":{\"OPERATOR_IMAGE\":\"quay.io/ambient_code/vteam_operator:${RELEASE_TAG}\"}}"
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 .)}}"
coderabbit-triage:
needs: [release]
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: read
steps:
- name: Checkout main branch
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: main
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: '3.12'
- name: Run CodeRabbit triage pipeline
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NEW_TAG: ${{ needs.release.outputs.new_tag }}
REPO: ${{ github.repository }}
working-directory: scripts/coderabbit-triage
run: |
PREV_TAG=$(git tag --list 'v*.*.*' --sort=-version:refname | grep -v "^${NEW_TAG}$" | head -1)
echo "Analyzing CodeRabbit reviews for ${NEW_TAG} (since ${PREV_TAG})..."
bash fetch.sh --repo "${REPO}" --release "${NEW_TAG}" --since "${PREV_TAG}"
python3 parse.py "data/${NEW_TAG}"
python3 analyze.py "data/${NEW_TAG}" metrics/
python3 report.py metrics/ --release "${NEW_TAG}" > ../../triage-summary.md
echo "### Summary"
cat ../../triage-summary.md
- name: Commit metrics and open PR
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NEW_TAG: ${{ needs.release.outputs.new_tag }}
run: |
BRANCH="chore/coderabbit-triage-${NEW_TAG}"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git checkout -b "${BRANCH}"
git add "scripts/coderabbit-triage/metrics/${NEW_TAG}.json"
git commit -m "chore: add CodeRabbit triage metrics for ${NEW_TAG}"
git push -u origin "${BRANCH}"
gh pr create \
--title "chore: CodeRabbit triage for ${NEW_TAG}" \
--body-file triage-summary.md \
--base main
- name: Append triage summary to release notes
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NEW_TAG: ${{ needs.release.outputs.new_tag }}
run: |
EXISTING_BODY=$(gh release view "${NEW_TAG}" --json body -q .body)
TRIAGE=$(cat triage-summary.md)
gh release edit "${NEW_TAG}" \
--notes "${EXISTING_BODY}
---
## CodeRabbit Triage Summary
${TRIAGE}"