CI-CD #93
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
| name: CI-CD | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| env: | |
| description: 'Please select your command' | |
| type: choice | |
| options: | |
| - SCHEMA_CHANGED | |
| - JUST_INDEX | |
| - MIGRATE_UP | |
| - MIGRATE_DOWN_UP | |
| build: | |
| description: 'Build new image' | |
| type: boolean | |
| default: false | |
| env: | |
| REGION: eu-central-1 | |
| PLATFORM: ${{ github.ref_name == 'main' && 'prod' || 'dev' }} | |
| REPO_NAME: ${{ github.event.repository.name }} | |
| ECR_REPOSITORY: ${{ github.event.repository.name }}-${{ github.ref_name == 'main' && 'prod' || 'dev' }} | |
| AWS_ACCOUNT_ID: ${{ github.ref_name == 'main' && secrets.ACCOUNT_ID_PROD || secrets.ACCOUNT_ID_DEV }} | |
| permissions: | |
| id-token: write | |
| contents: read | |
| jobs: | |
| validate-branch: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Check branch name | |
| run: | | |
| if [[ "${{ github.ref_name }}" != "main" && "${{ github.ref_name }}" != "dev" ]]; then | |
| echo "Error: Workflow can only run on 'main' or 'dev' branches" | |
| exit 1 | |
| fi | |
| build: | |
| name: Build | |
| runs-on: ubuntu-latest | |
| environment: ${{ github.ref_name == 'main' && 'production' || 'development' }} | |
| steps: | |
| - name: Configure AWS credentials | |
| uses: aws-actions/configure-aws-credentials@v4 | |
| with: | |
| role-to-assume: arn:aws:iam::${{env.AWS_ACCOUNT_ID}}:role/platform-github-action-role-${{env.PLATFORM}} | |
| aws-region: ${{ env.REGION }} | |
| role-duration-seconds: 3600 | |
| - name: Login to Amazon ECR | |
| id: login-ecr | |
| uses: aws-actions/amazon-ecr-login@v2 | |
| - name: Check out code | |
| uses: actions/checkout@v4 | |
| with: | |
| submodules: recursive | |
| - name: Set ECR environment variables | |
| run: | | |
| echo "ECR_REGISTRY=${{ steps.login-ecr.outputs.registry }}" >> $GITHUB_ENV | |
| echo "IMAGE_TAG=$PLATFORM-${GITHUB_SHA::8}" >> $GITHUB_ENV | |
| - name: Check if image exists | |
| run: | | |
| if aws ecr describe-images --repository-name $ECR_REPOSITORY --image-ids imageTag=$IMAGE_TAG 2>/dev/null; then | |
| echo "IMAGE_EXISTS=true" >> $GITHUB_ENV | |
| else | |
| echo "IMAGE_EXISTS=false" >> $GITHUB_ENV | |
| fi | |
| - name: If skip build or image exists, set image tag to latest remote | |
| id: get-latest-image-tag | |
| if: inputs.build == false || env.IMAGE_EXISTS == 'true' | |
| run: | | |
| LATEST_TAG=$(aws ecr describe-images --repository-name $ECR_REPOSITORY --query 'sort_by(imageDetails,& imagePushedAt)[-1].imageTags[0]' --output text) | |
| if [ -n "$LATEST_TAG" ]; then | |
| echo "IMAGE_TAG=$LATEST_TAG" >> $GITHUB_ENV | |
| fi | |
| - name: Build, tag, and push image to Amazon ECR | |
| id: build-image | |
| if: env.IMAGE_EXISTS == 'false' && inputs.build == true | |
| run: | | |
| docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . | |
| docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG | |
| - uses: act10ns/slack@v1 | |
| if: always() && inputs.build == true | |
| with: | |
| status: ${{ job.status }} | |
| channel: 'a-deployments' | |
| config: .github/config/slack.yml | |
| webhook-url: ${{ secrets.SLACK_WEBHOOK }} | |
| outputs: | |
| image_tag: ${{ env.IMAGE_TAG }} | |
| deploy: | |
| runs-on: ubuntu-latest | |
| needs: build | |
| environment: ${{ github.ref_name == 'main' && 'production' || 'development' }} | |
| if: always() && !cancelled() | |
| steps: | |
| - name: Configure AWS credentials | |
| uses: aws-actions/configure-aws-credentials@v4 | |
| with: | |
| role-to-assume: arn:aws:iam::${{env.AWS_ACCOUNT_ID}}:role/platform-github-action-role-${{env.PLATFORM}} | |
| aws-region: ${{ env.REGION }} | |
| role-duration-seconds: 3600 | |
| - name: Login to Amazon ECR | |
| id: login-ecr | |
| uses: aws-actions/amazon-ecr-login@v2 | |
| - name: Set ECR environment variables | |
| run: | | |
| echo "ECR_REGISTRY=${{ steps.login-ecr.outputs.registry }}" >> $GITHUB_ENV | |
| echo "IMAGE_TAG=${{ needs.build.outputs.image_tag }}" >> $GITHUB_ENV | |
| - name: Get Latest Image ARN | |
| id: get-image-arn | |
| run: | | |
| IMAGE_URI="$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" | |
| echo "Verifying image: $IMAGE_URI" | |
| if ! aws ecr describe-images --repository-name $ECR_REPOSITORY --image-ids imageTag=$IMAGE_TAG 2>/dev/null; then | |
| echo "Image verification failed: $IMAGE_URI not found in repository $ECR_REPOSITORY" | |
| exit 1 | |
| fi | |
| echo "Image verification successful" | |
| echo "IMAGE_URI=$IMAGE_URI" >> $GITHUB_ENV | |
| - name: Create Task Definition | |
| id: task-def | |
| run: | | |
| MIGRATE_SETUP='node -e "require('\''./generated/src/db/Migrations.res.js'\'').runUpMigrations(true, true)"' | |
| MIGRATE_UP='node -e "require('\''./generated/src/db/Migrations.res.js'\'').runUpMigrations(true)"' | |
| MIGRATE_DOWN='node -e "require('\''./generated/src/db/Migrations.res.js'\'').runDownMigrations(true)"' | |
| GRANT_PERMISSIONS='pnpm ts-node scripts/grant-aggregate-permissions.ts' | |
| RUN_INDEXER='TUI_OFF=true pnpm ts-node generated/src/Index.res.js' | |
| case "${{ inputs.env }}" in | |
| "SCHEMA_CHANGED") | |
| CMD="$MIGRATE_SETUP && $GRANT_PERMISSIONS && $RUN_INDEXER" | |
| ;; | |
| "MIGRATE_UP") | |
| CMD="$MIGRATE_UP && $GRANT_PERMISSIONS && $RUN_INDEXER" | |
| ;; | |
| "MIGRATE_DOWN_UP") | |
| CMD="$MIGRATE_DOWN && $MIGRATE_UP && $GRANT_PERMISSIONS && $RUN_INDEXER" | |
| ;; | |
| "JUST_INDEX") | |
| CMD="$GRANT_PERMISSIONS && $RUN_INDEXER" | |
| ;; | |
| *) | |
| echo "Invalid command type" && exit 1 | |
| ;; | |
| esac | |
| aws ecs describe-task-definition --task-definition indexer-${{ env.PLATFORM }} --query 'taskDefinition' > task-definition-full.json | |
| jq 'del(.taskDefinitionArn, .revision, .status, .requiresAttributes, .compatibilities, .registeredAt, .registeredBy)' task-definition-full.json > task-definition.json | |
| RUNNING_TASK_ARN=$(aws ecs list-tasks --cluster ${{env.PLATFORM}}-services-cluster --service-name ${{ env.REPO_NAME }}-${{env.PLATFORM}}-svc --desired-status RUNNING --query 'taskArns[0]' --output text) | |
| if [[ "$RUNNING_TASK_ARN" != "None" && -n "$RUNNING_TASK_ARN" ]]; then | |
| RUNNING_TASK_DEF_ARN=$(aws ecs describe-tasks --cluster ${{env.PLATFORM}}-services-cluster --tasks $RUNNING_TASK_ARN --query 'tasks[0].taskDefinitionArn' --output text) | |
| if [[ "$RUNNING_TASK_DEF_ARN" != "None" && -n "$RUNNING_TASK_DEF_ARN" ]]; then | |
| aws ecs describe-task-definition --task-definition indexer-${{ env.PLATFORM }} --query 'taskDefinition' > running-task-definition.json | |
| CURRENT_DB=$(jq -r '.containerDefinitions[] | select(.name == "graphql-engine") | .environment[] | select(.name == "HASURA_GRAPHQL_DATABASE_URL") | .value' running-task-definition.json | grep -o 'blue\|green') | |
| else | |
| CURRENT_DB=$(jq -r '.containerDefinitions[] | select(.name == "graphql-engine") | .environment[] | select(.name == "HASURA_GRAPHQL_DATABASE_URL") | .value' task-definition.json | grep -o 'blue\|green') | |
| fi | |
| else | |
| CURRENT_DB=$(jq -r '.containerDefinitions[] | select(.name == "graphql-engine") | .environment[] | select(.name == "HASURA_GRAPHQL_DATABASE_URL") | .value' task-definition.json | grep -o 'blue\|green') | |
| fi | |
| if [[ "${{ inputs.env }}" == "SCHEMA_CHANGED" || "${{ inputs.env }}" == "MIGRATE_DOWN_UP" ]]; then | |
| NEW_DB=$([ "$CURRENT_DB" = "blue" ] && echo "green" || echo "blue") | |
| echo "Switching database to $NEW_DB from $CURRENT_DB" | |
| else | |
| NEW_DB=$CURRENT_DB | |
| echo "Keeping current database $CURRENT_DB as chosen command is not SCHEMA_CHANGED or MIGRATE_DOWN_UP" | |
| fi | |
| jq --arg cmd "$CMD" --arg img "$IMAGE_URI" --arg new_db "$NEW_DB" ' | |
| (.containerDefinitions[] | select(.name == "graphql-engine") | .environment[] | select(.name == "HASURA_GRAPHQL_DATABASE_URL") | .value) |= sub("/(blue|green)\\?"; "/\($new_db)?") | | |
| (.containerDefinitions[] | select(.name == "indexer-${{ env.PLATFORM }}") | .environment[] | select(.name == "ENVIO_PG_DATABASE") | .value) = $new_db | | |
| (.containerDefinitions[] | select(.name == "indexer-${{ env.PLATFORM }}") | .command) = ["sh", "-c", $cmd] | | |
| (.containerDefinitions[] | select(.name == "indexer-${{ env.PLATFORM }}") | .image) = $img | |
| ' task-definition.json > updated-task-definition.json | |
| mv updated-task-definition.json task-definition.json | |
| echo "TASK_DEFINITION_ARN=$(aws ecs register-task-definition --cli-input-json file://task-definition.json --query 'taskDefinition.taskDefinitionArn' --output text)" >> $GITHUB_ENV | |
| - name: Create CodeDeploy Deployment | |
| timeout-minutes: 45 | |
| run: | | |
| cat > appspec.json << EOF | |
| { | |
| "version": "0.0", | |
| "Resources": [ | |
| { | |
| "TargetService": { | |
| "Type": "AWS::ECS::Service", | |
| "Properties": { | |
| "TaskDefinition": "${{ env.TASK_DEFINITION_ARN }}", | |
| "LoadBalancerInfo": { | |
| "ContainerName": "graphql-engine", | |
| "ContainerPort": 8080 | |
| }, | |
| "PlatformVersion": "LATEST" | |
| } | |
| } | |
| } | |
| ] | |
| } | |
| EOF | |
| APPSPEC_CONTENT=$(cat appspec.json | jq -c . | jq -R .) | |
| cat > revision.json << EOF | |
| { | |
| "revisionType": "AppSpecContent", | |
| "appSpecContent": { | |
| "content": ${APPSPEC_CONTENT} | |
| } | |
| } | |
| EOF | |
| echo "Revision JSON content:" | |
| cat revision.json | |
| aws deploy create-deployment \ | |
| --application-name ${{ env.REPO_NAME }}-${{ env.PLATFORM }} \ | |
| --deployment-group-name ${{ env.REPO_NAME }}-${{ env.PLATFORM }}-dg \ | |
| --revision file://revision.json \ | |
| --deployment-config-name CodeDeployDefault.ECSAllAtOnce \ | |
| --region ${{ env.REGION }} \ | |
| --description "Deploying ${{ env.REPO_NAME }}-${{ env.PLATFORM }} with task definition ${{ env.TASK_DEFINITION_ARN }}" | |
| DEPLOYMENT_ID=$(aws deploy list-deployments \ | |
| --application-name ${{ env.REPO_NAME }}-${{ env.PLATFORM }} \ | |
| --deployment-group-name ${{ env.REPO_NAME }}-${{ env.PLATFORM }}-dg \ | |
| --query 'deployments[0]' \ | |
| --output text) | |
| echo "Waiting for deployment $DEPLOYMENT_ID to complete..." | |
| while true; do | |
| STATUS=$(aws deploy get-deployment \ | |
| --deployment-id $DEPLOYMENT_ID \ | |
| --query 'deploymentInfo.status' \ | |
| --output text) | |
| if [[ "$STATUS" == "Succeeded" ]]; then | |
| echo "Deployment completed successfully!" | |
| break | |
| elif [[ "$STATUS" == "Failed" || "$STATUS" == "Stopped" ]]; then | |
| echo "Deployment failed with status: $STATUS" | |
| aws deploy get-deployment \ | |
| --deployment-id $DEPLOYMENT_ID \ | |
| --query 'deploymentInfo.errorInformation' \ | |
| --output json | |
| exit 1 | |
| fi | |
| echo "Deployment status: $STATUS. Waiting..." | |
| sleep 30 | |
| done | |
| - name: Update HTTPS Target Group | |
| run: | | |
| # Define the ECS cluster, service, and region | |
| # Get ECS service load balancer information | |
| SERVICE_INFO=$(aws ecs describe-services \ | |
| --cluster ${{env.PLATFORM}}-services-cluster \ | |
| --services ${{ env.REPO_NAME }}-${{env.PLATFORM}}-svc \ | |
| --query 'services[0].loadBalancers' \ | |
| --region ${{ env.REGION }} \ | |
| --output json) | |
| # Extract the target group ARN used by the service on HTTP port (port 80 or custom port) | |
| TARGET_GROUP_ARN=$(echo $SERVICE_INFO | jq -r '.[] | select(.containerPort == 8080) | .targetGroupArn') | |
| if [ -z "$TARGET_GROUP_ARN" ]; then | |
| echo "Error: Could not find HTTP target group" | |
| exit 1 | |
| fi | |
| echo "Found HTTP Target Group ARN: $TARGET_GROUP_ARN" | |
| # Get Load Balancer ARN associated with the target group | |
| LB_ARN=$(aws elbv2 describe-target-groups \ | |
| --target-group-arns "$TARGET_GROUP_ARN" \ | |
| --query 'TargetGroups[0].LoadBalancerArns[0]' \ | |
| --output text \ | |
| --region ${{ env.REGION }}) | |
| if [ -z "$LB_ARN" ]; then | |
| echo "Error: Could not find Load Balancer ARN" | |
| exit 1 | |
| fi | |
| echo "Found Load Balancer ARN: $LB_ARN" | |
| # Get the HTTPS Listener ARN (port 443) from the Load Balancer | |
| HTTPS_LISTENER_ARN=$(aws elbv2 describe-listeners \ | |
| --load-balancer-arn "$LB_ARN" \ | |
| --query 'Listeners[?Port==`443`].ListenerArn' \ | |
| --output text \ | |
| --region ${{ env.REGION }}) | |
| if [ -z "$HTTPS_LISTENER_ARN" ]; then | |
| echo "Error: Could not find HTTPS listener (port 443)" | |
| exit 1 | |
| fi | |
| echo "Found HTTPS Listener ARN: $HTTPS_LISTENER_ARN" | |
| # Modify the HTTPS listener to forward traffic to the HTTP target group | |
| aws elbv2 modify-listener \ | |
| --listener-arn "$HTTPS_LISTENER_ARN" \ | |
| --default-actions Type=forward,TargetGroupArn="$TARGET_GROUP_ARN" \ | |
| --region ${{ env.REGION }} | |
| echo "Successfully updated HTTPS listener to forward to HTTP target group" | |
| - name: Slack Notification | |
| uses: act10ns/slack@v1 | |
| with: | |
| status: ${{ job.status }} | |
| channel: 'a-deployments' | |
| config: .github/config/slack.yml | |
| webhook-url: ${{ secrets.SLACK_WEBHOOK }} | |
| steps: ${{ toJson(steps) }} | |
| if: always() |