Add isolated UAT and production environments

This commit is contained in:
hz
2026-07-20 20:32:22 -05:00
parent 4e0ca70157
commit 17747d96a0
30 changed files with 780 additions and 282 deletions
+71
View File
@@ -0,0 +1,71 @@
#!/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}"
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}" 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
dotnet test api/QuestionsApi.Tests/QuestionsApi.Tests.csproj -c Release
docker build --build-arg BUILD_REVISION="${BUILD_TAG}" -t "${API_IMAGE}" api/QuestionsApi
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}" \
--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 -
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
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
echo "Deployed ${DEPLOYMENT_ENVIRONMENT} revision ${BUILD_TAG}."
+41
View File
@@ -0,0 +1,41 @@
name: Deploy STEP3 PRD
on:
push:
branches:
- production
workflow_dispatch: {}
concurrency:
group: step3-prd
cancel-in-progress: false
jobs:
deploy:
runs-on: self-hosted
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Configure kubeconfig
env:
KUBECONFIG_CONTENT: ${{ secrets.KUBECONFIG }}
run: |
if [ -n "$KUBECONFIG_CONTENT" ]; then
mkdir -p /tmp/kube
printf '%s\n' "$KUBECONFIG_CONTENT" > /tmp/kube/config
echo "KUBECONFIG=/tmp/kube/config" >> "$GITHUB_ENV"
fi
- name: Deploy PRD
env:
DEPLOYMENT_ENVIRONMENT: prd
KUBE_NAMESPACE: step3-prd
PUBLIC_API_ORIGIN: https://s3q.ranaze.com
MONGODB_CONNECTION_STRING: ${{ secrets.PRD_MONGODB_CONNECTION_STRING }}
AUTH_JWT_KEY: ${{ secrets.PRD_AUTH_JWT_KEY }}
INTERNAL_PURGE_SECRET: ${{ secrets.PRD_INTERNAL_PURGE_SECRET }}
STRIPE_SECRET_KEY: ${{ secrets.PRD_STRIPE_SECRET_KEY }}
STRIPE_WEBHOOK_SECRET: ${{ secrets.PRD_STRIPE_WEBHOOK_SECRET }}
run: bash .gitea/scripts/deploy-environment.sh
-111
View File
@@ -1,111 +0,0 @@
name: Deploy STEP3 Questions API
on:
push:
branches:
- main
workflow_dispatch: {}
jobs:
deploy:
runs-on: self-hosted
env:
IMAGE_NAME: step3-questions:latest
IMAGE_TAR: /tmp/step3-questions.tar
NODES: "192.168.86.72 192.168.86.73 192.168.86.74"
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Build Docker image
run: |
cd api/QuestionsApi
docker build -t "${IMAGE_NAME}" .
- name: Save Docker image to TAR
run: |
docker save "${IMAGE_NAME}" -o "${IMAGE_TAR}"
- name: Copy TAR to nodes
run: |
for node in ${NODES}; do
echo "Copying image tar to $node ..."
scp -o StrictHostKeyChecking=no "${IMAGE_TAR}" hz@"$node":/tmp/step3-questions.tar
done
- name: Import image on nodes
run: |
for node in ${NODES}; do
echo "Importing image on $node ..."
ssh -o StrictHostKeyChecking=no hz@"$node" "sudo ctr -n k8s.io images import /tmp/step3-questions.tar"
done
- name: Clean TAR from nodes
run: |
for node in ${NODES}; do
echo "Removing image tar on $node ..."
ssh -o StrictHostKeyChecking=no hz@"$node" "rm -f /tmp/step3-questions.tar"
done
- name: Clean TAR on runner
run: |
rm -f "${IMAGE_TAR}"
- name: Configure kubeconfig
env:
KUBECONFIG_CONTENT: ${{ secrets.KUBECONFIG }}
run: |
if [ -n "$KUBECONFIG_CONTENT" ]; then
mkdir -p /tmp/kube
printf '%s\n' "$KUBECONFIG_CONTENT" > /tmp/kube/config
echo "KUBECONFIG=/tmp/kube/config" >> "$GITHUB_ENV"
echo "Using kubeconfig from KUBECONFIG secret."
else
echo "KUBECONFIG secret is not set for this repo; using runner default kubeconfig."
fi
- name: Create namespace if missing
run: |
kubectl create namespace step3-questions --dry-run=client -o yaml | kubectl apply -f -
- name: Configure internal API secret
env:
INTERNAL_PURGE_SECRET: ${{ secrets.INTERNAL_PURGE_SECRET }}
run: |
test -n "$INTERNAL_PURGE_SECRET"
kubectl create secret generic medmind-internal-api \
--from-literal=purge-secret="$INTERNAL_PURGE_SECRET" \
--dry-run=client -o yaml | kubectl apply -n step3-questions -f -
- name: Configure Stripe secrets
env:
STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
STRIPE_WEBHOOK_SECRET: ${{ secrets.STRIPE_WEBHOOK_SECRET }}
run: |
test -n "$STRIPE_SECRET_KEY"
test -n "$STRIPE_WEBHOOK_SECRET"
kubectl create secret generic medmind-stripe \
--from-literal=secret-key="$STRIPE_SECRET_KEY" \
--from-literal=webhook-secret="$STRIPE_WEBHOOK_SECRET" \
--dry-run=client -o yaml | kubectl apply -n step3-questions -f -
- name: Configure authentication secrets
env:
AUTH_JWT_KEY: ${{ secrets.AUTH_JWT_KEY }}
run: |
test -n "$AUTH_JWT_KEY"
kubectl create secret generic ranaze-auth \
--from-literal=jwt-key="$AUTH_JWT_KEY" \
--dry-run=client -o yaml | kubectl apply -n step3-questions -f -
- name: Apply Questions deployment & service
run: |
kubectl apply -f api/QuestionsApi/k8s/deployment.yaml -n step3-questions
kubectl apply -f api/QuestionsApi/k8s/service.yaml -n step3-questions
- name: Restart Questions deployment
run: |
kubectl rollout restart deployment/step3-questions -n step3-questions
kubectl rollout status deployment/step3-questions -n step3-questions
+41
View File
@@ -0,0 +1,41 @@
name: Deploy STEP3 UAT
on:
push:
branches:
- main
workflow_dispatch: {}
concurrency:
group: step3-uat
cancel-in-progress: false
jobs:
deploy:
runs-on: self-hosted
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Configure kubeconfig
env:
KUBECONFIG_CONTENT: ${{ secrets.KUBECONFIG }}
run: |
if [ -n "$KUBECONFIG_CONTENT" ]; then
mkdir -p /tmp/kube
printf '%s\n' "$KUBECONFIG_CONTENT" > /tmp/kube/config
echo "KUBECONFIG=/tmp/kube/config" >> "$GITHUB_ENV"
fi
- name: Deploy UAT
env:
DEPLOYMENT_ENVIRONMENT: uat
KUBE_NAMESPACE: step3-uat
PUBLIC_API_ORIGIN: https://us3q.ranaze.com
MONGODB_CONNECTION_STRING: ${{ secrets.UAT_MONGODB_CONNECTION_STRING }}
AUTH_JWT_KEY: ${{ secrets.UAT_AUTH_JWT_KEY }}
INTERNAL_PURGE_SECRET: ${{ secrets.UAT_INTERNAL_PURGE_SECRET }}
STRIPE_SECRET_KEY: ${{ secrets.UAT_STRIPE_SECRET_KEY }}
STRIPE_WEBHOOK_SECRET: ${{ secrets.UAT_STRIPE_WEBHOOK_SECRET }}
run: bash .gitea/scripts/deploy-environment.sh
-85
View File
@@ -1,85 +0,0 @@
name: Deploy STEP3 Web Game
on:
push:
branches:
- main
workflow_dispatch: {}
jobs:
deploy:
runs-on: self-hosted
env:
IMAGE_TAR: /tmp/step3-web.tar
NODES: "192.168.86.72 192.168.86.73 192.168.86.74"
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Set build identity
run: |
echo "BUILD_TAG=$(git rev-parse HEAD)" >> "$GITHUB_ENV"
- name: Reclaim failed build layers
run: |
rm -f "${IMAGE_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
- name: Build Web game image
run: |
docker build --build-arg BUILD_REVISION="${BUILD_TAG}" -f web/Dockerfile -t "step3-web:${BUILD_TAG}" .
- name: Save image
run: |
docker save "step3-web:${BUILD_TAG}" -o "${IMAGE_TAR}"
- name: Import image on Kubernetes nodes
run: |
for node in ${NODES}; do
scp -o StrictHostKeyChecking=no "${IMAGE_TAR}" hz@"$node":/tmp/step3-web.tar
ssh -o StrictHostKeyChecking=no hz@"$node" "sudo ctr -n k8s.io images import /tmp/step3-web.tar && rm -f /tmp/step3-web.tar"
done
- name: Clean runner build artifacts
if: always()
run: |
rm -f "${IMAGE_TAR}"
if [ -n "${BUILD_TAG}" ]; then
docker image rm "step3-web:${BUILD_TAG}" 2>/dev/null || true
fi
docker image prune -f
- name: Configure kubeconfig
env:
KUBECONFIG_CONTENT: ${{ secrets.KUBECONFIG }}
run: |
if [ -n "$KUBECONFIG_CONTENT" ]; then
mkdir -p /tmp/kube
printf '%s\n' "$KUBECONFIG_CONTENT" > /tmp/kube/config
echo "KUBECONFIG=/tmp/kube/config" >> "$GITHUB_ENV"
fi
- name: Deploy Web game
run: |
for attempt in $(seq 1 30); do
if kubectl get --raw "/api/v1/namespaces/step3-questions/services/step3-questions:http/proxy/api/Auth/legal" >/dev/null; then
echo "Merged authentication API is ready."
break
fi
if [ "$attempt" -eq 30 ]; then
echo "Merged authentication API did not become ready; preserving the current web deployment."
exit 1
fi
sleep 10
done
kubectl create namespace step3-web --dry-run=client -o yaml | kubectl apply -f -
kubectl apply -f web/k8s/service.yaml -n step3-web
sed "s|step3-web:latest|step3-web:${BUILD_TAG}|" web/k8s/deployment.yaml | kubectl apply -n step3-web -f -
kubectl rollout status deployment/step3-web -n step3-web --timeout=180s
+120
View File
@@ -0,0 +1,120 @@
name: Promote UAT content to PRD
on:
workflow_dispatch:
inputs:
mode:
description: "dry-run or apply"
required: true
default: dry-run
confirmation:
description: "Enter PROMOTE_TO_PRD when mode is apply"
required: false
default: ""
concurrency:
group: step3-content-promotion
cancel-in-progress: false
jobs:
promote:
runs-on: self-hosted
steps:
- name: Checkout production configuration
uses: actions/checkout@v4
with:
ref: production
- name: Configure kubeconfig
env:
KUBECONFIG_CONTENT: ${{ secrets.KUBECONFIG }}
run: |
if [ -n "$KUBECONFIG_CONTENT" ]; then
mkdir -p /tmp/kube
printf '%s\n' "$KUBECONFIG_CONTENT" > /tmp/kube/config
echo "KUBECONFIG=/tmp/kube/config" >> "$GITHUB_ENV"
fi
- name: Validate promotion request
env:
MODE: ${{ inputs.mode }}
CONFIRMATION: ${{ inputs.confirmation }}
run: |
test "$MODE" = "dry-run" || test "$MODE" = "apply"
if [ "$MODE" = "apply" ]; then
test "$CONFIRMATION" = "PROMOTE_TO_PRD"
fi
- name: Configure promotion credentials
env:
SOURCE_CONNECTION: ${{ secrets.UAT_MONGODB_CONNECTION_STRING }}
TARGET_CONNECTION: ${{ secrets.PRD_MONGODB_CONNECTION_STRING }}
run: |
test -n "$SOURCE_CONNECTION"
test -n "$TARGET_CONNECTION"
kubectl create secret generic step3-content-promotion \
--from-literal=source-connection="$SOURCE_CONNECTION" \
--from-literal=target-connection="$TARGET_CONNECTION" \
--dry-run=client -o yaml | kubectl apply -n step3-prd -f -
- name: Run promotion
env:
MODE: ${{ inputs.mode }}
CONFIRMATION: ${{ inputs.confirmation }}
run: |
IMAGE="$(kubectl get deployment step3-api -n step3-prd -o jsonpath='{.spec.template.spec.containers[0].image}')"
REVISION="${IMAGE##*:}"
POD="content-promotion-${GITHUB_RUN_NUMBER}"
APPLY_ARGS=""
if [ "$MODE" = "apply" ]; then
APPLY_ARGS="--apply --confirm $CONFIRMATION"
fi
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: ${POD}
namespace: step3-prd
labels:
app: step3-content-promotion
spec:
restartPolicy: Never
containers:
- name: promotion
image: ${IMAGE}
imagePullPolicy: IfNotPresent
command: ["/bin/sh", "-c"]
args:
- >-
dotnet QuestionsApi.dll promote-content
--source-database ranaze_step3_uat
--target-database ranaze_step3_prd
--operator "${GITHUB_ACTOR:-gitea-operator}"
--build-revision "${REVISION}"
${APPLY_ARGS}
env:
- name: SOURCE_MONGODB_CONNECTION_STRING
valueFrom:
secretKeyRef:
name: step3-content-promotion
key: source-connection
- name: TARGET_MONGODB_CONNECTION_STRING
valueFrom:
secretKeyRef:
name: step3-content-promotion
key: target-connection
EOF
for attempt in $(seq 1 120); do
PHASE="$(kubectl get pod "$POD" -n step3-prd -o jsonpath='{.status.phase}')"
if [ "$PHASE" = "Succeeded" ] || [ "$PHASE" = "Failed" ]; then break; fi
sleep 2
done
kubectl logs "$POD" -n step3-prd
test "$(kubectl get pod "$POD" -n step3-prd -o jsonpath='{.status.phase}')" = "Succeeded"
- name: Remove promotion credentials and pod
if: always()
run: |
kubectl delete pod "content-promotion-${GITHUB_RUN_NUMBER}" -n step3-prd --ignore-not-found
kubectl delete secret step3-content-promotion -n step3-prd --ignore-not-found