.github/.github/workflows/cd-dotnet-eb.yaml
renovate[bot] e5b0cf714d
Some checks failed
ci / ci / ci (push) Waiting to run
Release on reusable change / version (push) Has been cancelled
Release on reusable change / release (push) Has been cancelled
chore(deps): update github actions (#134)
2026-08-24 21:18:32 +00:00

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}"