From af2ee942cebfb14c2003760171bea7fef4d596c6 Mon Sep 17 00:00:00 2001 From: Adam Moussa Date: Tue, 28 Jul 2026 12:34:26 -0400 Subject: [PATCH 1/4] chore: remove retired pr-review and compliance-audit tooling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three pieces of tooling for retired flows were still carried in this repo, and the README documented them as if they were current. - `.github/workflows/compliance-audit.yaml`: deprecated 2026-06-10. Its schedule was already stripped, and its live state in the Actions API is `disabled_manually`, so it was workflow_dispatch-only and inert. It was also the last consumer of the `CLAUDE_CI_APP_ID` and `CLAUDE_CI_APP_PRIVATE_KEY` org secrets. - `scripts/rollout-review-workflow.sh`: a one-shot script that pushed a per-repo wrapper calling `claude-code-review.yaml`. That reusable workflow was deleted on 2026-05-13 and no longer exists in this repo or any of the 31 org repos, so the script could only ever open PRs for a workflow that resolves to nothing. - README `PR Reviews`, `Scripts`, and the `claude-code-ci` GitHub App setup steps, which described the same retired flows. The `ANTHROPIC_API_KEY` org secret is deliberately kept in the setup table: it is still read by the active `reviewer-eval.yml` workflow in `open-swe`. The `CLAUDE_CI_APP_*` secrets are now unreferenced across the org, but this change only removes their documentation. Neither the secrets nor the App itself are touched. Setup steps are renumbered 1-3 with no gap, and the `see §5` reference in the IAM section is repointed to §3. actionlint 1.7.12 (the version pinned in ci.yaml) passes clean over the remaining workflows. --- .github/workflows/compliance-audit.yaml | 138 ------------------------ README.md | 54 ++-------- scripts/rollout-review-workflow.sh | 84 --------------- 3 files changed, 9 insertions(+), 267 deletions(-) delete mode 100644 .github/workflows/compliance-audit.yaml delete mode 100755 scripts/rollout-review-workflow.sh diff --git a/.github/workflows/compliance-audit.yaml b/.github/workflows/compliance-audit.yaml deleted file mode 100644 index 52debf3..0000000 --- a/.github/workflows/compliance-audit.yaml +++ /dev/null @@ -1,138 +0,0 @@ -# DEPRECATED (2026-06-10): The weekly org-wide compliance audit has been retired. -# The workflow is disabled in the Actions tab (state: disabled_manually) and the -# scheduled trigger has been removed so it cannot run automatically. Repo -# compliance is now handled via the Claude Code App on pull requests and the -# engineering handbook directly. Left in place (manual-dispatch only) for -# historical reference; safe to delete in a future cleanup. -name: Compliance Audit (DEPRECATED) - -on: - # schedule removed on deprecation — no longer runs weekly. - workflow_dispatch: - -permissions: - id-token: write - contents: read - issues: write - -jobs: - get-repos: - runs-on: ubuntu-latest - outputs: - repos: ${{ steps.list.outputs.repos }} - steps: - - name: Generate GitHub App token - id: app-token - uses: actions/create-github-app-token@v3 - with: - app-id: ${{ secrets.CLAUDE_CI_APP_ID }} - private-key: ${{ secrets.CLAUDE_CI_APP_PRIVATE_KEY }} - owner: Sea-Haven-Industries - - - name: List org repos - id: list - env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} - run: | - EXCLUDE="shoc-frontend-new shoc-backend" - repos=$(gh repo list Sea-Haven-Industries \ - --no-archived \ - --json name \ - --jq "[.[].name | select(. as \$n | \"$EXCLUDE\" | split(\" \") | index(\$n) | not)] | @json" \ - --limit 100) - echo "repos=$repos" >> "$GITHUB_OUTPUT" - - audit: - needs: get-repos - runs-on: ubuntu-latest - timeout-minutes: 15 - strategy: - fail-fast: false - max-parallel: 3 - matrix: - repo: ${{ fromJson(needs.get-repos.outputs.repos) }} - steps: - - name: Generate GitHub App token - id: app-token - uses: actions/create-github-app-token@v3 - with: - app-id: ${{ secrets.CLAUDE_CI_APP_ID }} - private-key: ${{ secrets.CLAUDE_CI_APP_PRIVATE_KEY }} - owner: Sea-Haven-Industries - repositories: ${{ matrix.repo }},engineering-handbook - - - name: Checkout repo - uses: actions/checkout@v7 - with: - repository: Sea-Haven-Industries/${{ matrix.repo }} - token: ${{ steps.app-token.outputs.token }} - - - name: Checkout engineering handbook - uses: actions/checkout@v7 - with: - repository: Sea-Haven-Industries/engineering-handbook - token: ${{ steps.app-token.outputs.token }} - path: .engineering-handbook - - - name: Run compliance audit - id: audit - uses: anthropics/claude-code-action@be7b93b1907a4abad570368f3c74b6fe3807510b # v1 - with: - anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} - prompt: | - Audit this repository for Sea Haven Industries compliance. - - The engineering handbook in `.engineering-handbook/` is the authoritative source for all conventions. Read every markdown file in that directory to understand the full set of standards, then audit this repo against them. - - Focus on these categories: - - **Naming:** kebab-case for all resource names in IaC templates, stack name matches repo name - - **Secrets:** no secrets in Lambda env vars or SSM, secrets belong in Secrets Manager with `stack-name/secret-name` naming - - **Lambda defaults:** Python 3.12+ or Node 22.x, arm64, explicit 60-day log retention in IaC - - **CI/CD:** pipeline exists with CI on PR and CD on push to main, using reusable workflows from `.github` repo - - **Git/GitHub:** branch protection on main, PR-based workflow, repo has a description - - **SAM layout:** template.yaml at root, samconfig.toml gitignored with .example committed, src/ directory structure - - **Project hygiene:** README describes architecture, .gitignore covers .env/.aws-sam/__pycache__, CloudFormation outputs include ARNs and URLs - - Return structured output with: - - `has_violations`: true only when one or more actual compliance violations are found. - - `report`: a concise markdown report with pass/fail per applicable item. - - Only flag actual violations — skip items that don't apply to this repo (e.g., skip Lambda checks if no Lambdas exist). - Do not create or modify files, issues, pull requests, or comments. - claude_args: | - --json-schema '{"type":"object","properties":{"has_violations":{"type":"boolean","description":"True when one or more actual compliance violations are found."},"report":{"type":"string","description":"Concise markdown report with pass/fail per applicable compliance item."}},"required":["has_violations","report"],"additionalProperties":false}' - - - name: Create issue if violations found - if: ${{ fromJSON(steps.audit.outputs.structured_output).has_violations == true }} - env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} - AUDIT_RESULT: ${{ steps.audit.outputs.structured_output }} - run: | - gh label create compliance \ - --repo "Sea-Haven-Industries/${{ matrix.repo }}" \ - --description "Weekly compliance audit" \ - --color "D93F0B" 2>/dev/null || true - existing=$(gh issue list \ - --repo "Sea-Haven-Industries/${{ matrix.repo }}" \ - --label "compliance" \ - --state open \ - --json number \ - --jq 'length') - if [ "$existing" -eq 0 ]; then - report=$(jq -r '.report' <<< "$AUDIT_RESULT") - body_file=$(mktemp) - { - echo "The weekly compliance audit found violations in this repo." - echo - echo "## Audit report" - echo - printf '%s\n' "$report" - echo - echo "Check the [latest audit run](https://github.com/Sea-Haven-Industries/.github/actions/workflows/compliance-audit.yaml) for details." - } > "$body_file" - gh issue create \ - --repo "Sea-Haven-Industries/${{ matrix.repo }}" \ - --title "Compliance audit: violations found" \ - --body-file "$body_file" \ - --label "compliance" - fi diff --git a/README.md b/README.md index 8d1fc95..a812b15 100644 --- a/README.md +++ b/README.md @@ -32,8 +32,6 @@ Organization-level GitHub configuration for Sea Haven Industries. **`.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. @@ -46,21 +44,13 @@ Third-party action refs across the org follow a tiered policy: - **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 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. @@ -121,39 +111,17 @@ Recovery in either case is an administrator action, not a pipeline retry: clear ## Setup -### 1. Create a GitHub App +### 1. Org-level secrets -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 +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. -### 2. Install the App +| Secret | Value | Consumed by | +|--------|-------|-------------| +| `ANTHROPIC_API_KEY` | Anthropic API key | `reviewer-eval.yml` in `open-swe` | -1. From the app's settings page, click **Install App** -2. Select `Sea-Haven-Industries` -3. Choose **All repositories** +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). -### 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 +### 2. Add CI to a repo Create `.github/workflows/ci.yaml` in the target repo. Examples: @@ -218,7 +186,7 @@ jobs: uses: Sea-Haven-Industries/.github/.github/workflows/ci-typescript-cdk.yaml@main ``` -### 5. Add CD to a repo +### 3. Add CD to a repo Create `.github/workflows/deploy.yaml` in the target repo. Requires `AWS_DEPLOY_ROLE_ARN` repo secret. @@ -323,7 +291,3 @@ Enable optional steps as repos adopt them: | `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. diff --git a/scripts/rollout-review-workflow.sh b/scripts/rollout-review-workflow.sh deleted file mode 100755 index 19d0fb4..0000000 --- a/scripts/rollout-review-workflow.sh +++ /dev/null @@ -1,84 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ORG="Sea-Haven-Industries" -BRANCH="add-claude-review" -WORKFLOW_PATH=".github/workflows/claude-review.yaml" -COMMIT_MSG="Add Claude Code review workflow" - -WORKFLOW_CONTENT='name: Claude Code Review - -on: - pull_request: - types: [opened, synchronize] - -permissions: - contents: read - pull-requests: write - id-token: write - -jobs: - review: - uses: Sea-Haven-Industries/.github/.github/workflows/claude-code-review.yaml@main - secrets: - anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} -' - -SKIP_REPOS=(".github" "shoc-frontend-new" "shoc-backend") - -should_skip() { - local repo="$1" - for skip in "${SKIP_REPOS[@]}"; do - if [[ "$repo" == "$skip" ]]; then - return 0 - fi - done - return 1 -} - -echo "Fetching non-archived repos from $ORG..." -repos=$(gh repo list "$ORG" --no-archived --json name --jq '.[].name' --limit 100) - -for repo in $repos; do - if should_skip "$repo"; then - echo "SKIP $repo (in skip list)" - continue - fi - - echo "" - echo "--- $repo ---" - - if gh api "repos/$ORG/$repo/contents/$WORKFLOW_PATH" --jq '.sha' > /dev/null 2>&1; then - echo "SKIP $repo (workflow already exists)" - continue - fi - - default_branch=$(gh api "repos/$ORG/$repo" --jq '.default_branch') - - encoded=$(echo -n "$WORKFLOW_CONTENT" | base64) - - gh api "repos/$ORG/$repo/git/refs" \ - -f "ref=refs/heads/$BRANCH" \ - -f "sha=$(gh api "repos/$ORG/$repo/git/ref/heads/$default_branch" --jq '.object.sha')" \ - 2>/dev/null || true - - gh api "repos/$ORG/$repo/contents/$WORKFLOW_PATH" \ - -X PUT \ - -f "message=$COMMIT_MSG" \ - -f "content=$encoded" \ - -f "branch=$BRANCH" \ - > /dev/null - - pr_url=$(gh pr create \ - --repo "$ORG/$repo" \ - --base "$default_branch" \ - --head "$BRANCH" \ - --title "$COMMIT_MSG" \ - --body "Adds a thin workflow that delegates PR reviews to the central reusable workflow in \`Sea-Haven-Industries/.github\`. Requires the \`ANTHROPIC_API_KEY\` org secret to be set." \ - 2>/dev/null || echo "PR already exists") - - echo "DONE $repo → $pr_url" -done - -echo "" -echo "Rollout complete. Review and merge the PRs, then delete the feature branches." From 8344efb90270c3bbc81c066570de77e47434c290 Mon Sep 17 00:00:00 2001 From: Adam Moussa Date: Tue, 28 Jul 2026 12:34:29 -0400 Subject: [PATCH 2/4] ci: add starter workflows for the five uncovered reusables The workflow-templates catalog offered starter workflows for only 7 of the 12 reusable workflows in .github/workflows, so ci-python-app, ci-typescript-frontend, ci-static, ci-dotnet and cd-mobile-ios were invisible in the org's Actions > New workflow UI and had to be wired by hand. Add a template + properties.json pair for each. Each caller CI job is keyed `ci` so the check context resolves to the `ci / ci` required by the org ruleset, and every reusable ref is pinned to the same 40-char SHA the existing templates use. node-version: "24" is passed on the three reusables that declare the input (ci-typescript-frontend, ci-static, cd-mobile-ios); ci-python-app and ci-dotnet do not declare it, so it is omitted there. --- workflow-templates/ci-dotnet.properties.json | 7 +++++++ workflow-templates/ci-dotnet.yml | 14 +++++++++++++ .../ci-python-app.properties.json | 7 +++++++ workflow-templates/ci-python-app.yml | 14 +++++++++++++ workflow-templates/ci-static.properties.json | 7 +++++++ workflow-templates/ci-static.yml | 18 ++++++++++++++++ .../ci-typescript-frontend.properties.json | 7 +++++++ workflow-templates/ci-typescript-frontend.yml | 14 +++++++++++++ .../mobile-ios-deploy.properties.json | 7 +++++++ workflow-templates/mobile-ios-deploy.yml | 21 +++++++++++++++++++ 10 files changed, 116 insertions(+) create mode 100644 workflow-templates/ci-dotnet.properties.json create mode 100644 workflow-templates/ci-dotnet.yml create mode 100644 workflow-templates/ci-python-app.properties.json create mode 100644 workflow-templates/ci-python-app.yml create mode 100644 workflow-templates/ci-static.properties.json create mode 100644 workflow-templates/ci-static.yml create mode 100644 workflow-templates/ci-typescript-frontend.properties.json create mode 100644 workflow-templates/ci-typescript-frontend.yml create mode 100644 workflow-templates/mobile-ios-deploy.properties.json create mode 100644 workflow-templates/mobile-ios-deploy.yml diff --git a/workflow-templates/ci-dotnet.properties.json b/workflow-templates/ci-dotnet.properties.json new file mode 100644 index 0000000..26ceb8a --- /dev/null +++ b/workflow-templates/ci-dotnet.properties.json @@ -0,0 +1,7 @@ +{ + "name": "Sea Haven — CI (.NET)", + "description": "Runs dotnet restore, build (Release), and dotnet test for a solution or project via the org reusable workflow.", + "iconName": "octicon-checklist", + "categories": ["C#", "Continuous integration"], + "filePatterns": ["\\.csproj$", "\\.sln$"] +} diff --git a/workflow-templates/ci-dotnet.yml b/workflow-templates/ci-dotnet.yml new file mode 100644 index 0000000..29aace2 --- /dev/null +++ b/workflow-templates/ci-dotnet.yml @@ -0,0 +1,14 @@ +name: CI (.NET) +on: + pull_request: + branches: [main] + +jobs: + ci: + # Job id MUST stay `ci`: the reusable's job is also `ci`, so the check + # context resolves to the required `ci / ci`. + # + # Every input is optional. Common overrides: `solution` (defaults to *.sln + # in the working directory), `working-directory`, and `dotnet-version` + # (defaults to 8.0.x). This reusable has no `node-version` input. + uses: Sea-Haven-Industries/.github/.github/workflows/ci-dotnet.yaml@555d07c3a240689a81668026787eba089df4c975 # main diff --git a/workflow-templates/ci-python-app.properties.json b/workflow-templates/ci-python-app.properties.json new file mode 100644 index 0000000..b625e4e --- /dev/null +++ b/workflow-templates/ci-python-app.properties.json @@ -0,0 +1,7 @@ +{ + "name": "Sea Haven — CI (Python / app)", + "description": "Runs ruff check, ruff format --check, a pytest collect-only import check, and an optional subproject suite via the org reusable workflow. For Python repos that do not deploy via SAM or CDK.", + "iconName": "octicon-checklist", + "categories": ["Python", "Continuous integration"], + "filePatterns": ["requirements.*\\.txt$", "pyproject\\.toml$"] +} diff --git a/workflow-templates/ci-python-app.yml b/workflow-templates/ci-python-app.yml new file mode 100644 index 0000000..9e23b13 --- /dev/null +++ b/workflow-templates/ci-python-app.yml @@ -0,0 +1,14 @@ +name: CI (Python / app) +on: + pull_request: + branches: [main] + +jobs: + ci: + # Job id MUST stay `ci`: the reusable's aggregator job is also `ci`, so the + # check context resolves to the required `ci / ci`. + # + # Every input is optional. Common overrides: `source-dirs` (ruff targets), + # `requirements` (non-default requirements file), `subproject-dir` (a + # self-contained suite that must run in its own working directory). + uses: Sea-Haven-Industries/.github/.github/workflows/ci-python-app.yaml@555d07c3a240689a81668026787eba089df4c975 # main diff --git a/workflow-templates/ci-static.properties.json b/workflow-templates/ci-static.properties.json new file mode 100644 index 0000000..e22cb76 --- /dev/null +++ b/workflow-templates/ci-static.properties.json @@ -0,0 +1,7 @@ +{ + "name": "Sea Haven — CI (Static Site)", + "description": "Validates static HTML/CSS/JS sites (S3 + CloudFront repos): htmlhint, JSON-LD parsing, sitemap.xml well-formedness, and internal link resolution via the org reusable workflow.", + "iconName": "octicon-checklist", + "categories": ["HTML", "Continuous integration"], + "filePatterns": ["index\\.html$"] +} diff --git a/workflow-templates/ci-static.yml b/workflow-templates/ci-static.yml new file mode 100644 index 0000000..b7a1705 --- /dev/null +++ b/workflow-templates/ci-static.yml @@ -0,0 +1,18 @@ +name: CI (Static Site) +on: + pull_request: + branches: [main] + +jobs: + ci: + # Job id MUST stay `ci`: the reusable's job is also `ci`, so the check + # context resolves to the required `ci / ci`. + uses: Sea-Haven-Industries/.github/.github/workflows/ci-static.yaml@555d07c3a240689a81668026787eba089df4c975 # main + with: + # Pinned explicitly (handbook): local dev is Node 24 / npm 11, which + # generates lockfileVersion 3. Being explicit avoids lockfile drift. + node-version: "24" + # Defaults to source mode — the checks run against the repo root. For a + # templated site (Eleventy, Astro), add `build-command` plus `check-dir` + # so the checks validate the BUILT output that actually ships; otherwise + # they pass vacuously against source templates that contain no HTML. diff --git a/workflow-templates/ci-typescript-frontend.properties.json b/workflow-templates/ci-typescript-frontend.properties.json new file mode 100644 index 0000000..73d8113 --- /dev/null +++ b/workflow-templates/ci-typescript-frontend.properties.json @@ -0,0 +1,7 @@ +{ + "name": "Sea Haven — CI (TypeScript / frontend)", + "description": "Runs the Sea Haven standards gate, format:check, lint, build, unit tests, and a Playwright browser smoke for bundled Vite/React/Vue apps via the org reusable workflow.", + "iconName": "octicon-checklist", + "categories": ["TypeScript", "JavaScript", "Continuous integration"], + "filePatterns": ["package\\.json$", "vite\\.config\\.[jt]s$", "playwright\\.config\\.[jt]s$"] +} diff --git a/workflow-templates/ci-typescript-frontend.yml b/workflow-templates/ci-typescript-frontend.yml new file mode 100644 index 0000000..3b75187 --- /dev/null +++ b/workflow-templates/ci-typescript-frontend.yml @@ -0,0 +1,14 @@ +name: CI (TypeScript / frontend) +on: + pull_request: + branches: [main] + +jobs: + ci: + # Job id MUST stay `ci`: the reusable's job is also `ci`, so the check + # context resolves to the required `ci / ci`. + uses: Sea-Haven-Industries/.github/.github/workflows/ci-typescript-frontend.yaml@555d07c3a240689a81668026787eba089df4c975 # main + with: + # Pinned explicitly (handbook): local dev is Node 24 / npm 11, which + # generates lockfileVersion 3. Being explicit avoids lockfile drift. + node-version: "24" diff --git a/workflow-templates/mobile-ios-deploy.properties.json b/workflow-templates/mobile-ios-deploy.properties.json new file mode 100644 index 0000000..fea029e --- /dev/null +++ b/workflow-templates/mobile-ios-deploy.properties.json @@ -0,0 +1,7 @@ +{ + "name": "Sea Haven — Deploy (iOS / TestFlight)", + "description": "Builds the iOS app with Fastlane and uploads it to TestFlight on push to main, using the org reusable cd-mobile-ios workflow. Requires the AWS_DEPLOY_ROLE_ARN, MATCH_PASSWORD, ASC_KEY_ID, ASC_ISSUER_ID and ASC_KEY_CONTENT repo secrets.", + "iconName": "octicon-rocket", + "categories": ["Deployment", "Mobile", "JavaScript"], + "filePatterns": ["Gemfile$", "app\\.json$", "metro\\.config\\.[cm]?js$"] +} diff --git a/workflow-templates/mobile-ios-deploy.yml b/workflow-templates/mobile-ios-deploy.yml new file mode 100644 index 0000000..ece8fd5 --- /dev/null +++ b/workflow-templates/mobile-ios-deploy.yml @@ -0,0 +1,21 @@ +name: Deploy (iOS / TestFlight) +on: + push: + branches: [main] + +jobs: + deploy: + uses: Sea-Haven-Industries/.github/.github/workflows/cd-mobile-ios.yaml@555d07c3a240689a81668026787eba089df4c975 # main + with: + # Pinned explicitly (handbook): local dev is Node 24 / npm 11, which + # generates lockfileVersion 3. Being explicit avoids lockfile drift. + node-version: "24" + secrets: + # All five are required. deploy-role-arn is the repo's OIDC role, used + # here to read the fastlane match certificate store from S3; the four + # asc-*/match-* values come from App Store Connect and the match repo. + deploy-role-arn: ${{ secrets.AWS_DEPLOY_ROLE_ARN }} + match-password: ${{ secrets.MATCH_PASSWORD }} + asc-key-id: ${{ secrets.ASC_KEY_ID }} + asc-issuer-id: ${{ secrets.ASC_ISSUER_ID }} + asc-key-content: ${{ secrets.ASC_KEY_CONTENT }} From 7ece0b6c2aed7eaa828c68e8427a889965b003b7 Mon Sep 17 00:00:00 2001 From: Adam Moussa Date: Tue, 28 Jul 2026 12:35:00 -0400 Subject: [PATCH 3/4] fix(ci): pass deploy inputs via env, not shell interpolation GitHub Actions expressions are substituted into a run body as text before bash parses it, so a value carrying a quote, a command substitution, or a newline becomes shell syntax rather than data. cd-sam.yaml interpolated the parameter-overrides secret straight into a shell test and an assignment, putting secret material into the script body. cd-cdk.yaml interpolated the caller-supplied post-deploy-script input into a bash invocation, which is caller-controlled command injection rather than secret exposure. Both now use env-var indirection, matching the STACKS precedent in the CDK deploy step. PARAM_OVERRIDES is deliberately left unquoted at the point of use: parameter-overrides carries multiple Key=Value pairs that must reach sam deploy as separate argv entries, so quoting it would collapse every override into one argument and break deploys that use it. POST_DEPLOY_SCRIPT is a single path and is quoted. Behaviour is otherwise unchanged. An empty parameter-overrides still produces no --parameter-overrides flag at all, and an empty post-deploy-script is still skipped by the step-level if condition, which is a workflow expression and not shell. --- .github/workflows/cd-cdk.yaml | 7 ++++++- .github/workflows/cd-sam.yaml | 9 +++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cd-cdk.yaml b/.github/workflows/cd-cdk.yaml index 6a12f41..510cc45 100644 --- a/.github/workflows/cd-cdk.yaml +++ b/.github/workflows/cd-cdk.yaml @@ -134,7 +134,12 @@ jobs: - name: Post-deploy script if: ${{ inputs.post-deploy-script != '' }} - run: bash ${{ inputs.post-deploy-script }} + # Env-var indirection (not inline expression interpolation) so shell + # metacharacters in the input are never parsed as script; the input is a + # single script path, so $POST_DEPLOY_SCRIPT is quoted (no word-split). + env: + POST_DEPLOY_SCRIPT: ${{ inputs.post-deploy-script }} + run: bash "$POST_DEPLOY_SCRIPT" - name: Post-deploy health check if: ${{ inputs.stack-name != '' }} diff --git a/.github/workflows/cd-sam.yaml b/.github/workflows/cd-sam.yaml index 249998b..5628911 100644 --- a/.github/workflows/cd-sam.yaml +++ b/.github/workflows/cd-sam.yaml @@ -80,10 +80,15 @@ jobs: run: sam build --template ${{ inputs.sam-template }} - name: SAM deploy + # Env-var indirection (not inline expression interpolation) so shell + # metacharacters in the secret are never parsed as script; unquoted + # $PARAM_OVERRIDES deliberately word-splits multiple Key=Value pairs. + env: + PARAM_OVERRIDES: ${{ secrets.parameter-overrides }} run: | PARAMS="" - if [ -n "${{ secrets.parameter-overrides }}" ]; then - PARAMS="--parameter-overrides ${{ secrets.parameter-overrides }}" + if [ -n "$PARAM_OVERRIDES" ]; then + PARAMS="--parameter-overrides $PARAM_OVERRIDES" fi sam deploy \ --stack-name ${{ inputs.stack-name }} \ From 5889d52333521e3cf3c848529bcda580ff97f55f Mon Sep 17 00:00:00 2001 From: Adam Moussa Date: Tue, 28 Jul 2026 12:46:17 -0400 Subject: [PATCH 4/4] ci: enable actionlint's shellcheck integration in self-CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The self-CI gate ran `./actionlint -shellcheck=`, and the empty value silently disabled the shell-linting half of the check — so every `run:` body in the reusable workflows this repo publishes was unlinted, on the exact path that deploys to AWS. Measured against the pinned actionlint 1.7.12 and the shellcheck the ubuntu-latest runner ships (0.9.0-1), the real backlog was 5 findings, not the 4 the old comment claimed. Three were genuine and are fixed in the shell: - cd-cdk.yaml "Publish .NET project" (SC2046): the project path was interpolated inline and `$(dirname ...)` was unquoted, so a path containing whitespace split into several arguments. Now passed via env indirection and quoted, which also removes the last inline expression interpolation from that step. - cd-cdk.yaml / ci-python-sam.yaml "Install Python dependencies" (SC2044 x2): `for req in $(find ...)` word-split and globbed every path found. Replaced with a NUL-delimited `while read` loop. Two are deliberate and are suppressed per-line, with the reasoning in a comment directly above: - cd-sam.yaml `sam deploy ... $PARAMS` and cd-cdk.yaml `cdk deploy $STACKS` (SC2086 x2) rely on word-splitting so multiple parameter overrides / stack selectors reach the CLI as separate argv entries. Quoting them would collapse each into a single argument and break every parameterised or multi-stack deploy, so they keep the unquoted expansion and carry a scoped `# shellcheck disable=SC2086`. The gate now runs plain `./actionlint` (shellcheck defaults to the binary on PATH) and prints `shellcheck --version` first, so the check fails loudly if a future runner image drops it instead of quietly linting less. --- .github/workflows/cd-cdk.yaml | 28 ++++++++++++++++++++++++---- .github/workflows/cd-sam.yaml | 6 ++++++ .github/workflows/ci-python-sam.yaml | 7 +++++-- .github/workflows/ci.yaml | 22 ++++++++++++++++------ README.md | 2 +- 5 files changed, 52 insertions(+), 13 deletions(-) diff --git a/.github/workflows/cd-cdk.yaml b/.github/workflows/cd-cdk.yaml index 510cc45..1246d46 100644 --- a/.github/workflows/cd-cdk.yaml +++ b/.github/workflows/cd-cdk.yaml @@ -69,7 +69,18 @@ jobs: - name: Publish .NET project if: ${{ inputs.dotnet-publish-project != '' }} - run: dotnet publish ${{ inputs.dotnet-publish-project }} --configuration Release --runtime linux-arm64 --self-contained false --output $(dirname ${{ inputs.dotnet-publish-project }})/bin/Release/net8.0/linux-arm64/publish + # Env-var indirection (not inline expression interpolation) so shell + # metacharacters in the input are never parsed as script; the input is a + # single project path, so it stays quoted (no word-split) — an unquoted + # $(dirname ...) split the output path on whitespace. + env: + DOTNET_PUBLISH_PROJECT: ${{ inputs.dotnet-publish-project }} + run: | + dotnet publish "$DOTNET_PUBLISH_PROJECT" \ + --configuration Release \ + --runtime linux-arm64 \ + --self-contained false \ + --output "$(dirname "$DOTNET_PUBLISH_PROJECT")/bin/Release/net8.0/linux-arm64/publish" - uses: actions/setup-node@v7 with: @@ -89,10 +100,13 @@ jobs: - name: Install Python dependencies if: ${{ inputs.python-version != '' }} + # NUL-delimited read loop rather than `for req in $(find ...)`: the + # command-substitution form word-splits and globs every path it finds. + shell: bash run: | - for req in $(find . -name requirements.txt -not -path '*/node_modules/*'); do + while IFS= read -r -d '' req; do pip install -r "$req" - done + done < <(find . -name requirements.txt -not -path '*/node_modules/*' -print0) - uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6 with: @@ -130,7 +144,13 @@ jobs: # $STACKS deliberately word-splits multiple selectors. env: STACKS: ${{ inputs.stacks }} - run: npx -y cdk deploy $STACKS --require-approval never + run: | + # $STACKS is deliberately unquoted: it carries one or more + # space-separated CDK stack selectors (default "--all") that must reach + # `cdk deploy` as separate argv entries. Quoting it would collapse them + # into one bogus selector and break every multi-stack deploy. + # shellcheck disable=SC2086 + npx -y cdk deploy $STACKS --require-approval never - name: Post-deploy script if: ${{ inputs.post-deploy-script != '' }} diff --git a/.github/workflows/cd-sam.yaml b/.github/workflows/cd-sam.yaml index 5628911..5b81e36 100644 --- a/.github/workflows/cd-sam.yaml +++ b/.github/workflows/cd-sam.yaml @@ -90,6 +90,12 @@ jobs: if [ -n "$PARAM_OVERRIDES" ]; then PARAMS="--parameter-overrides $PARAM_OVERRIDES" fi + # $PARAMS is deliberately unquoted: it is either empty (no overrides, + # so no flag at all) or "--parameter-overrides Key=Value [Key=Value…]", + # which must reach `sam deploy` as separate argv entries. Quoting it + # would pass one empty or one concatenated argument and break every + # parameterised deploy. + # shellcheck disable=SC2086 sam deploy \ --stack-name ${{ inputs.stack-name }} \ --template-file .aws-sam/build/template.yaml \ diff --git a/.github/workflows/ci-python-sam.yaml b/.github/workflows/ci-python-sam.yaml index 03b8df5..ac35356 100644 --- a/.github/workflows/ci-python-sam.yaml +++ b/.github/workflows/ci-python-sam.yaml @@ -72,13 +72,16 @@ jobs: - name: Install Python dependencies if: ${{ inputs.run-tests || inputs.run-cdk-synth }} + # NUL-delimited read loop rather than `for req in $(find ...)`: the + # command-substitution form word-splits and globs every path it finds. + shell: bash run: | if [ "${{ inputs.run-tests }}" = "true" ]; then pip install pytest fi - for req in $(find . -name requirements.txt -not -path './.aws-sam/*'); do + while IFS= read -r -d '' req; do pip install -r "$req" - done + done < <(find . -name requirements.txt -not -path './.aws-sam/*' -print0) - name: Run tests if: ${{ inputs.run-tests }} diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 66d50d9..80e2e63 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -23,11 +23,19 @@ name: ci # supply-chain surface auditable. Bump ACTIONLINT_VERSION + ACTIONLINT_SHA256 # together (checksum from the release's *_checksums.txt). # -# actionlint's shellcheck integration is disabled (`-shellcheck=`) for now: it -# reports 4 pre-existing findings in the deploy/CI run-steps (SC2044 find-in-for -# loops, SC2046/SC2086 quoting, one of which is intentional word-splitting in the -# SAM deploy step). Those deserve a separate, tested cleanup rather than being -# bundled into the gate that unblocks the repo. Re-enable shellcheck once fixed. +# actionlint's shellcheck integration is ON (it defaults to the `shellcheck` on +# PATH; the ubuntu-latest runner image ships shellcheck 0.9.0, so nothing extra +# is installed). Do NOT re-add `-shellcheck=` — the empty value silently turns +# the whole shell-linting half of this gate back off. +# +# Two run-steps carry a narrowly-scoped `# shellcheck disable=SC2086` on the +# single line above the command, because the unquoted expansion there is the +# point: `sam deploy … $PARAMS` (cd-sam.yaml) and `cdk deploy $STACKS` +# (cd-cdk.yaml) rely on word-splitting to turn one variable into several argv +# entries. Quoting them would collapse multiple parameter overrides or stack +# selectors into one argument and break those deploys. Every other finding was +# fixed in the shell rather than suppressed. Suppressions stay per-line and +# commented — never file-wide, and never by weakening this invocation. on: pull_request: @@ -56,5 +64,7 @@ jobs: shell: bash - name: Lint workflows - run: ./actionlint -color -shellcheck= + run: | + shellcheck --version + ./actionlint -color shell: bash diff --git a/README.md b/README.md index 8d1fc95..51d9839 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Organization-level GitHub configuration for Sea Haven Industries. **`.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/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. **`.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.