Organization-level GitHub configuration — Claude Code review and compliance workflows
Find a file
Adam Moussa 2fbfb2e7cf
Merge pull request #106 from Sea-Haven-Industries/feat/release-on-reusable-change
feat(release): cut a tag and release when a reusable workflow changes
2026-07-28 17:05:28 -04:00
.github feat(release): cut a tag and release when a reusable workflow changes 2026-07-28 17:01:44 -04:00
workflow-templates feat(workflow-templates): add release and ci-mobile-ios templates, pass node-version on ci-python 2026-07-28 15:57:17 -04:00
.gitignore Add Claude Code review and compliance audit workflows 2026-05-06 15:10:48 -04:00
oidc-deploy-roles.yaml fix(iam): drop the redundant inline boundary-gated policy (Phase B) 2026-07-27 18:20:48 -04:00
README.md Merge branch 'main' into ci/enable-actionlint-shellcheck 2026-07-28 12:54:23 -04:00
SECURITY.md docs: fix README review drift + add community-health files (#48) 2026-06-10 15:35:31 -04:00
SUPPORT.md docs: fix README review drift + add community-health files (#48) 2026-06-10 15:35:31 -04:00

.github

Organization-level GitHub configuration for Sea Haven Industries.

What's in here

Reusable Workflows

.github/workflows/ci-python-sam.yaml — Reusable CI workflow for Python / SAM repos. Runs ruff check + ruff format --check, optional pytest, and optional sam validate --lint. Also usable for Python CDK repos by disabling SAM validate.

.github/workflows/ci-typescript-cdk.yaml — Reusable CI workflow for TypeScript / CDK repos. Runs npm ci + optional tsc --noEmit, optional ESLint, optional Jest, and optional cdk synth. Also supports Node.js SAM repos via an optional sam validate step.

.github/workflows/ci-typescript-frontend.yaml — Reusable CI workflow for bundled TypeScript front-end apps (Vite / React / Vue SPAs). Runs a Sea Haven standards gate (required npm scripts present, no AI-tool footers / hook bypasses / hardcoded secrets in added lines), then format:check, lint, build, vitest unit tests, and an optional Playwright browser smoke. Emits the single ci / ci status context — keep the caller job id ci.

.github/workflows/cd-sam.yaml — Reusable CD workflow for SAM repos. Runs sam build + sam deploy with OIDC credentials and a CloudFormation execution role. Triggers via workflow_call from per-repo deploy.yaml on push to main.

.github/workflows/cd-cdk.yaml — Reusable CD workflow for CDK repos (TypeScript and Python). Runs cdk deploy --all with OIDC credentials. Supports optional Python setup for Python CDK repos and QEMU emulation for cross-platform Docker builds.

.github/workflows/ci-python-app.yaml — Reusable CI for non-SAM Python apps (ruff check + format, optional pytest; no SAM validate).

.github/workflows/ci-dotnet.yaml — Reusable CI for .NET solutions (dotnet build, optional dotnet test; SDK version and solution path as inputs).

.github/workflows/ci-static.yaml — Reusable CI for static sites (e.g. Eleventy builds for seahaven-site).

.github/workflows/cd-mobile-ios.yaml — Reusable CD for iOS apps via Fastlane to TestFlight (Node + Ruby setup inputs).

.github/workflows/cd-dotnet-eb.yaml — Reusable CD for .NET apps on AWS Elastic Beanstalk. Publishes the project, packages a bundle, uploads it, creates an application version, and updates an existing environment with OIDC credentials — it never creates an environment. Serialised per environment via a concurrency group, and the post-deploy check fails the job if EB rolls the deploy back. The caller owns branch-to-environment mapping.

.github/workflows/callable-labeler.yaml — Org-wide PR auto-labeler. Label rules live inline here (single source of truth) — consumer repos need only a thin caller with contents: read, pull-requests: write, and issues: write; no per-repo labeler.yml.

.github/workflows/callable-dependency-review.yaml — Dependency review on PRs, failing on high severity. Requires Dependency Graph.

.github/workflows/ci.yaml — Self-CI for this repo: actionlint (checksum-verified install) over all workflow files, emitting the required ci / ci status context. Its shellcheck integration is enabled, so run: bodies are shell-linted too; the two deploy steps that rely on intentional word-splitting (sam deploy … $PARAMS, cdk deploy $STACKS) carry a per-line, commented # shellcheck disable=SC2086 rather than being quoted or globally exempted.

Workflow templates (workflow-templates/)

Starter workflows offered on the org's Actions → New workflow page: ci-python, ci-node, cdk-deploy, sam-deploy, dotnet-eb-deploy, dependency-review, labeler, triage. Each is a thin caller of the corresponding reusable workflow above (triage is standalone). Every template has a paired properties.json (name, description, icon, filePatterns for auto-suggestion). Replace any REPLACE-ME placeholders before enabling.

Action pinning policy

Third-party action refs across the org follow a tiered policy:

  • High-trust / high-blast-radius third-party actions are SHA-pinned with a trailing version comment (e.g. actions/labeler in callable-labeler.yaml), and binary installs are checksum-verified (actionlint in ci.yaml). Dependabot keeps the SHA current via its trailing-comment mechanism.
  • Common first-party actions (actions/checkout, actions/dependency-review-action, actions/github-script) are pinned to a major tag (@v7, @v5, …) and kept current by Dependabot version updates gated by CI.
  • Org reusable workflows are referenced at @main (uses: Sea-Haven-Industries/.github/.github/workflows/…@main). This is deliberate: caller and callable share one trust domain, and pinning callers to a SHA would freeze every consumer against central fixes. Templates in workflow-templates/ follow the same @main convention.

AWS deploy roles & IAM (oidc-deploy-roles.yaml)

oidc-deploy-roles.yaml is a bootstrap CloudFormation stack (github-oidc-deploy-roles, us-east-1, account 328440206208) that owns the IAM the CI/CD workflows assume. It contains:

  • The GitHub Actions OIDC provider (conditional — already exists in the account).
  • One OIDC deploy role per repo (githubdeploy-<repo>), assumed by that repo's deploy.yaml via OIDC and passed in as AWS_DEPLOY_ROLE_ARN. CDK repos use these to assume the cdk-hnb659fds-* bootstrap roles; SAM repos use these to run sam deploy.
  • The shared SAM CloudFormation execution role github-cfn-execution-role (SamCfnExecutionRole) — passed as cfn-role-arn by every SAM deploy.yaml (see §3). CloudFormation assumes it to provision the SAM stacks' resources.
  • The seahaven-lambda-execution-boundary managed policy.
  • The seahaven-cfn-exec-iam-management managed policy (SamCfnIamManagementPolicy), attached to github-cfn-execution-role. It holds that role's boundary-gated IAM statements plus the Deny backstops that keep the permissions boundary from being detached, rewritten, or applied to the deploy substrate's own roles. It lives in a managed policy rather than inline because the role's inline policies sit at 10,006 of IAM's hard 10,240-byte per-role limit; attached managed policies have a separate 6,144-byte budget.

Constraint for future maintainers. github-cfn-execution-role is explicitly denied from mutating the deploy substrate's own principals — itself, any githubdeploy-* role, and any seahaven-* managed policy. Those are owned by this stack and deployed manually with administrator credentials, so nothing legitimate needs that path. If you ever add automation that manages one of them, it must not run through github-cfn-execution-role or it will fail with AccessDenied.

Where the exec role's IAM statements live. All of them are in the attached seahaven-cfn-exec-iam-management managed policy — there is no inline copy. The role's inline policies were previously at 10,006 of the 10,240-byte limit, leaving no room to add anything; consolidating into the managed policy brought that to 8,261 bytes (1,979 free). If you need to add a permission to this role, prefer the managed policy: the inline budget is the scarce one.

⚠️ This stack has no CD pipeline — it is deployed manually. (It defines the very roles the pipelines use, so it can't deploy itself.)

# Review IAM changes FIRST (IAM changes also require the cross-family review per the handbook):
aws cloudformation deploy \
  --region us-east-1 \
  --stack-name github-oidc-deploy-roles \
  --template-file oidc-deploy-roles.yaml \
  --capabilities CAPABILITY_NAMED_IAM \
  --s3-bucket cdk-hnb659fds-assets-328440206208-us-east-1 \
  --no-execute-changeset
# inspect the printed change-set, then drop --no-execute-changeset to apply.

--s3-bucket is required — the template is larger than the 51,200-byte inline limit.

github-cfn-execution-role is scoped (no *FullAccess)

The execution role carries no blanket *FullAccess/IAMFullAccess — only per-service inline policies. Its iam:CreateRole / iam:AttachRolePolicy / iam:PutRolePolicy are conditioned on iam:PermissionsBoundary == seahaven-lambda-execution-boundary, so it can only create roles that carry the boundary (it cannot mint an unconstrained admin role). Adding a new AWS service to a SAM stack means adding that service's provisioning actions to this role, or the deploy fails.

seahaven-lambda-execution-boundary is the Lambda runtime ceiling

Every SAM-created Lambda execution role gets this boundary attached — SAM stacks set it on Globals.Function:

Globals:
  Function:
    PermissionsBoundary: arn:aws:iam::328440206208:policy/seahaven-lambda-execution-boundary

A function's effective permissions are the intersection of its own role policy and this boundary. A new runtime permission must also be added to the boundary, or it is silently denied at runtime (the deploy still succeeds — the failure only shows when the function runs). SAM does not support a custom Path on auto-generated function roles, so the boundary condition (not a role path) is the escalation guard.

Order of operations when changing the exec role or boundary

  1. Deploy the boundary change first.
  2. Redeploy the SAM stacks so their roles pick it up (while the exec role still permits it).
  3. Then tighten the exec role.

Wrong order breaks every SAM deploy. (History: INFRA-103 established the boundary, INFRA-97 scoped the role.) CDK repos are unaffected — they deploy via cdk-hnb659fds-* roles, not this execution role.

This ordering rule is about changing the boundary or the conditions that gate it. It does not apply to changes that only add permissions to the exec role.

Permissions boundaries can no longer be removed by CloudFormation

github-cfn-execution-role is explicitly denied iam:DeleteRolePermissionsBoundary. It can set the boundary (that is what SAM needs) but never remove one. Two consequences worth knowing before debugging a stuck stack:

  • Removing PermissionsBoundary from an existing role fails by design. CloudFormation issues DeleteRolePermissionsBoundary for that edit, gets AccessDenied, and the stack update rolls back. Removing the boundary from a SAM function is a security regression, so failing loudly is intended.
  • Rollback of an update that adds a boundary to an existing role would also fail, landing the stack in UPDATE_ROLLBACK_FAILED. This is currently unreachable — all 26 IAM roles across the five SAM stacks already carry the boundary (verified 2026-07-27), so no update can add one. It becomes reachable again only if a role is created without the boundary and given one later.

Recovery in either case is an administrator action, not a pipeline retry: clear the wedged stack with aws cloudformation continue-update-rollback --stack-name <stack> --resources-to-skip <RoleLogicalId>, or replace the role by renaming its logical id. Note cd-sam's pre-flight hard-fails on *ROLLBACK_COMPLETE, so that repo's deploys stay blocked until it is cleared.

Setup

1. Org-level secrets

Managed under Organization Settings > Secrets and variables > Actions. Each is set to selected repositories visibility — grant it to a repo before a workflow there can read it.

Secret Value Consumed by
ANTHROPIC_API_KEY Anthropic API key reviewer-eval.yml in open-swe

The CI and CD workflows below need no org secret — CD authenticates to AWS via OIDC using the per-repo AWS_DEPLOY_ROLE_ARN secret (see §3).

2. Add CI to a repo

Create .github/workflows/ci.yaml in the target repo. Examples:

Python SAM repo (e.g., afterhours-shift-manager, expense-approval-bot):

name: CI
on:
  pull_request:
    branches: [main]

jobs:
  ci:
    uses: Sea-Haven-Industries/.github/.github/workflows/ci-python-sam.yaml@main

TypeScript CDK repo (e.g., seahaven-door-unlock-api, seahaven-slack-bot):

name: CI
on:
  pull_request:
    branches: [main]

jobs:
  ci:
    uses: Sea-Haven-Industries/.github/.github/workflows/ci-typescript-cdk.yaml@main

Node.js SAM repo (e.g., payments-dashboard):

name: CI
on:
  pull_request:
    branches: [main]

jobs:
  ci:
    uses: Sea-Haven-Industries/.github/.github/workflows/ci-typescript-cdk.yaml@main
    with:
      run-typecheck: false
      run-cdk-synth: false
      run-sam-validate: true

Mixed stack (e.g., exec-aide — TypeScript CDK + Python Lambdas):

name: CI
on:
  pull_request:
    branches: [main]

jobs:
  python:
    uses: Sea-Haven-Industries/.github/.github/workflows/ci-python-sam.yaml@main
    with:
      source-dirs: "src"
      run-sam-validate: false
  typescript:
    uses: Sea-Haven-Industries/.github/.github/workflows/ci-typescript-cdk.yaml@main

3. Add CD to a repo

Create .github/workflows/deploy.yaml in the target repo. Requires AWS_DEPLOY_ROLE_ARN repo secret.

SAM repo (e.g., afterhours-shift-manager):

name: Deploy
on:
  push:
    branches: [main]

jobs:
  deploy:
    uses: Sea-Haven-Industries/.github/.github/workflows/cd-sam.yaml@main
    with:
      stack-name: afterhours-shift-manager
      cfn-role-arn: arn:aws:iam::328440206208:role/github-cfn-execution-role
    secrets:
      deploy-role-arn: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}

The cfn-role-arn (github-cfn-execution-role) is scoped and boundary-gated — adding a new AWS service or a new Lambda runtime permission to a SAM stack may require updating that role and/or seahaven-lambda-execution-boundary first. See AWS deploy roles & IAM.

TypeScript CDK repo (e.g., seahaven-door-unlock-api):

name: Deploy
on:
  push:
    branches: [main]

jobs:
  deploy:
    uses: Sea-Haven-Industries/.github/.github/workflows/cd-cdk.yaml@main
    secrets:
      deploy-role-arn: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}

Python CDK repo (e.g., po-ingest):

name: Deploy
on:
  push:
    branches: [main]

jobs:
  deploy:
    uses: Sea-Haven-Industries/.github/.github/workflows/cd-cdk.yaml@main
    with:
      python-version: "3.12"
      cdk-dir: cdk
    secrets:
      deploy-role-arn: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}

