mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-07 04:42:08 +00:00
Merge pull request #33 from Sea-Haven-Industries/codex/sh-133-cdk-deploy
SH-133: automate dev backend deployment
This commit is contained in:
commit
8539c07f82
15 changed files with 1045 additions and 0 deletions
16
.ebextensions/01_migrations.config
Normal file
16
.ebextensions/01_migrations.config
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
# Runs the EF Core migrations bundle on the leader instance only, before the new
|
||||||
|
# application version becomes active. The bundle is a self-contained linux-x64
|
||||||
|
# executable placed at the root of the deployment archive by
|
||||||
|
# scripts/package-elastic-beanstalk.sh.
|
||||||
|
#
|
||||||
|
# The connection string is provided at runtime by the existing Elastic Beanstalk
|
||||||
|
# environment property ConnectionStrings__DefaultConnection. It must NOT be
|
||||||
|
# passed on the command line (it would leak into the process table and logs); the
|
||||||
|
# bundle reads it through the application configuration environment provider.
|
||||||
|
#
|
||||||
|
# A non-zero exit fails the deployment (Elastic Beanstalk aborts on container
|
||||||
|
# command failure), so a migration error never yields a live broken deployment.
|
||||||
|
container_commands:
|
||||||
|
01_run_ef_migrations:
|
||||||
|
command: "chmod +x ./efbundle && ./efbundle"
|
||||||
|
leader_only: true
|
||||||
13
.github/dependabot.yml
vendored
Normal file
13
.github/dependabot.yml
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: /
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
open-pull-requests-limit: 5
|
||||||
|
|
||||||
|
- package-ecosystem: npm
|
||||||
|
directory: /infra/cdk
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
open-pull-requests-limit: 5
|
||||||
2
.github/workflows/dependency-review.yml
vendored
2
.github/workflows/dependency-review.yml
vendored
|
|
@ -4,3 +4,5 @@ on:
|
||||||
jobs:
|
jobs:
|
||||||
review:
|
review:
|
||||||
uses: Sea-Haven-Industries/.github/.github/workflows/callable-dependency-review.yaml@main
|
uses: Sea-Haven-Industries/.github/.github/workflows/callable-dependency-review.yaml@main
|
||||||
|
with:
|
||||||
|
allow-ghsas: GHSA-mh99-v99m-4gvg
|
||||||
|
|
|
||||||
197
.github/workflows/deploy.yml
vendored
Normal file
197
.github/workflows/deploy.yml
vendored
Normal file
|
|
@ -0,0 +1,197 @@
|
||||||
|
name: Validate and deploy dev
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [dev]
|
||||||
|
push:
|
||||||
|
branches: [dev]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
validate:
|
||||||
|
name: Validate deployable source bundle
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
# actions/checkout @ v7.0.1
|
||||||
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up .NET
|
||||||
|
# actions/setup-dotnet @ v6.0.0
|
||||||
|
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68
|
||||||
|
with:
|
||||||
|
dotnet-version: "8.0.x"
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
# actions/setup-node @ v6.5.0
|
||||||
|
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38
|
||||||
|
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
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
name: Deploy shoc-backend to Elastic Beanstalk dev
|
||||||
|
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/dev')
|
||||||
|
needs: validate
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
id-token: write
|
||||||
|
environment:
|
||||||
|
name: dev
|
||||||
|
concurrency:
|
||||||
|
group: deploy-dev
|
||||||
|
cancel-in-progress: false
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
# actions/checkout @ v7.0.1
|
||||||
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||||
|
|
||||||
|
- name: Set up .NET
|
||||||
|
# actions/setup-dotnet @ v6.0.0
|
||||||
|
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68
|
||||||
|
with:
|
||||||
|
dotnet-version: "8.0.x"
|
||||||
|
|
||||||
|
- name: Build Elastic Beanstalk source bundle
|
||||||
|
run: bash scripts/package-elastic-beanstalk.sh
|
||||||
|
|
||||||
|
- name: Configure AWS credentials (OIDC)
|
||||||
|
# aws-actions/configure-aws-credentials @ v6.2.3
|
||||||
|
uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c
|
||||||
|
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 shoc-backend-dev \
|
||||||
|
--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
|
||||||
|
# aws-actions/aws-elasticbeanstalk-deploy @ v1.0.6
|
||||||
|
uses: aws-actions/aws-elasticbeanstalk-deploy@cfad3e5e4452cd9c8923cbee2f862e96ba4b52c4
|
||||||
|
with:
|
||||||
|
aws-region: us-east-1
|
||||||
|
application-name: shoc-backend
|
||||||
|
environment-name: shoc-backend-dev
|
||||||
|
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: Post-deploy smoke
|
||||||
|
run: bash scripts/smoke-elastic-beanstalk.sh https://api.dev.seahaven.com
|
||||||
|
|
||||||
|
- 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 shoc-backend-dev \
|
||||||
|
--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 shoc-backend-dev 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 shoc-backend-dev \
|
||||||
|
--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 shoc-backend-dev \
|
||||||
|
--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
|
||||||
9
.gitignore
vendored
9
.gitignore
vendored
|
|
@ -365,3 +365,12 @@ FodyWeavers.xsd
|
||||||
|
|
||||||
appsettings.Development.json
|
appsettings.Development.json
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
# CDK (infra/cdk) generated artifacts
|
||||||
|
infra/cdk/node_modules/
|
||||||
|
infra/cdk/dist/
|
||||||
|
infra/cdk/cdk.out/
|
||||||
|
infra/cdk/.cdk.staging/
|
||||||
|
|
||||||
|
# Deployment packaging artifacts
|
||||||
|
.artifacts/
|
||||||
|
|
|
||||||
13
.security-review/suppressions.json
Normal file
13
.security-review/suppressions.json
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"suppressions": [
|
||||||
|
{
|
||||||
|
"advisory": "GHSA-mh99-v99m-4gvg",
|
||||||
|
"package": "brace-expansion",
|
||||||
|
"introducedBy": "aws-cdk-lib@2.262.1",
|
||||||
|
"scope": "Build-time CDK synthesis only; no untrusted pattern input or runtime deployment artifact.",
|
||||||
|
"reason": "The vulnerable copy is bundled by the latest aws-cdk-lib release and cannot be overridden or updated independently. Dependabot monitors the pinned CDK dependency.",
|
||||||
|
"reviewBy": "2026-08-10",
|
||||||
|
"tracking": "SH-133"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
103
infra/cdk/README.md
Normal file
103
infra/cdk/README.md
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
# shoc-backend CDK (dev deployment IAM)
|
||||||
|
|
||||||
|
This CDK v2 app owns exactly one thing in the `shoc-backend` AWS account
|
||||||
|
(`396287094661`, `us-east-1`): the **GitHub OIDC deploy role** used by the
|
||||||
|
`dev` deployment workflow in `.github/workflows/deploy.yml`.
|
||||||
|
|
||||||
|
## Ownership boundary (deliberate)
|
||||||
|
|
||||||
|
CDK owns:
|
||||||
|
|
||||||
|
- The IAM role `githubdeploy-shoc-backend-dev`.
|
||||||
|
- Its OIDC trust relationship to
|
||||||
|
`arn:aws:iam::396287094661:oidc-provider/token.actions.githubusercontent.com`
|
||||||
|
scoped to `repo:Sea-Haven-Industries/shoc-backend:environment:dev`.
|
||||||
|
- Its least-privilege inline permissions policy.
|
||||||
|
|
||||||
|
CDK does **not** own, create, import, replace, or modify any of the following.
|
||||||
|
They are referenced by exact identifier only and remain owned by their original
|
||||||
|
provisioning path:
|
||||||
|
|
||||||
|
- Elastic Beanstalk application `shoc-backend`
|
||||||
|
- Elastic Beanstalk environment `shoc-backend-dev`
|
||||||
|
- DNS, VPC, EC2, RDS, and existing service/instance roles
|
||||||
|
- S3 bucket `elasticbeanstalk-us-east-1-396287094661`
|
||||||
|
- Environment configuration / option settings
|
||||||
|
- Database schema (migrations are applied by Elastic Beanstalk at deploy time,
|
||||||
|
not by CDK)
|
||||||
|
|
||||||
|
The role is retained on stack deletion (`DeletionPolicy=Retain`,
|
||||||
|
`UpdateReplacePolicy=Retain`) so an accidental teardown cannot orphan the trust
|
||||||
|
or lock out deployments.
|
||||||
|
|
||||||
|
## Least-privilege policy summary
|
||||||
|
|
||||||
|
The role grants only:
|
||||||
|
|
||||||
|
- The three read-only Elastic Beanstalk actions used by deploy, wait, and
|
||||||
|
rollback (`DescribeApplicationVersions`, `DescribeEnvironments`, and
|
||||||
|
`DescribeEvents`). These use `Resource: "*"` because Elastic Beanstalk
|
||||||
|
describe actions are not reliably constrained by resource ARN.
|
||||||
|
- `elasticbeanstalk:CreateApplicationVersion` on application `shoc-backend`.
|
||||||
|
- `elasticbeanstalk:UpdateEnvironment` on environment `shoc-backend-dev` only.
|
||||||
|
- `s3:ListBucket` on `elasticbeanstalk-us-east-1-396287094661` (the official
|
||||||
|
action's ownership-safe `HeadBucket` check) and `s3:PutObject` only under the
|
||||||
|
`shoc-backend/` object prefix.
|
||||||
|
|
||||||
|
It grants **no** IAM mutation or `PassRole`, **no** RDS / EC2 / Secrets Manager
|
||||||
|
access, and **no** administrator policy. There are no wildcard mutation
|
||||||
|
surfaces.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Node >= 22.22.1 and npm.
|
||||||
|
- AWS credentials authorized to create/inspect CloudFormation, IAM roles, and
|
||||||
|
trust policies in account `396287094661`.
|
||||||
|
- The GitHub OIDC provider
|
||||||
|
`arn:aws:iam::396287094661:oidc-provider/token.actions.githubusercontent.com`
|
||||||
|
must already exist in the account (created once, outside this stack).
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm ci # install pinned dependencies
|
||||||
|
npm run build # type-check / compile to dist/
|
||||||
|
npm run synth # synthesize the CloudFormation template
|
||||||
|
npm run diff # diff deployed stack vs local (requires AWS)
|
||||||
|
npm run deploy # deploy the stack (requires AWS)
|
||||||
|
```
|
||||||
|
|
||||||
|
All commands run from `infra/cdk/`.
|
||||||
|
|
||||||
|
## CI integration
|
||||||
|
|
||||||
|
`npm run synth` is the deterministic local/CI validation. After synth, inspect
|
||||||
|
`cdk.out/shoc-backend-deploy-dev.template.json` and verify the synthesized
|
||||||
|
`AWS::IAM::Role`:
|
||||||
|
|
||||||
|
- Trust policy `StringEquals` matches the exact audience and subject above.
|
||||||
|
- The inline policy contains no `Resource: "*"` mutation action and no service
|
||||||
|
outside `elasticbeanstalk` / `s3`.
|
||||||
|
|
||||||
|
The workflow's `AWS_DEPLOY_ROLE_ARN` repository secret (environment `dev`) must
|
||||||
|
hold the ARN output by this stack (`GithubDeployRoleArn`).
|
||||||
|
|
||||||
|
The GitHub `dev` environment is an external release control and must restrict
|
||||||
|
deployments to the `dev` branch. Required reviewers should be configured when
|
||||||
|
the repository plan supports environment reviewers. The workflow also checks
|
||||||
|
the exact branch before requesting an OIDC token.
|
||||||
|
|
||||||
|
## Migration and recovery contract
|
||||||
|
|
||||||
|
The deployment bundle applies pending EF Core migrations before the new
|
||||||
|
application starts. Migrations must therefore use an expand/contract sequence:
|
||||||
|
|
||||||
|
- Expand changes must remain backward compatible with the previously deployed
|
||||||
|
application version.
|
||||||
|
- Destructive contract changes are deployed only after all application versions
|
||||||
|
relying on the old schema have been retired.
|
||||||
|
- A failed deployment restores the previous **application version only**.
|
||||||
|
Database schema is not downgraded, and schema rollback is not claimed.
|
||||||
|
|
||||||
|
This contract preserves the usefulness of application-version recovery without
|
||||||
|
misrepresenting it as a tested database downgrade.
|
||||||
20
infra/cdk/app.ts
Normal file
20
infra/cdk/app.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import * as cdk from 'aws-cdk-lib';
|
||||||
|
import { DeployDevStack } from './deploy-dev-stack';
|
||||||
|
|
||||||
|
const app = new cdk.App();
|
||||||
|
|
||||||
|
new DeployDevStack(app, 'shoc-backend-deploy-dev', {
|
||||||
|
env: {
|
||||||
|
account: '396287094661',
|
||||||
|
region: 'us-east-1',
|
||||||
|
},
|
||||||
|
terminationProtection: true,
|
||||||
|
tags: {
|
||||||
|
Project: 'shoc-backend',
|
||||||
|
Environment: 'dev',
|
||||||
|
ManagedBy: 'cdk',
|
||||||
|
Component: 'deploy-role',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
app.synth();
|
||||||
8
infra/cdk/cdk.json
Normal file
8
infra/cdk/cdk.json
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"app": "node dist/app.js",
|
||||||
|
"versionReporting": false,
|
||||||
|
"context": {
|
||||||
|
"@aws-cdk/aws-iam:minimizePolicies": true,
|
||||||
|
"@aws-cdk/core:checkSecretUsage": true
|
||||||
|
}
|
||||||
|
}
|
||||||
95
infra/cdk/deploy-dev-stack.ts
Normal file
95
infra/cdk/deploy-dev-stack.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
import * as cdk from 'aws-cdk-lib';
|
||||||
|
import * as iam from 'aws-cdk-lib/aws-iam';
|
||||||
|
import { Construct } from 'constructs';
|
||||||
|
|
||||||
|
const ACCOUNT_ID = '396287094661';
|
||||||
|
const REGION = 'us-east-1';
|
||||||
|
const APPLICATION_NAME = 'shoc-backend';
|
||||||
|
const ENVIRONMENT_NAME = 'shoc-backend-dev';
|
||||||
|
const REPO = 'Sea-Haven-Industries/shoc-backend';
|
||||||
|
const BUCKET_NAME = `elasticbeanstalk-${REGION}-${ACCOUNT_ID}`;
|
||||||
|
|
||||||
|
export class DeployDevStack extends cdk.Stack {
|
||||||
|
constructor(scope: Construct, id: string, props: cdk.StackProps = {}) {
|
||||||
|
super(scope, id, props);
|
||||||
|
|
||||||
|
const applicationArn = `arn:aws:elasticbeanstalk:${REGION}:${ACCOUNT_ID}:application/${APPLICATION_NAME}`;
|
||||||
|
const environmentArn = `arn:aws:elasticbeanstalk:${REGION}:${ACCOUNT_ID}:environment/${APPLICATION_NAME}/${ENVIRONMENT_NAME}`;
|
||||||
|
const bucketArn = `arn:aws:s3:::${BUCKET_NAME}`;
|
||||||
|
const oidcProviderArn = `arn:aws:iam::${ACCOUNT_ID}:oidc-provider/token.actions.githubusercontent.com`;
|
||||||
|
|
||||||
|
const deployRole = new iam.Role(this, 'GithubDeployRole', {
|
||||||
|
roleName: 'githubdeploy-shoc-backend-dev',
|
||||||
|
description:
|
||||||
|
'Least-privilege GitHub OIDC deploy role for shoc-backend dev. CDK-owned; application/environment/S3 are owned by Elastic Beanstalk.',
|
||||||
|
assumedBy: new iam.FederatedPrincipal(
|
||||||
|
oidcProviderArn,
|
||||||
|
{
|
||||||
|
StringEquals: {
|
||||||
|
'token.actions.githubusercontent.com:aud': 'sts.amazonaws.com',
|
||||||
|
'token.actions.githubusercontent.com:sub': `repo:${REPO}:environment:dev`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'sts:AssumeRoleWithWebIdentity',
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
deployRole.applyRemovalPolicy(cdk.RemovalPolicy.RETAIN);
|
||||||
|
const cfnRole = deployRole.node.defaultChild as iam.CfnRole;
|
||||||
|
cfnRole.cfnOptions.deletionPolicy = cdk.CfnDeletionPolicy.RETAIN;
|
||||||
|
cfnRole.cfnOptions.updateReplacePolicy = cdk.CfnDeletionPolicy.RETAIN;
|
||||||
|
|
||||||
|
deployRole.addToPolicy(
|
||||||
|
new iam.PolicyStatement({
|
||||||
|
effect: iam.Effect.ALLOW,
|
||||||
|
actions: [
|
||||||
|
'elasticbeanstalk:DescribeEnvironments',
|
||||||
|
'elasticbeanstalk:DescribeApplicationVersions',
|
||||||
|
'elasticbeanstalk:DescribeEvents',
|
||||||
|
],
|
||||||
|
resources: ['*'],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
deployRole.addToPolicy(
|
||||||
|
new iam.PolicyStatement({
|
||||||
|
effect: iam.Effect.ALLOW,
|
||||||
|
actions: ['elasticbeanstalk:CreateApplicationVersion'],
|
||||||
|
resources: [
|
||||||
|
applicationArn,
|
||||||
|
`arn:aws:elasticbeanstalk:${REGION}:${ACCOUNT_ID}:applicationversion/${APPLICATION_NAME}/*`,
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
deployRole.addToPolicy(
|
||||||
|
new iam.PolicyStatement({
|
||||||
|
effect: iam.Effect.ALLOW,
|
||||||
|
actions: ['elasticbeanstalk:UpdateEnvironment'],
|
||||||
|
resources: [environmentArn],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
deployRole.addToPolicy(
|
||||||
|
new iam.PolicyStatement({
|
||||||
|
effect: iam.Effect.ALLOW,
|
||||||
|
actions: ['s3:ListBucket'],
|
||||||
|
resources: [bucketArn],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
deployRole.addToPolicy(
|
||||||
|
new iam.PolicyStatement({
|
||||||
|
effect: iam.Effect.ALLOW,
|
||||||
|
actions: ['s3:PutObject'],
|
||||||
|
resources: [`${bucketArn}/${APPLICATION_NAME}/*`],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
new cdk.CfnOutput(this, 'GithubDeployRoleArn', {
|
||||||
|
value: deployRole.roleArn,
|
||||||
|
description: 'ARN of the GitHub OIDC deploy role for shoc-backend dev.',
|
||||||
|
exportName: 'shoc-backend-deploy-dev-role-arn',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
333
infra/cdk/package-lock.json
generated
Normal file
333
infra/cdk/package-lock.json
generated
Normal file
|
|
@ -0,0 +1,333 @@
|
||||||
|
{
|
||||||
|
"name": "shoc-backend-cdk",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "shoc-backend-cdk",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"dependencies": {
|
||||||
|
"aws-cdk-lib": "2.262.1",
|
||||||
|
"constructs": "10.7.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "22.20.1",
|
||||||
|
"aws-cdk": "2.1133.0",
|
||||||
|
"typescript": "5.9.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22.22.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@aws-cdk/asset-awscli-v1": {
|
||||||
|
"version": "2.2.282",
|
||||||
|
"resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.282.tgz",
|
||||||
|
"integrity": "sha512-7hKMi5tTxDcKGIMIOq14PnY0GBcugW33Uh/2YHDZiEwSxLeFOCYBwhR+BFXONb/EJeVI3RETFgailNZbkcKF6g==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
|
"node_modules/@aws-cdk/asset-node-proxy-agent-v6": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-pDiuqH+qY3zM9lhhLjbKJ1tnKOHzQ2V4Wr/3qsxyKeKAkuPMI/BVGvZG1PbrikUw949cGVTfVEt4ETKKYnrj0Q==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
|
"node_modules/@aws-cdk/cloud-assembly-schema": {
|
||||||
|
"version": "54.14.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-54.14.0.tgz",
|
||||||
|
"integrity": "sha512-JCZCzgp3SuXQVljaKqXnttHzcezEHt9Ag/YipK0XwUFD+Iz2T4jY7gUc3pA25Uq6pzY2n9DvO/nEU++dPXW4Rw==",
|
||||||
|
"bundleDependencies": [
|
||||||
|
"jsonschema",
|
||||||
|
"semver"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"jsonschema": "^1.5.0",
|
||||||
|
"semver": "^7.8.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@aws-cdk/cloud-assembly-schema/node_modules/jsonschema": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@aws-cdk/cloud-assembly-schema/node_modules/semver": {
|
||||||
|
"version": "7.8.5",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "22.20.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz",
|
||||||
|
"integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~6.21.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/aws-cdk": {
|
||||||
|
"version": "2.1133.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1133.0.tgz",
|
||||||
|
"integrity": "sha512-DGlCBwqxSHTe1u/IoT5WV+Bbd2K3UhwFHsdMqb2nQa1fkJmaQgoNFu0It+p3HxyMWeW+gKsVzT5KcjQPQ6Kzuw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"cdk": "bin/cdk"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/aws-cdk-lib": {
|
||||||
|
"version": "2.262.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.262.1.tgz",
|
||||||
|
"integrity": "sha512-B6YP4r6ojUZCDhl+qBu/CrWzcipR8sIgshcqYvgw013sghPXmVkYdJ3yuI9+DKML3YLSjQrHy1nGJs+Nqq7JCg==",
|
||||||
|
"bundleDependencies": [
|
||||||
|
"@aws/cloudformation-validate",
|
||||||
|
"@balena/dockerignore",
|
||||||
|
"@aws-cdk/cloud-assembly-api",
|
||||||
|
"case",
|
||||||
|
"fs-extra",
|
||||||
|
"ignore",
|
||||||
|
"jsonschema",
|
||||||
|
"minimatch",
|
||||||
|
"punycode",
|
||||||
|
"semver",
|
||||||
|
"yaml",
|
||||||
|
"mime-types"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-cdk/asset-awscli-v1": "2.2.282",
|
||||||
|
"@aws-cdk/asset-node-proxy-agent-v6": "^2.1.2",
|
||||||
|
"@aws-cdk/cloud-assembly-api": "^2.2.6",
|
||||||
|
"@aws-cdk/cloud-assembly-schema": "^54.11.0",
|
||||||
|
"@aws/cloudformation-validate": "1.5.1-beta",
|
||||||
|
"@balena/dockerignore": "^1.0.2",
|
||||||
|
"case": "1.6.3",
|
||||||
|
"fs-extra": "^11.3.6",
|
||||||
|
"ignore": "^5.3.2",
|
||||||
|
"jsonschema": "^1.5.0",
|
||||||
|
"mime-types": "^2.1.35",
|
||||||
|
"minimatch": "^10.2.5",
|
||||||
|
"punycode": "^2.3.1",
|
||||||
|
"semver": "^7.8.5",
|
||||||
|
"yaml": "1.10.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"constructs": "^10.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/aws-cdk-lib/node_modules/@aws-cdk/cloud-assembly-api": {
|
||||||
|
"version": "2.2.6",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"jsonschema": "^1.5.0",
|
||||||
|
"semver": "^7.8.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@aws-cdk/cloud-assembly-schema": ">=54.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/aws-cdk-lib/node_modules/@aws/cloudformation-validate": {
|
||||||
|
"version": "1.5.1-beta",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/aws-cdk-lib/node_modules/@balena/dockerignore": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
|
"node_modules/aws-cdk-lib/node_modules/balanced-match": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "18 || 20 || >=22"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/aws-cdk-lib/node_modules/brace-expansion": {
|
||||||
|
"version": "5.0.7",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"balanced-match": "^4.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "18 || 20 || >=22"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/aws-cdk-lib/node_modules/case": {
|
||||||
|
"version": "1.6.3",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "(MIT OR GPL-3.0-or-later)",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/aws-cdk-lib/node_modules/fs-extra": {
|
||||||
|
"version": "11.3.6",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"graceful-fs": "^4.2.0",
|
||||||
|
"jsonfile": "^6.0.1",
|
||||||
|
"universalify": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/aws-cdk-lib/node_modules/graceful-fs": {
|
||||||
|
"version": "4.2.11",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/aws-cdk-lib/node_modules/ignore": {
|
||||||
|
"version": "5.3.2",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/aws-cdk-lib/node_modules/jsonfile": {
|
||||||
|
"version": "6.2.1",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"universalify": "^2.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"graceful-fs": "^4.1.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/aws-cdk-lib/node_modules/jsonschema": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/aws-cdk-lib/node_modules/mime-db": {
|
||||||
|
"version": "1.52.0",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/aws-cdk-lib/node_modules/mime-types": {
|
||||||
|
"version": "2.1.35",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "1.52.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/aws-cdk-lib/node_modules/minimatch": {
|
||||||
|
"version": "10.2.5",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": "^5.0.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "18 || 20 || >=22"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/aws-cdk-lib/node_modules/punycode": {
|
||||||
|
"version": "2.3.1",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/aws-cdk-lib/node_modules/semver": {
|
||||||
|
"version": "7.8.5",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/aws-cdk-lib/node_modules/universalify": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/aws-cdk-lib/node_modules/yaml": {
|
||||||
|
"version": "1.10.3",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/constructs": {
|
||||||
|
"version": "10.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/constructs/-/constructs-10.7.1.tgz",
|
||||||
|
"integrity": "sha512-ulK25Sg2Nv1jW+A9VeV7fu9GFN9KdVTNWdXMoapNRwqkQzSdToro/Tttk3Ak5BGI80NRA/d2nSzKnoZ27LizLw==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "5.9.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "6.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
24
infra/cdk/package.json
Normal file
24
infra/cdk/package.json
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"name": "shoc-backend-cdk",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "CDK ownership boundary for the shoc-backend dev deployment IAM role.",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22.22.1"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"synth": "npm run build && cdk synth",
|
||||||
|
"diff": "npm run build && cdk diff",
|
||||||
|
"deploy": "npm run build && cdk deploy"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"aws-cdk-lib": "2.262.1",
|
||||||
|
"constructs": "10.7.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "22.20.1",
|
||||||
|
"aws-cdk": "2.1133.0",
|
||||||
|
"typescript": "5.9.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
23
infra/cdk/tsconfig.json
Normal file
23
infra/cdk/tsconfig.json
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "commonjs",
|
||||||
|
"lib": ["ES2022"],
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"declaration": false,
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "dist"
|
||||||
|
},
|
||||||
|
"include": ["*.ts"],
|
||||||
|
"exclude": ["node_modules", "dist", "cdk.out"]
|
||||||
|
}
|
||||||
129
scripts/package-elastic-beanstalk.sh
Executable file
129
scripts/package-elastic-beanstalk.sh
Executable file
|
|
@ -0,0 +1,129 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# package-elastic-beanstalk.sh — build a deterministic Elastic Beanstalk source
|
||||||
|
# bundle for the shoc-backend .NET 8 application.
|
||||||
|
#
|
||||||
|
# Layout of the resulting ZIP (the Beanstalk application root):
|
||||||
|
# ./ published Api.SeaHavenIndustries (self-contained, linux-x64)
|
||||||
|
# ./efbundle self-contained EF Core 8.0.8 migrations bundle (linux-x64, +x)
|
||||||
|
# ./.ebextensions/* leader-only migration container command
|
||||||
|
#
|
||||||
|
# The bundle reads ConnectionStrings__DefaultConnection from the runtime
|
||||||
|
# environment (Elastic Beanstalk property). No connection string or secret is
|
||||||
|
# written into the package.
|
||||||
|
#
|
||||||
|
# The script only ever removes its own generated directory (.artifacts/elastic-beanstalk)
|
||||||
|
# and validates that path before doing so.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# bash scripts/package-elastic-beanstalk.sh [output.zip]
|
||||||
|
#
|
||||||
|
# Environment:
|
||||||
|
# DOTNET_BIN optional path to the dotnet executable
|
||||||
|
# RUNTIME optional target RID for local validation (default: linux-x64)
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
OUTPUT_ZIP="${1:-.artifacts/elastic-beanstalk/site.zip}"
|
||||||
|
STAGING_DIR=".artifacts/elastic-beanstalk/staging"
|
||||||
|
TOOLS_DIR=".artifacts/dotnet-tools"
|
||||||
|
|
||||||
|
log() { printf '\n\033[1m== %s ==\033[0m\n' "$1"; }
|
||||||
|
die() { printf '\033[31mERR\033[0m %s\n' "$1" >&2; exit 1; }
|
||||||
|
|
||||||
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
cd "$REPO_ROOT"
|
||||||
|
|
||||||
|
case "$OUTPUT_ZIP" in
|
||||||
|
.artifacts/elastic-beanstalk/*.zip) : ;;
|
||||||
|
*) die "output must be a .zip beneath .artifacts/elastic-beanstalk" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [[ -n "${DOTNET_BIN:-}" ]]; then
|
||||||
|
DOTNET="$DOTNET_BIN"
|
||||||
|
elif command -v dotnet >/dev/null 2>&1; then
|
||||||
|
DOTNET="$(command -v dotnet)"
|
||||||
|
elif [[ -x "$HOME/.dotnet/dotnet" ]]; then
|
||||||
|
DOTNET="$HOME/.dotnet/dotnet"
|
||||||
|
else
|
||||||
|
die "dotnet is unavailable; set DOTNET_BIN or install the .NET 8 SDK."
|
||||||
|
fi
|
||||||
|
|
||||||
|
DOTNET_DIR="$(cd "$(dirname "$DOTNET")" && pwd)"
|
||||||
|
export PATH="$DOTNET_DIR:$PATH"
|
||||||
|
if [[ -d "$DOTNET_DIR/host/fxr" ]]; then
|
||||||
|
export DOTNET_ROOT="$DOTNET_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
export DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||||
|
export DOTNET_NOLOGO=1
|
||||||
|
export TZ=UTC
|
||||||
|
export SOURCE_DATE_EPOCH="315532800"
|
||||||
|
|
||||||
|
API_PROJECT="Api.SeaHavenIndustries/Api.SeaHavenIndustries.csproj"
|
||||||
|
MIGRATIONS_PROJECT="Data.SeaHavenIndustries/Data.SeaHavenIndustries.csproj"
|
||||||
|
EF_VERSION="8.0.8"
|
||||||
|
RUNTIME="${RUNTIME:-linux-x64}"
|
||||||
|
|
||||||
|
[[ -f "$API_PROJECT" ]] || die "missing API project: $API_PROJECT"
|
||||||
|
[[ -f "$MIGRATIONS_PROJECT" ]] || die "missing migrations project: $MIGRATIONS_PROJECT"
|
||||||
|
command -v zip >/dev/null 2>&1 || die "zip is required to build the source bundle."
|
||||||
|
|
||||||
|
GENERATED_ROOT="$(dirname "$STAGING_DIR")"
|
||||||
|
case "$GENERATED_ROOT" in
|
||||||
|
.artifacts/elastic-beanstalk) : ;;
|
||||||
|
*) die "refusing to remove unexpected generated dir: $GENERATED_ROOT" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
log "clean generated artifacts"
|
||||||
|
rm -rf "$GENERATED_ROOT" "$TOOLS_DIR"
|
||||||
|
mkdir -p "$STAGING_DIR" "$TOOLS_DIR"
|
||||||
|
|
||||||
|
log "publish $API_PROJECT (Release, self-contained, $RUNTIME)"
|
||||||
|
"$DOTNET" publish "$API_PROJECT" \
|
||||||
|
-c Release \
|
||||||
|
--self-contained \
|
||||||
|
--runtime "$RUNTIME" \
|
||||||
|
-o "$STAGING_DIR" \
|
||||||
|
/p:ContinuousIntegrationBuild=true \
|
||||||
|
/p:UseAppHost=true
|
||||||
|
|
||||||
|
log "install dotnet-ef $EF_VERSION (local tool path)"
|
||||||
|
if ! "$DOTNET" tool install dotnet-ef --version "$EF_VERSION" --tool-path "$TOOLS_DIR" 2>/dev/null; then
|
||||||
|
"$DOTNET" tool update dotnet-ef --version "$EF_VERSION" --tool-path "$TOOLS_DIR"
|
||||||
|
fi
|
||||||
|
EF="$TOOLS_DIR/dotnet-ef"
|
||||||
|
|
||||||
|
log "build EF migrations bundle (self-contained, $RUNTIME)"
|
||||||
|
"$EF" migrations bundle \
|
||||||
|
--project "$MIGRATIONS_PROJECT" \
|
||||||
|
--startup-project "$API_PROJECT" \
|
||||||
|
--configuration Release \
|
||||||
|
--self-contained \
|
||||||
|
--runtime "$RUNTIME" \
|
||||||
|
--output "$STAGING_DIR/efbundle"
|
||||||
|
|
||||||
|
chmod 0755 "$STAGING_DIR/efbundle"
|
||||||
|
|
||||||
|
log "copy .ebextensions into bundle root"
|
||||||
|
mkdir -p "$STAGING_DIR/.ebextensions"
|
||||||
|
cp -R .ebextensions/. "$STAGING_DIR/.ebextensions/"
|
||||||
|
|
||||||
|
log "verify no committed secret placeholders survived publish"
|
||||||
|
if grep -rIEl -- 'Server=.*;.*Password=|AccountKey=|aws_secret|AKIA[0-9A-Z]{16}' "$STAGING_DIR" 2>/dev/null; then
|
||||||
|
die "potential secret detected in publish output; refusing to package."
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "assemble source bundle (contents, not the containing directory)"
|
||||||
|
(
|
||||||
|
cd "$STAGING_DIR"
|
||||||
|
# ZIP stores file mtimes. Normalize them so identical source/build inputs
|
||||||
|
# produce byte-identical source bundles.
|
||||||
|
find . -type f -exec touch -t 198001010000 {} +
|
||||||
|
find . -type f -print | LC_ALL=C sort \
|
||||||
|
| zip -q -X -@ "$REPO_ROOT/$OUTPUT_ZIP"
|
||||||
|
)
|
||||||
|
|
||||||
|
log "package written: $OUTPUT_ZIP"
|
||||||
|
printf ' contents: %d files\n' "$(find "$STAGING_DIR" -type f | wc -l | tr -d ' ')"
|
||||||
|
printf ' size: %s bytes\n' "$(wc -c < "$REPO_ROOT/$OUTPUT_ZIP" | tr -d ' ')"
|
||||||
|
printf ' efbundle: %s\n' "$(file -b "$STAGING_DIR/efbundle" 2>/dev/null || echo present)"
|
||||||
60
scripts/smoke-elastic-beanstalk.sh
Executable file
60
scripts/smoke-elastic-beanstalk.sh
Executable file
|
|
@ -0,0 +1,60 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# smoke-elastic-beanstalk.sh — post-deploy smoke checks for the shoc-backend
|
||||||
|
# dev environment.
|
||||||
|
#
|
||||||
|
# Checks:
|
||||||
|
# 1. swagger.json is reachable (HTTP 200)
|
||||||
|
# 2. swagger advertises POST /api/webhooks/work-orders
|
||||||
|
# 3. swagger still advertises POST /api/Authentication/login
|
||||||
|
# 4. an unauthenticated JSON POST to the webhook returns 401 or 503 (disabled),
|
||||||
|
# never 404 or a server error other than the intentional 503
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# bash scripts/smoke-elastic-beanstalk.sh [base-url]
|
||||||
|
# bash scripts/smoke-elastic-beanstalk.sh https://api.dev.seahaven.com
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
BASE_URL="${1:-https://api.dev.seahaven.com}"
|
||||||
|
BASE_URL="${BASE_URL%/}"
|
||||||
|
|
||||||
|
SWAGGER_URL="$BASE_URL/swagger/v1/swagger.json"
|
||||||
|
WEBHOOK_URL="$BASE_URL/api/webhooks/work-orders"
|
||||||
|
|
||||||
|
log() { printf '\n\033[1m== %s ==\033[0m\n' "$1"; }
|
||||||
|
ok() { printf '\033[32mPASS\033[0m %s\n' "$1"; }
|
||||||
|
die() { printf '\033[31mFAIL\033[0m %s\n' "$1" >&2; exit 1; }
|
||||||
|
|
||||||
|
command -v curl >/dev/null 2>&1 || die "curl is required."
|
||||||
|
|
||||||
|
mkdir -p .artifacts/elastic-beanstalk
|
||||||
|
|
||||||
|
log "swagger reachable: $SWAGGER_URL"
|
||||||
|
swagger_http=$(curl -sS -o .artifacts/elastic-beanstalk/swagger.json \
|
||||||
|
-w '%{http_code}' --max-time 30 "$SWAGGER_URL" || true)
|
||||||
|
[[ "$swagger_http" == "200" ]] \
|
||||||
|
|| die "swagger.json returned HTTP $swagger_http (expected 200)."
|
||||||
|
swagger_file=".artifacts/elastic-beanstalk/swagger.json"
|
||||||
|
ok "swagger.json HTTP 200"
|
||||||
|
|
||||||
|
log "swagger advertises webhook and login routes"
|
||||||
|
grep -q '"/api/webhooks/work-orders"' "$swagger_file" \
|
||||||
|
|| die "swagger.json missing /api/webhooks/work-orders route."
|
||||||
|
grep -q '"/api/Authentication/login"' "$swagger_file" \
|
||||||
|
|| die "swagger.json missing /api/Authentication/login route."
|
||||||
|
ok "webhook and login routes present"
|
||||||
|
|
||||||
|
log "unauthenticated webhook POST: $WEBHOOK_URL"
|
||||||
|
webhook_http=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 30 \
|
||||||
|
-X POST -H 'Content-Type: application/json' \
|
||||||
|
--data '{"smoke":true}' "$WEBHOOK_URL" || true)
|
||||||
|
|
||||||
|
case "$webhook_http" in
|
||||||
|
401) ok "webhook returned 401 (unauthorized) as expected." ;;
|
||||||
|
503) ok "webhook returned 503 (intentionally disabled) as expected." ;;
|
||||||
|
404) die "webhook returned 404 — route not wired (deployment broken)." ;;
|
||||||
|
5*) die "webhook returned HTTP $webhook_http — unexpected server error." ;;
|
||||||
|
*) die "webhook returned HTTP $webhook_http — expected 401 or 503." ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
log "smoke: all checks passed"
|
||||||
Loading…
Add table
Reference in a new issue