Skip to content

Deploy v8.3.0 to preview #3607

Deploy v8.3.0 to preview

Deploy v8.3.0 to preview #3607

Workflow file for this run

name: Deploy
run-name: Deploy ${{ inputs.git_ref_to_deploy || github.ref_name }} to ${{ inputs.environment }}
on:
workflow_call:
inputs:
environment:
required: true
type: string
server_types:
required: true
type: string
run_pre_deploy_migrations:
required: false
type: boolean
default: true
workflow_dispatch:
inputs:
git_ref_to_deploy:
description: | # Use blank unicode character (U+2800) to force line-break
Use code from: ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
(Git ref to deploy, for example, a tag, branch name or commit SHA. Will use workflow ref
if not provided.)
type: string
environment:
description: Deployment environment
required: true
type: choice
options:
- qa
- test
- preview
- training
- production
- sandbox-alpha
- sandbox-beta
- performance
- pentest
server_types:
description: Server types to deploy
required: true
type: choice
options:
- all
- web
- sidekiq
- ops
default: all
run_pre_deploy_migrations:
description: Run `pre-deploy` job (schema-migrations) before service deployment.
required: false
type: boolean
default: true
concurrency:
group: deploy-${{ inputs.environment }}
permissions: {}
env:
git_ref_to_deploy: ${{ inputs.git_ref_to_deploy }}
environment: ${{ inputs.environment }}
server_types: ${{ inputs.server_types }}
aws_account_id: ${{ inputs.environment == 'production' && '820242920762' || '393416225559' }}
cluster_name: mavis-${{ inputs.environment }}
app_version: ${{ inputs.git_ref_to_deploy == '' && github.ref_name || inputs.git_ref_to_deploy }}
jobs:
validate-and-resolve-sha:
runs-on: ubuntu-latest
outputs:
git-sha: ${{ steps.get-git-sha.outputs.git-sha }}
steps:
- name: Validate inputs
run: |
if [[ "$environment" == "preview" || "$environment" == "production" ]]; then
if [[ -z "$git_ref_to_deploy" ]]; then
echo "Error: git_ref_to_deploy is required for preview and production environments."
exit 1
fi
fi
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ inputs.git_ref_to_deploy || github.sha }}
- name: Get git sha
id: get-git-sha
run: echo "git-sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
build-and-push-images:
permissions:
id-token: write
needs: validate-and-resolve-sha
uses: ./.github/workflows/build-and-push-image.yml
with:
git_sha: ${{ needs.validate-and-resolve-sha.outputs.git-sha }}
prepare-deployment:
name: Prepare deployment
runs-on: ubuntu-latest
needs: [validate-and-resolve-sha, build-and-push-images]
permissions:
id-token: write
strategy:
fail-fast: true
matrix:
service: >-
${{ inputs.server_types == 'all' && fromJSON('["web", "sidekiq", "ops"]') ||
fromJSON(format('["{0}"]', inputs.server_types)) }}
env:
repository_name: ${{ matrix.service == 'ops' && 'mavis/ops' || 'mavis/webapp' }}
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
id: checkout-code
with:
ref: ${{ needs.validate-and-resolve-sha.outputs.git-sha }}
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.0
with:
role-to-assume: arn:aws:iam::${{ env.aws_account_id }}:role/GithubDeployECSService
aws-region: eu-west-2
- name: Get image digest
id: get-image-digest
env:
image_tag: ${{ needs.validate-and-resolve-sha.outputs.git-sha }}
run: |
digest=$(aws ecr describe-images \
--repository-name "$repository_name" \
--image-ids "imageTag=$image_tag" \
--query 'imageDetails[0].imageDigest' \
--output text)
echo "digest=$digest" >> "$GITHUB_OUTPUT"
- name: Populate task definition
id: create-task-definition
uses: aws-actions/amazon-ecs-render-task-definition@77954e213ba1f9f9cb016b86a1d4f6fcdea0d57e # v1.8.4
with:
task-definition-family:
mavis-${{ matrix.service }}-task-definition-${{ inputs.environment }}-template
container-name: application
# yamllint disable-line rule:line-length
image:
${{ env.aws_account_id }}.dkr.ecr.eu-west-2.amazonaws.com/${{ env.repository_name }}@${{
steps.get-image-digest.outputs.digest }}
environment-variables: |
APP_VERSION=${{ env.app_version }}
- name: Rename task definition file
run: >-
mv ${{ steps.create-task-definition.outputs.task-definition }} ${{ runner.temp }}/${{
matrix.service }}-task-definition.json
- name: Upload artifact for ${{ matrix.service }} task definition
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ inputs.environment }}-${{ matrix.service }}-task-definition
path: ${{ runner.temp }}/${{ matrix.service }}-task-definition.json
notify-approval-required:
name: Notify approval required
if: ${{ inputs.environment == 'production' }}
runs-on: ubuntu-latest
needs: prepare-deployment
steps:
- name: Notify approval required
uses: slackapi/slack-github-action@af78098f536edbc4de71162a307590698245be95
with:
webhook: ${{ secrets.SLACK_MAVIS_RELEASES_WEBHOOK_URL }}
webhook-type: incoming-webhook
# yamllint disable rule:line-length
payload: |
text: ":hourglass: Approval required :hourglass:"
blocks:
- type: "section"
text:
type: "mrkdwn"
text: "${{ github.workflow }} requires approval"
- type: "section"
fields:
- type: "mrkdwn"
text: "<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View workflow run>"
# yamllint enable rule:line-length
approve-deployments:
name: Wait for approval if required
runs-on: ubuntu-latest
needs: prepare-deployment
environment: ${{ inputs.environment }}
steps:
- run: echo "Proceeding with deployment to $environment"
pre-deploy:
name: Pre-deploy
runs-on: ubuntu-latest
needs: approve-deployments
if:
${{ inputs.run_pre_deploy_migrations || inputs.environment == 'production' ||
inputs.environment == 'preview' || inputs.environment == 'training' }}
permissions:
id-token: write
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.0
with:
role-to-assume: arn:aws:iam::${{ env.aws_account_id }}:role/GithubDeployECSService
aws-region: eu-west-2
- name: Download ops task definition artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
path: ${{ runner.temp }}
name: ${{ inputs.environment }}-ops-task-definition
- name: Register pre deploy task definition
id: register-pre-deploy-task-definition
run: |
family_name="mavis-pre-deploy-task-definition-$environment"
file_path="${{ runner.temp }}/migration-task-definition.json"
jq --arg f "$family_name" '.family = $f' \
"${{ runner.temp }}/ops-task-definition.json" > "$file_path"
task_definition_arn=$(aws ecs register-task-definition \
--cli-input-json file://$file_path \
--query 'taskDefinition.taskDefinitionArn' \
--output text
)
echo "task_definition_arn=$task_definition_arn" >> "$GITHUB_OUTPUT"
- name: Run schema migrations
id: run-schema-migrations
env:
SLACK_MAVIS_RELEASES_WEBHOOK_URL: ${{ secrets.SLACK_MAVIS_RELEASES_WEBHOOK_URL }}
# yamllint disable rule:line-length
run: |
PRE_DEPLOY_TASK_DEFINITION_ARN=${{ steps.register-pre-deploy-task-definition.outputs.task_definition_arn }}
SUBNET_ID=$(aws ec2 describe-subnets --filters "Name=tag:Name,Values=private-subnet-$environment-a" --query 'Subnets[0].SubnetId' --output text)
SECURITY_GROUP_ID=$(aws ec2 describe-security-groups --filters "Name=group-name,Values=ops-service-$environment" --query 'SecurityGroups[0].GroupId' --output text)
MAX_ATTEMPTS=3
ATTEMPT=1
while [ $ATTEMPT -le $MAX_ATTEMPTS ]; do
TASK_ARN=$(aws ecs run-task \
--cluster "$cluster_name" \
--task-definition "$PRE_DEPLOY_TASK_DEFINITION_ARN" \
--launch-type FARGATE \
--network-configuration "awsvpcConfiguration={subnets=[$SUBNET_ID],securityGroups=[$SECURITY_GROUP_ID]}" \
--overrides '{
"containerOverrides": [{
"name": "application",
"command": ["bin/rails","db:migrate"]
}]
}' \
--query 'tasks[0].taskArn' \
--output text)
echo "Waiting for task to complete: $TASK_ARN"
# shellcheck disable=SC2001
TASK_ID=$(sed 's:^.*/::' <<< "$TASK_ARN")
AWS_CONSOLE_URL="https://eu-west-2.console.aws.amazon.com/ecs/v2/clusters/$cluster_name/tasks/$TASK_ID/logs"
echo "View logs in AWS Console: $AWS_CONSOLE_URL"
if [ "$environment" = 'production' ]; then
./.github/send_slack_notification.sh "${{ secrets.SLACK_MAVIS_RELEASES_WEBHOOK_URL }}" "$AWS_CONSOLE_URL" "Running schema migrations attempt $ATTEMPT/$MAX_ATTEMPTS"
fi
MAX_WAIT_TIME=3600
POLL_INTERVAL=10 # Poll every 10 seconds
ELAPSED=0
while [ "$ELAPSED" -lt "$MAX_WAIT_TIME" ]; do
TASK_STATUS=$(aws ecs describe-tasks \
--cluster "$cluster_name" \
--tasks "$TASK_ID" \
--query 'tasks[0].lastStatus' \
--output text)
if [ "$TASK_STATUS" = "STOPPED" ]; then
echo "Task has stopped"
break
fi
sleep "$POLL_INTERVAL"
ELAPSED=$((ELAPSED + POLL_INTERVAL))
done
if [ "$ELAPSED" -ge "$MAX_WAIT_TIME" ]; then
echo "ERROR: Migration task did not complete within $MAX_WAIT_TIME seconds."
exit 1
fi
EXIT_CODE=$(aws ecs describe-tasks \
--cluster "$cluster_name" \
--tasks "$TASK_ARN" \
--query 'tasks[0].containers[0].exitCode' \
--output text)
echo "Container exit code: $EXIT_CODE"
if [ "$EXIT_CODE" = "0" ]; then
echo "Migrations completed"
break
else
echo "ECS task failed with exit code: $EXIT_CODE"
if [ "$ATTEMPT" = "$MAX_ATTEMPTS" ]; then
exit 1
fi
ATTEMPT=$((ATTEMPT+1))
fi
done
# yamllint enable rule:line-length
- name: Notify migrations completed
if: ${{ env.environment == 'production' }}
uses: slackapi/slack-github-action@af78098f536edbc4de71162a307590698245be95
with:
webhook: ${{ secrets.SLACK_MAVIS_RELEASES_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload: |
text: "Schema migrations finished successfully :white_check_mark:"
blocks:
- type: "section"
text:
type: "mrkdwn"
text: "Schema migrations finished successfully :white_check_mark:"
deploy-service:
name: Deploy service
runs-on: ubuntu-latest
needs: [prepare-deployment, approve-deployments, pre-deploy]
if: ${{ !cancelled() }}
permissions:
id-token: write
strategy:
fail-fast: false
matrix:
service: >-
${{ inputs.server_types == 'all' && fromJSON('["web", "sidekiq", "ops"]') ||
fromJSON(format('["{0}"]', inputs.server_types)) }}
steps:
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.0
with:
role-to-assume: arn:aws:iam::${{ env.aws_account_id }}:role/GithubDeployECSService
aws-region: eu-west-2
- name: Download ${{ matrix.service }} task definition artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
path: ${{ runner.temp }}
name: ${{ inputs.environment }}-${{ matrix.service }}-task-definition
- name: Change family of task definition
run: |
file_path="${{ runner.temp }}/${{ matrix.service }}-task-definition.json"
family_name="mavis-${{ matrix.service }}-task-definition-$environment"
jq --arg f "$family_name" '.family = $f' "$file_path" > tmpfile && mv tmpfile "$file_path"
- name: Deploy ${{ matrix.service }} service
id: ecs-deploy
uses: aws-actions/amazon-ecs-deploy-task-definition@fc8fc60f3a60ffd500fcb13b209c59d221ac8c8c # v2.6.1
with:
task-definition: ${{ runner.temp }}/${{ matrix.service }}-task-definition.json
cluster: ${{ env.cluster_name }}
service: mavis-${{ inputs.environment }}-${{ matrix.service }}
force-new-deployment: true
wait-for-service-stability: true
- name: Check if deployment was successful
# yamllint disable rule:line-length
run: |
current_task_definition_arn=$(aws ecs describe-services --cluster "mavis-$environment" --services "mavis-$environment-${{ matrix.service }}" --query services[0].deployments[0].taskDefinition | jq -r ".")
new_task_definition_arn=${{ steps.ecs-deploy.outputs.task-definition-arn }}
echo "Current task definition arn: $current_task_definition_arn"
echo "Expected task definition arn after deployment: $new_task_definition_arn"
if [ "$current_task_definition_arn" != "$new_task_definition_arn" ]; then
echo "Task definition arns don't match, likely due to a rollback to the previous version. Deployment failed."
exit 1
fi
# yamllint enable rule:line-length
post-deploy:
name: Post-deploy
runs-on: ubuntu-latest
needs: [approve-deployments, deploy-service]
if: ${{ !cancelled() && needs.deploy-service.result != 'failure' }}
permissions:
id-token: write
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.0
with:
role-to-assume: arn:aws:iam::${{ env.aws_account_id }}:role/GithubDeployECSService
aws-region: eu-west-2
- name: Download ops task definition artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
path: ${{ runner.temp }}
name: ${{ inputs.environment }}-ops-task-definition
- name: Register post deploy task definition
id: register-post-deploy-task-definition
run: |
family_name="mavis-post-deploy-task-definition-$environment"
file_path="${{ runner.temp }}/migration-task-definition.json"
jq --arg f "$family_name" '.family = $f' \
"${{ runner.temp }}/ops-task-definition.json" > "$file_path"
task_definition_arn=$(aws ecs register-task-definition \
--cli-input-json file://$file_path \
--query 'taskDefinition.taskDefinitionArn' \
--output text
)
echo "task_definition_arn=$task_definition_arn" >> "$GITHUB_OUTPUT"
- name: Run data migrations
id: run-data-migrations
env:
SLACK_MAVIS_RELEASES_WEBHOOK_URL: ${{ secrets.SLACK_MAVIS_RELEASES_WEBHOOK_URL }}
# yamllint disable rule:line-length
run: |
POST_DEPLOY_TASK_DEFINITION_ARN=${{ steps.register-post-deploy-task-definition.outputs.task_definition_arn }}
SUBNET_ID=$(aws ec2 describe-subnets --filters "Name=tag:Name,Values=private-subnet-$environment-a" --query 'Subnets[0].SubnetId' --output text)
SECURITY_GROUP_ID=$(aws ec2 describe-security-groups --filters "Name=group-name,Values=ops-service-$environment" --query 'SecurityGroups[0].GroupId' --output text)
MAX_ATTEMPTS=3
ATTEMPT=1
while [ $ATTEMPT -le $MAX_ATTEMPTS ]; do
TASK_ARN=$(aws ecs run-task \
--cluster "$cluster_name" \
--task-definition "$POST_DEPLOY_TASK_DEFINITION_ARN" \
--launch-type FARGATE \
--network-configuration "awsvpcConfiguration={subnets=[$SUBNET_ID],securityGroups=[$SECURITY_GROUP_ID]}" \
--overrides '{
"containerOverrides": [{
"name": "application",
"command": ["bin/rails","data:migrate"]
}]
}' \
--query 'tasks[0].taskArn' \
--output text)
echo "Waiting for task to complete: $TASK_ARN"
# shellcheck disable=SC2001
TASK_ID=$(sed 's:^.*/::' <<< "$TASK_ARN")
AWS_CONSOLE_URL="https://eu-west-2.console.aws.amazon.com/ecs/v2/clusters/$cluster_name/tasks/$TASK_ID/logs"
echo "View logs in AWS Console: $AWS_CONSOLE_URL"
if [ "$environment" = 'production' ]; then
./.github/send_slack_notification.sh "${{ secrets.SLACK_MAVIS_RELEASES_WEBHOOK_URL }}" "$AWS_CONSOLE_URL" "Running data migrations attempt $ATTEMPT/$MAX_ATTEMPTS"
fi
MAX_WAIT_TIME=3600
POLL_INTERVAL=10 # Poll every 10 seconds
ELAPSED=0
while [ "$ELAPSED" -lt "$MAX_WAIT_TIME" ]; do
TASK_STATUS=$(aws ecs describe-tasks \
--cluster "$cluster_name" \
--tasks "$TASK_ID" \
--query 'tasks[0].lastStatus' \
--output text)
if [ "$TASK_STATUS" = "STOPPED" ]; then
echo "Task has stopped"
break
fi
sleep "$POLL_INTERVAL"
ELAPSED=$((ELAPSED + POLL_INTERVAL))
done
if [ "$ELAPSED" -ge "$MAX_WAIT_TIME" ]; then
echo "ERROR: Data migration task did not complete within $MAX_WAIT_TIME seconds."
exit 1
fi
EXIT_CODE=$(aws ecs describe-tasks \
--cluster "$cluster_name" \
--tasks "$TASK_ARN" \
--query 'tasks[0].containers[0].exitCode' \
--output text)
echo "Container exit code: $EXIT_CODE"
if [ "$EXIT_CODE" = "0" ]; then
echo "Data migrations completed"
break
else
echo "ECS task failed with exit code: $EXIT_CODE"
if [ "$ATTEMPT" = "$MAX_ATTEMPTS" ]; then
exit 1
fi
ATTEMPT=$((ATTEMPT+1))
fi
done
# yamllint enable rule:line-length
- name: Notify data migrations completed
if: ${{ env.environment == 'production' }}
uses: slackapi/slack-github-action@af78098f536edbc4de71162a307590698245be95
with:
webhook: ${{ secrets.SLACK_MAVIS_RELEASES_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload: |
text: "Data migrations finished successfully :white_check_mark:"
blocks:
- type: "section"
text:
type: "mrkdwn"
text: "Data migrations finished successfully :white_check_mark:"
notify-deployment-complete:
name: Notify deployment status
runs-on: ubuntu-latest
needs: [pre-deploy, deploy-service, post-deploy]
if: ${{ !cancelled() && inputs.environment == 'production' }}
steps:
- name: Notify deployment success
if: >-
${{ needs.deploy-service.result == 'success' || needs.post-deploy.result == 'success' }}
uses: slackapi/slack-github-action@af78098f536edbc4de71162a307590698245be95
with:
webhook: ${{ secrets.SLACK_MAVIS_RELEASES_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload: |
text: "*${{ env.app_version }}* is deployed :ship:"
blocks:
- type: "section"
text:
type: "mrkdwn"
text: "*${{ env.app_version }}* is deployed :ship:"
- name: Notify deployment failure
if: >-
${{ needs.deploy-service.result == 'failure' || needs.pre-deploy.result == 'failure' }}
uses: slackapi/slack-github-action@af78098f536edbc4de71162a307590698245be95
with:
webhook: ${{ secrets.SLACK_MAVIS_RELEASES_WEBHOOK_URL }}
webhook-type: incoming-webhook
# yamllint disable rule:line-length
payload: |
text: "Deployment of *${{ env.app_version }}* failed :x:"
blocks:
- type: "section"
text:
type: "mrkdwn"
text: "Deployment of *${{ env.app_version }}* failed :x:"
- type: "section"
fields:
- type: "mrkdwn"
text: "*Failed job:*\n${{ needs.deploy-service.result == 'failure' && 'deploy-service' || 'pre-deploy' || 'post-deploy' }}"
- type: "mrkdwn"
text: "<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View workflow run>"
# yamllint enable rule:line-length