#!/usr/bin/env bash set -euo pipefail : "${DEPLOYMENT_ENVIRONMENT:?DEPLOYMENT_ENVIRONMENT is required}" : "${KUBE_NAMESPACE:?KUBE_NAMESPACE is required}" : "${PUBLIC_API_ORIGIN:?PUBLIC_API_ORIGIN is required}" : "${MONGODB_CONNECTION_STRING:?MONGODB_CONNECTION_STRING is required}" : "${AUTH_JWT_KEY:?AUTH_JWT_KEY is required}" : "${INTERNAL_PURGE_SECRET:?INTERNAL_PURGE_SECRET is required}" : "${STRIPE_SECRET_KEY:?STRIPE_SECRET_KEY is required}" : "${STRIPE_WEBHOOK_SECRET:?STRIPE_WEBHOOK_SECRET is required}" NODES="${NODES:-192.168.86.72 192.168.86.73 192.168.86.74}" BUILD_TAG="$(git rev-parse HEAD)" API_IMAGE="step3-questions:${BUILD_TAG}" WEB_IMAGE="step3-web:${BUILD_TAG}" TEST_IMAGE="step3-tests:${BUILD_TAG}" API_TAR="/tmp/step3-questions-${DEPLOYMENT_ENVIRONMENT}.tar" WEB_TAR="/tmp/step3-web-${DEPLOYMENT_ENVIRONMENT}.tar" cleanup() { rm -f "${API_TAR}" "${WEB_TAR}" docker image rm "${API_IMAGE}" "${WEB_IMAGE}" "${TEST_IMAGE}" 2>/dev/null || true docker image prune -f >/dev/null || true } trap cleanup EXIT rm -f "${API_TAR}" "${WEB_TAR}" docker container prune -f docker image prune -f docker image prune -af --filter "until=24h" docker builder prune -af || true docker system df df -h docker build -f api/QuestionsApi.Tests/Dockerfile -t "${TEST_IMAGE}" . docker build --build-arg BUILD_REVISION="${BUILD_TAG}" -f api/QuestionsApi/Dockerfile -t "${API_IMAGE}" . docker build \ --build-arg BUILD_REVISION="${BUILD_TAG}" \ --build-arg DEPLOYMENT_ENVIRONMENT="${DEPLOYMENT_ENVIRONMENT}" \ --build-arg PUBLIC_API_ORIGIN="${PUBLIC_API_ORIGIN}" \ -f web/Dockerfile -t "${WEB_IMAGE}" . docker save "${API_IMAGE}" -o "${API_TAR}" docker save "${WEB_IMAGE}" -o "${WEB_TAR}" for node in ${NODES}; do scp -o StrictHostKeyChecking=no "${API_TAR}" hz@"${node}":/tmp/step3-questions.tar scp -o StrictHostKeyChecking=no "${WEB_TAR}" hz@"${node}":/tmp/step3-web.tar ssh -o StrictHostKeyChecking=no hz@"${node}" \ "sudo ctr -n k8s.io images import /tmp/step3-questions.tar && sudo ctr -n k8s.io images import /tmp/step3-web.tar && rm -f /tmp/step3-questions.tar /tmp/step3-web.tar" done kubectl apply -f "k8s/overlays/${DEPLOYMENT_ENVIRONMENT}/namespace.yaml" kubectl create secret generic step3-runtime-secrets \ --from-literal=mongodb-connection-string="${MONGODB_CONNECTION_STRING}" \ --from-literal=jwt-key="${AUTH_JWT_KEY}" \ --from-literal=purge-secret="${INTERNAL_PURGE_SECRET}" \ --from-literal=stripe-secret-key="${STRIPE_SECRET_KEY}" \ --from-literal=stripe-webhook-secret="${STRIPE_WEBHOOK_SECRET}" \ --from-literal=google-client-id="${GOOGLE_CLIENT_ID:-}" \ --from-literal=google-client-secret="${GOOGLE_CLIENT_SECRET:-}" \ --dry-run=client -o yaml | kubectl apply -n "${KUBE_NAMESPACE}" -f - kubectl kustomize "k8s/overlays/${DEPLOYMENT_ENVIRONMENT}" \ | sed "s|step3-questions:latest|${API_IMAGE}|g; s|step3-web:latest|${WEB_IMAGE}|g" \ | kubectl apply -f - # Secret and ConfigMap values are consumed as environment variables at pod startup. # Force a rollout so rerunning an unchanged revision still applies rotated secrets. kubectl rollout restart deployment/step3-api -n "${KUBE_NAMESPACE}" kubectl rollout status deployment/step3-api -n "${KUBE_NAMESPACE}" --timeout=240s kubectl get --raw "/api/v1/namespaces/${KUBE_NAMESPACE}/services/step3-api:http/proxy/readyz" >/dev/null if [ "${DEPLOYMENT_ENVIRONMENT}" = "uat" ]; then # A completed old replica can briefly remain visible immediately after a # rollout. Select the newest pod that is both Running and container-ready # instead of relying on the first item returned by Kubernetes. PROMOTION_SUCCEEDED=0 for attempt in 1 2 3; do API_POD="$(kubectl get pods -n "${KUBE_NAMESPACE}" -l app=step3-api \ --field-selector=status.phase=Running \ --sort-by=.metadata.creationTimestamp \ -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.containerStatuses[0].ready}{"\n"}{end}' \ | awk '$2 == "true" { name = $1 } END { print name }')" if [ -n "${API_POD}" ] \ && kubectl wait -n "${KUBE_NAMESPACE}" --for=condition=Ready "pod/${API_POD}" --timeout=60s \ && kubectl exec -n "${KUBE_NAMESPACE}" "${API_POD}" -- \ dotnet QuestionsApi.dll promote-dynamic-questions \ --source /app/content/dynamic_questions \ --apply; then PROMOTION_SUCCEEDED=1 break fi echo "Dynamic question promotion attempt ${attempt} did not find a stable ready API pod; retrying." >&2 sleep 2 done if [ "${PROMOTION_SUCCEEDED}" -ne 1 ]; then echo "Dynamic question promotion failed after 3 attempts." >&2 exit 1 fi fi kubectl rollout status deployment/step3-web -n "${KUBE_NAMESPACE}" --timeout=240s kubectl get --raw "/api/v1/namespaces/${KUBE_NAMESPACE}/services/step3-web:http/proxy/healthz" >/dev/null if [ "${DEPLOYMENT_ENVIRONMENT}" = "uat" ]; then # The images are already imported on every Kubernetes node. Reclaim runner # space before pulling the Playwright browser image. docker image rm "${API_IMAGE}" "${WEB_IMAGE}" "${TEST_IMAGE}" 2>/dev/null || true docker builder prune -af >/dev/null || true UAT_API_ORIGIN="${PUBLIC_API_ORIGIN}" \ UAT_WEB_ORIGIN="${PUBLIC_WEB_ORIGIN:-https://ustep3.ranaze.com}" \ EXPECTED_REVISION="${BUILD_TAG}" \ bash .gitea/scripts/run-uat-smoke.sh fi echo "Deployed ${DEPLOYMENT_ENVIRONMENT} revision ${BUILD_TAG}."