mirror of
https://github.com/Sea-Haven-Industries/engineering-handbook.git
synced 2026-10-03 19:03:18 +00:00
Compare commits
3 commits
3bc054c80e
...
7d5d04985a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d5d04985a | ||
|
|
651dff5dd3 | ||
|
|
4b5d39fb91 |
15 changed files with 933 additions and 37 deletions
|
|
@ -5,6 +5,7 @@ Engineering conventions and best practices for Sea Haven Industries.
|
|||
## Contents
|
||||
|
||||
- [Naming Conventions](naming-conventions.md) -- kebab-case everywhere, no exceptions
|
||||
- [Development Environment](dev-environment.md) -- workstation directory layout, pyenv, Node, launchd/TCC
|
||||
- [Git Workflow](git-workflow.md) -- feature branches, incremental commits, deploy-then-merge
|
||||
- [Commit Messages](commit-messages.md) -- imperative mood, 50/72 rule, explain "why"
|
||||
- [Pull Requests](pull-requests.md) -- scope, title, description format, merge strategy
|
||||
|
|
@ -12,8 +13,13 @@ Engineering conventions and best practices for Sea Haven Industries.
|
|||
- [GitHub Standards](github-standards.md) -- branch defaults, repo hygiene, Dependabot
|
||||
- [AWS Infrastructure](aws-infrastructure.md) -- SAM vs CDK, Lambda defaults, CloudFormation
|
||||
- [SAM Project Layout](sam-project-layout.md) -- standard directory structure for serverless projects
|
||||
- [Lambda Starter Template](lambda-template.md) -- minimal SAM scaffold for a new Python Lambda
|
||||
- [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
|
||||
- [Bedrock](bedrock.md) -- cross-region inference profiles, alias pinning, KB Docker requirement
|
||||
- [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
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
59
bedrock.md
Normal file
59
bedrock.md
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
# AWS Bedrock
|
||||
|
||||
Conventions and gotchas for using AWS Bedrock — Agents, Knowledge Bases, and model invocations.
|
||||
|
||||
## Foundation Model IDs
|
||||
|
||||
For **Claude 4.x models on Bedrock Agents**, always use the cross-region inference profile ID, not the direct model ID.
|
||||
|
||||
| Use | Example |
|
||||
|---|---|
|
||||
| Direct invocation (`bedrock-runtime`) | `anthropic.claude-sonnet-4-5-20250929-v1:0` |
|
||||
| Bedrock Agent `foundationModel` | `us.anthropic.claude-sonnet-4-5-20250929-v1:0` |
|
||||
|
||||
The direct model ID is only valid for `bedrock-runtime` invocations. Using it on `CfnAgent.foundationModel` makes the agent prepare successfully but throws `ResourceNotFoundException` at invocation time.
|
||||
|
||||
## IAM Permissions
|
||||
|
||||
Bedrock Agents need both the underlying foundation model and the cross-region inference profile in their IAM policy:
|
||||
|
||||
```json
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": "bedrock:InvokeModel",
|
||||
"Resource": [
|
||||
"arn:aws:bedrock:*::foundation-model/anthropic.claude-sonnet-4-5-20250929-v1:0",
|
||||
"arn:aws:bedrock:us-east-1:<account>:inference-profile/us.anthropic.claude-sonnet-4-5-20250929-v1:0"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The foundation model ARN uses the wildcard region (`*`) since cross-region inference can route to any of the profile's regions.
|
||||
|
||||
## Alias Version Pinning
|
||||
|
||||
When updating an agent's `foundationModel` or `instruction` via CDK or CloudFormation, the alias does **not** automatically re-point to the new version.
|
||||
|
||||
CloudFormation only updates the `DRAFT` version when `CfnAgent` changes. The alias stays pinned to the previous version unless the alias resource itself changes.
|
||||
|
||||
Force version rotation by bumping the alias `description` in the same change set:
|
||||
|
||||
```typescript
|
||||
new bedrock.CfnAgentAlias(this, 'Alias', {
|
||||
agentAliasName: 'live',
|
||||
agentId: agent.attrAgentId,
|
||||
description: `bumped 2026-05-14 — model update`, // change this to force a new version
|
||||
});
|
||||
```
|
||||
|
||||
## Knowledge Bases
|
||||
|
||||
The `@cdklabs/generative-ai-cdk-constructs` `VectorKnowledgeBase` L2 construct requires Docker Desktop on the deploying machine — it uses `Code.fromDockerBuild` for a custom resource Lambda. CI runners need Docker available.
|
||||
|
||||
For Knowledge Base S3 buckets, follow the standard tagging convention from [AWS Infrastructure](aws-infrastructure.md#s3): `Purpose` and `ManagedBy` tags.
|
||||
|
||||
## Action Groups
|
||||
|
||||
Action group Lambdas receive a Bedrock-specific event shape — they are not invoked as plain Lambda function URLs. The `apiPath` and `httpMethod` fields in the event identify which OpenAPI operation triggered the call. Validate them before dispatching.
|
||||
|
||||
Document the action group's OpenAPI schema in the same repo as the Lambda code; CloudFormation stores it inline.
|
||||
110
cicd.md
110
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-<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
|
||||
|
||||
- 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
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,20 @@ Code review exists to catch defects, share knowledge, and maintain consistency.
|
|||
- Ask questions instead of making assumptions. "Is this intentional?" is better than "This is wrong."
|
||||
- If a PR is good, say so. A simple "Looks good" is fine.
|
||||
|
||||
## Deferred Findings
|
||||
|
||||
When a reviewer identifies a finding that won't be addressed in the current PR, the PR author must create a GitHub issue for it before the PR merges. No exceptions -- if it's worth commenting on, it's worth tracking.
|
||||
|
||||
### Requirements
|
||||
|
||||
- The GitHub issue must reference the PR number and link to the specific review comment.
|
||||
- The PR author must reply to the review comment with a link to the created issue, acknowledging the deferral.
|
||||
- This applies to all severity levels: bugs, nits, refactors, missing tests, documentation gaps.
|
||||
|
||||
### Why
|
||||
|
||||
Deferred findings handled informally (retro notes, mental to-do lists, "we'll get to it") fall through the cracks. An issue in the backlog is the minimum bar for accountability.
|
||||
|
||||
## Turnaround
|
||||
|
||||
- Aim to review within one business day of being requested
|
||||
|
|
|
|||
41
constructs/README.md
Normal file
41
constructs/README.md
Normal 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 |
|
||||
181
constructs/vpn-ec2-instance.ts
Normal file
181
constructs/vpn-ec2-instance.ts
Normal 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}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
55
dev-environment.md
Normal file
55
dev-environment.md
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
# Development Environment
|
||||
|
||||
Local workstation conventions for Sea Haven engineering work.
|
||||
|
||||
## Directory Layout
|
||||
|
||||
```
|
||||
~/
|
||||
├── Documents/
|
||||
│ ├── repositories/ # Git repos only — no loose files, scripts, or data
|
||||
│ ├── working-docs/ # Cross-cutting docs, audits, drafts, reference material
|
||||
│ └── ...
|
||||
├── Desktop/ # Temporary workspace — nothing permanent
|
||||
├── Downloads/ # Transient — delete installers after use
|
||||
├── .ssh/ # All keys, certs, PEMs, credential files (chmod 600)
|
||||
└── (standard macOS dirs)
|
||||
```
|
||||
|
||||
- `repositories/` contains only git-tracked project directories. No loose scripts, CSVs, or data files.
|
||||
- `working-docs/` is for documentation that spans multiple repos — not a git repo.
|
||||
- Never store secrets in `repositories/` or `Downloads/`.
|
||||
- `.env` files are gitignored and local-only.
|
||||
- No credentials in Notion, Confluence, Slack, or other plaintext docs.
|
||||
|
||||
## Python
|
||||
|
||||
Use **pyenv**, not Homebrew Python. Homebrew auto-upgrades Python on minor releases (3.13 → 3.14) which breaks compiled tools like SAM CLI and pip packages with C extensions.
|
||||
|
||||
- Global default: Python 3.12 (matches Lambda runtime — see [AWS Infrastructure](aws-infrastructure.md#lambda-defaults))
|
||||
- SAM CLI installed via pip under pyenv, not via Homebrew
|
||||
- `pyenv init` belongs in `~/.zshrc`
|
||||
|
||||
When troubleshooting Python issues, check that pyenv is active before anything else. `which python` should point inside `~/.pyenv/`.
|
||||
|
||||
## Node.js
|
||||
|
||||
- Local: Node 24 / npm 11 (generates `lockfileVersion: 3`)
|
||||
- Lambda: Node 22.x or 24.x (set explicitly in IaC)
|
||||
- Reusable workflows: always pass `node-version: "24"` (the default is 22 / npm 10, which can fail `npm ci` on npm 11 lockfiles)
|
||||
|
||||
## macOS launchd and the TCC Sandbox
|
||||
|
||||
macOS TCC blocks launchd from reading `~/Documents/`, `~/Desktop/`, `~/Downloads/`, and iCloud folders. A script in `~/Documents/repositories/...` fails silently when launched by launchd even when `chmod +x` and manual invocation both succeed. The failure surfaces as `LastExitStatus = 32256` (exit 126) in `launchctl list`.
|
||||
|
||||
Any launchd agent or scheduled automation must reference a script outside TCC-protected directories. Keep the source in the repo for version control, and install the runnable copy to `~/.local/bin/` or `~/Library/Application Support/<name>/`. Do not grant Full Disk Access to `/bin/bash`.
|
||||
|
||||
If you edit the repo source, re-copy to the installed location — launchd reads the installed copy.
|
||||
|
||||
## Cleanup Cadence
|
||||
|
||||
| Cadence | Tasks |
|
||||
|---|---|
|
||||
| Weekly | Clear Desktop of anything older than 2 weeks |
|
||||
| Monthly | Check Downloads for stale installers; check `repositories/` for loose files; prune old VS Code extension versions |
|
||||
| Quarterly | Audit Docker (`docker system df`), npm/yarn caches; review GitHub repos for archival candidates |
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
19
hooks/pre-push
Executable file
19
hooks/pre-push
Executable 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."
|
||||
113
lambda-template.md
Normal file
113
lambda-template.md
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
# Lambda Starter Template
|
||||
|
||||
Minimal SAM scaffold for a new Python Lambda. Drop into `template.yaml` and adjust names. Follows the defaults in [aws-infrastructure.md](aws-infrastructure.md#lambda-defaults).
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
my-stack/
|
||||
├── template.yaml
|
||||
├── samconfig.toml.example
|
||||
├── src/
|
||||
│ └── handler/
|
||||
│ ├── app.py
|
||||
│ └── requirements.txt
|
||||
└── .gitignore
|
||||
```
|
||||
|
||||
See [sam-project-layout.md](sam-project-layout.md) for the full directory convention.
|
||||
|
||||
## template.yaml
|
||||
|
||||
```yaml
|
||||
AWSTemplateFormatVersion: "2010-09-09"
|
||||
Transform: AWS::Serverless-2016-10-31
|
||||
Description: my-stack — one-line purpose
|
||||
|
||||
Globals:
|
||||
Function:
|
||||
Runtime: python3.12
|
||||
Architecture: arm64
|
||||
Timeout: 30
|
||||
MemorySize: 256
|
||||
LoggingConfig:
|
||||
LogFormat: JSON
|
||||
|
||||
Resources:
|
||||
HandlerFunction:
|
||||
Type: AWS::Serverless::Function
|
||||
Properties:
|
||||
FunctionName: my-stack-handler
|
||||
CodeUri: src/handler/
|
||||
Handler: app.handler
|
||||
Environment:
|
||||
Variables:
|
||||
CONFIG_SECRET: my-stack/config
|
||||
Policies:
|
||||
- AWSLambdaBasicExecutionRole
|
||||
- Statement:
|
||||
- Effect: Allow
|
||||
Action: secretsmanager:GetSecretValue
|
||||
Resource: !Sub arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:my-stack/*
|
||||
|
||||
HandlerLogGroup:
|
||||
Type: AWS::Logs::LogGroup
|
||||
Properties:
|
||||
LogGroupName: !Sub /aws/lambda/${HandlerFunction}
|
||||
RetentionInDays: 60
|
||||
|
||||
Outputs:
|
||||
HandlerArn:
|
||||
Description: Handler Lambda ARN
|
||||
Value: !GetAtt HandlerFunction.Arn
|
||||
```
|
||||
|
||||
Key points:
|
||||
|
||||
- `Globals.Function` sets runtime, architecture, and JSON logging once for the whole template.
|
||||
- The `LogGroup` is declared **explicitly** with `RetentionInDays: 60`. Omit it and CloudWatch creates the log group on first invocation with no retention — logs accumulate forever.
|
||||
- IAM scopes `secretsmanager:GetSecretValue` to the stack's secret prefix only. Add specific permissions as needed; never use `AdministratorAccess`.
|
||||
|
||||
## src/handler/app.py
|
||||
|
||||
```python
|
||||
import json
|
||||
import os
|
||||
|
||||
import boto3
|
||||
|
||||
_secrets_client = boto3.client("secretsmanager")
|
||||
_config = None
|
||||
|
||||
|
||||
def _get_config():
|
||||
global _config
|
||||
if _config is None:
|
||||
resp = _secrets_client.get_secret_value(SecretId=os.environ["CONFIG_SECRET"])
|
||||
_config = json.loads(resp["SecretString"])
|
||||
return _config
|
||||
|
||||
|
||||
def handler(event, context):
|
||||
config = _get_config()
|
||||
# ... your logic ...
|
||||
return {"statusCode": 200, "body": json.dumps({"ok": True})}
|
||||
```
|
||||
|
||||
The module-level `_config` global caches the secret across warm invocations. The first call per cold start hits Secrets Manager; subsequent calls reuse the cached value. See [secrets-and-config.md](secrets-and-config.md) for the rationale.
|
||||
|
||||
## src/handler/requirements.txt
|
||||
|
||||
Keep this file in every function directory even when empty — SAM looks for it during `sam build`.
|
||||
|
||||
```
|
||||
# Per-function dependencies. Leave empty if the function uses only boto3 and stdlib.
|
||||
```
|
||||
|
||||
## Naming Reminders
|
||||
|
||||
- `FunctionName` must be kebab-case and start with the stack name (`my-stack-handler`).
|
||||
- Secret IDs use `stack-name/secret-name`.
|
||||
- Stack name itself is set in `samconfig.toml`, not the template — match the repo name.
|
||||
|
||||
See [naming-conventions.md](naming-conventions.md).
|
||||
|
|
@ -37,3 +37,12 @@ new MyStack(app, 'MyStack', {
|
|||
| `expenseApprovalBot` | `expense-approval-bot` | camelCase |
|
||||
| `Expense-Approval-Bot` | `expense-approval-bot` | Mixed case |
|
||||
| `feature/AddParser` | `feature/add-parser` | PascalCase in branch |
|
||||
|
||||
## Legacy Stacks
|
||||
|
||||
A handful of stacks predate this convention and remain PascalCase because renaming would require stack replacement (data loss, deploy windows). Examples in active use:
|
||||
|
||||
- `SeaHavenDoorUnlockStack`
|
||||
- `WorkorderIngestStack`
|
||||
|
||||
Do not rename these solely to enforce kebab-case if doing so means tearing down and recreating production resources. New stacks must follow the convention; legacy stacks may keep their names until a planned migration brings them in line.
|
||||
|
|
|
|||
|
|
@ -35,7 +35,57 @@ One directory per Lambda function. Each contains its own handler (`app.py`) and
|
|||
|
||||
### `src/shared/`
|
||||
|
||||
Optional. Used for code shared across multiple functions, typically deployed as a Lambda layer.
|
||||
Optional. Used for code shared across multiple functions, typically deployed as a Lambda layer. See the next section for layout details.
|
||||
|
||||
## Lambda Layers
|
||||
|
||||
When sharing code across functions via a layer, the source layout matters because SAM transforms `ContentUri` depending on whether `BuildMethod` is set.
|
||||
|
||||
### Correct layout (with `BuildMethod`)
|
||||
|
||||
```
|
||||
src/shared/
|
||||
├── shared/ # Package goes here directly — SAM wraps in python/ at build time
|
||||
│ ├── __init__.py
|
||||
│ └── utils.py
|
||||
└── requirements.txt # Layer-level pip dependencies
|
||||
```
|
||||
|
||||
Template:
|
||||
|
||||
```yaml
|
||||
SharedLayer:
|
||||
Type: AWS::Serverless::LayerVersion
|
||||
Properties:
|
||||
LayerName: my-stack-shared
|
||||
ContentUri: src/shared/
|
||||
CompatibleRuntimes:
|
||||
- python3.12
|
||||
CompatibleArchitectures:
|
||||
- arm64
|
||||
Metadata:
|
||||
BuildMethod: python3.12
|
||||
BuildArchitecture: arm64
|
||||
```
|
||||
|
||||
### BuildMethod nesting gotcha
|
||||
|
||||
With `BuildMethod: python3.12`, SAM copies `ContentUri` into a `python/` subdirectory during build, then pip-installs `requirements.txt` deps into that same `python/` directory.
|
||||
|
||||
Do **not** include a `python/` wrapper in your source — SAM adds it. The wrong layout:
|
||||
|
||||
```
|
||||
src/shared/
|
||||
├── python/ # SAM wraps this again → python/python/shared/ — module unreachable
|
||||
│ └── shared/
|
||||
└── requirements.txt
|
||||
```
|
||||
|
||||
A real production incident (~22 hours of outage) traced to this exact pattern when a refactor moved layer code under an extra `python/` directory.
|
||||
|
||||
### Without BuildMethod (raw zip)
|
||||
|
||||
If the layer has no pip dependencies and you omit `BuildMethod`, SAM zips `ContentUri` as-is — you DO need the `python/` wrapper. Reserve raw zip for layers that ship only Python source.
|
||||
|
||||
## Standard .gitignore
|
||||
|
||||
|
|
|
|||
50
scripts/health-check-template.sh
Executable file
50
scripts/health-check-template.sh
Executable 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
232
scripts/provision-repo.sh
Executable 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"
|
||||
Loading…
Add table
Reference in a new issue