mirror of
https://github.com/Sea-Haven-Industries/engineering-handbook.git
synced 2026-09-30 06:53:15 +00:00
The SAM deploy example passed `cfn-role-arn` as a secret and omitted `deploy-role-arn` entirely. Both are wrong against cd-sam.yaml, which declares `cfn-role-arn` as a required string INPUT and `deploy-role-arn` as a required SECRET. A repo scaffolded from the example failed twice: an unexpected secret, plus a missing required input and secret. The two ARNs are distinct roles that the old example effectively conflated into one, so document them side by side: cfn-role-arn is the CloudFormation execution role the stack deploys as, deploy-role-arn is the OIDC role the workflow assumes. Also record which inputs have defaults so callers pass only what they must. The account ID stays a `<account-id>` placeholder, per the same rule that removed the hardcoded management-account ARN from the templates.
151 lines
6.6 KiB
Markdown
151 lines
6.6 KiB
Markdown
# CI/CD Pipelines
|
|
|
|
## Requirement
|
|
|
|
Every deployable repo must have a CI/CD pipeline. No manual deploys to production. If it deploys to AWS, it needs a pipeline.
|
|
|
|
## Platform
|
|
|
|
GitHub Actions is the standard CI/CD platform. All pipelines use reusable workflows from the `Sea-Haven-Industries/.github` org repo (`.github/workflows/`).
|
|
|
|
## Workflow Structure
|
|
|
|
Every repo gets two thin workflow files in `.github/workflows/`:
|
|
|
|
| File | Trigger | Purpose |
|
|
|---|---|---|
|
|
| `ci.yaml` | `pull_request` on `main` | Lint, typecheck, test, synth/validate |
|
|
| `deploy.yaml` | `push` on `main` | Deploy to AWS |
|
|
|
|
### CDK Stacks (TypeScript)
|
|
|
|
```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@<full-commit-sha> # main
|
|
with:
|
|
node-version: "24"
|
|
|
|
# .github/workflows/deploy.yaml
|
|
name: Deploy
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
jobs:
|
|
deploy:
|
|
uses: Sea-Haven-Industries/.github/.github/workflows/cd-cdk.yaml@<full-commit-sha> # 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@<full-commit-sha> # main
|
|
|
|
# .github/workflows/deploy.yaml
|
|
name: Deploy
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
jobs:
|
|
deploy:
|
|
uses: Sea-Haven-Industries/.github/.github/workflows/cd-sam.yaml@<full-commit-sha> # main
|
|
with:
|
|
stack-name: "your-stack-name"
|
|
cfn-role-arn: "arn:aws:iam::<account-id>:role/github-cfn-execution-role"
|
|
secrets:
|
|
deploy-role-arn: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
|
|
```
|
|
|
|
`cd-sam.yaml` takes two required inputs and one required secret. The two role ARNs are not interchangeable; they are different roles with different jobs:
|
|
|
|
| Name | Kind | Purpose |
|
|
|---|---|---|
|
|
| `stack-name` | input (`with:`) | The CloudFormation stack name |
|
|
| `cfn-role-arn` | input (`with:`) | The **CloudFormation execution role** the stack is deployed *as*. This is `github-cfn-execution-role` in the repo's target account. Substitute that account's ID for `<account-id>`; the account must have the deploy substrate provisioned before the first deploy. Do not point new repos at the management account. |
|
|
| `deploy-role-arn` | secret (`secrets:`) | The **OIDC role the workflow assumes**, from the repo's `AWS_DEPLOY_ROLE_ARN` secret (see [Authentication](#authentication)) |
|
|
|
|
Passing `cfn-role-arn` under `secrets:` fails: it is an input, so the run errors on an unexpected secret *and* a missing required input.
|
|
|
|
The remaining inputs have defaults and are only needed when a repo differs from them: `region` (`us-east-1`), `sam-template` (`template.yaml`), and `python-version` (`3.12`). The optional `parameter-overrides` secret passes `Key=Value` pairs through to `sam deploy`.
|
|
|
|
## 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
|
|
|
|
- All workflow files: kebab-case
|
|
- Reusable workflow references: pinned to a full 40-character commit SHA with a `# main` comment — never a branch or tag ref
|
|
|
|
## Workflow Ref Pinning
|
|
|
|
Reusable workflow references are pinned to a full commit SHA of the central `.github` repo, with a trailing `# main` comment:
|
|
|
|
```yaml
|
|
uses: Sea-Haven-Industries/.github/.github/workflows/ci-python-sam.yaml@<full-commit-sha> # main
|
|
```
|
|
|
|
Branch refs are mutable: a compromised or bad commit on the central repo would flow instantly into every consumer's CI and deploy path. A SHA pin turns that same change into a reviewable Dependabot PR instead. The comment tells Dependabot (and readers) which ref the pin tracks.
|
|
|
|
Per the pinning principle, pins are for reproducibility, not for freezing time. Two prerequisites keep them moving:
|
|
|
|
1. Every repo's `dependabot.yml` must include the `github-actions` ecosystem (weekly), so pin-advance PRs are opened automatically.
|
|
2. Dependabot must be granted access to the internal `.github` repo at the org level (Org Settings → Advanced Security → Global settings → "Grant Dependabot access to repositories"). Without the grant, update jobs fail with `git_dependencies_not_reachable` and pins freeze silently — consumers stop receiving central workflow fixes with no visible signal beyond the failed Dependabot run.
|
|
|
|
When adding a caller workflow by hand, pin to the current tip of the central repo's `main` (`gh api /repos/Sea-Haven-Industries/.github/commits/main --jq .sha`) and let Dependabot advance it from there.
|
|
|
|
## When to Add a Pipeline
|
|
|
|
- When creating a new deployable project — the pipeline is part of the initial setup, not a follow-up
|
|
- When working on an existing project that lacks one — flag it and add it as part of the current work
|
|
|
|
A project is not production-ready without CI/CD.
|
|
|
|
## PR Auto-Labeling
|
|
|
|
Pull requests are auto-labeled org-wide by a reusable workflow in `.github`. The label rules live once, centrally, inside the reusable workflow itself (written to the runner at execution time), so each repo needs only a short caller and **no per-repo `labeler.yml`**:
|
|
|
|
```yaml
|
|
# .github/workflows/labeler.yml — the per-repo caller
|
|
name: Labeler
|
|
on:
|
|
pull_request:
|
|
branches: [main]
|
|
permissions:
|
|
contents: read
|
|
pull-requests: write
|
|
issues: write
|
|
jobs:
|
|
label:
|
|
uses: Sea-Haven-Industries/.github/.github/workflows/callable-labeler.yaml@<full-commit-sha> # main
|
|
```
|
|
|
|
- The trigger is plain `pull_request`, not `pull_request_target`: private repos take no fork PRs, so the lower-privilege event is sufficient and avoids the pwn-request surface. Because `pull_request` runs the workflow from the merge commit, the Labeler check appears on the PR that first adds the caller — an absent or failed check means a missing permission, not expected behaviour.
|
|
- The caller MUST grant all three permissions. Reusable-workflow permissions can only be downgraded from the caller, so omitting `issues: write` (needed to create labels that don't exist yet) or any other grant causes a silent `startup_failure`.
|
|
- Adding the caller is part of new-repo provisioning.
|