mirror of
https://github.com/Sea-Haven-Industries/.github.git
synced 2026-09-30 05:53:12 +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-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-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/`)
|
||||
|
||||
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
|
||||
|
||||
|
|
@ -274,6 +276,28 @@ jobs:
|
|||
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:
|
||||
|
||||
| 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