From f68b828c8f1b444cb86cfb997528d9ebbede5f22 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Fri, 6 Mar 2026 17:40:50 -0600 Subject: [PATCH] ci: add test gates for production deployments Adds Forgejo Actions CI/CD workflows: Production deployment (.forgejo/workflows/production.yml): - Runs on push to 'production' branch - Test gates (must pass before deploy): * All ExUnit tests with coverage * All e2e Playwright tests - Only deploys if both test suites pass - Builds Docker image - Pushes to git.mcintire.me registry - Updates k8s/deployment.yaml with new image tag - FluxCD auto-applies changes Main branch testing (.forgejo/workflows/test.yml): - Runs on push to 'main' or pull requests - Runs ExUnit tests with coverage - Runs e2e tests - No deployment (staging still via Dokku) Updated CLAUDE.md with CI/CD pipeline documentation. --- .forgejo/workflows/production.yml | 238 ++++++++++++++++++++++++++++++ .forgejo/workflows/test.yml | 190 ++++++++++++++++++++++++ CLAUDE.md | 25 +++- 3 files changed, 446 insertions(+), 7 deletions(-) create mode 100644 .forgejo/workflows/production.yml create mode 100644 .forgejo/workflows/test.yml diff --git a/.forgejo/workflows/production.yml b/.forgejo/workflows/production.yml new file mode 100644 index 00000000..4d17424d --- /dev/null +++ b/.forgejo/workflows/production.yml @@ -0,0 +1,238 @@ +name: Production Deployment + +on: + push: + branches: + - production + +env: + REGISTRY: git.mcintire.me + IMAGE_NAME: graham/towerops-web + +jobs: + test-exunit: + name: Run ExUnit Tests + runs-on: ubuntu-latest + + services: + postgres: + image: timescale/timescaledb:latest-pg16 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: towerops_test + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Elixir + uses: erlef/setup-beam@v1 + with: + version-type: strict + elixir-version: '1.18.1' + otp-version: '27.2' + + - name: Cache deps + uses: actions/cache@v4 + with: + path: deps + key: ${{ runner.os }}-deps-${{ hashFiles('mix.lock') }} + restore-keys: ${{ runner.os }}-deps- + + - name: Cache _build + uses: actions/cache@v4 + with: + path: _build + key: ${{ runner.os }}-build-${{ hashFiles('lib/**/*.ex') }}-${{ hashFiles('mix.lock') }} + restore-keys: ${{ runner.os }}-build- + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y libsnmp-dev snmp-mibs-downloader postgresql-client + + - name: Install dependencies + run: mix deps.get + + - name: Compile (warnings as errors) + run: mix compile --warnings-as-errors + env: + MIX_ENV: test + + - name: Run tests + run: mix test + env: + MIX_ENV: test + DATABASE_HOST: postgres + DATABASE_PORT: 5432 + + test-e2e: + name: Run E2E Tests + runs-on: ubuntu-latest + needs: test-exunit + + services: + postgres: + image: timescale/timescaledb:latest-pg16 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: towerops_dev + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Elixir + uses: erlef/setup-beam@v1 + with: + version-type: strict + elixir-version: '1.18.1' + otp-version: '27.2' + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Cache deps + uses: actions/cache@v4 + with: + path: deps + key: ${{ runner.os }}-deps-${{ hashFiles('mix.lock') }} + restore-keys: ${{ runner.os }}-deps- + + - name: Cache _build + uses: actions/cache@v4 + with: + path: _build + key: ${{ runner.os }}-build-${{ hashFiles('lib/**/*.ex') }}-${{ hashFiles('mix.lock') }} + restore-keys: ${{ runner.os }}-build- + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y libsnmp-dev snmp-mibs-downloader postgresql-client + + - name: Install Elixir dependencies + run: mix deps.get + + - name: Setup database + run: mix ecto.setup + env: + MIX_ENV: dev + DATABASE_HOST: postgres + DATABASE_PORT: 5432 + + - name: Start Phoenix server in background + run: mix phx.server & + env: + MIX_ENV: dev + DATABASE_HOST: postgres + DATABASE_PORT: 5432 + SECRET_KEY_BASE: ${{ secrets.SECRET_KEY_BASE || 'dev_secret_key_base_for_testing_only_min_64_chars_required_here' }} + PHX_HOST: localhost + + - name: Wait for Phoenix to start + run: | + timeout 60 bash -c 'until curl -f http://localhost:4000/health; do sleep 2; done' + + - name: Install Playwright dependencies + working-directory: e2e + run: | + npm ci + npx playwright install --with-deps + + - name: Run e2e tests + working-directory: e2e + run: npm test + env: + CI: true + + - name: Upload test results + if: failure() + uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: e2e/playwright-report/ + retention-days: 7 + + build-and-deploy: + name: Build and Deploy to Production + runs-on: ubuntu-latest + needs: [test-exunit, test-e2e] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Need full history for git operations + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.REGISTRY_USERNAME }} + password: ${{ secrets.REGISTRY_PASSWORD }} + + - name: Generate image tag + id: tag + run: | + BRANCH=${GITHUB_REF#refs/heads/} + TIMESTAMP=$(date +%s) + SHORT_SHA=$(git rev-parse --short=7 HEAD) + TAG="${BRANCH}-${TIMESTAMP}-${SHORT_SHA}" + echo "tag=${TAG}" >> $GITHUB_OUTPUT + echo "Full image tag: ${TAG}" + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: k8s/Dockerfile + push: true + tags: | + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }} + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:production + cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache + cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache,mode=max + build-args: | + MIX_ENV=prod + + - name: Update deployment manifest + run: | + IMAGE_TAG="${{ steps.tag.outputs.tag }}" + sed -i "s|image: ${REGISTRY}/${IMAGE_NAME}:.*|image: ${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}|g" k8s/deployment.yaml + + git config user.name "Forgejo Actions" + git config user.email "actions@git.mcintire.me" + git add k8s/deployment.yaml + git commit -m "chore: update production image to ${IMAGE_TAG} [skip ci]" || echo "No changes to commit" + git push origin production + + - name: Deployment summary + run: | + echo "### ✅ Production Deployment Complete" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Image:** \`${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }}\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "FluxCD will automatically apply the changes to the cluster." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Monitor deployment:**" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY + echo "kubectl rollout status deployment/towerops -n towerops" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY diff --git a/.forgejo/workflows/test.yml b/.forgejo/workflows/test.yml new file mode 100644 index 00000000..017eb77f --- /dev/null +++ b/.forgejo/workflows/test.yml @@ -0,0 +1,190 @@ +name: Tests + +on: + push: + branches: + - main + pull_request: + branches: + - main + - production + +env: + MIX_ENV: test + +jobs: + test-exunit: + name: ExUnit Tests + runs-on: ubuntu-latest + + services: + postgres: + image: timescale/timescaledb:latest-pg16 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: towerops_test + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Elixir + uses: erlef/setup-beam@v1 + with: + version-type: strict + elixir-version: '1.18.1' + otp-version: '27.2' + + - name: Cache deps + uses: actions/cache@v4 + with: + path: deps + key: ${{ runner.os }}-deps-${{ hashFiles('mix.lock') }} + restore-keys: ${{ runner.os }}-deps- + + - name: Cache _build + uses: actions/cache@v4 + with: + path: _build + key: ${{ runner.os }}-build-${{ hashFiles('lib/**/*.ex') }}-${{ hashFiles('mix.lock') }} + restore-keys: ${{ runner.os }}-build- + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y libsnmp-dev snmp-mibs-downloader postgresql-client + + - name: Install dependencies + run: mix deps.get + + - name: Check formatting + run: mix format --check-formatted + + - name: Compile (warnings as errors) + run: mix compile --warnings-as-errors + + - name: Run tests with coverage + run: mix test --cover + + - name: Run Credo + run: mix credo --strict + + - name: Upload coverage reports + if: always() + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: cover/ + retention-days: 7 + + test-e2e: + name: E2E Tests + runs-on: ubuntu-latest + needs: test-exunit + + services: + postgres: + image: timescale/timescaledb:latest-pg16 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: towerops_dev + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Elixir + uses: erlef/setup-beam@v1 + with: + version-type: strict + elixir-version: '1.18.1' + otp-version: '27.2' + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Cache deps + uses: actions/cache@v4 + with: + path: deps + key: ${{ runner.os }}-deps-${{ hashFiles('mix.lock') }} + restore-keys: ${{ runner.os }}-deps- + + - name: Cache _build + uses: actions/cache@v4 + with: + path: _build + key: ${{ runner.os }}-build-${{ hashFiles('lib/**/*.ex') }}-${{ hashFiles('mix.lock') }} + restore-keys: ${{ runner.os }}-build- + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y libsnmp-dev snmp-mibs-downloader postgresql-client + + - name: Install Elixir dependencies + run: mix deps.get + env: + MIX_ENV: dev + + - name: Setup database + run: mix ecto.setup + env: + MIX_ENV: dev + DATABASE_HOST: postgres + DATABASE_PORT: 5432 + + - name: Start Phoenix server in background + run: mix phx.server & + env: + MIX_ENV: dev + DATABASE_HOST: postgres + DATABASE_PORT: 5432 + SECRET_KEY_BASE: dev_secret_key_base_for_testing_only_min_64_chars_required_here + PHX_HOST: localhost + + - name: Wait for Phoenix to start + run: | + timeout 60 bash -c 'until curl -f http://localhost:4000/health; do sleep 2; done' + + - name: Install Playwright dependencies + working-directory: e2e + run: | + npm ci + npx playwright install --with-deps + + - name: Run e2e tests + working-directory: e2e + run: npm test + env: + CI: true + + - name: Upload test results + if: failure() + uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: e2e/playwright-report/ + retention-days: 7 + + - name: Upload screenshots on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: playwright-screenshots + path: e2e/test-results/ + retention-days: 7 diff --git a/CLAUDE.md b/CLAUDE.md index a340cb8d..dc6e0d50 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -258,11 +258,15 @@ Towerops uses branch-based deployment with Forgejo CI/CD: - URL: staging.towerops.app (or configured Dokku domain) **Production (Kubernetes):** -- Push to `production` branch triggers Docker build + k8s deployment -- Builds Docker image from `k8s/Dockerfile` -- Pushes to container registry -- Updates `k8s/deployment.yaml` with new image tag -- FluxCD detects change and applies to cluster +- Push to `production` branch triggers CI/CD pipeline +- **Test Gates (must pass):** + - All ExUnit tests (`mix test`) + - All e2e tests (Playwright) +- Only after tests pass: + - Builds Docker image from `k8s/Dockerfile` + - Pushes to container registry + - Updates `k8s/deployment.yaml` with new image tag + - FluxCD detects change and applies to cluster ### Deploying Changes @@ -289,8 +293,15 @@ git push origin production # Or fast-forward if no conflicts git push origin main:production -# CI builds Docker image and updates k8s manifests -# FluxCD applies within ~5 minutes +# CI/CD Pipeline: +# 1. Runs all ExUnit tests (must pass) +# 2. Runs all e2e tests (must pass) +# 3. Builds Docker image +# 4. Pushes to container registry +# 5. Updates k8s/deployment.yaml with new image tag +# 6. FluxCD auto-applies changes (~5 minutes) +# +# Watch progress: https://git.mcintire.me/graham/towerops-web/actions ``` **Direct Dokku deploy (bypass CI):**