Skip to content

chore(ci): cache & restructure GHAs + Docker, fix race in deploy (#53) #1

chore(ci): cache & restructure GHAs + Docker, fix race in deploy (#53)

chore(ci): cache & restructure GHAs + Docker, fix race in deploy (#53) #1

Workflow file for this run

name: Deploy
on:
push:
branches: [main]
permissions:
contents: read
# Don't cancel mid-deploy: cancelling Terraform apply would leave state
# inconsistent. Queue follow-up runs instead.
concurrency:
group: deploy-${{ github.ref }}
cancel-in-progress: false
jobs:
build-app:
name: Build & Push App Image
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push primary Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./docker/Dockerfile
push: true
tags: rnavt/people-manager:latest,rnavt/people-manager:${{ github.sha }}
cache-from: type=gha,scope=app
cache-to: type=gha,scope=app,mode=max
build-migration:
name: Build & Push Migration Image
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push migration Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./docker/Dockerfile.migration
push: true
tags: rnavt/people-manager:latest-migration,rnavt/people-manager:${{ github.sha }}-migration
cache-from: type=gha,scope=migration
cache-to: type=gha,scope=migration,mode=max
terraform:
name: Terraform Apply & Migrate
# Wait for both images so Terraform never references a tag that hasn't
# been pushed yet.
needs: [build-app, build-migration]
runs-on: ubuntu-latest
environment: production
defaults:
run:
shell: bash
env:
TF_PLUGIN_CACHE_DIR: ${{ github.workspace }}/.terraform.d/plugin-cache
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-west-2
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
- name: Cache Terraform plugins
uses: actions/cache@v4
with:
path: ${{ env.TF_PLUGIN_CACHE_DIR }}
key: ${{ runner.os }}-terraform-${{ hashFiles('terraform/.terraform.lock.hcl', 'terraform/**/*.tf') }}
restore-keys: ${{ runner.os }}-terraform-
- name: Create plugin cache dir
run: mkdir -p "$TF_PLUGIN_CACHE_DIR"
- name: Terraform Init
working-directory: terraform/
run: terraform init -backend-config "bucket=terraform-state-nilo2024" -backend-config "key=people-manager.tfstate"
- name: Terraform Format
working-directory: terraform/
run: terraform fmt -check -recursive
- name: Terraform Plan
working-directory: terraform/
env:
TF_VAR_docker_image_tag: ${{ github.sha }}
TF_VAR_migration_docker_image_tag: ${{ github.sha }}-migration
TF_VAR_GOOGLE_OAUTH_CLIENT_ID: ${{ secrets.GOOGLE_OAUTH_CLIENT_ID }}
TF_VAR_GOOGLE_OAUTH_CLIENT_SECRET: ${{ secrets.GOOGLE_OAUTH_CLIENT_SECRET }}
TF_VAR_JWT_SECRET: ${{ secrets.JWT_SECRET }}
TF_LOG: info
run: terraform plan
- name: Terraform Apply
working-directory: terraform/
env:
TF_VAR_docker_image_tag: ${{ github.sha }}
TF_VAR_migration_docker_image_tag: ${{ github.sha }}-migration
TF_VAR_GOOGLE_OAUTH_CLIENT_ID: ${{ secrets.GOOGLE_OAUTH_CLIENT_ID }}
TF_VAR_GOOGLE_OAUTH_CLIENT_SECRET: ${{ secrets.GOOGLE_OAUTH_CLIENT_SECRET }}
TF_VAR_JWT_SECRET: ${{ secrets.JWT_SECRET }}
TF_LOG: info
run: terraform apply -auto-approve
- name: Detect migration file changes
id: migration_changes
run: |
set -euo pipefail
CHANGED_MIGRATIONS=$(git diff --name-only "${{ github.event.before }}" "${{ github.sha }}" -- packages/backend/migrations)
if [ -n "$CHANGED_MIGRATIONS" ]; then
echo "Detected migration changes:"
echo "$CHANGED_MIGRATIONS"
echo "run_migrations=true" >> "$GITHUB_OUTPUT"
else
echo "No migration changes detected."
echo "run_migrations=false" >> "$GITHUB_OUTPUT"
fi
- name: Run ECS migration task
if: steps.migration_changes.outputs.run_migrations == 'true'
run: |
set -euo pipefail
CLUSTER_NAME="people-manager-cluster"
SERVICE_NAME="people-manager-service"
MIGRATION_TASK_FAMILY="people-manager-db-migration-task"
SUBNETS=$(aws ecs describe-services \
--cluster "$CLUSTER_NAME" \
--services "$SERVICE_NAME" \
--query 'services[0].networkConfiguration.awsvpcConfiguration.subnets' \
--output text | tr '\t' ',')
SECURITY_GROUPS=$(aws ecs describe-services \
--cluster "$CLUSTER_NAME" \
--services "$SERVICE_NAME" \
--query 'services[0].networkConfiguration.awsvpcConfiguration.securityGroups' \
--output text | tr '\t' ',')
ASSIGN_PUBLIC_IP=$(aws ecs describe-services \
--cluster "$CLUSTER_NAME" \
--services "$SERVICE_NAME" \
--query 'services[0].networkConfiguration.awsvpcConfiguration.assignPublicIp' \
--output text)
TASK_ARN=$(aws ecs run-task \
--cluster "$CLUSTER_NAME" \
--launch-type FARGATE \
--task-definition "$MIGRATION_TASK_FAMILY" \
--network-configuration "awsvpcConfiguration={subnets=[$SUBNETS],securityGroups=[$SECURITY_GROUPS],assignPublicIp=$ASSIGN_PUBLIC_IP}" \
--count 1 \
--query 'tasks[0].taskArn' \
--output text)
aws ecs wait tasks-stopped --cluster "$CLUSTER_NAME" --tasks "$TASK_ARN"
EXIT_CODE=$(aws ecs describe-tasks \
--cluster "$CLUSTER_NAME" \
--tasks "$TASK_ARN" \
--query 'tasks[0].containers[0].exitCode' \
--output text)
if [ "$EXIT_CODE" != "0" ]; then
echo "Migration task failed with exit code: $EXIT_CODE"
exit 1
fi