shoc-backend/.github/workflows/deploy.yml

286 lines
11 KiB
YAML

name: Validate and deploy
on:
pull_request:
branches: [dev, staging, main]
workflow_dispatch:
permissions:
contents: read
jobs:
validate:
name: Validate deployable source bundle
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Set up .NET
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: "8.0.x"
- name: Set up Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "22.22.1"
cache: npm
cache-dependency-path: infra/cdk/package-lock.json
- name: Repository quality gate
run: bash scripts/governance-check.sh
- name: Validate CDK deployment infrastructure
run: |
npm ci --prefix infra/cdk
npm run synth --prefix infra/cdk
- name: Build Elastic Beanstalk source bundle
run: bash scripts/package-elastic-beanstalk.sh
- name: Inspect source bundle contract
run: |
set -euo pipefail
unzip -t .artifacts/elastic-beanstalk/site.zip
unzip -Z1 .artifacts/elastic-beanstalk/site.zip \
> .artifacts/elastic-beanstalk/zip-contents.txt
grep -Fxq "efbundle" .artifacts/elastic-beanstalk/zip-contents.txt
grep -Fxq ".ebextensions/01_migrations.config" \
.artifacts/elastic-beanstalk/zip-contents.txt
grep -Fxq ".ebextensions/02_webhook_config.config" \
.artifacts/elastic-beanstalk/zip-contents.txt
unzip -p .artifacts/elastic-beanstalk/site.zip \
.ebextensions/02_webhook_config.config \
> .artifacts/elastic-beanstalk/webhook-config.txt
grep -Fxq ' WorkOrderWebhook__Enabled: "true"' \
.artifacts/elastic-beanstalk/webhook-config.txt
grep -Fxq ' WorkOrderWebhook__Region: us-east-1' \
.artifacts/elastic-beanstalk/webhook-config.txt
grep -Fxq \
' WorkOrderWebhook__SecretId: arn:aws:secretsmanager:us-east-1:011934824531:secret:workorder-ingest/shoc-webhook-hmac-puYTcB' \
.artifacts/elastic-beanstalk/webhook-config.txt
deploy:
name: Deploy shoc-backend to Elastic Beanstalk
if: >
github.event_name == 'workflow_dispatch' &&
contains(fromJSON('["refs/heads/dev","refs/heads/staging"]'), github.ref)
needs: validate
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
environment:
name: ${{ github.ref_name }}
concurrency:
group: deploy-${{ github.ref_name }}
cancel-in-progress: false
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Resolve deploy target
id: target
run: |
set -euo pipefail
case "${GITHUB_REF_NAME}" in
dev)
application=shoc-backend
environment=shoc-backend-dev
smoke_url=https://api.dev.seahaven.com
;;
staging)
application=shoc-backend
environment=shoc-backend-staging
smoke_url=https://api.staging.seahaven.com
;;
*)
echo "Unsupported ref ${GITHUB_REF_NAME}" >&2
exit 1
;;
esac
{
echo "application=${application}"
echo "environment=${environment}"
echo "smoke_url=${smoke_url}"
} >> "${GITHUB_OUTPUT}"
{
echo "EB_APPLICATION_NAME=${application}"
echo "EB_ENVIRONMENT_NAME=${environment}"
echo "SMOKE_URL=${smoke_url}"
} >> "${GITHUB_ENV}"
- name: Set up .NET
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: "8.0.x"
- name: Build Elastic Beanstalk source bundle
run: bash scripts/package-elastic-beanstalk.sh
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3
with:
role-to-assume: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
aws-region: us-east-1
audience: sts.amazonaws.com
- name: Capture current environment version
run: |
set -euo pipefail
prev="$(aws elasticbeanstalk describe-environments \
--environment-names "${EB_ENVIRONMENT_NAME}" \
--region us-east-1 \
--query 'Environments[0].VersionLabel' \
--output text)"
echo "$prev" > .artifacts/elastic-beanstalk/previous-version.txt
echo "Previous version label: $prev"
- name: Deploy prebuilt bundle to existing environment
uses: aws-actions/aws-elasticbeanstalk-deploy@7883cdd454c162051bf6fc13389536b045149b4c # v1.0.8
with:
aws-region: us-east-1
application-name: ${{ steps.target.outputs.application }}
environment-name: ${{ steps.target.outputs.environment }}
version-label: ${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}
deployment-package-path: .artifacts/elastic-beanstalk/site.zip
s3-bucket-name: elasticbeanstalk-us-east-1-396287094661
create-application-if-not-exists: "false"
create-environment-if-not-exists: "false"
create-s3-bucket-if-not-exists: "false"
use-existing-application-version-if-available: "false"
wait-for-deployment: "true"
wait-for-environment-recovery: "true"
- name: Verify exact application version is active
run: |
set -euo pipefail
expected="${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}"
status="Unknown"
current="Unknown"
health="Unknown"
for _ in $(seq 1 80); do
read -r status current health < <(
aws elasticbeanstalk describe-environments \
--environment-names "${EB_ENVIRONMENT_NAME}" \
--region us-east-1 \
--query 'Environments[0].[Status,VersionLabel,Health]' \
--output text
)
echo "environment status: $status; version: $current; health: $health"
if [ "$status" = "Ready" ]; then
if [ "$current" = "$expected" ] && { [ "$health" = "Green" ] || [ "$health" = "Yellow" ]; }; then
echo "Expected application version is Ready and healthy."
exit 0
fi
echo "Environment became Ready without activating expected version $expected." >&2
exit 1
fi
sleep 15
done
echo "Expected application version did not become Ready within the deployment window." >&2
exit 1
- name: Post-deploy smoke
run: bash scripts/smoke-elastic-beanstalk.sh "${SMOKE_URL}"
- name: Verify webhook secret source is operational
run: |
set -euo pipefail
response_file="$(mktemp)"
trap 'rm -f "$response_file"' EXIT
status="$(curl --silent --show-error \
--output "$response_file" \
--write-out '%{http_code}' \
--request POST \
--header 'Content-Type: application/json' \
--header "X-SH-Timestamp: $(date +%s)" \
--header 'X-SH-Key-Id: deployment-smoke-invalid-key' \
--header "X-SH-Signature: v1=$(printf '0%.0s' {1..64})" \
--data '{}' \
"${SMOKE_URL}/api/webhooks/work-orders")"
if [ "$status" != "401" ]; then
echo "Expected enabled webhook with an operational secret source to reject the invalid probe with 401; received $status." >&2
sed -n '1,20p' "$response_file" >&2
exit 1
fi
- name: Restore previous application version on failure (schema is not reverted)
if: failure()
run: |
set -euo pipefail
prev_file=".artifacts/elastic-beanstalk/previous-version.txt"
if [ ! -f "$prev_file" ]; then
echo "No previous version captured; nothing to roll back." >&2
exit 0
fi
prev="$(cat "$prev_file")"
if [ -z "$prev" ] || [ "$prev" = "null" ] || [ "$prev" = "None" ] || [ "$prev" = "N/A" ]; then
echo "No previous version recorded; nothing to roll back." >&2
exit 0
fi
echo "Waiting for any in-flight environment update to settle..."
status="Unknown"
current="Unknown"
health="Unknown"
for _ in $(seq 1 80); do
read -r status current health < <(
aws elasticbeanstalk describe-environments \
--environment-names "${EB_ENVIRONMENT_NAME}" \
--region us-east-1 \
--query 'Environments[0].[Status,VersionLabel,Health]' \
--output text
)
echo "environment status: $status; version: $current; health: $health"
if [ "$status" = "Ready" ]; then
break
fi
sleep 15
done
if [ "$status" != "Ready" ]; then
echo "Environment did not settle before rollback." >&2
exit 1
fi
if [ "$current" = "$prev" ]; then
echo "Environment is already on previous version $prev."
exit 0
fi
echo "Restoring ${EB_ENVIRONMENT_NAME} application code to version label: $prev"
echo "Database migrations are not reverted; deployable migrations must follow the expand/contract policy."
aws elasticbeanstalk update-environment \
--environment-name "${EB_ENVIRONMENT_NAME}" \
--version-label "$prev" \
--region us-east-1
echo "Waiting for previous version to become healthy..."
for _ in $(seq 1 80); do
read -r status current health < <(
aws elasticbeanstalk describe-environments \
--environment-names "${EB_ENVIRONMENT_NAME}" \
--region us-east-1 \
--query 'Environments[0].[Status,VersionLabel,Health]' \
--output text
)
echo "environment status: $status; version: $current; health: $health"
if [ "$status" = "Ready" ]; then
if [ "$current" = "$prev" ] && { [ "$health" = "Green" ] || [ "$health" = "Yellow" ]; }; then
echo "Application version restore complete; previous code is Ready and healthy."
exit 0
fi
echo "Rollback reached Ready in an unexpected version/health state." >&2
exit 1
fi
sleep 15
done
echo "Environment did not return to Ready within rollback window." >&2
exit 1