mirror of
https://github.com/Sea-Haven-Industries/.github.git
synced 2026-09-30 07:03:11 +00:00
274 lines
11 KiB
YAML
274 lines
11 KiB
YAML
name: CD — .NET Elastic Beanstalk
|
|
|
|
# Caller example:
|
|
#
|
|
# jobs:
|
|
# deploy:
|
|
# uses: Sea-Haven-Industries/.github/.github/workflows/cd-dotnet-eb.yaml@<full-commit-sha> # v1.0.4
|
|
# with:
|
|
# project: "Api.Example/Api.Example.csproj"
|
|
# eb-application: "example-api"
|
|
# eb-environment: "example-api-dev"
|
|
# procfile-command: "dotnet Api.Example.dll"
|
|
# secrets:
|
|
# deploy-role-arn: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
|
|
#
|
|
# The caller owns branch-to-environment mapping (one job per branch); this
|
|
# workflow is deliberately branch-agnostic.
|
|
|
|
on:
|
|
workflow_call:
|
|
inputs:
|
|
dotnet-version:
|
|
description: ".NET SDK version"
|
|
type: string
|
|
default: "8.0.x"
|
|
working-directory:
|
|
description: "Directory containing the solution/project"
|
|
type: string
|
|
default: "."
|
|
project:
|
|
description: "Project file to publish (e.g. Api.Example/Api.Example.csproj)"
|
|
type: string
|
|
required: true
|
|
eb-application:
|
|
description: "Elastic Beanstalk application name"
|
|
type: string
|
|
required: true
|
|
eb-environment:
|
|
description: "Elastic Beanstalk environment name to deploy"
|
|
type: string
|
|
required: true
|
|
region:
|
|
description: "AWS region"
|
|
type: string
|
|
default: "us-east-1"
|
|
bundle-bucket:
|
|
description: "S3 bucket for the application bundle (default: the account's elasticbeanstalk-<region>-<account-id> bucket)"
|
|
type: string
|
|
default: ""
|
|
procfile-command:
|
|
description: "Command for a generated Procfile (e.g. 'dotnet Api.Example.dll'). Leave empty if the repo commits its own Procfile."
|
|
type: string
|
|
default: ""
|
|
version-label:
|
|
description: "EB application version label (default: <run-number>-<short-sha>)"
|
|
type: string
|
|
default: ""
|
|
wait-timeout-minutes:
|
|
description: "How long to wait for the environment to finish updating"
|
|
type: number
|
|
default: 20
|
|
secrets:
|
|
deploy-role-arn:
|
|
description: "OIDC deploy role ARN"
|
|
required: true
|
|
|
|
permissions:
|
|
id-token: write
|
|
contents: read
|
|
|
|
jobs:
|
|
deploy:
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 30
|
|
# Serialise per environment so two pushes cannot deploy over each other.
|
|
# cancel-in-progress is FALSE on purpose: unlike CI, aborting midway can
|
|
# leave an environment mid-update.
|
|
concurrency:
|
|
group: cd-dotnet-eb-${{ inputs.eb-application }}-${{ inputs.eb-environment }}
|
|
cancel-in-progress: false
|
|
defaults:
|
|
run:
|
|
working-directory: ${{ inputs.working-directory }}
|
|
steps:
|
|
- uses: actions/checkout@v7.0.1
|
|
|
|
- uses: actions/setup-dotnet@v6
|
|
with:
|
|
dotnet-version: ${{ inputs.dotnet-version }}
|
|
|
|
# Every step below uses env-var indirection rather than inline expression
|
|
# interpolation, so shell metacharacters in an input can never be parsed
|
|
# as script. Note the repo's actionlint runs with shellcheck disabled
|
|
# (see ci.yaml), so this is not enforced automatically — keep it by hand.
|
|
- name: Publish
|
|
env:
|
|
PROJECT: ${{ inputs.project }}
|
|
run: dotnet publish "$PROJECT" --configuration Release --output publish
|
|
|
|
- name: Write Procfile
|
|
if: ${{ inputs.procfile-command != '' }}
|
|
env:
|
|
PROCFILE_COMMAND: ${{ inputs.procfile-command }}
|
|
run: |
|
|
printf 'web: %s\n' "$PROCFILE_COMMAND" > publish/Procfile
|
|
|
|
- name: Verify Procfile
|
|
run: |
|
|
if [[ ! -f publish/Procfile ]]; then
|
|
echo "::error::No Procfile in the published output. Either set the procfile-command input or commit a Procfile that is copied to the publish directory."
|
|
exit 1
|
|
fi
|
|
echo "Procfile: $(cat publish/Procfile)"
|
|
|
|
- name: Package bundle
|
|
run: |
|
|
cd publish
|
|
zip -qr ../bundle.zip .
|
|
cd ..
|
|
echo "Bundle size: $(du -h bundle.zip | cut -f1)"
|
|
|
|
- uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3
|
|
with:
|
|
role-to-assume: ${{ secrets.deploy-role-arn }}
|
|
aws-region: ${{ inputs.region }}
|
|
|
|
- name: Pre-flight checks
|
|
env:
|
|
EB_APP: ${{ inputs.eb-application }}
|
|
EB_ENV: ${{ inputs.eb-environment }}
|
|
run: |
|
|
echo "Pre-flight: checking environment $EB_ENV..."
|
|
STATUS=$(aws elasticbeanstalk describe-environments \
|
|
--application-name "$EB_APP" \
|
|
--environment-names "$EB_ENV" \
|
|
--query 'Environments[0].Status' --output text 2>/dev/null || echo "NOT_FOUND")
|
|
case "$STATUS" in
|
|
None|NOT_FOUND|Terminated|Terminating)
|
|
echo "::error::Environment $EB_ENV not found (or terminated) in application $EB_APP. This workflow deploys to an existing environment; it does not create one."
|
|
exit 1
|
|
;;
|
|
Ready)
|
|
echo "Pre-flight: environment is Ready — OK to deploy."
|
|
;;
|
|
*)
|
|
echo "::error::Environment $EB_ENV is in status $STATUS — wait for it to reach Ready."
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
- name: Resolve deploy parameters
|
|
env:
|
|
BUNDLE_BUCKET: ${{ inputs.bundle-bucket }}
|
|
VERSION_LABEL: ${{ inputs.version-label }}
|
|
REGION: ${{ inputs.region }}
|
|
RUN_NUMBER: ${{ github.run_number }}
|
|
RUN_ATTEMPT: ${{ github.run_attempt }}
|
|
SHA: ${{ github.sha }}
|
|
run: |
|
|
if [[ -z "$BUNDLE_BUCKET" ]]; then
|
|
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
|
|
BUNDLE_BUCKET="elasticbeanstalk-${REGION}-${ACCOUNT_ID}"
|
|
fi
|
|
if [[ -z "$VERSION_LABEL" ]]; then
|
|
# run_attempt is part of the label so a re-run of the same commit
|
|
# does not collide with the version the first attempt created —
|
|
# create-application-version rejects a duplicate label.
|
|
VERSION_LABEL="${RUN_NUMBER}.${RUN_ATTEMPT}-${SHA:0:7}"
|
|
fi
|
|
echo "BUNDLE_BUCKET=$BUNDLE_BUCKET" >> "$GITHUB_ENV"
|
|
echo "VERSION_LABEL=$VERSION_LABEL" >> "$GITHUB_ENV"
|
|
echo "Bundle bucket: $BUNDLE_BUCKET"
|
|
echo "Version label: $VERSION_LABEL"
|
|
|
|
- name: Upload bundle
|
|
env:
|
|
EB_APP: ${{ inputs.eb-application }}
|
|
run: |
|
|
S3_KEY="${EB_APP}/${VERSION_LABEL}.zip"
|
|
aws s3 cp bundle.zip "s3://${BUNDLE_BUCKET}/${S3_KEY}"
|
|
echo "S3_KEY=$S3_KEY" >> "$GITHUB_ENV"
|
|
|
|
- name: Create application version
|
|
env:
|
|
EB_APP: ${{ inputs.eb-application }}
|
|
RUN_ID: ${{ github.run_id }}
|
|
run: |
|
|
aws elasticbeanstalk create-application-version \
|
|
--application-name "$EB_APP" \
|
|
--version-label "$VERSION_LABEL" \
|
|
--source-bundle "S3Bucket=${BUNDLE_BUCKET},S3Key=${S3_KEY}" \
|
|
--description "GitHub Actions run ${RUN_ID}" \
|
|
--no-auto-create-application \
|
|
--output text > /dev/null
|
|
echo "Created application version $VERSION_LABEL"
|
|
|
|
- name: Deploy to environment
|
|
env:
|
|
EB_APP: ${{ inputs.eb-application }}
|
|
EB_ENV: ${{ inputs.eb-environment }}
|
|
run: |
|
|
aws elasticbeanstalk update-environment \
|
|
--application-name "$EB_APP" \
|
|
--environment-name "$EB_ENV" \
|
|
--version-label "$VERSION_LABEL" \
|
|
--output text > /dev/null
|
|
echo "Deployment of $VERSION_LABEL to $EB_ENV started."
|
|
|
|
- name: Post-deploy health check
|
|
env:
|
|
EB_APP: ${{ inputs.eb-application }}
|
|
EB_ENV: ${{ inputs.eb-environment }}
|
|
WAIT_TIMEOUT_MINUTES: ${{ inputs.wait-timeout-minutes }}
|
|
run: |
|
|
# Deliberately NOT `aws elasticbeanstalk wait environment-updated`:
|
|
# that waiter is hardcoded to 20 attempts x 20s (~6m40s) and the CLI
|
|
# exposes no way to extend it, so a slower rolling deploy would fail
|
|
# the job while the deployment was still healthy. Poll instead.
|
|
DEADLINE=$(( SECONDS + WAIT_TIMEOUT_MINUTES * 60 ))
|
|
echo "Waiting up to ${WAIT_TIMEOUT_MINUTES}m for $EB_ENV to finish updating..."
|
|
while true; do
|
|
read -r STATUS HEALTH HEALTH_STATUS DEPLOYED CNAME <<< "$(aws elasticbeanstalk describe-environments \
|
|
--application-name "$EB_APP" \
|
|
--environment-names "$EB_ENV" \
|
|
--query 'Environments[0].[Status,Health,HealthStatus,VersionLabel,CNAME]' --output text)"
|
|
if [[ "$STATUS" != "Updating" ]]; then
|
|
break
|
|
fi
|
|
if (( SECONDS >= DEADLINE )); then
|
|
echo "::error::Environment $EB_ENV was still Updating after ${WAIT_TIMEOUT_MINUTES}m. The deploy may still be in progress — check the EB console before retrying."
|
|
exit 1
|
|
fi
|
|
sleep 15
|
|
done
|
|
|
|
echo "Status: $STATUS | Health: $HEALTH ($HEALTH_STATUS) | Version: $DEPLOYED"
|
|
|
|
# Assert all three conditions. Elastic Beanstalk reports a rolled-back
|
|
# deploy as a perfectly healthy Ready environment — it is simply
|
|
# running the PREVIOUS version — so the version check is what turns a
|
|
# failed deploy into a failed job.
|
|
if [[ "$STATUS" != "Ready" ]]; then
|
|
echo "::error::Environment $EB_ENV ended in status $STATUS after deploy."
|
|
exit 1
|
|
fi
|
|
# HealthStatus is only meaningful with enhanced health; environments on
|
|
# basic health report Unknown/No Data, so fall back to the colour.
|
|
case "$HEALTH_STATUS" in
|
|
Ok|Info)
|
|
;;
|
|
Unknown|"No Data"|None|"")
|
|
if [[ "$HEALTH" != "Green" ]]; then
|
|
echo "::error::Environment $EB_ENV health is $HEALTH after deploy."
|
|
exit 1
|
|
fi
|
|
;;
|
|
*)
|
|
echo "::error::Environment $EB_ENV health is $HEALTH_STATUS after deploy."
|
|
exit 1
|
|
;;
|
|
esac
|
|
if [[ "$DEPLOYED" != "$VERSION_LABEL" ]]; then
|
|
echo "::error::Environment $EB_ENV is running $DEPLOYED, not the deployed version $VERSION_LABEL — Elastic Beanstalk rolled the deploy back."
|
|
exit 1
|
|
fi
|
|
|
|
echo "Recent environment events:"
|
|
aws elasticbeanstalk describe-events \
|
|
--application-name "$EB_APP" \
|
|
--environment-name "$EB_ENV" \
|
|
--max-items 10 \
|
|
--query 'Events[*].[Severity,Message]' --output table
|
|
|
|
echo "Health check passed. Environment URL: http://${CNAME}"
|