CDK repo with arm64 Docker builds (e.g., exec-aide):

name: Deploy
on:
  push:
    branches: [main]

jobs:
  deploy:
    uses: Sea-Haven-Industries/.github/.github/workflows/cd-cdk.yaml@main
    with:
      enable-qemu: true
    secrets:
      deploy-role-arn: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}

.NET app on Elastic Beanstalk (e.g., shoc-backend):

name: Deploy
on:
  push:
    branches: [dev]

jobs:
  deploy:
    uses: Sea-Haven-Industries/.github/.github/workflows/cd-dotnet-eb.yaml@main
    with:
      project: Api.SeaHavenIndustries/Api.SeaHavenIndustries.csproj
      eb-application: shoc-backend
      eb-environment: shoc-backend-dev
      procfile-command: dotnet Api.SeaHavenIndustries.dll
    secrets:
      deploy-role-arn: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}

The environment must already exist — this workflow deploys a new application version to it and never creates one. Branch-to-environment mapping belongs in the caller: add one job per branch (e.g. dev → …-dev, main → …-staging) rather than parameterising the reusable by branch. procfile-command generates the Procfile the Amazon Linux .NET platform needs; omit it only if the repo commits its own Procfile into the publish output. Deploys are serialised per environment, and the job fails if Elastic Beanstalk rolls the version back.

Enable optional steps as repos adopt them:

Input Default Turn on when...
run-tests false Repo has pytest tests or Jest tests
run-lint false Repo has an ESLint config
run-typecheck true Repo has tsconfig.json
run-cdk-synth true Repo is CDK-based
run-sam-validate true (Python) / false (TS) Repo has a SAM template