diff --git a/.github/workflows/cd-infra.yml b/.github/workflows/cd-infra.yml new file mode 100644 index 00000000..61c29001 --- /dev/null +++ b/.github/workflows/cd-infra.yml @@ -0,0 +1,122 @@ +name: Infra CD + +# Path-filtered CDK deploy for /infra, per env, OIDC-only (no static keys). +# +# push to dev → CI (tsc+jest+synth) → deploy OpenSweDevStack (AUTO, CI-green-gated) +# push to main → CI → deploy OpenSweProdStack (manual approval: env "prod") +# +# Why this is NOT the reusable cd-cdk.yaml: that workflow runs `cdk deploy --all`, +# which would deploy ALL THREE stacks (incl. the OTHER env + the shared IAM stack) +# from a single-env push — breaking the per-env dev/prod boundary. So we target one +# stack explicitly per env. (Infra CI still uses the reusable ci-typescript-cdk.) +# +# The shared IAM stack (open-swe-iam — owns BOTH envs' OIDC deploy roles) is +# intentionally NOT deployed here: it is a privileged, human-gated apply (T6), so a +# routine dev push can never alter prod's deploy role. +# +# OIDC subject alignment (must match the per-env trust in infra/lib/config.ts): +# - deploy-dev declares NO `environment:` → token sub = repo:…:ref:refs/heads/dev, +# which is exactly what githubdeploy-open-swe-infra-dev trusts. +# - deploy-prod declares `environment: prod` → token sub = repo:…:environment:prod, +# which githubdeploy-open-swe-infra-prod trusts AND which triggers the GitHub +# Environment's required-reviewer (manual approval) gate. +# +# Prerequisites (post-T6, when the roles exist): +# - repo variables AWS_DEPLOY_ROLE_INFRA_DEV / AWS_DEPLOY_ROLE_INFRA_PROD = the +# githubdeploy-open-swe-infra- role ARNs (open-swe-iam CfnOutputs). +# - a GitHub Environment named "prod" with Adam as a required reviewer. + +permissions: + contents: read + +on: + push: + branches: [dev, main] + paths: + - "infra/**" + - ".github/workflows/cd-infra.yml" + workflow_dispatch: + +concurrency: + # one infra deploy per branch at a time; never cancel an in-flight deploy. + group: cd-infra-${{ github.ref }} + cancel-in-progress: false + +jobs: + # CI-green precondition — re-run tsc + jest + synth on the pushed commit before + # any deploy. A failure here blocks the deploy jobs (needs: ci). + ci: + name: Infra CI (pre-deploy) + uses: Sea-Haven-Industries/.github/.github/workflows/ci-typescript-cdk.yaml@main + with: + node-version: "24" + working-directory: infra + cache-dependency-path: infra/package-lock.json + run-typecheck: true + run-tests: true + run-cdk-synth: true + + deploy-dev: + name: Deploy open-swe-dev + needs: ci + if: ${{ github.ref == 'refs/heads/dev' }} + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + id-token: write + contents: read + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v4 + with: + node-version: "24" + cache: npm + cache-dependency-path: infra/package-lock.json + - name: Install deps + working-directory: infra + run: npm ci + - uses: aws-actions/configure-aws-credentials@v6 + with: + role-to-assume: ${{ vars.AWS_DEPLOY_ROLE_INFRA_DEV }} + aws-region: us-east-1 + - name: CDK deploy (dev only) + working-directory: infra + run: npx cdk deploy OpenSweDevStack --require-approval never + - name: Stack outputs + run: | + aws cloudformation describe-stacks --stack-name open-swe-dev \ + --query 'Stacks[0].Outputs[*].[OutputKey,OutputValue]' --output table + + deploy-prod: + name: Deploy open-swe-prod + needs: ci + if: ${{ github.ref == 'refs/heads/main' }} + runs-on: ubuntu-latest + timeout-minutes: 30 + # Manual-approval gate: the "prod" Environment requires a reviewer (Adam). + # Also makes the OIDC sub …:environment:prod (matches the prod role trust). + environment: prod + permissions: + id-token: write + contents: read + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v4 + with: + node-version: "24" + cache: npm + cache-dependency-path: infra/package-lock.json + - name: Install deps + working-directory: infra + run: npm ci + - uses: aws-actions/configure-aws-credentials@v6 + with: + role-to-assume: ${{ vars.AWS_DEPLOY_ROLE_INFRA_PROD }} + aws-region: us-east-1 + - name: CDK deploy (prod only) + working-directory: infra + run: npx cdk deploy OpenSweProdStack --require-approval never + - name: Stack outputs + run: | + aws cloudformation describe-stacks --stack-name open-swe-prod \ + --query 'Stacks[0].Outputs[*].[OutputKey,OutputValue]' --output table diff --git a/.github/workflows/ci-infra.yml b/.github/workflows/ci-infra.yml new file mode 100644 index 00000000..8e82152d --- /dev/null +++ b/.github/workflows/ci-infra.yml @@ -0,0 +1,26 @@ +name: Infra CI + +# Path-filtered CI for the /infra CDK app (TypeScript). The existing "Agent CI" +# (ci.yml) covers the Python agent; this adds tsc + jest + cdk synth for /infra so +# infra changes are gated on a PR the same way. Runs only when /infra changes. + +permissions: + contents: read + +on: + pull_request: + paths: + - "infra/**" + - ".github/workflows/ci-infra.yml" + +jobs: + infra-ci: + name: Infra CI (tsc + jest + synth) + uses: Sea-Haven-Industries/.github/.github/workflows/ci-typescript-cdk.yaml@main + with: + node-version: "24" + working-directory: infra + cache-dependency-path: infra/package-lock.json + run-typecheck: true + run-tests: true + run-cdk-synth: true diff --git a/infra/.gitignore b/infra/.gitignore index a160da54..ba0cddd0 100644 --- a/infra/.gitignore +++ b/infra/.gitignore @@ -3,6 +3,8 @@ cdk.out/ *.js *.d.ts *.js.map +# ...but jest.config.js is hand-authored config, not build output — keep it. +!jest.config.js # deps node_modules/ diff --git a/infra/README.md b/infra/README.md index 495e4669..e1d4b0a6 100644 --- a/infra/README.md +++ b/infra/README.md @@ -240,15 +240,56 @@ npx cdk synth open-swe-prod npm test # jest — naming Aspect ``` +## CI/CD (T18 — `.github/workflows/ci-infra.yml` + `cd-infra.yml`) + +Path-filtered, OIDC-only (no static keys). The Python agent keeps its own +`ci.yml` ("Agent CI"); these two add the `/infra` half. + +| Workflow | Trigger | Does | +|---|---|---| +| `ci-infra.yml` | PR touching `infra/**` | `tsc` + `jest` + `cdk synth` (reusable `ci-typescript-cdk.yaml`). | +| `cd-infra.yml` | push to `dev`/`main` touching `infra/**`, or dispatch | CI (pre-deploy) → per-env `cdk deploy`. | + +`cd-infra.yml` flow: + +- **push to `dev`** → CI green → **auto** `cdk deploy OpenSweDevStack` (assumes + `githubdeploy-open-swe-infra-dev`; the job declares **no** `environment:`, so the + OIDC subject is `…:ref:refs/heads/dev` — matching that role's trust). +- **push to `main`** → CI green → `cdk deploy OpenSweProdStack` behind the + **`prod` GitHub Environment** (required reviewer = Adam). The `environment: prod` + declaration both fires the manual-approval gate and makes the OIDC subject + `…:environment:prod` — matching `githubdeploy-open-swe-infra-prod`'s trust. + +**Why not the reusable `cd-cdk.yaml`:** it runs `cdk deploy --all`, which from a +single-env push would deploy the *other* env + the shared IAM stack — breaking the +per-env boundary. So CD targets one stack explicitly per env. The shared +`open-swe-iam` stack is **not** deployed by CD (privileged, human-gated — T6). + +**Gating note:** infra CI is enforced at the *deploy* boundary (`cd-infra`'s +`deploy-*` jobs `needs: ci`), not as a branch-protection required check — +path-filtering a *required* check would deadlock app-only PRs (a skipped required +check never satisfies). Making `Infra CI` a required check later needs a +skip-aware shim or dropping its path filter. + +**Prerequisites (set post-T6, when the roles exist):** + +- repo **variables** `AWS_DEPLOY_ROLE_INFRA_DEV` / `AWS_DEPLOY_ROLE_INFRA_PROD` + = the `githubdeploy-open-swe-infra-` role ARNs (`open-swe-iam` outputs). +- a GitHub **Environment** named `prod` with Adam as a required reviewer. + +> App-side CD (CI → S3 artifact → SSM deploy via `githubdeploy-open-swe-app-`) +> is **T19**, not here. + ## Deploy ordering (when the gate clears — NOT yet) -1. **`open-swe-iam` first** — create `githubdeploy-open-swe-infra` + set the repo - `AWS_DEPLOY_ROLE_ARN` secret before any infra/secrets CI step (BLOCK#3). +1. **`open-swe-iam` first** — apply the IAM stack (T6, human-gated), then set the + repo `AWS_DEPLOY_ROLE_INFRA_{DEV,PROD}` variables from its role-ARN outputs and + configure the `prod` Environment reviewer (BLOCK#3). 2. **Security gate** — T4 GPT-4.1 IAM cross-review + T5 `/sh-security-review` on the synth; resolve every confirmed critical/high. 3. **IAM applied** (T6) — only after the gate. -4. Env stacks (`open-swe-dev`, then `open-swe-prod`) build out at T12+, prod gated - by a GitHub Environment manual approval. +4. Env stacks: first `open-swe-dev` (T14, manual validate), then CD auto-deploys + dev on push; `open-swe-prod` (T21) behind the `prod` Environment approval. ## Version policy diff --git a/infra/jest.config.js b/infra/jest.config.js new file mode 100644 index 00000000..66512147 --- /dev/null +++ b/infra/jest.config.js @@ -0,0 +1,9 @@ +module.exports = { + testEnvironment: "node", + roots: ["/test"], + testMatch: ["**/*.test.ts"], + preset: "ts-jest", + transform: { + "^.+\\.tsx?$": ["ts-jest", { tsconfig: "tsconfig.json" }], + }, +};