Feat rebrand armature (#43)
All checks were successful
CI/CD / detect-changes (push) Successful in 3s
CI/CD / test-frontend (push) Successful in 5s
CI/CD / test-go-pg (push) Successful in 2m34s
CI/CD / test-sqlite (push) Successful in 2m46s
CI/CD / build-and-deploy (push) Successful in 1m55s

Co-authored-by: Jeffrey Smith <jasafpro@gmail.com>
Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
This commit was merged in pull request #43.
This commit is contained in:
2026-03-31 23:25:37 +00:00
committed by xcaliber
parent fb5284f667
commit 680ec3b897
321 changed files with 956 additions and 1033 deletions

View File

@@ -1,6 +1,6 @@
# .gitea/workflows/ci.yaml
# ============================================
# Switchboard Core - CI/CD Pipeline (v0.17.3)
# Armature - CI/CD Pipeline (v0.17.3)
# ============================================
# Single unified image (Go backend + nginx frontend).
# v0.1.0: Dropped FE/BE image split per ROADMAP design decision.
@@ -29,22 +29,22 @@
# Tags (v*) → always full pipeline
#
# Deployment mapping (single domain, path-based):
# PR → FE + BE :dev → switchboard.DOMAIN/dev/ (DB wipe + fresh schema)
# Push to main → FE + BE :test → switchboard.DOMAIN/test/ (migrate only)
# Tag v* → FE + BE :latest → switchboard.DOMAIN/ (migrate only)
# PR → FE + BE :dev → armature.DOMAIN/dev/ (DB wipe + fresh schema)
# Push to main → FE + BE :test → armature.DOMAIN/test/ (migrate only)
# Tag v* → FE + BE :latest → armature.DOMAIN/ (migrate only)
# → Unified :latest → Docker Hub (not deployed)
#
# Database lifecycle:
# CI: bootstrap (admin creds) → create DB + role + extensions
# CI: dev wipe (app creds) → drop tables for fresh install test
# BE: auto-migrate on startup → schema_migrations tracking
# BE: bootstrap admin from SWITCHBOARD_ADMIN_* env vars (upsert on every restart)
# BE: bootstrap admin from ARMATURE_ADMIN_* env vars (upsert on every restart)
#
# Shared PG safety:
# - Bootstrap uses IF NOT EXISTS (idempotent)
# - Dev wipe REFUSES to run on databases not ending in _dev
# - Admin creds never reach the backend pods
# - Each env has its own DB name (switchboard_core_{dev,test,})
# - Each env has its own DB name (armature_{dev,test,})
#
# Required Gitea Variables:
# REGISTRY, NAMESPACE, DOMAIN, POSTGRES_HOST
@@ -58,7 +58,7 @@
# Required Gitea Secrets:
# POSTGRES_USER, POSTGRES_PASSWORD
# POSTGRES_ADMIN_USER, POSTGRES_ADMIN_PASSWORD
# SWITCHBOARD_ADMIN_USERNAME, SWITCHBOARD_ADMIN_PASSWORD, SWITCHBOARD_ADMIN_EMAIL
# ARMATURE_ADMIN_USERNAME, ARMATURE_ADMIN_PASSWORD, ARMATURE_ADMIN_EMAIL
# ENCRYPTION_KEY — AES-256 key for API key encryption (openssl rand -base64 32)
# PROVIDER_KEY — API key for live provider integration tests (optional, tests skip if missing)
# DOCKERHUB_USERNAME, DOCKERHUB_TOKEN (optional)
@@ -88,8 +88,8 @@ env:
CERT_ISSUER: ${{ vars.CERT_ISSUER_PROD || 'letsencrypt-prod' }}
POSTGRES_HOST: ${{ vars.POSTGRES_HOST || 'postgres-primary.postgres.svc.cluster.local' }}
POSTGRES_PORT: ${{ vars.POSTGRES_PORT || '5432' }}
IMAGE: ${{ vars.REGISTRY }}/switchboard/core
DOCKERHUB_IMAGE: ${{ vars.DOCKERHUB_IMAGE || 'gobha/switchboard-core' }}
IMAGE: ${{ vars.REGISTRY }}/armature
DOCKERHUB_IMAGE: ${{ vars.DOCKERHUB_IMAGE || 'gobha/armature' }}
jobs:
# ── Stage 0: Detect Changed Paths ──────────────
@@ -319,7 +319,7 @@ jobs:
PGPASSWORD: ${{ secrets.POSTGRES_ADMIN_PASSWORD }}
APP_USER: ${{ secrets.POSTGRES_USER }}
APP_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
DB_NAME: switchboard_core_ci
DB_NAME: armature_ci
run: |
echo "━━━ CI Test Database Setup ━━━"
# Create DB for Go integration tests (admin creds)
@@ -363,7 +363,7 @@ jobs:
PGUSER: ${{ secrets.POSTGRES_ADMIN_USER }}
PGPASSWORD: ${{ secrets.POSTGRES_ADMIN_PASSWORD }}
run: |
psql -c "DROP DATABASE IF EXISTS switchboard_core_ci;" postgres
psql -c "DROP DATABASE IF EXISTS armature_ci;" postgres
echo "✓ Dropped CI test database"
# ── Stage 2: Build, Database, Deploy ─────────
@@ -395,9 +395,9 @@ jobs:
- name: Determine environment
id: setup
run: |
# All environments share one host: switchboard.DOMAIN
# All environments share one host: armature.DOMAIN
# Environments are separated by path prefix (BASE_PATH)
DEPLOY_HOST="switchboard.${DOMAIN}"
DEPLOY_HOST="armature.${DOMAIN}"
echo "DEPLOY_HOST=${DEPLOY_HOST}" >> "$GITHUB_OUTPUT"
if [[ "${{ gitea.event_name }}" == "pull_request" ]]; then
@@ -406,7 +406,7 @@ jobs:
echo "IMAGE_TAG=dev" >> "$GITHUB_OUTPUT"
echo "BASE_PATH=/dev" >> "$GITHUB_OUTPUT"
echo "DEPLOY_SUFFIX=-dev" >> "$GITHUB_OUTPUT"
echo "DB_NAME=switchboard_core_dev" >> "$GITHUB_OUTPUT"
echo "DB_NAME=armature_dev" >> "$GITHUB_OUTPUT"
echo "DB_WIPE=true" >> "$GITHUB_OUTPUT"
echo "REPLICAS=1" >> "$GITHUB_OUTPUT"
echo "MEMORY_REQUEST=256Mi" >> "$GITHUB_OUTPUT"
@@ -422,7 +422,7 @@ jobs:
echo "EXTRA_TAG=${VERSION}" >> "$GITHUB_OUTPUT"
echo "BASE_PATH=" >> "$GITHUB_OUTPUT"
echo "DEPLOY_SUFFIX=" >> "$GITHUB_OUTPUT"
echo "DB_NAME=switchboard_core" >> "$GITHUB_OUTPUT"
echo "DB_NAME=armature" >> "$GITHUB_OUTPUT"
echo "DB_WIPE=false" >> "$GITHUB_OUTPUT"
echo "REPLICAS=2" >> "$GITHUB_OUTPUT"
echo "MEMORY_REQUEST=256Mi" >> "$GITHUB_OUTPUT"
@@ -437,7 +437,7 @@ jobs:
echo "IMAGE_TAG=test" >> "$GITHUB_OUTPUT"
echo "BASE_PATH=/test" >> "$GITHUB_OUTPUT"
echo "DEPLOY_SUFFIX=-test" >> "$GITHUB_OUTPUT"
echo "DB_NAME=switchboard_core_test" >> "$GITHUB_OUTPUT"
echo "DB_NAME=armature_test" >> "$GITHUB_OUTPUT"
echo "DB_WIPE=false" >> "$GITHUB_OUTPUT"
echo "REPLICAS=1" >> "$GITHUB_OUTPUT"
echo "MEMORY_REQUEST=256Mi" >> "$GITHUB_OUTPUT"
@@ -579,14 +579,17 @@ jobs:
# ── Push to Docker Hub (release only) ────────
- name: Push to Docker Hub
if: steps.setup.outputs.is_release == 'true'
env:
DH_USER: ${{ secrets.DOCKERHUB_USERNAME }}
DH_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
run: |
docker tag ${IMAGE}:${{ steps.setup.outputs.IMAGE_TAG }} ${DOCKERHUB_IMAGE}:latest
docker tag ${IMAGE}:${{ steps.setup.outputs.IMAGE_TAG }} ${DOCKERHUB_IMAGE}:${{ steps.setup.outputs.EXTRA_TAG }}
if [[ -n "${{ secrets.DOCKERHUB_TOKEN }}" ]]; then
echo "${{ secrets.DOCKERHUB_TOKEN }}" | \
docker login -u "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
if [[ -n "${DH_TOKEN}" ]]; then
echo "${DH_TOKEN}" | docker login -u "${DH_USER}" --password-stdin
docker push ${DOCKERHUB_IMAGE}:latest
docker push ${DOCKERHUB_IMAGE}:${{ steps.setup.outputs.EXTRA_TAG }}
echo "✓ Pushed to Docker Hub"
else
echo "⚠ Docker Hub credentials not configured, skipping push"
fi
@@ -600,31 +603,31 @@ jobs:
env:
PG_USER: ${{ secrets.POSTGRES_USER }}
PG_PASS: ${{ secrets.POSTGRES_PASSWORD }}
SW_ADMIN_USER: ${{ secrets.SWITCHBOARD_ADMIN_USERNAME }}
SW_ADMIN_PASS: ${{ secrets.SWITCHBOARD_ADMIN_PASSWORD }}
SW_ADMIN_EMAIL: ${{ secrets.SWITCHBOARD_ADMIN_EMAIL }}
SW_ADMIN_USER: ${{ secrets.ARMATURE_ADMIN_USERNAME }}
SW_ADMIN_PASS: ${{ secrets.ARMATURE_ADMIN_PASSWORD }}
SW_ADMIN_EMAIL: ${{ secrets.ARMATURE_ADMIN_EMAIL }}
SEED_USERS: ${{ vars.SEED_USERS }}
ENCRYPTION_KEY: ${{ secrets.ENCRYPTION_KEY }}
run: |
kubectl create secret generic switchboard-db-credentials \
kubectl create secret generic armature-db-credentials \
--namespace=${NAMESPACE} \
--from-literal=POSTGRES_USER="${PG_USER}" \
--from-literal=POSTGRES_PASSWORD="${PG_PASS}" \
--dry-run=client -o yaml | kubectl apply -f -
kubectl create secret generic switchboard-admin \
kubectl create secret generic armature-admin \
--namespace=${NAMESPACE} \
--from-literal=username="${SW_ADMIN_USER}" \
--from-literal=password="${SW_ADMIN_PASS}" \
--from-literal=email="${SW_ADMIN_EMAIL}" \
--dry-run=client -o yaml | kubectl apply -f -
kubectl create secret generic switchboard-seed-users \
kubectl create secret generic armature-seed-users \
--namespace=${NAMESPACE} \
--from-literal=users="${SEED_USERS}" \
--dry-run=client -o yaml | kubectl apply -f -
kubectl create secret generic switchboard-encryption \
kubectl create secret generic armature-encryption \
--namespace=${NAMESPACE} \
--from-literal=ENCRYPTION_KEY="${ENCRYPTION_KEY}" \
--dry-run=client -o yaml | kubectl apply -f -
@@ -639,7 +642,7 @@ jobs:
S3_REGION: ${{ vars.S3_REGION || 'us-east-1' }}
S3_PREFIX: ${{ vars.S3_PREFIX }}
run: |
kubectl create secret generic switchboard-s3 \
kubectl create secret generic armature-s3 \
--namespace=${NAMESPACE} \
--from-literal=S3_ENDPOINT="${S3_ENDPOINT}" \
--from-literal=S3_BUCKET="${S3_BUCKET}" \
@@ -673,7 +676,7 @@ jobs:
envsubst < k8s/storage-pvc.yaml > /tmp/storage-pvc.yaml
fi
envsubst < k8s/switchboard.yaml > /tmp/switchboard.yaml
envsubst < k8s/armature.yaml > /tmp/armature.yaml
envsubst < k8s/middleware-retry.yaml > /tmp/middleware-retry.yaml
envsubst < k8s/ingress.yaml > /tmp/ingress.yaml
@@ -687,7 +690,7 @@ jobs:
echo " ⚠ STORAGE_CLASS not set — file storage disabled"
fi
kubectl apply -f /tmp/switchboard.yaml
kubectl apply -f /tmp/armature.yaml
# Traefik retry middleware (v0.28.8) — requires RBAC for traefik.io CRDs.
# First-time setup: kubectl apply -f k8s/rbac-traefik.yaml (cluster admin)
@@ -706,8 +709,8 @@ jobs:
# Traefik invalidates the entire router if it references a missing middleware,
# which kills all routes for this path prefix.
if [[ "${MW_READY}" == "true" ]]; then
MW_REF="${NAMESPACE}-switchboard-retry${DEPLOY_SUFFIX}@kubernetescrd"
kubectl annotate ingress "switchboard${DEPLOY_SUFFIX}" \
MW_REF="${NAMESPACE}-armature-retry${DEPLOY_SUFFIX}@kubernetescrd"
kubectl annotate ingress "armature${DEPLOY_SUFFIX}" \
"traefik.ingress.kubernetes.io/router.middlewares=${MW_REF}" \
--namespace="${NAMESPACE}" --overwrite
echo " ✓ Ingress annotated with retry middleware"
@@ -718,20 +721,20 @@ jobs:
SUFFIX: ${{ steps.setup.outputs.DEPLOY_SUFFIX }}
ENV: ${{ steps.setup.outputs.ENVIRONMENT }}
run: |
kubectl rollout restart deployment/switchboard${SUFFIX} -n ${NAMESPACE}
kubectl rollout status deployment/switchboard${SUFFIX} -n ${NAMESPACE} --timeout=180s
kubectl rollout restart deployment/armature${SUFFIX} -n ${NAMESPACE}
kubectl rollout status deployment/armature${SUFFIX} -n ${NAMESPACE} --timeout=180s
echo ""
echo "━━━ Pod Status ━━━"
kubectl get pods -n ${NAMESPACE} -l app=switchboard,env=${ENV}
kubectl get pods -n ${NAMESPACE} -l app=armature,env=${ENV}
READY=$(kubectl get deployment switchboard${SUFFIX} -n ${NAMESPACE} -o jsonpath='{.status.readyReplicas}')
READY=$(kubectl get deployment armature${SUFFIX} -n ${NAMESPACE} -o jsonpath='{.status.readyReplicas}')
if [[ "${READY:-0}" -gt 0 ]]; then
echo "✅ Healthy: ${READY} pods ready"
else
echo "❌ Unhealthy: ${READY:-0} pods ready"
kubectl logs -n ${NAMESPACE} -l app=switchboard,env=${ENV} --tail=30
kubectl logs -n ${NAMESPACE} -l app=armature,env=${ENV} --tail=30
exit 1
fi