Add CDK version policy, update Node 24 and GitHub Actions CI/CD (#6)

* Update CDK version policy, Node 24 runtime, and GitHub Actions CI/CD

- Pin blessed aws-cdk-lib version (2.253.1) with upgrade procedure
- Update Lambda runtime default from Node 22 to Node 24
- Rewrite CI/CD page to reflect GitHub Actions reusable workflows
  (was still referencing CodePipeline/CodeBuild)

* Add pre-push hook for npm ci validation

Catches lock file drift locally before it breaks CI. Includes
install instructions in git-workflow.md.

* Add repo provisioning script

Automates the new-repo checklist: GitHub repo creation, OIDC deploy
role, repo secret, security features, CI/CD workflow stubs, and
pre-push hook installation. Supports both SAM and CDK stack types.

* Add shared VpnEc2Instance CDK construct

Reference construct for the VPN-accessible EC2 pattern used by
file-share and forgejo. Includes VPC/subnet lookup, SG, IAM role,
encrypted EBS, and DLM snapshots. Copy into lib/constructs/.

* Add post-deploy health check template

Template script for project-specific health checks. Copy to
scripts/health-check.sh — CD workflows run it automatically.
This commit is contained in:
Adam Moussa 2026-05-14 18:39:13 -04:00 • committed by GitHub
parent 3bc054c80e
commit 4b5d39fb91
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 629 additions and 36 deletions

View file

@ -14,6 +14,9 @@ Engineering conventions and best practices for Sea Haven Industries.
- [SAM Project Layout](sam-project-layout.md) -- standard directory structure for serverless projects - [SAM Project Layout](sam-project-layout.md) -- standard directory structure for serverless projects
- [Secrets and Configuration](secrets-and-config.md) -- Secrets Manager vs SSM Parameter Store - [Secrets and Configuration](secrets-and-config.md) -- Secrets Manager vs SSM Parameter Store
- [CI/CD Pipelines](cicd.md) -- every deployable repo gets a pipeline, no manual deploys - [CI/CD Pipelines](cicd.md) -- every deployable repo gets a pipeline, no manual deploys
- [Git Hooks](hooks/) -- recommended pre-push and pre-commit hooks
- [Scripts](scripts/) -- repo provisioning, automation tooling
- [CDK Constructs](constructs/) -- shared VPN EC2 instance construct and other reusable patterns
## Contributing ## Contributing

View file

@ -13,13 +13,25 @@ These apply to every Lambda in every project. Verify, don't assume.
| Setting | Value | | Setting | Value |
|---|---| |---|---|
| Runtime | Python 3.12 or Node 22.x | | Runtime | Python 3.12 or Node 24.x |
| Architecture | arm64 | | Architecture | arm64 |
| Log retention | 60 days (explicit in IaC template) | | Log retention | 60 days (explicit in IaC template) |
| Naming | kebab-case, matching the stack name prefix | | Naming | kebab-case, matching the stack name prefix |
Never rely on the CloudWatch default for log retention. Always set `RetentionInDays` explicitly in the template. Never rely on the CloudWatch default for log retention. Always set `RetentionInDays` explicitly in the template.
## CDK Version Policy
Pin `aws-cdk-lib` to a known-good version. The current blessed version is **2.253.1**.
Why: aws-cdk-lib bundles transitive dependencies (`inBundle: true`). Certain versions (e.g., 2.254.0) break `npm ci` with phantom missing-package errors. npm `overrides` cannot fix bundled deps. Always test `npm ci` locally before pushing a version bump.
When upgrading, verify on a branch first:
1. Update `package.json` to the new version
2. Run `rm -rf node_modules package-lock.json && npm install`
3. Run `npm ci` — if it fails, the version is not safe
4. Run `npx cdk synth` — if it fails, the version is not safe
## CloudFormation Outputs ## CloudFormation Outputs
Every stack should export: Every stack should export:

110
cicd.md
View file

@ -4,51 +4,91 @@
Every deployable repo must have a CI/CD pipeline. No manual deploys to production. If it deploys to AWS, it needs a pipeline. Every deployable repo must have a CI/CD pipeline. No manual deploys to production. If it deploys to AWS, it needs a pipeline.
## Pipeline Types ## Platform
### SAM / CDK Stacks GitHub Actions is the standard CI/CD platform. All pipelines use reusable workflows from the `Sea-Haven-Industries/.github` org repo (`.github/workflows/`).
Use CodePipeline + CodeBuild, triggered on push to `main`. ## Workflow Structure
| Stage | Action | Every repo gets two thin workflow files in `.github/workflows/`:
|---|---|
| Source | GitHub connection (push to `main`) |
| Build | CodeBuild: `sam build && sam package` or `cdk deploy` |
| Deploy | CloudFormation changeset execute |
- Build environment: ARM (`aarch64`) to match Lambda architecture | File | Trigger | Purpose |
- Runtime: Match the project's Lambda runtime (Python 3.12, Node 22.x) |---|---|---|
- Pipeline artifacts bucket: `{stack-name}-pipeline-artifacts` | `ci.yaml` | `pull_request` on `main` | Lint, typecheck, test, synth/validate |
| `deploy.yaml` | `push` on `main` | Deploy to AWS |
### Frontend / Static Sites ### CDK Stacks (TypeScript)
Use CodePipeline or GitHub Actions for build + deploy + cache invalidation. ```yaml
# .github/workflows/ci.yaml
name: CI
on:
pull_request:
branches: [main]
jobs:
ci:
uses: Sea-Haven-Industries/.github/.github/workflows/ci-typescript-cdk.yaml@main
with:
node-version: "24"
| Stage | Action | # .github/workflows/deploy.yaml
|---|---| name: Deploy
| Source | GitHub connection (push to `main`) | on:
| Build | Install dependencies, build static assets | push:
| Deploy | S3 sync + CloudFront invalidation | branches: [main]
jobs:
deploy:
uses: Sea-Haven-Industries/.github/.github/workflows/cd-cdk.yaml@main
with:
node-version: "24"
secrets:
deploy-role-arn: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
```
### SAM Stacks (Python)
```yaml
# .github/workflows/ci.yaml
name: CI
on:
pull_request:
branches: [main]
jobs:
ci:
uses: Sea-Haven-Industries/.github/.github/workflows/ci-python-sam.yaml@main
# .github/workflows/deploy.yaml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
uses: Sea-Haven-Industries/.github/.github/workflows/cd-sam.yaml@main
with:
stack-name: "your-stack-name"
secrets:
cfn-role-arn: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
```
## Authentication
Deploy workflows authenticate to AWS via OIDC (no long-lived credentials). Each repo needs:
1. An IAM role named `githubdeploy-<repo-name>` with:
- OIDC trust policy for `token.actions.githubusercontent.com`
- Subject condition: `repo:Sea-Haven-Industries/<repo>:ref:refs/heads/main`
- Inline policy allowing `sts:AssumeRole` on CDK/SAM bootstrap roles
2. A repo secret `AWS_DEPLOY_ROLE_ARN` containing the role ARN
## Node.js Version
Always pass `node-version: "24"` to reusable workflows. Local dev uses Node 24 / npm 11 which generates lockfileVersion 3. The workflow defaults match this, but be explicit to avoid drift.
## Naming ## Naming
- Pipeline: `{stack-name}-pipeline` - All workflow files: kebab-case
- CodeBuild project: `{stack-name}-build` - Reusable workflow references: `@main` branch
- Artifacts bucket: `{stack-name}-pipeline-artifacts`
All kebab-case, matching the stack and repo name.
## What the Pipeline Should Do
At minimum:
1. **Build** — install dependencies, compile/transpile, package
2. **Deploy** — push to the target environment via CloudFormation or S3
Optionally:
3. **Test** — run unit/integration tests before deploy
4. **Lint** — check code style and formatting
## When to Add a Pipeline ## When to Add a Pipeline

41
constructs/README.md Normal file
View file

@ -0,0 +1,41 @@
# Shared CDK Constructs
Reference CDK constructs for common Sea Haven infrastructure patterns. Copy into your project's `lib/constructs/` directory.
## VpnEc2Instance
Encapsulates the full EC2-on-VPN pattern: VPC/subnet lookup, security group with VPN + VPC ingress, IAM role (SSM + Secrets Manager), encrypted EBS, and DLM daily snapshots.
### Usage
```typescript
import { VpnEc2Instance } from "./constructs/vpn-ec2-instance";
const server = new VpnEc2Instance(this, "Server", {
name: "file-share",
ingressPorts: [
{ port: 445, description: "SMB" },
{ port: 8080, description: "FileBrowser" },
],
secretsPrefix: "file-share",
dataVolumeSize: 500,
userData: myUserData,
});
// Access underlying resources for further configuration:
// server.instance, server.securityGroup, server.role
```
### Props
| Prop | Type | Default | Description |
|---|---|---|---|
| `name` | string | required | Resource name prefix (kebab-case) |
| `ingressPorts` | `IngressPort[]` | required | Ports to open from VPN and VPC CIDRs |
| `secretsPrefix` | string | required | Secrets Manager path prefix for IAM policy |
| `instanceType` | `InstanceType` | t4g.small | EC2 instance type |
| `rootVolumeSize` | number | 20 | Root EBS volume in GiB |
| `dataVolumeSize` | number | — | Optional second EBS volume in GiB (mounted at /dev/xvdf) |
| `userData` | `UserData` | — | EC2 user data script |
| `additionalPolicies` | `PolicyStatement[]` | — | Extra IAM policies for the instance role |
| `snapshotRetentionDays` | number | 30 | DLM snapshot retention count |

View file

@ -0,0 +1,181 @@
/**
* Shared CDK construct: VPN-accessible EC2 instance on the Sea Haven private subnet.
*
* Encapsulates the repeating pattern from file-share and forgejo stacks:
* - Looks up the Sea Haven VPC and private subnet
* - Creates a security group with VPN (10.10.0.0/16) and VPC (10.20.0.0/16) ingress
* - Creates an IAM role with SSM and Secrets Manager access
* - Launches a t4g ARM64 AL2023 instance with encrypted EBS
* - Sets up DLM daily snapshots with 30-day retention
* - Exports InstanceId and PrivateIp as CloudFormation outputs
*
* Copy this file into your project's lib/constructs/ directory and import it.
*/
import * as cdk from "aws-cdk-lib";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as iam from "aws-cdk-lib/aws-iam";
import * as dlm from "aws-cdk-lib/aws-dlm";
import { Construct } from "constructs";
export interface IngressPort {
readonly port: number;
readonly description: string;
}
export interface VpnEc2InstanceProps {
readonly name: string;
readonly instanceType?: ec2.InstanceType;
readonly rootVolumeSize?: number;
readonly dataVolumeSize?: number;
readonly ingressPorts: IngressPort[];
readonly secretsPrefix: string;
readonly userData?: ec2.UserData;
readonly additionalPolicies?: iam.PolicyStatement[];
readonly snapshotRetentionDays?: number;
}
const VPC_ID = "vpc-0d3d4b67bd0cf8a68";
const PRIVATE_SUBNET_ID = "subnet-04e38c507e96f1926";
const PRIVATE_SUBNET_AZ = "us-east-1a";
const ACCOUNT_ID = "328440206208";
const REGION = "us-east-1";
const VPN_CIDR = "10.10.0.0/16";
const VPC_CIDR = "10.20.0.0/16";
export class VpnEc2Instance extends Construct {
public readonly instance: ec2.Instance;
public readonly securityGroup: ec2.SecurityGroup;
public readonly role: iam.Role;
constructor(scope: Construct, id: string, props: VpnEc2InstanceProps) {
super(scope, id);
const vpc = ec2.Vpc.fromLookup(this, "Vpc", { vpcId: VPC_ID });
const subnet = ec2.Subnet.fromSubnetAttributes(this, "PrivateSubnet", {
subnetId: PRIVATE_SUBNET_ID,
availabilityZone: PRIVATE_SUBNET_AZ,
});
this.securityGroup = new ec2.SecurityGroup(this, "SecurityGroup", {
vpc,
securityGroupName: props.name,
description: `${props.name} — VPN and VPC access`,
allowAllOutbound: true,
});
for (const ingress of props.ingressPorts) {
this.securityGroup.addIngressRule(
ec2.Peer.ipv4(VPN_CIDR),
ec2.Port.tcp(ingress.port),
`${ingress.description} from VPN`
);
this.securityGroup.addIngressRule(
ec2.Peer.ipv4(VPC_CIDR),
ec2.Port.tcp(ingress.port),
`${ingress.description} from VPC`
);
}
this.role = new iam.Role(this, "InstanceRole", {
roleName: `${props.name}-instance`,
assumedBy: new iam.ServicePrincipal("ec2.amazonaws.com"),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonSSMManagedInstanceCore"),
],
});
this.role.addToPolicy(
new iam.PolicyStatement({
actions: ["secretsmanager:GetSecretValue"],
resources: [
`arn:aws:secretsmanager:${REGION}:${ACCOUNT_ID}:secret:${props.secretsPrefix}/*`,
],
})
);
if (props.additionalPolicies) {
for (const policy of props.additionalPolicies) {
this.role.addToPolicy(policy);
}
}
const blockDevices: ec2.BlockDevice[] = [
{
deviceName: "/dev/xvda",
volume: ec2.BlockDeviceVolume.ebs(props.rootVolumeSize ?? 20, {
volumeType: ec2.EbsDeviceVolumeType.GP3,
encrypted: true,
}),
},
];
if (props.dataVolumeSize) {
blockDevices.push({
deviceName: "/dev/xvdf",
volume: ec2.BlockDeviceVolume.ebs(props.dataVolumeSize, {
volumeType: ec2.EbsDeviceVolumeType.GP3,
encrypted: true,
}),
});
}
this.instance = new ec2.Instance(this, "Instance", {
instanceName: props.name,
vpc,
vpcSubnets: { subnets: [subnet] },
instanceType:
props.instanceType ??
ec2.InstanceType.of(ec2.InstanceClass.T4G, ec2.InstanceSize.SMALL),
machineImage: ec2.MachineImage.latestAmazonLinux2023({
cpuType: ec2.AmazonLinuxCpuType.ARM_64,
}),
securityGroup: this.securityGroup,
role: this.role,
userData: props.userData,
blockDevices,
});
const backupTag = `${props.name}-backup`;
cdk.Tags.of(this.instance).add(backupTag, "true");
const dlmRole = new iam.Role(this, "DlmRole", {
roleName: `${props.name}-dlm`,
assumedBy: new iam.ServicePrincipal("dlm.amazonaws.com"),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName(
"service-role/AWSDataLifecycleManagerServiceRole"
),
],
});
new dlm.CfnLifecyclePolicy(this, "SnapshotPolicy", {
description: `Nightly EBS snapshots for ${props.name}`,
state: "ENABLED",
executionRoleArn: dlmRole.roleArn,
policyDetails: {
resourceTypes: ["INSTANCE"],
targetTags: [{ key: backupTag, value: "true" }],
schedules: [
{
name: `${props.name}-nightly`,
createRule: { interval: 24, intervalUnit: "HOURS", times: ["06:00"] },
retainRule: { count: props.snapshotRetentionDays ?? 30 },
copyTags: true,
tagsToAdd: [{ key: backupTag, value: "true" }],
},
],
},
});
new cdk.CfnOutput(this, "InstanceId", {
value: this.instance.instanceId,
});
new cdk.CfnOutput(this, "PrivateIp", {
value: this.instance.instancePrivateIp,
description: `Private IP for ${props.name}`,
});
}
}

