diff --git a/.github/workflows/cd-ec2-ansible.yml b/.github/workflows/cd-ec2-ansible.yml index f64a833..441279a 100644 --- a/.github/workflows/cd-ec2-ansible.yml +++ b/.github/workflows/cd-ec2-ansible.yml @@ -32,7 +32,7 @@ on: description: "Target environment" required: true type: choice - options: [staging, prod] + options: [dev, staging, prod] default: staging operation: description: "Operation to run" @@ -56,6 +56,132 @@ concurrency: cancel-in-progress: ${{ !startsWith(github.ref, 'refs/tags/v') }} jobs: + build-dev: + name: "Build & Push [dev] (${{ matrix.service }})" + runs-on: ubuntu-latest + timeout-minutes: 20 + if: > + github.event_name == 'workflow_dispatch' + && inputs.environment == 'dev' + && inputs.operation == 'deploy' + environment: dev + strategy: + matrix: + service: [backend, frontend] + fail-fast: true + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.sha }} + + - name: Authenticate with AWS + uses: ./.github/actions/aws-auth + with: + role-arn: ${{ secrets.AWS_ROLE_TO_ASSUME }} + aws-region: ${{ secrets.AWS_REGION }} + + - name: Log in to Amazon ECR + id: ecr-login + uses: aws-actions/amazon-ecr-login@v2 + + - name: Build, tag & push + uses: ./.github/actions/docker-build + with: + context: ./${{ matrix.service }} + dockerfile: ./${{ matrix.service }}/Dockerfile + image-name: mypythonproject1/${{ matrix.service }} + registry: ${{ steps.ecr-login.outputs.registry }} + tags: | + type=raw,value=dev + type=sha,prefix=dev-,format=short + build-args: | + BUILD_ENV=dev + GIT_SHA=${{ github.sha }} + platforms: linux/amd64 + scan: "true" + scan-severity: "CRITICAL,HIGH" + scan-exit-code: "0" + cache-scope: ${{ matrix.service }}-dev + + ec2-deploy-dev: + name: "EC2 Deploy [dev] via Ansible" + runs-on: ubuntu-latest + timeout-minutes: 5 + needs: [build-dev] + if: needs.build-dev.result == 'success' + environment: dev + steps: + - name: Dispatch ansible-ec2-deploy to infra3 repo + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.INFRA_DEPLOY_TOKEN }} + script: | + const imageTag = `dev-${context.sha.substring(0, 7)}`; + await github.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: 'mypythonproject1-infra3', + workflow_id: 'ansible-ec2-deploy.yml', + ref: 'main', + inputs: { + environment: 'dev', + operation: 'deploy', + image_tag: imageTag, + }, + }); + core.notice(`Dispatched ansible-ec2-deploy on mypythonproject1-infra3 (dev, tag=${imageTag})`); + + ec2-verify-dev: + name: "Verify EC2 Deploy [dev]" + runs-on: ubuntu-latest + timeout-minutes: 30 + needs: [ec2-deploy-dev] + if: needs.ec2-deploy-dev.result == 'success' + environment: dev + steps: + - name: Wait for infra3 ansible workflow result + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.INFRA_DEPLOY_TOKEN }} + script: | + const owner = context.repo.owner; + const repo = 'mypythonproject1-infra3'; + const expectedTag = `${context.sha.substring(0, 7)}`; + const tagNeedle = `tag=dev-${expectedTag}`; + const deadline = Date.now() + 30 * 60 * 1000; + + while (Date.now() < deadline) { + const resp = await github.rest.actions.listWorkflowRuns({ + owner, + repo, + workflow_id: 'ansible-ec2-deploy.yml', + event: 'workflow_dispatch', + per_page: 20, + }); + + const matchedRun = resp.data.workflow_runs.find((r) => { + const title = (r.display_title || '').toLowerCase(); + return title.includes('[dev]') && title.includes(tagNeedle.toLowerCase()); + }); + + if (!matchedRun || matchedRun.status !== 'completed') { + await new Promise((resolve) => setTimeout(resolve, 15000)); + continue; + } + + if (matchedRun.conclusion !== 'success') { + core.setFailed(`infra3 ansible deploy failed: ${matchedRun.html_url}`); + return; + } + + core.notice(`infra3 ansible deploy succeeded: ${matchedRun.html_url}`); + return; + } + + core.setFailed('Timed out waiting for infra3 ansible dev deployment run to complete'); + build-staging: name: "Build & Push [staging] (${{ matrix.service }})" runs-on: ubuntu-latest diff --git a/.github/workflows/cd-ecs-fargate.yml b/.github/workflows/cd-ecs-fargate.yml index a26ba61..7755641 100644 --- a/.github/workflows/cd-ecs-fargate.yml +++ b/.github/workflows/cd-ecs-fargate.yml @@ -39,7 +39,7 @@ on: description: "Target environment" required: true type: choice - options: [staging, prod] + options: [dev, staging, prod] default: staging permissions: @@ -62,6 +62,136 @@ concurrency: # 1a. Build + push staging images # --------------------------------------------------------------------------- jobs: + build-dev: + name: "Build & Push [dev] (${{ matrix.service }})" + runs-on: ubuntu-latest + timeout-minutes: 20 + if: > + github.event_name == 'workflow_dispatch' + && inputs.environment == 'dev' + environment: dev + strategy: + matrix: + service: [backend, frontend] + fail-fast: true + permissions: + contents: read + id-token: write + outputs: + registry: ${{ steps.ecr-login.outputs.registry }} + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.sha }} + + - name: Authenticate with AWS + uses: ./.github/actions/aws-auth + with: + role-arn: ${{ secrets.AWS_ROLE_TO_ASSUME }} + aws-region: ${{ secrets.AWS_REGION }} + + - name: Log in to Amazon ECR + id: ecr-login + uses: aws-actions/amazon-ecr-login@v2 + + - name: Build, tag & push + uses: ./.github/actions/docker-build + with: + context: ./${{ matrix.service }} + dockerfile: ./${{ matrix.service }}/Dockerfile + image-name: mypythonproject1/${{ matrix.service }} + registry: ${{ steps.ecr-login.outputs.registry }} + tags: | + type=raw,value=dev + type=sha,prefix=dev-,format=short + build-args: | + BUILD_ENV=dev + GIT_SHA=${{ github.sha }} + platforms: linux/amd64 + scan: "true" + scan-severity: "CRITICAL,HIGH" + scan-exit-code: "0" + cache-scope: ${{ matrix.service }}-dev + + infra-dev: + name: "Infra Apply [dev]" + runs-on: ubuntu-latest + timeout-minutes: 5 + needs: [build-dev] + environment: dev + steps: + - name: Dispatch terraform apply to infra repo + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.INFRA_DEPLOY_TOKEN }} + script: | + await github.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: 'mypythonproject1-infra', + workflow_id: 'terraform-plan-apply.yml', + ref: 'main', + inputs: { + environment: 'dev', + action: 'apply', + }, + }); + console.log('Dispatched terraform-plan-apply on mypythonproject1-infra (dev)'); + + - name: Wait for infra apply + run: | + echo "Waiting 10 minutes for Terraform apply to complete..." + sleep 600 + + deploy-dev: + name: "ECS Deploy [dev] (${{ matrix.service }})" + runs-on: ubuntu-latest + timeout-minutes: 20 + needs: [infra-dev] + environment: dev + strategy: + matrix: + service: [backend, frontend] + fail-fast: false + permissions: + id-token: write + steps: + - name: Authenticate with AWS + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} + aws-region: ${{ secrets.AWS_REGION }} + + - name: Force new ECS deployment + run: | + CLUSTER="mypythonproject1-dev-cluster" + SERVICE="mypythonproject1-dev-${{ matrix.service }}-service" + aws ecs update-service \ + --cluster "$CLUSTER" \ + --service "$SERVICE" \ + --force-new-deployment \ + --region "${{ secrets.AWS_REGION }}" + + - name: Wait for service stability + timeout-minutes: 15 + run: | + CLUSTER="mypythonproject1-dev-cluster" + SERVICE="mypythonproject1-dev-${{ matrix.service }}-service" + aws ecs wait services-stable \ + --cluster "$CLUSTER" \ + --services "$SERVICE" \ + --region "${{ secrets.AWS_REGION }}" + + smoke-test-dev: + name: "Smoke Test [dev]" + needs: [deploy-dev] + uses: ./.github/workflows/_smoke-test.yml + with: + environment: dev + app-url: ${{ vars.APP_URL }} + warmup-seconds: 15 + deploy-sha: ${{ github.sha }} + secrets: inherit + build-staging: name: "Build & Push [staging] (${{ matrix.service }})" runs-on: ubuntu-latest @@ -80,7 +210,6 @@ jobs: id-token: write outputs: registry: ${{ steps.ecr-login.outputs.registry }} - image-backend: ${{ steps.meta.outputs.tags }} steps: - uses: actions/checkout@v4 with: @@ -125,7 +254,7 @@ jobs: needs: [build-staging] environment: staging steps: - - name: Dispatch terraform-apply to infra repo + - name: Dispatch terraform apply to infra repo uses: actions/github-script@v7 with: github-token: ${{ secrets.INFRA_DEPLOY_TOKEN }} @@ -133,14 +262,14 @@ jobs: await github.rest.actions.createWorkflowDispatch({ owner: context.repo.owner, repo: 'mypythonproject1-infra', - workflow_id: 'terraform-apply.yml', + workflow_id: 'terraform-plan-apply.yml', ref: 'main', inputs: { environment: 'staging', - image_tag: 'staging', + action: 'apply', }, }); - console.log('Dispatched terraform-apply on mypythonproject1-infra (staging)'); + console.log('Dispatched terraform-plan-apply on mypythonproject1-infra (staging)'); # Give Terraform apply ~10 min to complete before ECS deploy - name: Wait for infra apply @@ -316,23 +445,22 @@ jobs: needs: [build-production] environment: prod steps: - - name: Dispatch terraform-apply to infra repo + - name: Dispatch terraform apply to infra repo uses: actions/github-script@v7 with: github-token: ${{ secrets.INFRA_DEPLOY_TOKEN }} script: | - const tag = context.ref.replace('refs/tags/', ''); await github.rest.actions.createWorkflowDispatch({ owner: context.repo.owner, repo: 'mypythonproject1-infra', - workflow_id: 'terraform-apply.yml', + workflow_id: 'terraform-plan-apply.yml', ref: 'main', inputs: { - environment: 'production', - image_tag: tag, + environment: 'prod', + action: 'apply', }, }); - console.log(`Dispatched terraform-apply on mypythonproject1-infra (production, tag=${tag})`); + console.log('Dispatched terraform-plan-apply on mypythonproject1-infra (prod)'); - name: Wait for infra apply run: |