From ac8562ea513a866d11f15d9c97a87e08d995ef70 Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Thu, 7 May 2026 02:30:30 -0700 Subject: [PATCH] feat: implement full CI/CD pipeline with staged deployments - PR workflow: build, deploy to CI site, wait for health, run acceptance tests - Release workflow: deploy to dev using mounted compose path with health check - Add deploy-prod workflow (workflow_dispatch) for human-gated prod deployment - Add just recipes: deploy-prod, status (view current image tags across envs) - Runners now have /apps mount for access to all environment compose files --- .gitea/workflows/deploy-prod.yml | 52 +++++++++++++++++++++++++++++++ .gitea/workflows/pr.yml | 53 +++++++++++++++++++++++++++++++- .gitea/workflows/release.yml | 46 ++++++++++++--------------- justfile | 44 ++++++++++++++++++++++++++ 4 files changed, 168 insertions(+), 27 deletions(-) create mode 100644 .gitea/workflows/deploy-prod.yml diff --git a/.gitea/workflows/deploy-prod.yml b/.gitea/workflows/deploy-prod.yml new file mode 100644 index 0000000..c6b6770 --- /dev/null +++ b/.gitea/workflows/deploy-prod.yml @@ -0,0 +1,52 @@ +name: Deploy Production + +on: + workflow_dispatch: + inputs: + version: + description: 'Version tag to deploy (e.g., v0.1.21)' + required: true + type: string + +env: + REGISTRY: docker.notsosm.art + IMAGE_NAME: euchre-camp + PROD_APPS_PATH: /apps/youthful_simon + +jobs: + deploy-prod: + runs-on: ubuntu-latest + container: + image: docker.notsosm.art/euchre-camp/ci-base:latest + options: --user root + + steps: + - name: Deploy to production + run: | + VERSION="${{ inputs.version }}" + COMPOSE_FILE="${{ env.PROD_APPS_PATH }}/docker-compose.yml" + + echo "Deploying ${VERSION} to production..." + + # Update prod compose file with the release tag + # Note: Standardizing to docker.notsosm.art registry + sed -i "s|image: euchre-camp/euchre-camp:[a-zA-Z0-9.-]*|image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${VERSION}|" ${COMPOSE_FILE} + sed -i "s|image: docker.notsosm.art/euchre-camp:[a-zA-Z0-9.-]*|image: docker.notsosm.art/euchre-camp:${VERSION}|" ${COMPOSE_FILE} + + # Pull and restart the prod container + cd ${{ env.PROD_APPS_PATH }} + docker compose pull app + docker compose up -d app + + # Wait for production site to be healthy + echo "Waiting for production site to be healthy..." + for i in {1..30}; do + if curl -sf https://euchre.notsosm.art/api/health > /dev/null 2>&1; then + echo "✅ Production successfully deployed with version ${VERSION}" + exit 0 + fi + sleep 0.5 + done + echo "❌ Production deployment failed" + docker compose logs app + exit 1 \ No newline at end of file diff --git a/.gitea/workflows/pr.yml b/.gitea/workflows/pr.yml index 9cde7aa..0096e89 100644 --- a/.gitea/workflows/pr.yml +++ b/.gitea/workflows/pr.yml @@ -5,6 +5,10 @@ on: branches: - main +env: + REGISTRY: docker.notsosm.art + IMAGE_NAME: euchre-camp + jobs: unit-tests: runs-on: ubuntu-latest @@ -27,8 +31,9 @@ jobs: - name: Run unit tests run: bun test src/__tests__/unit/ src/__tests__/*.test.tsx src/__tests__/auth-simple.test.ts - acceptance-tests: + build-and-deploy-ci: runs-on: ubuntu-latest + needs: unit-tests container: image: docker.notsosm.art/euchre-camp/ci-base:latest options: --user root @@ -45,11 +50,57 @@ jobs: env: DATABASE_URL: postgresql://user:pass@localhost:5432/dummy + - name: Extract PR number and commit info + id: info + run: | + echo "pr_number=$(echo $GITHUB_REF | grep -oP 'refs/pull/\K[0-9]+')" >> $GITHUB_OUTPUT + echo "short_sha=$(echo $GITHUB_SHA | cut -c1-7)" >> $GITHUB_OUTPUT + + - name: Build Docker image for PR + run: | + IMAGE_TAG="pr-${{ steps.info.outputs.pr_number }}-${{ steps.info.outputs.short_sha }}" + docker build \ + --target runner \ + --build-arg GIT_COMMIT=$GITHUB_SHA \ + -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${IMAGE_TAG} \ + . + + - name: Update CI site compose and restart + run: | + IMAGE_TAG="pr-${{ steps.info.outputs.pr_number }}-${{ steps.info.outputs.short_sha }}" + COMPOSE_FILE="/apps/euchre_camp_ci/docker-compose.yml" + + # Update the image tag in the compose file + sed -i "s|image: docker.notsosm.art/euchre-camp:[a-zA-Z0-9.-]*|image: docker.notsosm.art/euchre-camp:${IMAGE_TAG}|" ${COMPOSE_FILE} + + # Pull the new image and restart the CI stack + cd /apps/euchre_camp_ci + docker compose pull app + docker compose up -d app + + - name: Wait for CI site to be healthy + run: | + for i in {1..30}; do + if curl -sf https://euchre-ci.notsosm.art/api/health > /dev/null 2>&1; then + echo "CI site is healthy" + exit 0 + fi + sleep 0.5 + done + echo "CI site failed to become healthy after 15 seconds" + docker compose -f /apps/euchre_camp_ci/docker-compose.yml logs app + exit 1 + - name: Run acceptance tests run: DATABASE_URL="${{ secrets.CI_DATABASE_URL }}" bun test:acceptance env: CI: true + - name: Cleanup PR images + if: always() + run: | + docker rmi ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:pr-${{ steps.info.outputs.pr_number }}-${{ steps.info.outputs.short_sha }} || true + analyze-bump-type: runs-on: ubuntu-latest needs: unit-tests diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 9394d39..6e906e5 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -163,32 +163,26 @@ jobs: if: steps.commit.outputs.committed == 'true' run: | echo "Deploying version ${{ steps.version.outputs.new_version }} to dev environment..." - - # Update docker-compose.yml with new image tag using full registry path - # The registry is docker.notsosm.art and image is euchre-camp + + # Update dev compose file with new image tag IMAGE_TAG="${{ steps.version.outputs.new_version }}" - sed -i "s|image: docker.notsosm.art/euchre-camp:[0-9.]*|image: docker.notsosm.art/euchre-camp:${IMAGE_TAG}|" docker-compose.yml - - # Copy the updated docker-compose.yml to the deployment location - # The runners are on the same Docker server where the container is running - sudo mkdir -p /home/euchre_camp - sudo cp docker-compose.yml /home/euchre_camp/ - sudo chown -R euchre:euchre /home/euchre_camp - + COMPOSE_FILE="/apps/intelligent_silasak/docker-compose.yml" + sed -i "s|image: docker.notsosm.art/euchre-camp:[a-zA-Z0-9.-]*|image: docker.notsosm.art/euchre-camp:${IMAGE_TAG}|" ${COMPOSE_FILE} + # Pull and restart the dev container - cd /home/euchre_camp - docker-compose pull app - docker-compose up -d app - + cd /apps/intelligent_silasak + docker compose pull app + docker compose up -d app + # Wait for container to be healthy - echo "Waiting for container to start..." - sleep 10 - - # Check if container is running - if docker ps --filter "name=euchre-camp-app" --format "{{.Status}}" | grep -q "Up"; then - echo "✅ Dev environment successfully deployed with version ${{ steps.version.outputs.new_version }}" - else - echo "❌ Dev environment deployment failed" - docker-compose logs app - exit 1 - fi + echo "Waiting for dev site to be healthy..." + for i in {1..30}; do + if curl -sf https://euchre-dev.notsosm.art/api/health > /dev/null 2>&1; then + echo "✅ Dev environment successfully deployed with version ${{ steps.version.outputs.new_version }}" + exit 0 + fi + sleep 0.5 + done + echo "❌ Dev environment deployment failed" + docker compose logs app + exit 1 diff --git a/justfile b/justfile index 25d35b3..2c73d0c 100644 --- a/justfile +++ b/justfile @@ -240,6 +240,50 @@ workflow-status: @echo "Test Workflow: Runs unit tests on all branch pushes" @echo "Release Workflow: Runs on main branch pushes (version bump + Docker build)" +# --- Production Deployment --- + +# Deploy a specific version to production (human gate) +# Usage: just deploy-prod v0.1.21 +deploy-prod version: + @echo "Deploying {{version}} to production..." + @echo "This requires the image to already be pushed to the registry." + @echo "" + @read -p "Have you verified this version works in dev? (y/N) " confirm; \ + if [ "$$confirm" != "y" ]; then \ + echo "Cancelled. Please verify in dev first."; \ + exit 1; \ + fi + cd /apps/youthful_simon && \ + sed -i "s|image: docker.notsosm.art/euchre-camp:[a-zA-Z0-9.-]*|image: docker.notsosm.art/euchre-camp:{{version}}|" docker-compose.yml && \ + sed -i "s|image: euchre-camp/euchre-camp:[a-zA-Z0-9.-]*|image: docker.notsosm.art/euchre-camp:{{version}}|" docker-compose.yml && \ + docker compose pull app && \ + docker compose up -d app && \ + echo "Waiting for production site to be healthy..." && \ + for i in {1..30}; do \ + if curl -sf https://euchre.notsosm.art/api/health > /dev/null 2>&1; then \ + echo "✅ Production successfully deployed with version {{version}}"; \ + exit 0; \ + fi; \ + sleep 0.5; \ + done && \ + echo "❌ Production deployment failed - health check timed out"; \ + docker compose logs app; \ + exit 1 + +# Show current deployment status across all environments +status: + @echo "=== EuchreCamp Deployment Status ===" + @echo "" + @echo "CI Site (euchre-camp-ci):" + @grep "image:" /apps/euchre_camp_ci/docker-compose.yml | head -1 + @echo "" + @echo "Dev Site (euchre-camp-dev):" + @grep "image:" /apps/intelligent_silasak/docker-compose.yml | head -1 + @echo "" + @echo "Prod Site (euchre-camp):" + @grep "image:" /apps/youthful_simon/docker-compose.yml | head -1 + @echo "" + # --- SDLC Database Operations --- # Sync production data to development database (manual operation)