From 4b5d39fb9189a2115340c2fa86ce357fdc56578d Mon Sep 17 00:00:00 2001 From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com> Date: Thu, 14 May 2026 18:39:13 -0400 Subject: [PATCH] Add CDK version policy, update Node 24 and GitHub Actions CI/CD (#6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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. --- README.md | 3 + aws-infrastructure.md | 14 +- cicd.md | 110 ++++++++++----- constructs/README.md | 41 ++++++ constructs/vpn-ec2-instance.ts | 181 ++++++++++++++++++++++++ git-workflow.md | 15 ++ hooks/pre-push | 19 +++ scripts/health-check-template.sh | 50 +++++++ scripts/provision-repo.sh | 232 +++++++++++++++++++++++++++++++ 9 files changed, 629 insertions(+), 36 deletions(-) create mode 100644 constructs/README.md create mode 100644 constructs/vpn-ec2-instance.ts create mode 100755 hooks/pre-push create mode 100755 scripts/health-check-template.sh create mode 100755 scripts/provision-repo.sh diff --git a/README.md b/README.md index 1a48ee4..fa1e284 100644 --- a/README.md +++ b/README.md @@ -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 - [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 +- [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 diff --git a/aws-infrastructure.md b/aws-infrastructure.md index c6f8acf..e63c734 100644 --- a/aws-infrastructure.md +++ b/aws-infrastructure.md @@ -13,13 +13,25 @@ These apply to every Lambda in every project. Verify, don't assume. | Setting | Value | |---|---| -| Runtime | Python 3.12 or Node 22.x | +| Runtime | Python 3.12 or Node 24.x | | Architecture | arm64 | | Log retention | 60 days (explicit in IaC template) | | Naming | kebab-case, matching the stack name prefix | 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 Every stack should export: diff --git a/cicd.md b/cicd.md index b8f5cc8..176a6ba 100644 --- a/cicd.md +++ b/cicd.md @@ -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. -## 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 | -|---|---| -| Source | GitHub connection (push to `main`) | -| Build | CodeBuild: `sam build && sam package` or `cdk deploy` | -| Deploy | CloudFormation changeset execute | +Every repo gets two thin workflow files in `.github/workflows/`: -- Build environment: ARM (`aarch64`) to match Lambda architecture -- Runtime: Match the project's Lambda runtime (Python 3.12, Node 22.x) -- Pipeline artifacts bucket: `{stack-name}-pipeline-artifacts` +| File | Trigger | Purpose | +|---|---|---| +| `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 | -|---|---| -| Source | GitHub connection (push to `main`) | -| Build | Install dependencies, build static assets | -| Deploy | S3 sync + CloudFront invalidation | +# .github/workflows/deploy.yaml +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 }} +``` + +### 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-` with: + - OIDC trust policy for `token.actions.githubusercontent.com` + - Subject condition: `repo:Sea-Haven-Industries/: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 -- Pipeline: `{stack-name}-pipeline` -- CodeBuild project: `{stack-name}-build` -- 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 +- All workflow files: kebab-case +- Reusable workflow references: `@main` branch ## When to Add a Pipeline diff --git a/constructs/README.md b/constructs/README.md new file mode 100644 index 0000000..602c17d --- /dev/null +++ b/constructs/README.md @@ -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 | diff --git a/constructs/vpn-ec2-instance.ts b/constructs/vpn-ec2-instance.ts new file mode 100644 index 0000000..3398c40 --- /dev/null +++ b/constructs/vpn-ec2-instance.ts @@ -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}`, + }); + } +} diff --git a/git-workflow.md b/git-workflow.md index e717cfb..236c62d 100644 --- a/git-workflow.md +++ b/git-workflow.md @@ -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. +## 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 After a PR is merged: diff --git a/hooks/pre-push b/hooks/pre-push new file mode 100755 index 0000000..01021c6 --- /dev/null +++ b/hooks/pre-push @@ -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." diff --git a/scripts/health-check-template.sh b/scripts/health-check-template.sh new file mode 100755 index 0000000..18d80d1 --- /dev/null +++ b/scripts/health-check-template.sh @@ -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." diff --git a/scripts/provision-repo.sh b/scripts/provision-repo.sh new file mode 100755 index 0000000..220c213 --- /dev/null +++ b/scripts/provision-repo.sh @@ -0,0 +1,232 @@ +#!/usr/bin/env bash +# Provision a new Sea Haven Industries repo with all required infrastructure. +# Usage: ./provision-repo.sh [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 [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 </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 </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"