View file

@ -73,6 +73,21 @@ git push origin v1.2.0
Start at `v0.1.0` for new projects. Move to `v1.0.0` when the interface is stable and has external consumers. Start at `v0.1.0` for new projects. Move to `v1.0.0` when the interface is stable and has external consumers.
## Git Hooks
Recommended hooks live in [`hooks/`](hooks/) — copy them into `.git/hooks/` when setting up a project.
| Hook | Purpose |
|---|---|
| `pre-push` | Runs `npm ci` to catch lock file drift before it breaks CI |
Install for a Node.js project:
```bash
cp ~/Documents/repositories/engineering-handbook/hooks/pre-push .git/hooks/pre-push
chmod +x .git/hooks/pre-push
```
## Cleaning Up ## Cleaning Up
After a PR is merged: After a PR is merged:

19
hooks/pre-push Executable file
View file

@ -0,0 +1,19 @@
#!/usr/bin/env bash
# Pre-push hook: verify npm ci passes before pushing Node.js projects.
# Catches lock file drift that would break CI.
#
# Install: cp hooks/pre-push .git/hooks/pre-push && chmod +x .git/hooks/pre-push
set -euo pipefail
if [[ ! -f package-lock.json ]]; then
exit 0
fi
echo "pre-push: running npm ci..."
if ! npm ci --ignore-scripts --no-audit --no-fund 2>/dev/null; then
echo "pre-push: npm ci failed — lock file may be out of sync."
echo "Run: rm -rf node_modules package-lock.json && npm install"
exit 1
fi
echo "pre-push: npm ci passed."

