cleanup #212
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # ============================================================================= | ||
| # Scaleway Deployment Workflow | ||
| # ============================================================================= | ||
| # Deploys Cella to Scaleway infrastructure using Terraform | ||
| # Triggered on pushes to main (prod) or manually for other environments | ||
| # ============================================================================= | ||
| export const stxRequestSchema = z | ||
| permissions: | ||
| contents: read | ||
| on: | ||
| # Automatic deployment to production on main branch | ||
| push: | ||
| branches: | ||
| - main | ||
| paths: | ||
| - 'backend/**' | ||
| - 'cdc/**' | ||
| - 'frontend/**' | ||
| - 'infra/**' | ||
| - '.github/workflows/deploy.yml' | ||
| # Manual deployment for any environment | ||
| workflow_dispatch: | ||
| inputs: | ||
| environment: | ||
| description: 'Environment to deploy to' | ||
| required: true | ||
| default: 'dev' | ||
| type: choice | ||
| options: | ||
| - dev | ||
| - staging | ||
| - prod | ||
| # Prevent concurrent deployments to the same environment | ||
| concurrency: | ||
| group: deploy-${{ github.event.inputs.environment || 'prod' }} | ||
| cancel-in-progress: false | ||
| env: | ||
| SCW_REGION: nl-ams | ||
| TERRAFORM_VERSION: '1.7.0' | ||
| jobs: | ||
| # ------------------------------------------------------------------------- | ||
| # Determine which environment to deploy | ||
| # ------------------------------------------------------------------------- | ||
| setup: | ||
| runs-on: ubuntu-latest | ||
| outputs: | ||
| environment: ${{ steps.set-env.outputs.environment }} | ||
| image_tag: ${{ steps.set-env.outputs.image_tag }} | ||
| steps: | ||
| - name: Determine environment | ||
| id: set-env | ||
| - key: NODE_VERSION | ||
| if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | ||
| echo "environment=${{ github.event.inputs.environment }}" >> $GITHUB_OUTPUT | ||
| else | ||
| echo "environment=prod" >> $GITHUB_OUTPUT | ||
| fi | ||
| echo "image_tag=${{ github.sha }}" >> $GITHUB_OUTPUT | ||
| # ------------------------------------------------------------------------- | ||
| # Build and push Docker images | ||
| # ------------------------------------------------------------------------- | ||
| build-backend: | ||
| runs-on: ubuntu-latest | ||
| needs: setup | ||
| environment: ${{ needs.setup.outputs.environment }} | ||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 | ||
| - name: Set up Docker Buildx | ||
| uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 | ||
| - name: Login to Scaleway Container Registry | ||
| uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 | ||
| with: | ||
| registry: rg.${{ env.SCW_REGION }}.scw.cloud | ||
| username: nologin | ||
| password: ${{ secrets.SCW_SECRET_KEY }} | ||
| - name: Build and push backend image | ||
| uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5 | ||
| with: | ||
| context: . | ||
| file: backend/Dockerfile | ||
| push: true | ||
| tags: | | ||
| rg.${{ env.SCW_REGION }}.scw.cloud/${{ needs.setup.outputs.environment }}cella/backend:${{ needs.setup.outputs.image_tag }} | ||
| rg.${{ env.SCW_REGION }}.scw.cloud/${{ needs.setup.outputs.environment }}cella/backend:latest | ||
| cache-from: type=gha | ||
| cache-to: type=gha,mode=max | ||
| build-cdc: | ||
| runs-on: ubuntu-latest | ||
| needs: setup | ||
| environment: ${{ needs.setup.outputs.environment }} | ||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 | ||
| - name: Set up Docker Buildx | ||
| uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 | ||
| - name: Login to Scaleway Container Registry | ||
| uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 | ||
| with: | ||
| registry: rg.${{ env.SCW_REGION }}.scw.cloud | ||
| username: nologin | ||
| password: ${{ secrets.SCW_SECRET_KEY }} | ||
| - name: Build and push CDC image | ||
| uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5 | ||
| with: | ||
| context: . | ||
| file: cdc/Dockerfile | ||
| push: true | ||
| tags: | | ||
| rg.${{ env.SCW_REGION }}.scw.cloud/${{ needs.setup.outputs.environment }}cella/cdc:${{ needs.setup.outputs.image_tag }} | ||
| rg.${{ env.SCW_REGION }}.scw.cloud/${{ needs.setup.outputs.environment }}cella/cdc:latest | ||
| cache-from: type=gha | ||
| cache-to: type=gha,mode=max | ||
| # ------------------------------------------------------------------------- | ||
| # Build and deploy frontend to Object Storage | ||
| # ------------------------------------------------------------------------- | ||
| build-frontend: | ||
| runs-on: ubuntu-latest | ||
| needs: setup | ||
| environment: ${{ needs.setup.outputs.environment }} | ||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 | ||
| - name: Setup pnpm | ||
| uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4 | ||
| with: | ||
| version: 9 | ||
| - name: Setup Node.js | ||
| uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 | ||
| with: | ||
| node-version: 24 | ||
| cache: 'pnpm' | ||
| - name: Install dependencies | ||
| run: pnpm install --frozen-lockfile | ||
| - name: Build frontend | ||
| run: pnpm --filter frontend build | ||
| env: | ||
| VITE_BACKEND_URL: ${{ vars.BACKEND_URL }} | ||
| VITE_FRONTEND_URL: ${{ vars.FRONTEND_URL }} | ||
| - name: Install Scaleway CLI | ||
| run: | | ||
| curl -s https://raw.githubusercontent.com/scaleway/scaleway-cli/master/scripts/get.sh | sh | ||
| scw version | ||
| - name: Configure Scaleway CLI | ||
| run: | | ||
| scw config set access-key=${{ secrets.SCW_ACCESS_KEY }} | ||
| scw config set secret-key=${{ secrets.SCW_SECRET_KEY }} | ||
| scw config set default-project-id=${{ secrets.SCW_PROJECT_ID }} | ||
| scw config set default-region=${{ env.SCW_REGION }} | ||
| - name: Upload frontend to Object Storage | ||
| run: | | ||
| BUCKET_NAME="${{ needs.setup.outputs.environment }}-cella-frontend" | ||
| # Sync all files with appropriate cache headers | ||
| # Hashed assets get immutable caching | ||
| for file in frontend/dist/assets/*; do | ||
| scw object put "$file" \ | ||
| --bucket "$BUCKET_NAME" \ | ||
| --key "assets/$(basename $file)" \ | ||
| --cache-control "public, max-age=31536000, immutable" | ||
| done | ||
| # index.html gets no-cache to always fetch latest | ||
| scw object put frontend/dist/index.html \ | ||
| --bucket "$BUCKET_NAME" \ | ||
| --key "index.html" \ | ||
| --cache-control "no-cache, no-store, must-revalidate" | ||
| # Other static files | ||
| for file in frontend/dist/*.{js,css,ico,svg,png,jpg,webp} 2>/dev/null || true; do | ||
| [ -f "$file" ] && scw object put "$file" \ | ||
| --bucket "$BUCKET_NAME" \ | ||
| --key "$(basename $file)" \ | ||
| --cache-control "public, max-age=86400" | ||
| done | ||
| - name: Purge Edge Services cache for index.html | ||
| run: | | ||
| # Purge only index.html - hashed assets don't need purging | ||
| PIPELINE_ID=$(scw edge-services pipeline list -o json | jq -r '.[] | select(.name | contains("${{ needs.setup.outputs.environment }}-cella")) | .id') | ||
| if [ -n "$PIPELINE_ID" ]; then | ||
| scw edge-services purge-request create \ | ||
| pipeline-id="$PIPELINE_ID" \ | ||
| assets.0="index.html" | ||
| fi | ||
| # ------------------------------------------------------------------------- | ||
| # Apply Terraform infrastructure | ||
| # ------------------------------------------------------------------------- | ||
| terraform: | ||
| runs-on: ubuntu-latest | ||
| needs: [setup, build-backend, build-cdc] | ||
| environment: ${{ needs.setup.outputs.environment }} | ||
| defaults: | ||
| run: | ||
| working-directory: infra | ||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 | ||
| - name: Setup Terraform | ||
| uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3 | ||
| with: | ||
| terraform_version: ${{ env.TERRAFORM_VERSION }} | ||
| - name: Configure Scaleway credentials | ||
| run: | | ||
| echo "SCW_ACCESS_KEY=${{ secrets.SCW_ACCESS_KEY }}" >> $GITHUB_ENV | ||
| echo "SCW_SECRET_KEY=${{ secrets.SCW_SECRET_KEY }}" >> $GITHUB_ENV | ||
| echo "SCW_DEFAULT_PROJECT_ID=${{ secrets.SCW_PROJECT_ID }}" >> $GITHUB_ENV | ||
| echo "SCW_DEFAULT_REGION=${{ env.SCW_REGION }}" >> $GITHUB_ENV | ||
| - name: Terraform Init | ||
| run: | | ||
| terraform init \ | ||
| -backend-config="bucket=cella-terraform-state" \ | ||
| -backend-config="key=${{ needs.setup.outputs.environment }}/terraform.tfstate" \ | ||
| -backend-config="region=${{ env.SCW_REGION }}" \ | ||
| -backend-config="endpoint=s3.${{ env.SCW_REGION }}.scw.cloud" \ | ||
| -backend-config="access_key=${{ secrets.SCW_ACCESS_KEY }}" \ | ||
| -backend-config="secret_key=${{ secrets.SCW_SECRET_KEY }}" \ | ||
| -backend-config="skip_credentials_validation=true" \ | ||
| -backend-config="skip_region_validation=true" \ | ||
| -backend-config="skip_metadata_api_check=true" | ||
| - name: Select Terraform workspace | ||
| run: | | ||
| terraform workspace select ${{ needs.setup.outputs.environment }} || \ | ||
| terraform workspace new ${{ needs.setup.outputs.environment }} | ||
| - name: Terraform Plan | ||
| run: | | ||
| terraform plan \ | ||
| -var-file="environments/${{ needs.setup.outputs.environment }}.tfvars" \ | ||
| -var="backend_image_tag=${{ needs.setup.outputs.image_tag }}" \ | ||
| -var="cdc_image_tag=${{ needs.setup.outputs.image_tag }}" \ | ||
| -var="argon_secret=${{ secrets.ARGON_SECRET }}" \ | ||
| -var="cookie_secret=${{ secrets.COOKIE_SECRET }}" \ | ||
| -var="unsubscribe_token_secret=${{ secrets.UNSUBSCRIBE_TOKEN_SECRET }}" \ | ||
| -var="cdc_ws_secret=${{ secrets.CDC_WS_SECRET }}" \ | ||
| -out=tfplan | ||
| - name: Terraform Apply | ||
| run: terraform apply -auto-approve tfplan | ||
| - name: Output deployment info | ||
| run: | | ||
| echo "## Deployment Complete 🚀" >> $GITHUB_STEP_SUMMARY | ||
| echo "" >> $GITHUB_STEP_SUMMARY | ||
| echo "**Environment:** ${{ needs.setup.outputs.environment }}" >> $GITHUB_STEP_SUMMARY | ||
| echo "**Image Tag:** ${{ needs.setup.outputs.image_tag }}" >> $GITHUB_STEP_SUMMARY | ||
| echo "" >> $GITHUB_STEP_SUMMARY | ||
| echo "### URLs" >> $GITHUB_STEP_SUMMARY | ||
| terraform output -raw frontend_url >> $GITHUB_STEP_SUMMARY | ||
| echo "" >> $GITHUB_STEP_SUMMARY | ||
| terraform output -raw backend_url >> $GITHUB_STEP_SUMMARY | ||
| # ------------------------------------------------------------------------- | ||
| # Post-deployment health check | ||
| # ------------------------------------------------------------------------- | ||
| health-check: | ||
| runs-on: ubuntu-latest | ||
| needs: [setup, terraform, build-frontend] | ||
| environment: ${{ needs.setup.outputs.environment }} | ||
| steps: | ||
| - name: Wait for deployment to stabilize | ||
| run: sleep 30 | ||
| - name: Check backend health | ||
| run: | | ||
| HEALTH_URL="${{ vars.BACKEND_URL }}/health" | ||
| echo "Checking health at $HEALTH_URL" | ||
| for i in {1..10}; do | ||
| response=$(curl -s -o /dev/null -w "%{http_code}" "$HEALTH_URL" || echo "000") | ||
| if [ "$response" = "200" ]; then | ||
| echo "✅ Backend is healthy!" | ||
| exit 0 | ||
| fi | ||
| echo "Attempt $i: Got $response, waiting..." | ||
| sleep 10 | ||
| done | ||
| echo "❌ Backend health check failed" | ||
| exit 1 | ||
| - name: Check frontend availability | ||
| run: | | ||
| FRONTEND_URL="${{ vars.FRONTEND_URL }}" | ||
| echo "Checking frontend at $FRONTEND_URL" | ||
| response=$(curl -s -o /dev/null -w "%{http_code}" "$FRONTEND_URL" || echo "000") | ||
| if [ "$response" = "200" ]; then | ||
| echo "✅ Frontend is available!" | ||
| else | ||
| echo "⚠️ Frontend returned $response" | ||
| fi | ||