.github/README.md
2026-08-03 19:44:57 -04:00

25 KiB

.github

Organization-level GitHub configuration for Sea Haven Industries.

Git and PR conventions

Branch naming

feature/, fix/, hotfix/, chore/, docs/, refactor/, release/ + kebab-case description. Branch names do not contain Jira keys.

Commit format

type(scope): description — lowercase, imperative, no trailing period, header ≤ 72 chars. Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert, release. Breaking change: feat!: + BREAKING CHANGE: footer.

PR title

type(scope): description (DEV-123) — the Jira key is required at the end in parentheses. Active projects: DEV (product), PLAT (platform), SEC (security). INFRA is a closed archive. Jira-exempt only: Dependabot PRs and permission-controlled emergency reverts.

PR body

Exactly four headings in order: ## Summary, ## Validation, ## Tests, ## Notes. Use None. under Notes if empty.

Deploy path

The two sanctioned deploy paths are merge to main triggering the pipeline and workflow_dispatch on that same pipeline. No manual workstation deploys to production.

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/ci-mobile-ios.yaml — Reusable CI for React Native iOS apps: dependency install, typecheck, optional lint and unit tests, and an unsigned compile (nothing uploaded). Emits the aggregated ci / ci status context.

.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-pr-policy.yaml — Reusable PR metadata gate. Validates PR title convention (type/scope/Jira key), branch naming, four-section body, commit subjects, AI attribution footers, and workflow file pin compliance — all via GitHub API, no checkout. Emits policy / pr when the caller job is named policy. Optional secrets JIRA_CLOUD_ID, JIRA_SERVICE_ACCOUNT_EMAIL, and JIRA_API_TOKEN must all be set for human PRs; Dependabot skips Jira/branch/body but still runs commit and workflow supply-chain checks. Emergency revert PRs may skip Jira with the emergency-revert label applied by a human collaborator with maintain or admin permission.

The supply-chain check operates in diff mode: for modified or renamed workflow files, the gate fetches the base-branch version at pr.base.sha and reports only violations whose normalized fingerprint is absent from the base. Added files must be fully compliant. Historical drift already present in the base branch is handled by the drift audit/remediation backlog, not by this gate. A failure to fetch the base version is a POLICY-INFRA error and the file is not silently grandfathered.

Workflow file constraints enforced by the supply-chain scanner. Changed workflow files scanned by this policy must use block-style structural keys and inline run:/uses: values. The scanner fails closed on YAML forms it cannot safely resolve: flow-style step mappings (- { uses: ... }, - { run: ... }), sequence-item anchor declarations (- &anchor { uses: ... } and the multiline form - &anchor followed by a flow mapping on the next line), escaped or Unicode-encoded structural keys in double-quoted strings ("u\u0073es", "r\u0075n"), and YAML aliases or anchors on run:, uses:, or permissions: values (run: *cmd, uses: &anchor ...). docker:// action refs must carry an immutable sha256 digest pin (docker://<image>@sha256:<64 lowercase hex>); mutable tags and bare image names are rejected. Use the literal unquoted key forms and inline values in all workflow steps.

.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/release.yaml — Reusable release workflow: creates an annotated git tag at a commit and publishes a GitHub Release pointing at it. The version is an input (not read from a manifest).

.github/workflows/release-on-merge.yaml — Repo automation (not callable): cuts a tag and GitHub Release for this repo whenever a merge to main changes a reusable workflow, so Dependabot has a release to advance consumer SHA pins to (see the pinning policy below).

.github/workflows/policy.yaml — Intentionally absent from this PR. The self-caller must pin Sea-Haven-Industries/.github/.github/workflows/callable-pr-policy.yaml to a released 40-char SHA with a matching # vX.Y.Z comment; a mutable local ./ path reference is rejected by the supply-chain gate on modified workflow files. The follow-up PR can be opened once this PR merges and release-on-merge.yaml cuts the first release containing callable-pr-policy.yaml, then using gh api /repos/Sea-Haven-Industries/.github/commits/vX.Y.Z --jq .sha to obtain the pin.

.github/workflows/labeler.yaml — This repo's own thin caller of callable-labeler.yaml, so the labeler runs on .github's own PRs.

.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: cdk-deploy, ci-dotnet, ci-mobile-ios, ci-node, ci-python, ci-python-app, ci-static, ci-typescript-frontend, dependency-review, dotnet-eb-deploy, labeler, mobile-ios-deploy, release, sam-deploy, 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. Templates are not scanned by Dependabot, so refresh their pinned SHAs opportunistically when editing one.

A pr-policy starter template can be added to workflow-templates/ only after the PR that introduces callable-pr-policy.yaml merges and release-on-merge.yaml cuts the first release containing it. Until then, consumer repos must add the caller workflow manually (see §3).

Ref pinning policy

