# .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. **`.github/workflows/compliance-audit.yaml`** — **DEPRECATED (2026-06-10).** The weekly scheduled org-wide audit has been retired: the schedule was removed and the workflow is disabled in the Actions tab (manual `workflow_dispatch` only, kept for historical reference). Repo compliance is now handled by the Claude Code App on pull requests and the engineering handbook directly. Safe to delete in a future cleanup. ### 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. ### PR Reviews PR reviews are handled by the **official Claude Code GitHub App** (installed org-wide, enabled as a required check in the org ruleset) — there is **no review workflow in this repo**. The earlier custom `claude-code-review.yaml` reusable workflow and its per-repo wrapper were retired on 2026-05-13 when the App took over. ### Scripts **`scripts/rollout-review-workflow.sh`** — **Legacy / superseded.** One-time script that pushed the old PR-review wrapper workflow to all org repos. Obsolete since reviews moved to the official Claude Code App (2026-05-13); retained only for historical reference. ### 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-`), 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 §5). 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`. > **Phase A / Phase B.** The boundary-gated statements are currently duplicated: the new managed policy carries the corrected set, and the older inline `iam-role-management-boundary-gated` policy is still present. That overlap is deliberate and temporary — an explicit Deny beats an Allow anywhere in the policy set, so the corrected version already governs, and keeping the inline copy meant CloudFormation removed nothing during the change. **Phase B deletes the inline copy** (inline usage 10,006 → 8,261). Do not delete it as "redundant" outside that planned change. > ⚠️ **This stack has no CD pipeline — it is deployed manually.** (It defines the very roles the pipelines use, so it can't deploy itself.) ```bash # 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`: ```yaml 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 --resources-to-skip `, 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. Create a GitHub App 1. Go to **Organization Settings > Developer settings > GitHub Apps > New GitHub App** 2. Name it `claude-code-ci` (or similar) 3. Set Homepage URL to your org URL 4. Disable Webhook (uncheck "Active") 5. Set these **Repository permissions:** - **Contents:** Read and write - **Issues:** Read and write - **Metadata:** Read-only - **Pull requests:** Read and write 6. Set **Where can this app be installed?** to "Only on this account" 7. Click **Create GitHub App** 8. Note the **App ID** from the app's settings page 9. Under **Private keys**, click **Generate a private key** — save the `.pem` file ### 2. Install the App 1. From the app's settings page, click **Install App** 2. Select `Sea-Haven-Industries` 3. Choose **All repositories** ### 3. Add org-level secrets Go to **Organization Settings > Secrets and variables > Actions** and add: | Secret | Value | |--------|-------| | `ANTHROPIC_API_KEY` | Your Claude API key | | `CLAUDE_CI_APP_ID` | The App ID from step 1 | | `CLAUDE_CI_APP_PRIVATE_KEY` | The full contents of the `.pem` file from step 1 | ### 4. 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): ```yaml 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): ```yaml 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): ```yaml 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): ```yaml 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 ``` ### 5. 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): ```yaml 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](#aws-deploy-roles--iam-oidc-deploy-rolesyaml). **TypeScript CDK repo** (e.g., seahaven-door-unlock-api): ```yaml 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): ```yaml 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): ```yaml 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): ```yaml 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 | ### 6. PR reviews PR reviews run via the **official Claude Code GitHub App** — install it on the org and enable it as a required check in the ruleset. No per-repo workflow or rollout is needed; the legacy `rollout-review-workflow.sh` is retained only for historical reference.