Repository navigation
Merge pull request #153 from google/fedebongio-openclaw #118
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: Deploy SAM Hub | |
| on: | |
| push: | |
| branches: | |
| - main | |
| tags: | |
| - 'v*.*.*' | |
| workflow_dispatch: | |
| env: | |
| REGISTRY: ghcr.io | |
| jobs: | |
| publish: | |
| name: Publish Images | |
| runs-on: ubuntu-latest | |
| permissions: | |
| packages: write | |
| contents: read | |
| timeout-minutes: 100 | |
| steps: | |
| - name: Check out code | |
| uses: actions/checkout@v7 | |
| - name: Set up QEMU | |
| uses: docker/setup-qemu-action@v4 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v4 | |
| - name: Log in to the Container registry | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Extract metadata for Hub | |
| id: meta-hub | |
| uses: docker/metadata-action@v6 | |
| with: | |
| images: ${{ env.REGISTRY }}/google/sam-hub | |
| tags: | | |
| type=ref,event=branch | |
| type=ref,event=tag | |
| type=raw,value=${{ github.sha }} | |
| type=raw,value=latest,enable={{is_default_branch}} | |
| type=raw,value=stable,enable=${{ startsWith(github.ref, 'refs/tags/v') }} | |
| - name: Build and push Hub image | |
| uses: docker/build-push-action@v7 | |
| with: | |
| context: . | |
| file: Dockerfile.sam-hub | |
| platforms: ${{ github.ref_type == 'tag' && 'linux/amd64,linux/arm64' || 'linux/amd64' }} | |
| push: true | |
| tags: ${{ steps.meta-hub.outputs.tags }} | |
| labels: ${{ steps.meta-hub.outputs.labels }} | |
| - name: Extract metadata for Node | |
| id: meta-node | |
| uses: docker/metadata-action@v6 | |
| with: | |
| images: ${{ env.REGISTRY }}/google/sam-node | |
| tags: | | |
| type=ref,event=branch | |
| type=ref,event=tag | |
| type=raw,value=${{ github.sha }} | |
| type=raw,value=latest,enable={{is_default_branch}} | |
| type=raw,value=stable,enable=${{ startsWith(github.ref, 'refs/tags/v') }} | |
| - name: Build and push Node image | |
| uses: docker/build-push-action@v7 | |
| with: | |
| context: . | |
| file: Dockerfile.sam-node | |
| platforms: ${{ github.ref_type == 'tag' && 'linux/amd64,linux/arm64' || 'linux/amd64' }} | |
| push: true | |
| tags: ${{ steps.meta-node.outputs.tags }} | |
| labels: ${{ steps.meta-node.outputs.labels }} | |
| deploy: | |
| name: Deploy to GKE | |
| runs-on: ubuntu-latest | |
| needs: publish | |
| # Dynamically select the GitHub Environment based on the trigger | |
| environment: ${{ github.ref_type == 'tag' && 'hub' || 'bananas' }} | |
| permissions: | |
| contents: 'read' | |
| id-token: 'write' | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v7 | |
| - name: Determine Image Tag | |
| run: | | |
| if [[ "${{ github.ref_type }}" == "tag" ]]; then | |
| echo "IMAGE_TAG=${{ github.ref_name }}" >> $GITHUB_ENV | |
| else | |
| echo "IMAGE_TAG=${{ github.sha }}" >> $GITHUB_ENV | |
| fi | |
| - name: Google Auth | |
| uses: google-github-actions/auth@v3 | |
| with: | |
| workload_identity_provider: ${{ vars.WIF_PROVIDER_NAME }} | |
| service_account: ${{ vars.SERVICE_ACCOUNT_EMAIL }} | |
| - name: Set up GKE credentials | |
| uses: google-github-actions/get-gke-credentials@v3 | |
| with: | |
| cluster_name: ${{ vars.CLUSTER_NAME }} | |
| location: ${{ vars.CLUSTER_REGION }} | |
| - name: Prepare Namespaces | |
| run: | | |
| export ENV_NAME="${{ vars.ENV_NAME }}" | |
| export NAMESPACE="sam-${ENV_NAME}" | |
| export CANARY_NAMESPACE="sam-canary-${ENV_NAME}" | |
| kubectl create namespace dex --dry-run=client -o yaml | kubectl apply -f - | |
| kubectl create namespace ${NAMESPACE} --dry-run=client -o yaml | kubectl apply -f - | |
| kubectl create namespace ${CANARY_NAMESPACE} --dry-run=client -o yaml | kubectl apply -f - | |
| - name: Provision Dex Secrets | |
| env: | |
| GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }} | |
| GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }} | |
| GITHUB_CLIENT_ID: ${{ secrets.GHUB_CLIENT_ID }} | |
| GITHUB_CLIENT_SECRET: ${{ secrets.GHUB_CLIENT_SECRET }} | |
| CLI_OAUTH_SECRET: ${{ secrets.CLI_OAUTH_SECRET }} | |
| run: | | |
| # Declaratively apply secrets without writing them to disk | |
| kubectl create secret generic dex-secrets \ | |
| --namespace=dex \ | |
| --from-literal=google-client-id="${GOOGLE_CLIENT_ID}" \ | |
| --from-literal=google-client-secret="${GOOGLE_CLIENT_SECRET}" \ | |
| --from-literal=github-client-id="${GITHUB_CLIENT_ID}" \ | |
| --from-literal=github-client-secret="${GITHUB_CLIENT_SECRET}" \ | |
| --from-literal=cli-oauth-secret="${CLI_OAUTH_SECRET}" \ | |
| --dry-run=client -o yaml | kubectl apply -f - | |
| - name: Deploy Dex Resources | |
| run: | | |
| print_rollout_diagnostics() { | |
| local namespace="$1" | |
| local deployment="$2" | |
| local selector="$3" | |
| echo "Collecting diagnostics for deployment/${deployment} in namespace ${namespace}..." | |
| kubectl describe deployment/${deployment} -n ${namespace} || true | |
| kubectl get pods -n ${namespace} -l "${selector}" -o wide || true | |
| kubectl describe pods -n ${namespace} -l "${selector}" || true | |
| kubectl get events -n ${namespace} --sort-by=.lastTimestamp || true | |
| for pod in $(kubectl get pods -n ${namespace} -l "${selector}" -o name 2>/dev/null); do | |
| echo "==== Describe ${pod} ====" | |
| kubectl describe -n ${namespace} "${pod}" || true | |
| for container in $(kubectl get -n ${namespace} "${pod}" -o jsonpath='{.spec.containers[*].name}' 2>/dev/null); do | |
| echo "==== Logs for ${pod} container ${container} ====" | |
| kubectl logs -n ${namespace} "${pod#pod/}" -c "${container}" --tail=-1 || true | |
| done | |
| done | |
| } | |
| # Apply the Dex manifests | |
| kubectl apply -f .github/k8s/dex-config.yaml | |
| kubectl apply -f .github/k8s/dex-deployment.yaml | |
| # Force a restart to pick up any changes to the secrets, and wait for readiness | |
| kubectl rollout restart deployment/dex -n dex | |
| kubectl rollout status deployment/dex -n dex --timeout=120s || { | |
| echo "Dex Deployment failed!" | |
| print_rollout_diagnostics "dex" "dex" "app=dex" | |
| echo "Rolling back Dex deployment..." | |
| kubectl rollout undo deployment/dex -n dex | |
| exit 1 | |
| } | |
| - name: Provision Hub Secrets | |
| run: | | |
| export ENV_NAME="${{ vars.ENV_NAME }}" | |
| export NAMESPACE="sam-${ENV_NAME}" | |
| # Idempotently provision the sam-hub-secret if it doesn't already exist | |
| if ! kubectl get secret sam-hub-secret -n ${NAMESPACE} >/dev/null 2>&1; then | |
| echo "Generating a new random 32-byte hex key for SAM_HUB_KEY..." | |
| HUB_KEY=$(openssl rand -hex 32) | |
| kubectl create secret generic sam-hub-secret \ | |
| --namespace=${NAMESPACE} \ | |
| --from-literal=sam-hub-key="${HUB_KEY}" | |
| else | |
| echo "sam-hub-secret already exists in namespace ${NAMESPACE}. Skipping generation." | |
| fi | |
| - name: Deploy Hub | |
| run: | | |
| print_rollout_diagnostics() { | |
| local namespace="$1" | |
| local resource="$2" | |
| local selector="$3" | |
| echo "Collecting diagnostics for ${resource} in namespace ${namespace}..." | |
| kubectl describe ${resource} -n ${namespace} || true | |
| kubectl get pods -n ${namespace} -l "${selector}" -o wide || true | |
| kubectl describe pods -n ${namespace} -l "${selector}" || true | |
| kubectl get events -n ${namespace} --sort-by=.lastTimestamp || true | |
| for pod in $(kubectl get pods -n ${namespace} -l "${selector}" -o name 2>/dev/null); do | |
| echo "==== Describe ${pod} ====" | |
| kubectl describe -n ${namespace} "${pod}" || true | |
| for container in $(kubectl get -n ${namespace} "${pod}" -o jsonpath='{.spec.containers[*].name}' 2>/dev/null); do | |
| echo "==== Logs for ${pod} container ${container} ====" | |
| kubectl logs -n ${namespace} "${pod#pod/}" -c "${container}" --tail=-1 || true | |
| done | |
| done | |
| } | |
| sudo apt-get install -y gettext-base | |
| export ENV_NAME="${{ vars.ENV_NAME }}" | |
| export LOAD_BALANCER_IP="${{ vars.LOAD_BALANCER_IP }}" | |
| export EXTERNAL_MULTIADDR="${{ vars.EXTERNAL_MULTIADDR }}" | |
| export NAMESPACE="sam-${ENV_NAME}" | |
| export IMAGE_TAG="${{ env.IMAGE_TAG }}" | |
| export GCP_PROJECT_ID="${{ vars.GCP_PROJECT_ID }}" | |
| export CLUSTER_REGION="${{ vars.CLUSTER_REGION }}" | |
| export CLUSTER_NAME="${{ vars.CLUSTER_NAME }}" | |
| envsubst '${ENV_NAME} ${NAMESPACE} ${GCP_PROJECT_ID} ${CLUSTER_NAME} ${CLUSTER_REGION} ${IMAGE_TAG}' < .github/k8s/sam-hub-template.yaml | kubectl apply -f - | |
| envsubst '${ENV_NAME} ${NAMESPACE} ${GCP_PROJECT_ID}' < .github/k8s/dns-sync-cronjob-template.yaml | kubectl apply -f - | |
| kubectl rollout status statefulset/sam-hub-${ENV_NAME} -n ${NAMESPACE} --timeout=120s || { | |
| echo "Hub Deployment failed!" | |
| print_rollout_diagnostics "${NAMESPACE}" "statefulset/sam-hub-${ENV_NAME}" "app=sam-hub-${ENV_NAME}" | |
| echo "Rolling back Hub deployment..." | |
| kubectl rollout undo statefulset/sam-hub-${ENV_NAME} -n ${NAMESPACE} | |
| kubectl rollout status statefulset/sam-hub-${ENV_NAME} -n ${NAMESPACE} | |
| exit 1 | |
| } | |
| echo "Triggering immediate DNS sync after Hub rollout..." | |
| kubectl delete job dns-sync-rollout-${ENV_NAME} -n ${NAMESPACE} --ignore-not-found || true | |
| kubectl create job --from=cronjob/dns-sync-cronjob dns-sync-rollout-${ENV_NAME} -n ${NAMESPACE} | |
| echo "Waiting for DNS sync job to complete..." | |
| kubectl wait --for=condition=complete job/dns-sync-rollout-${ENV_NAME} -n ${NAMESPACE} --timeout=180s || { | |
| echo "DNS sync job failed or timed out!" | |
| kubectl describe job/dns-sync-rollout-${ENV_NAME} -n ${NAMESPACE} | |
| kubectl logs -n ${NAMESPACE} -l job-name=dns-sync-rollout-${ENV_NAME} --tail=-1 || true | |
| exit 1 | |
| } | |
| - name: Deploy Canaries | |
| run: | | |
| print_rollout_diagnostics() { | |
| local namespace="$1" | |
| local deployment="$2" | |
| local selector="$3" | |
| echo "Collecting diagnostics for deployment/${deployment} in namespace ${namespace}..." | |
| kubectl describe deployment/${deployment} -n ${namespace} || true | |
| kubectl get pods -n ${namespace} -l "${selector}" -o wide || true | |
| kubectl describe pods -n ${namespace} -l "${selector}" || true | |
| kubectl get events -n ${namespace} --sort-by=.lastTimestamp || true | |
| for pod in $(kubectl get pods -n ${namespace} -l "${selector}" -o name 2>/dev/null); do | |
| echo "==== Describe ${pod} ====" | |
| kubectl describe -n ${namespace} "${pod}" || true | |
| for container in $(kubectl get -n ${namespace} "${pod}" -o jsonpath='{.spec.containers[*].name}' 2>/dev/null); do | |
| echo "==== Logs for ${pod} container ${container} ====" | |
| kubectl logs -n ${namespace} "${pod#pod/}" -c "${container}" --tail=-1 || true | |
| done | |
| done | |
| } | |
| export ENV_NAME="${{ vars.ENV_NAME }}" | |
| export CANARY_NAMESPACE="sam-canary-${ENV_NAME}" | |
| export NAMESPACE="sam-${ENV_NAME}" | |
| export IMAGE_TAG="${{ env.IMAGE_TAG }}" | |
| export GEMINI_API_KEY="${{ secrets.GEMINI_API_KEY }}" | |
| export HF_TOKEN="${{ secrets.HF_TOKEN }}" | |
| # Read and indent banana bot playground script for the ConfigMap template | |
| export BANANA_BOT_SCRIPT=$(cat docs/snippets/banana_bot_playground.py | sed 's/^/ /') | |
| # Provision the openclaw-secret, preserving the gateway-token if it exists | |
| GATEWAY_TOKEN=$(kubectl get secret openclaw-secret-${ENV_NAME} -n ${CANARY_NAMESPACE} -o jsonpath="{.data.gateway-token}" 2>/dev/null | base64 -d || openssl rand -hex 16) | |
| kubectl create secret generic openclaw-secret-${ENV_NAME} \ | |
| --namespace=${CANARY_NAMESPACE} \ | |
| --from-literal=gateway-token="${GATEWAY_TOKEN}" \ | |
| --from-literal=gemini-api-key="${GEMINI_API_KEY}" \ | |
| --dry-run=client -o yaml | kubectl apply -f - | |
| kubectl create secret generic vllm-secret-${ENV_NAME} \ | |
| --namespace=${CANARY_NAMESPACE} \ | |
| --from-literal=hf-token="${HF_TOKEN}" \ | |
| --dry-run=client -o yaml | kubectl apply -f - | |
| envsubst < .github/k8s/sam-node-template.yaml | kubectl apply -f - | |
| envsubst < .github/k8s/sam-node-openclaw-template.yaml | kubectl apply -f - | |
| envsubst < .github/k8s/sam-node-vllm-template.yaml | kubectl apply -f - | |
| envsubst < .github/k8s/sam-node-everything-template.yaml | kubectl apply -f - | |
| envsubst < .github/k8s/sam-node-cop-template.yaml | kubectl apply -f - | |
| kubectl rollout status deployment/cop-canary-${ENV_NAME} -n ${CANARY_NAMESPACE} --timeout=120s || { | |
| echo "Cop Canary Deployment failed!" | |
| print_rollout_diagnostics "${CANARY_NAMESPACE}" "cop-canary-${ENV_NAME}" "app=cop-canary-${ENV_NAME}" | |
| exit 1 | |
| } | |
| kubectl rollout status deployment/sam-canary-${ENV_NAME} -n ${CANARY_NAMESPACE} --timeout=120s || { | |
| echo "Canary Deployment failed!" | |
| print_rollout_diagnostics "${CANARY_NAMESPACE}" "sam-canary-${ENV_NAME}" "app=sam-canary-${ENV_NAME}" | |
| echo "Rolling back Canary deployment..." | |
| kubectl rollout undo deployment/sam-canary-${ENV_NAME} -n ${CANARY_NAMESPACE} | |
| kubectl rollout status deployment/sam-canary-${ENV_NAME} -n ${CANARY_NAMESPACE} || true | |
| exit 1 | |
| } | |
| kubectl rollout status deployment/openclaw-canary-${ENV_NAME} -n ${CANARY_NAMESPACE} --timeout=120s || { | |
| echo "OpenClaw Canary Deployment failed!" | |
| print_rollout_diagnostics "${CANARY_NAMESPACE}" "openclaw-canary-${ENV_NAME}" "app=openclaw-canary-${ENV_NAME}" | |
| echo "Rolling back OpenClaw Canary deployment..." | |
| kubectl rollout undo deployment/openclaw-canary-${ENV_NAME} -n ${CANARY_NAMESPACE} | |
| kubectl rollout status deployment/openclaw-canary-${ENV_NAME} -n ${CANARY_NAMESPACE} || true | |
| exit 1 | |
| } | |
| kubectl rollout status deployment/vllm-canary-${ENV_NAME} -n ${CANARY_NAMESPACE} --timeout=300s || { | |
| echo "vLLM Canary Deployment failed!" | |
| print_rollout_diagnostics "${CANARY_NAMESPACE}" "vllm-canary-${ENV_NAME}" "app=vllm-canary-${ENV_NAME}" | |
| echo "Rolling back vLLM Canary deployment..." | |
| kubectl rollout undo deployment/vllm-canary-${ENV_NAME} -n ${CANARY_NAMESPACE} | |
| kubectl rollout status deployment/vllm-canary-${ENV_NAME} -n ${CANARY_NAMESPACE} || true | |
| exit 1 | |
| } | |
| kubectl rollout status deployment/everything-canary-${ENV_NAME} -n ${CANARY_NAMESPACE} --timeout=120s || { | |
| echo "Everything Canary Deployment failed!" | |
| print_rollout_diagnostics "${CANARY_NAMESPACE}" "everything-canary-${ENV_NAME}" "app=everything-canary-${ENV_NAME}" | |
| echo "Rolling back Everything Canary deployment..." | |
| kubectl rollout undo deployment/everything-canary-${ENV_NAME} -n ${CANARY_NAMESPACE} | |
| kubectl rollout status deployment/everything-canary-${ENV_NAME} -n ${CANARY_NAMESPACE} || true | |
| exit 1 | |
| } |