mirror of
https://github.com/Sea-Haven-Industries/.github.git
synced 2026-09-30 07:03:11 +00:00
Merge pull request #94 from Sea-Haven-Industries/cd/add-dotnet-eb
Some checks are pending
ci / ci / ci (push) Waiting to run
Some checks are pending
ci / ci / ci (push) Waiting to run
ci(cd-dotnet-eb): add reusable CD workflow for .NET on Elastic Beanstalk
This commit is contained in:
commit
b7d7be2727
4 changed files with 329 additions and 1 deletions
274
.github/workflows/cd-dotnet-eb.yaml
vendored
Normal file
274
.github/workflows/cd-dotnet-eb.yaml
vendored
Normal file
|
|
@ -0,0 +1,274 @@
|
||||||
|
name: CD — .NET Elastic Beanstalk
|
||||||
|
|
||||||
|
# Caller example:
|
||||||
|
#
|
||||||
|
# jobs:
|
||||||
|
# deploy:
|
||||||
|
# uses: Sea-Haven-Industries/.github/.github/workflows/cd-dotnet-eb.yaml@<full-commit-sha> # main
|
||||||
|
# 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
|
||||||
|
|
||||||
|
- 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
|
||||||
|
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}"
|
||||||
26
README.md
26
README.md
|
|
@ -24,6 +24,8 @@ Organization-level GitHub configuration for Sea Haven Industries.
|
||||||
|
|
||||||
**`.github/workflows/cd-mobile-ios.yaml`** — Reusable CD for iOS apps via Fastlane to TestFlight (Node + Ruby setup inputs).
|
**`.github/workflows/cd-mobile-ios.yaml`** — Reusable CD for iOS apps via Fastlane to TestFlight (Node + Ruby setup inputs).
|
||||||
|
|
||||||
|
**`.github/workflows/cd-dotnet-eb.yaml`** — Reusable CD for .NET apps on AWS Elastic Beanstalk. Publishes the project, packages a bundle, uploads it, creates an application version, and updates an **existing** environment with OIDC credentials — it never creates an environment. Serialised per environment via a `concurrency` group, and the post-deploy check fails the job if EB rolls the deploy back. The caller owns branch-to-environment mapping.
|
||||||
|
|
||||||
**`.github/workflows/callable-labeler.yaml`** — Org-wide PR auto-labeler. Label rules live inline here (single source of truth) — consumer repos need only a thin caller with `contents: read`, `pull-requests: write`, and `issues: write`; no per-repo labeler.yml.
|
**`.github/workflows/callable-labeler.yaml`** — Org-wide PR auto-labeler. Label rules live inline here (single source of truth) — consumer repos need only a thin caller with `contents: read`, `pull-requests: write`, and `issues: write`; no per-repo labeler.yml.
|
||||||
|
|
||||||
**`.github/workflows/callable-dependency-review.yaml`** — Dependency review on PRs, failing on high severity. Requires Dependency Graph.
|
**`.github/workflows/callable-dependency-review.yaml`** — Dependency review on PRs, failing on high severity. Requires Dependency Graph.
|
||||||
|
|
@ -34,7 +36,7 @@ Organization-level GitHub configuration for Sea Haven Industries.
|
||||||
|
|
||||||
### Workflow templates (`workflow-templates/`)
|
### Workflow templates (`workflow-templates/`)
|
||||||
|
|
||||||
Starter workflows offered on the org's **Actions → New workflow** page: `ci-python`, `ci-node`, `cdk-deploy`, `sam-deploy`, `dependency-review`, `labeler`, `triage`. Each is a thin caller of the corresponding reusable workflow above (`triage` is standalone). Every template has a paired `properties.json` (name, description, icon, `filePatterns` for auto-suggestion). Replace any `REPLACE-ME` placeholders before enabling.
|
Starter workflows offered on the org's **Actions → New workflow** page: `ci-python`, `ci-node`, `cdk-deploy`, `sam-deploy`, `dotnet-eb-deploy`, `dependency-review`, `labeler`, `triage`. Each is a thin caller of the corresponding reusable workflow above (`triage` is standalone). Every template has a paired `properties.json` (name, description, icon, `filePatterns` for auto-suggestion). Replace any `REPLACE-ME` placeholders before enabling.
|
||||||
|
|
||||||
### Action pinning policy
|
### Action pinning policy
|
||||||
|
|
||||||
|
|
@ -274,6 +276,28 @@ jobs:
|
||||||
deploy-role-arn: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
|
deploy-role-arn: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**.NET app on Elastic Beanstalk** (e.g., shoc-backend):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: Deploy
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [dev]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
uses: Sea-Haven-Industries/.github/.github/workflows/cd-dotnet-eb.yaml@main
|
||||||
|
with:
|
||||||
|
project: Api.SeaHavenIndustries/Api.SeaHavenIndustries.csproj
|
||||||
|
eb-application: shoc-backend
|
||||||
|
eb-environment: shoc-backend-dev
|
||||||
|
procfile-command: dotnet Api.SeaHavenIndustries.dll
|
||||||
|
secrets:
|
||||||
|
deploy-role-arn: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
|
||||||
|
```
|
||||||
|
|
||||||
|
> The environment must already exist — this workflow deploys a new application version to it and never creates one. Branch-to-environment mapping belongs in the caller: add one job per branch (e.g. `dev` → `…-dev`, `main` → `…-staging`) rather than parameterising the reusable by branch. `procfile-command` generates the Procfile the Amazon Linux .NET platform needs; omit it only if the repo commits its own Procfile into the publish output. Deploys are serialised per environment, and the job fails if Elastic Beanstalk rolls the version back.
|
||||||
|
|
||||||
Enable optional steps as repos adopt them:
|
Enable optional steps as repos adopt them:
|
||||||
|
|
||||||
| Input | Default | Turn on when... |
|
| Input | Default | Turn on when... |
|
||||||
|
|
|
||||||
7
workflow-templates/dotnet-eb-deploy.properties.json
Normal file
7
workflow-templates/dotnet-eb-deploy.properties.json
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"name": "Sea Haven — Deploy (.NET Elastic Beanstalk)",
|
||||||
|
"description": "Publishes a .NET project and deploys it to an existing AWS Elastic Beanstalk environment via OIDC, using the org reusable cd-dotnet-eb workflow. Set project, eb-application, eb-environment and procfile-command before enabling.",
|
||||||
|
"iconName": "octicon-rocket",
|
||||||
|
"categories": ["Deployment", "C#"],
|
||||||
|
"filePatterns": ["\\.csproj$", "\\.sln$"]
|
||||||
|
}
|
||||||
23
workflow-templates/dotnet-eb-deploy.yml
Normal file
23
workflow-templates/dotnet-eb-deploy.yml
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
name: Deploy (.NET Elastic Beanstalk)
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
uses: Sea-Haven-Industries/.github/.github/workflows/cd-dotnet-eb.yaml@555d07c3a240689a81668026787eba089df4c975 # main
|
||||||
|
with:
|
||||||
|
# Required: the project to publish, relative to the repo root.
|
||||||
|
project: REPLACE-ME-project-csproj
|
||||||
|
# Required: the Elastic Beanstalk application name.
|
||||||
|
eb-application: REPLACE-ME-eb-application
|
||||||
|
# Required: the EXISTING environment to deploy to. This workflow updates
|
||||||
|
# an environment; it does not create one. Map branches to environments by
|
||||||
|
# adding one job per branch rather than parameterising by branch.
|
||||||
|
eb-environment: REPLACE-ME-eb-environment
|
||||||
|
# The Amazon Linux .NET platform needs a Procfile. Set the entry-point
|
||||||
|
# command here to have one generated, or delete this line if the repo
|
||||||
|
# commits its own Procfile into the published output.
|
||||||
|
procfile-command: REPLACE-ME-dotnet-entry-assembly-dll
|
||||||
|
secrets:
|
||||||
|
deploy-role-arn: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
|
||||||
Loading…
Add table
Reference in a new issue