All workflow refs across the org are pinned to full commit SHAs:

  • Org reusable workflows are referenced at a full commit SHA of this repo with a trailing comment naming the ref or release the pin tracks:

    uses: Sea-Haven-Industries/.github/.github/workflows/ci-python-sam.yaml@<full-commit-sha> # v1.0.3
    

    Branch refs are mutable: a bad commit on this repo would flow instantly into every consumer's CI and deploy path, while a SHA pin turns the same change into a reviewable Dependabot PR. Two prerequisites keep pins advancing instead of freezing: every consumer repo's dependabot.yml must include the github-actions ecosystem (weekly), and Dependabot must be granted access to this 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. release-on-merge.yaml tags this repo on every reusable-workflow change so Dependabot has releases to diff against. When adding a caller by hand, pin to the latest release commit (gh api /repos/Sea-Haven-Industries/.github/commits/vX.Y.Z --jq .sha), annotate it with # vX.Y.Z, and let Dependabot advance it from there.

  • Third-party and first-party actions (actions/checkout, actions/setup-python, actions/labeler, …) — a subset are already SHA-pinned (e.g. actions/labeler, aws-actions/*, docker/setup-qemu-action, ruby/setup-ruby); the remainder (actions/checkout, actions/setup-node, actions/setup-python, actions/setup-dotnet, actions/dependency-review-action) currently use floating major-version tags. Full SHA pinning for this group is deferred (PLAT backlog); Dependabot will keep SHA and comment current once pins are set.

  • Binary installs are checksum-verified (actionlint in ci.yaml).

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. 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 from UPDATE_ROLLBACK_FAILED 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. A completed update rollback lands in UPDATE_ROLLBACK_COMPLETE, which is stable and can accept a corrective update; cd-sam blocks only first-create ROLLBACK_COMPLETE and failed or in-progress states.

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).

Three additional org-level secrets are required for the PR policy Jira check. Set each to selected repositories visibility and grant to each consumer repo:

Secret Value Consumed by
JIRA_CLOUD_ID Atlassian Cloud ID UUID (find in Jira Settings → Products → Jira Software) callable-pr-policy.yaml
JIRA_SERVICE_ACCOUNT_EMAIL Email of the service account with read access to DEV/PLAT/SEC projects callable-pr-policy.yaml
JIRA_API_TOKEN API token for that account (generated at id.atlassian.com/manage-profile/security/api-tokens) callable-pr-policy.yaml

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@81cf168170f356d1423d7736f7ce93fd6611ad53 # v1.0.4

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@81cf168170f356d1423d7736f7ce93fd6611ad53 # v1.0.4

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@81cf168170f356d1423d7736f7ce93fd6611ad53 # v1.0.4
    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@81cf168170f356d1423d7736f7ce93fd6611ad53 # v1.0.4
    with:
      source-dirs: "src"
      run-sam-validate: false
  typescript:
    uses: Sea-Haven-Industries/.github/.github/workflows/ci-typescript-cdk.yaml@81cf168170f356d1423d7736f7ce93fd6611ad53 # v1.0.4

3. Add PR policy to a repo

Create .github/workflows/policy.yaml in the target repo. The Jira secrets must already be granted to the repo (see §1).

name: PR Policy
on:
  pull_request:
    types: [opened, reopened, synchronize, edited, labeled, unlabeled, ready_for_review]

concurrency:
  group: policy-${{ github.event.pull_request.number }}
  cancel-in-progress: true

permissions:
  contents: read
  issues: read
  pull-requests: read

jobs:
  policy:
    uses: Sea-Haven-Industries/.github/.github/workflows/callable-pr-policy.yaml@<full-commit-sha> # vX.Y.Z
    secrets:
      JIRA_CLOUD_ID: ${{ secrets.JIRA_CLOUD_ID }}
      JIRA_SERVICE_ACCOUNT_EMAIL: ${{ secrets.JIRA_SERVICE_ACCOUNT_EMAIL }}
      JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}

Replace <full-commit-sha> with the SHA of the release that contains callable-pr-policy.yaml:

gh api /repos/Sea-Haven-Industries/.github/commits/vX.Y.Z --jq .sha

The check-run name is policy / pr. If your branch-protection ruleset requires this context, add it after the first PR passes.

Known platform limitation — GITHUB_TOKEN label and metadata events. When the policy workflow re-runs on labeled or edited events, the metadata edits themselves (label adds, title edits) must be performed by a GitHub App or a PAT that owns its own event stream. Edits made through GITHUB_TOKEN do not reliably emit a new pull_request event to trigger re-evaluation; the check stays in its prior state until the next push or manual re-run. Org automation that applies labels (such as the emergency-revert label) must therefore use a GitHub App token or a PAT — not GITHUB_TOKEN — or the policy gate will not re-run automatically after the label is applied. This is a GitHub platform constraint, not a deficiency that can be solved at the workflow level.

4. 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@81cf168170f356d1423d7736f7ce93fd6611ad53 # v1.0.4
    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@81cf168170f356d1423d7736f7ce93fd6611ad53 # v1.0.4
    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@81cf168170f356d1423d7736f7ce93fd6611ad53 # v1.0.4
    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@81cf168170f356d1423d7736f7ce93fd6611ad53 # v1.0.4
    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@81cf168170f356d1423d7736f7ce93fd6611ad53 # v1.0.4
    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