mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 17:23:15 +00:00
Some checks are pending
Build & publish app artifacts / Publish + deploy (dev) (push) Waiting to run
Build & publish app artifacts / Publish + deploy (prod) (push) Waiting to run
Infra CD / Infra CI (pre-deploy) (push) Waiting to run
Infra CD / Deploy open-swe-dev (push) Blocked by required conditions
Infra CD / Deploy open-swe-prod (push) Blocked by required conditions
Agent CI / Agent lint (push) Waiting to run
Agent CI / Agent format check (push) Waiting to run
Agent CI / Agent unit tests (push) Waiting to run
Agent CI / Playwright E2E (push) Waiting to run
T20 CD safety nets. Two gaps closed before the first real prod deploy: 1. Promotion gate. promote_dev_to_prod.yml previously fast-forwarded dev→main unconditionally. It now hard-gates on check-dev-green.sh: every check-run on the dev HEAD must be completed+passing AND the Agent CI suite (lint/format/unit/E2E) must be present+success, or the promotion blocks (fails safe on a missing/renamed check). The promote run excludes its OWN check-run by run-id (unforgeable), never by the mutable name "promote", so a colliding red check cannot hide. Fields are read with a 0x1F separator so an empty conclusion (every in_progress check) cannot shift columns. ci.yml now also runs on push:dev so dev HEAD actually carries that signal (a PR check alone can be admin-merged past). 2. Rollback + last-good. publish-and-deploy.sh advances releases/last-good/ only after a successful roll (deploy.sh gates on `systemctl is-active`), and makes releases/latest/ transactional — reverting to the prior release if the roll fails so a replaced box never self-deploys a broken release. New rollback.yml + rollback.sh re-point latest at last-good (or an explicit sha) and re-fire the deploy; prod is gated by the `prod` Environment approval, same as a deploy. The shared fire/wait/aggregate-gate logic is factored into roll-box.sh (used by both forward and backward rolls). Least-privilege: drop the unused s3:DeleteObject from the app deploy role — publish/rollback/deploy only Get+Put (S3-to-S3 copy), and the rollback fallback now depends on immutable release history staying intact. Lifecycle expiry (not CI) handles old-version cleanup. Gate logic unit-tested (7 cases + jq round-trip). IAM change + release-safety control cross-reviewed by GPT-4.1: APPROVE, no blocks. Claude-Session: https://claude.ai/code/session_01DMhLf4G5V8MStJQyAW95hi
161 lines
7.1 KiB
TypeScript
161 lines
7.1 KiB
TypeScript
import * as iam from "aws-cdk-lib/aws-iam";
|
|
import { Construct } from "constructs";
|
|
import {
|
|
ACCOUNT,
|
|
EnvName,
|
|
GITHUB_OIDC_PROVIDER_ARN,
|
|
REGION,
|
|
oidcSubject,
|
|
} from "../config";
|
|
|
|
/**
|
|
* Per-ENV GitHub Actions OIDC deploy roles. Created ONCE per env in the
|
|
* dedicated `open-swe-iam` stack. Two roles per env, per the locked architecture's
|
|
* "dual OIDC roles":
|
|
*
|
|
* - githubdeploy-open-swe-infra-<env> → CFN/IAM (CDK) deploys of that env's stack
|
|
* - githubdeploy-open-swe-app-<env> → app deploys (env-tag-scoped SSM + S3 read)
|
|
*
|
|
* T5 OSWE-IAC-01/02 fix: roles are split per env and the trust subject is
|
|
* env-scoped (dev = dev branch ref; prod = the GitHub `prod` Environment subject,
|
|
* so the manual-approval gate is IAM-enforced). A dev-branch token therefore
|
|
* cannot SendCommand to the prod box nor assume a prod deploy role.
|
|
*
|
|
* Reviewed at T4 (GPT-4.1 IAM cross-review) + T5 (/sh-security-review) and
|
|
* deployed FIRST (BLOCK#3 "OIDC-role-first" ordering) before any other infra or
|
|
* secrets CI step.
|
|
*/
|
|
export class GithubDeployRoles extends Construct {
|
|
public readonly infraRole: iam.Role;
|
|
public readonly appRole: iam.Role;
|
|
|
|
constructor(scope: Construct, id: string, envName: EnvName) {
|
|
super(scope, id);
|
|
|
|
// The provider already exists account-wide — reference, never re-create.
|
|
const provider = iam.OpenIdConnectProvider.fromOpenIdConnectProviderArn(
|
|
this,
|
|
"GithubOidcProvider",
|
|
GITHUB_OIDC_PROVIDER_ARN,
|
|
);
|
|
|
|
// T4 BLOCK#2 + T5 IAC-01/02: exact env-scoped subject via StringEquals (no
|
|
// StringLike, no `*`). prod = environment:prod (manual-approval gate),
|
|
// dev = the dev branch ref.
|
|
const trust = new iam.WebIdentityPrincipal(provider.openIdConnectProviderArn, {
|
|
StringEquals: {
|
|
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
|
|
"token.actions.githubusercontent.com:sub": oidcSubject(envName),
|
|
},
|
|
});
|
|
|
|
// ---- githubdeploy-open-swe-infra-<env> --------------------------------
|
|
this.infraRole = new iam.Role(this, "InfraDeployRole", {
|
|
roleName: `githubdeploy-open-swe-infra-${envName}`,
|
|
assumedBy: trust,
|
|
description: `GitHub OIDC role for CDK deploys of the open-swe-${envName} infra stack (assumes CDK bootstrap roles).`,
|
|
});
|
|
|
|
// Org-standard CDK deploy pattern (mirrors githubdeploy-seahaven-account-
|
|
// baseline / -forgejo / -apm-wo-analysis): the deploy role only needs to
|
|
// assume the CDK bootstrap roles. The actual CloudFormation + IAM + resource
|
|
// permissions are exercised by the bootstrap `cfn-exec-role`, whose scope is
|
|
// owned by the CDKToolkit stack — NOT granted directly here.
|
|
//
|
|
// T4 BLOCK#1: GPT-4.1 flagged the `cdk-hnb659fds-*` wildcard and recommended
|
|
// enumerating the four exact ARNs. ACCEPTED EXCEPTION (Adam, 2026-06-26): kept
|
|
// as the verified org-wide convention (githubdeploy-seahaven-account-baseline
|
|
// uses the identical wildcard). Only `cdk bootstrap` creates roles with this
|
|
// prefix, so practical escalation risk is low.
|
|
// T5 residual (OSWE-IAC-02): the single account-wide cfn-exec-role means the
|
|
// dev infra role can technically deploy any stack; per-env trust gates WHO can
|
|
// assume, and the prod role requires the environment:prod approval. Per-env
|
|
// bootstrap qualifiers would close the residual fully (future hardening).
|
|
this.infraRole.addToPolicy(
|
|
new iam.PolicyStatement({
|
|
sid: "AssumeCdkBootstrapRoles",
|
|
actions: ["sts:AssumeRole"],
|
|
resources: [`arn:aws:iam::${ACCOUNT}:role/cdk-hnb659fds-*`],
|
|
}),
|
|
);
|
|
|
|
// ---- githubdeploy-open-swe-app-<env> ----------------------------------
|
|
this.appRole = new iam.Role(this, "AppDeployRole", {
|
|
roleName: `githubdeploy-open-swe-app-${envName}`,
|
|
assumedBy: trust,
|
|
description: `GitHub OIDC role for open-swe-${envName} app deploys: env-tag-scoped ssm:SendCommand + read of the ${envName} S3 artifact bucket.`,
|
|
});
|
|
|
|
// T5 OSWE-IAC-01 fix: SendCommand only to instances tagged project=open-swe
|
|
// AND env=<this env> (a SINGLE value, not {dev,prod}). The dev app role can
|
|
// never command the prod box and vice versa — env isolation in IAM.
|
|
this.appRole.addToPolicy(
|
|
new iam.PolicyStatement({
|
|
sid: "SsmSendCommandTagScoped",
|
|
actions: ["ssm:SendCommand"],
|
|
resources: [`arn:aws:ec2:${REGION}:${ACCOUNT}:instance/*`],
|
|
conditions: {
|
|
StringEquals: {
|
|
"ssm:resourceTag/project": "open-swe",
|
|
"ssm:resourceTag/env": envName,
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
|
|
// SendCommand also has to reference the command document. Scope to this env's
|
|
// open-swe deploy document ONLY.
|
|
// T4 BLOCK#3 (CLOSED at T19): GPT-4.1 flagged AWS-RunShellScript as an
|
|
// arbitrary-shell escalation path. The dedicated `open-swe-${envName}-deploy`
|
|
// SSM document (app-service.ts) now runs the fixed, parameter-less command
|
|
// `bash /opt/open-swe/bin/deploy.sh`, so AWS-RunShellScript is dropped here:
|
|
// this role can run ONLY that one document, and only on its own env's box
|
|
// (tag-scoped by the SsmSendCommandTagScoped statement above).
|
|
this.appRole.addToPolicy(
|
|
new iam.PolicyStatement({
|
|
sid: "SsmSendCommandDocuments",
|
|
actions: ["ssm:SendCommand"],
|
|
resources: [`arn:aws:ssm:${REGION}:${ACCOUNT}:document/open-swe-${envName}-deploy`],
|
|
}),
|
|
);
|
|
|
|
// Poll command results. These read actions do not support resource-level
|
|
// scoping, so `*` is required by the API (T4 FIX: API limitation, documented).
|
|
this.appRole.addToPolicy(
|
|
new iam.PolicyStatement({
|
|
sid: "SsmReadCommandStatus",
|
|
actions: [
|
|
"ssm:GetCommandInvocation",
|
|
"ssm:ListCommands",
|
|
"ssm:ListCommandInvocations",
|
|
],
|
|
resources: ["*"],
|
|
}),
|
|
);
|
|
|
|
// Read+WRITE access to THIS env's artifact bucket only (T19): the
|
|
// build-artifacts workflow uploads app.tar.gz / spa.tar.gz under releases/*,
|
|
// then fires the deploy document so the box pulls them via its instance role.
|
|
// Object actions are scoped to releases/* (the only prefix CI writes), and to
|
|
// THIS env's bucket — a dev token can never write the prod bucket. No
|
|
// bucket-level mutation (no PutBucket*/Delete bucket) — that stays with CDK.
|
|
// GetObject + PutObject (S3-to-S3 copy = Get source + Put dest) is all the
|
|
// publish/rollback path uses; s3:DeleteObject is deliberately NOT granted so a
|
|
// CI token cannot erase an immutable release or the releases/last-good rollback
|
|
// fallback (lifecycle expiry handles old-version cleanup, not CI).
|
|
this.appRole.addToPolicy(
|
|
new iam.PolicyStatement({
|
|
sid: "ReadWriteArtifactObjects",
|
|
actions: ["s3:GetObject", "s3:PutObject"],
|
|
resources: [`arn:aws:s3:::open-swe-${envName}-assets/releases/*`],
|
|
}),
|
|
);
|
|
this.appRole.addToPolicy(
|
|
new iam.PolicyStatement({
|
|
sid: "ListArtifactBucket",
|
|
actions: ["s3:ListBucket", "s3:GetBucketLocation"],
|
|
resources: [`arn:aws:s3:::open-swe-${envName}-assets`],
|
|
}),
|
|
);
|
|
}
|
|
}
|