View file

@ -0,0 +1,50 @@
#!/usr/bin/env bash
# Post-deploy health check template.
# Copy to your project as scripts/health-check.sh and customize.
# Called automatically by CD workflows after deploy.
# Args: $1 = stack name, $2 = AWS region
set -euo pipefail
STACK_NAME="${1:?Stack name required}"
REGION="${2:-us-east-1}"
echo "Running health checks for ${STACK_NAME}..."
# Example: verify a Lambda function is invocable
# FUNCTION_NAME="${STACK_NAME}-process-order"
# aws lambda invoke \
# --function-name "$FUNCTION_NAME" \
# --payload '{"healthCheck": true}' \
# --region "$REGION" \
# /tmp/health-check-response.json
# cat /tmp/health-check-response.json
# Example: verify an EC2 instance is running
# INSTANCE_ID=$(aws cloudformation describe-stacks \
# --stack-name "$STACK_NAME" \
# --query 'Stacks[0].Outputs[?OutputKey==`InstanceId`].OutputValue' \
# --output text --region "$REGION")
# STATE=$(aws ec2 describe-instances \
# --instance-ids "$INSTANCE_ID" \
# --query 'Reservations[0].Instances[0].State.Name' \
# --output text --region "$REGION")
# if [[ "$STATE" != "running" ]]; then
# echo "ERROR: Instance $INSTANCE_ID is $STATE, expected running"
# exit 1
# fi
# echo "Instance $INSTANCE_ID is running."
# Example: verify an API Gateway endpoint responds
# API_URL=$(aws cloudformation describe-stacks \
# --stack-name "$STACK_NAME" \
# --query 'Stacks[0].Outputs[?OutputKey==`ApiUrl`].OutputValue' \
# --output text --region "$REGION")
# HTTP_CODE=$(curl -sf -o /dev/null -w '%{http_code}' "$API_URL/health")
# if [[ "$HTTP_CODE" != "200" ]]; then
# echo "ERROR: API health endpoint returned $HTTP_CODE"
# exit 1
# fi
# echo "API endpoint healthy."
echo "All health checks passed."

232
scripts/provision-repo.sh Executable file
View file

@ -0,0 +1,232 @@
#!/usr/bin/env bash
# Provision a new Sea Haven Industries repo with all required infrastructure.
# Usage: ./provision-repo.sh <repo-name> [sam|cdk]
#
# Creates: GitHub repo, OIDC deploy role, repo secret, security features,
# CI/CD workflow stubs, and pre-push hook.
set -euo pipefail
REPO_NAME="${1:?Usage: provision-repo.sh <repo-name> [sam|cdk]}"
STACK_TYPE="${2:-sam}"
ORG="Sea-Haven-Industries"
ACCOUNT_ID="328440206208"
REGION="us-east-1"
OIDC_PROVIDER="arn:aws:iam::${ACCOUNT_ID}:oidc-provider/token.actions.githubusercontent.com"
ROLE_NAME="githubdeploy-${REPO_NAME}"
if [[ ! "$REPO_NAME" =~ ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$ ]]; then
echo "Error: repo name must be kebab-case (lowercase, hyphens only, no leading/trailing hyphens)"
exit 1
fi
echo "=== Provisioning ${ORG}/${REPO_NAME} (${STACK_TYPE}) ==="
# 1. Create GitHub repo
echo ""
echo "[1/6] Creating GitHub repo..."
if gh repo view "${ORG}/${REPO_NAME}" &>/dev/null; then
echo " Repo already exists — skipping."
else
gh repo create "${ORG}/${REPO_NAME}" \
--private \
--description "${REPO_NAME} — Sea Haven Industries" \
--clone=false
echo " Created ${ORG}/${REPO_NAME}"
fi
# 2. Create OIDC deploy role
echo ""
echo "[2/6] Creating IAM deploy role: ${ROLE_NAME}..."
TRUST_POLICY=$(cat <<POLICY
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Federated": "${OIDC_PROVIDER}"},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
},
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:${ORG}/${REPO_NAME}:ref:refs/heads/main"
}
}
}]
}
POLICY
)
if aws iam get-role --role-name "${ROLE_NAME}" &>/dev/null; then
echo " Role already exists — skipping."
else
aws iam create-role \
--role-name "${ROLE_NAME}" \
--assume-role-policy-document "${TRUST_POLICY}" \
--tags "Key=Project,Value=${REPO_NAME}" "Key=ManagedBy,Value=provision-script" \
--query 'Role.Arn' --output text
if [[ "$STACK_TYPE" == "cdk" ]]; then
DEPLOY_POLICY=$(cat <<DPOLICY
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": "arn:aws:iam::${ACCOUNT_ID}:role/cdk-hnb659fds-*"
}]
}
DPOLICY
)
else
DEPLOY_POLICY=$(cat <<DPOLICY
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": "arn:aws:iam::${ACCOUNT_ID}:role/cdk-hnb659fds-*"
},
{
"Effect": "Allow",
"Action": "cloudformation:*",
"Resource": "arn:aws:cloudformation:${REGION}:${ACCOUNT_ID}:stack/${REPO_NAME}/*"
},
{
"Effect": "Allow",
"Action": "iam:PassRole",
"Resource": "arn:aws:iam::${ACCOUNT_ID}:role/${REPO_NAME}-*"
},
{
"Effect": "Allow",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::aws-sam-cli-managed-default-samclisourcebucket-*",
"arn:aws:s3:::aws-sam-cli-managed-default-samclisourcebucket-*/*"
]
}
]
}
DPOLICY
)
fi
aws iam put-role-policy \
--role-name "${ROLE_NAME}" \
--policy-name "deploy" \
--policy-document "${DEPLOY_POLICY}"
echo " Created role with deploy policy."
fi
ROLE_ARN="arn:aws:iam::${ACCOUNT_ID}:role/${ROLE_NAME}"
# 3. Set repo secret
echo ""
echo "[3/6] Setting AWS_DEPLOY_ROLE_ARN secret..."
gh secret set AWS_DEPLOY_ROLE_ARN \
--repo "${ORG}/${REPO_NAME}" \
--body "${ROLE_ARN}"
echo " Secret set."
# 4. Enable security features
echo ""
echo "[4/6] Enabling security features..."
gh api "repos/${ORG}/${REPO_NAME}/vulnerability-alerts" -X PUT 2>/dev/null || true
gh api "repos/${ORG}/${REPO_NAME}" -X PATCH \
-f security_and_analysis.dependabot_security_updates.status=enabled \
-f security_and_analysis.secret_scanning.status=enabled \
--silent 2>/dev/null || true
echo " Dependabot alerts, security updates, and secret scanning enabled."
# 5. Create CI/CD workflow stubs
echo ""
echo "[5/6] Creating CI/CD workflow files..."
REPO_DIR="${HOME}/Documents/repositories/${REPO_NAME}"
if [[ ! -d "${REPO_DIR}" ]]; then
echo " Repo not cloned locally — skipping workflow file creation."
echo " Clone it and re-run, or create .github/workflows/ manually."
else
mkdir -p "${REPO_DIR}/.github/workflows"
if [[ "$STACK_TYPE" == "cdk" ]]; then
cat > "${REPO_DIR}/.github/workflows/ci.yaml" <<'CIEOF'
name: CI
on:
pull_request:
branches: [main]
jobs:
ci:
uses: Sea-Haven-Industries/.github/.github/workflows/ci-typescript-cdk.yaml@main
with:
node-version: "24"
CIEOF
cat > "${REPO_DIR}/.github/workflows/deploy.yaml" <<'CDEOF'
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
uses: Sea-Haven-Industries/.github/.github/workflows/cd-cdk.yaml@main
with:
node-version: "24"
secrets:
deploy-role-arn: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
CDEOF
else
cat > "${REPO_DIR}/.github/workflows/ci.yaml" <<'CIEOF'
name: CI
on:
pull_request:
branches: [main]
jobs:
ci:
uses: Sea-Haven-Industries/.github/.github/workflows/ci-python-sam.yaml@main
CIEOF
cat > "${REPO_DIR}/.github/workflows/deploy.yaml" <<'CDEOF'
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
uses: Sea-Haven-Industries/.github/.github/workflows/cd-sam.yaml@main
with:
stack-name: "REPO_PLACEHOLDER"
secrets:
cfn-role-arn: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
CDEOF
sed -i '' "s/REPO_PLACEHOLDER/${REPO_NAME}/" "${REPO_DIR}/.github/workflows/deploy.yaml"
fi
echo " Created ci.yaml and deploy.yaml"
fi
# 6. Install pre-push hook
echo ""
echo "[6/6] Installing pre-push hook..."
if [[ -d "${REPO_DIR}/.git" ]]; then
HOOK_SRC="${HOME}/Documents/repositories/engineering-handbook/hooks/pre-push"
if [[ -f "$HOOK_SRC" ]]; then
cp "$HOOK_SRC" "${REPO_DIR}/.git/hooks/pre-push"
chmod +x "${REPO_DIR}/.git/hooks/pre-push"
echo " Installed pre-push hook."
else
echo " Hook source not found — skipping."
fi
else
echo " No local .git — skipping."
fi
echo ""
echo "=== Provisioning complete ==="
echo ""
echo "Remaining manual steps:"
echo " 1. Create any Secrets Manager secrets needed (${REPO_NAME}/secret-name)"
echo " 2. Commit and push the workflow files"
echo " 3. Verify CI passes on first PR"
echo " 4. Create a project memory entry"