From 8248018af57bede74a5ef4b00e3306a6ff773b38 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 7 Sep 2026 21:52:06 +0000 Subject: [PATCH 1/5] ci(codebuild): add oidc linux x64/arm64 status checks Wire GitHub Actions to dedicated CodeBuild projects via OIDC so PR/main CI can run the heavy Linux suite on x64 and arm64 without long-lived keys. Existing workflows stay required; Windows CodeBuild is documented as a follow-up only. Co-authored-by: Mathis --- .github/workflows/ci.yml | 5 + .github/workflows/codebuild.yml | 149 +++++++++++ AGENTS.md | 1 + README.md | 2 + deploy/aws/codebuild/README.md | 148 +++++++++++ deploy/aws/codebuild/buildspec-ci.yml | 59 +++++ deploy/aws/codebuild/cloudformation.yaml | 247 ++++++++++++++++++ deploy/aws/codebuild/iam-gha-permissions.json | 28 ++ deploy/aws/codebuild/iam-trust-policy.json | 21 ++ deploy/aws/codebuild/install-deps.sh | 143 ++++++++++ deploy/aws/codebuild/run-ci.sh | 66 +++++ docs/CI_SECRETS.md | 19 ++ docs/CONTRIBUTING.md | 6 + docs/README.md | 1 + docs/guides/development.md | 6 + docs/guides/quality.md | 2 + scripts/readiness/test_codebuild.py | 163 ++++++++++++ 17 files changed, 1066 insertions(+) create mode 100644 .github/workflows/codebuild.yml create mode 100644 deploy/aws/codebuild/README.md create mode 100644 deploy/aws/codebuild/buildspec-ci.yml create mode 100644 deploy/aws/codebuild/cloudformation.yaml create mode 100644 deploy/aws/codebuild/iam-gha-permissions.json create mode 100644 deploy/aws/codebuild/iam-trust-policy.json create mode 100755 deploy/aws/codebuild/install-deps.sh create mode 100755 deploy/aws/codebuild/run-ci.sh create mode 100644 scripts/readiness/test_codebuild.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 69093bf4..c77a9024 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,10 @@ name: CI +# Linux clippy/test/coverage/TUI also run on AWS CodeBuild when +# vars.AWS_CODEBUILD_ROLE_ARN is set. Status checks: cortex-cli-gha-x64 +# and cortex-cli-gha-arm64. See deploy/aws/codebuild/README.md. +# Jobs in this workflow stay required and unchanged. + on: push: branches: [main] diff --git a/.github/workflows/codebuild.yml b/.github/workflows/codebuild.yml new file mode 100644 index 00000000..ad70fc97 --- /dev/null +++ b/.github/workflows/codebuild.yml @@ -0,0 +1,149 @@ +# AWS CodeBuild CI for CortexLM/cli (Linux x64 + arm64). +# Marker: CLI_CODEBUILD_CI_READY +# +# Assumes a dedicated IAM role via GitHub OIDC. No long-lived AWS keys. +# Role ARN and region come from repository *variables*, not secrets. +# See deploy/aws/codebuild/README.md for the one-time admin steps. +# +# Does not replace ci.yml / release.yml / publish-r2.yml / homebrew.yml / +# winget.yml / version-bump.yml / test-stability.yml. + +name: CodeBuild CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +concurrency: + group: codebuild-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + id-token: write + statuses: write + +jobs: + wiring: + name: CodeBuild wiring + runs-on: ubuntu-latest + timeout-minutes: 10 + outputs: + enabled: ${{ steps.gate.outputs.enabled }} + same_repo: ${{ steps.gate.outputs.same_repo }} + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - run: python -m pip install -r scripts/readiness/requirements.txt + - name: Validate CodeBuild assets + run: python -B -m unittest discover -s scripts/readiness -p test_codebuild.py -v + - name: Decide whether StartBuild is configured + id: gate + env: + ROLE_ARN: ${{ vars.AWS_CODEBUILD_ROLE_ARN }} + PR_HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name || github.repository }} + run: | + same_repo=false + if [ "$PR_HEAD_REPO" = "${{ github.repository }}" ]; then + same_repo=true + fi + echo "same_repo=$same_repo" >> "$GITHUB_OUTPUT" + if [ -z "$ROLE_ARN" ]; then + echo "enabled=false" >> "$GITHUB_OUTPUT" + echo "AWS_CODEBUILD_ROLE_ARN is unset. Skipping StartBuild. One-time IAM is in deploy/aws/codebuild/README.md" + exit 0 + fi + case "$ROLE_ARN" in + arn:aws:iam::*:role/*) ;; + *) + echo "::error::AWS_CODEBUILD_ROLE_ARN must be an IAM role ARN (set a variable, do not commit it)" + exit 1 + ;; + esac + echo "enabled=true" >> "$GITHUB_OUTPUT" + + codebuild: + name: ${{ matrix.context }} + needs: wiring + if: needs.wiring.outputs.enabled == 'true' && needs.wiring.outputs.same_repo == 'true' + runs-on: ubuntu-latest + timeout-minutes: 120 + strategy: + fail-fast: false + matrix: + include: + - context: cortex-cli-gha-x64 + project_var: AWS_CODEBUILD_PROJECT_X64 + project_default: cortex-cli-gha-x64 + - context: cortex-cli-gha-arm64 + project_var: AWS_CODEBUILD_PROJECT_ARM64 + project_default: cortex-cli-gha-arm64 + env: + STATUS_CONTEXT: ${{ matrix.context }} + QUALITY_BASE: ${{ github.event.pull_request.base.sha || github.event.before }} + CORTEX_GITHUB_REPOSITORY: ${{ github.repository }} + steps: + - uses: actions/checkout@v7 + - name: Resolve head SHA + id: rev + run: echo "sha=${{ github.event.pull_request.head.sha || github.sha }}" >> "$GITHUB_OUTPUT" + - name: Post pending status + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SHA: ${{ steps.rev.outputs.sha }} + run: | + gh api "repos/${{ github.repository }}/statuses/${SHA}" \ + --field state=pending \ + --field context="${STATUS_CONTEXT}" \ + --field description="AWS CodeBuild starting" \ + --field target_url="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" + - name: Configure AWS credentials (OIDC) + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ vars.AWS_CODEBUILD_ROLE_ARN }} + aws-region: ${{ vars.AWS_REGION || 'us-east-1' }} + role-session-name: ${{ matrix.context }} + - name: Read buildspec + id: spec + run: | + { + echo "yaml<> "$GITHUB_OUTPUT" + - name: Run CodeBuild + uses: aws-actions/aws-codebuild-run-build@v1 + with: + project-name: ${{ vars[matrix.project_var] || matrix.project_default }} + disable-source-override: true + buildspec-override: ${{ steps.spec.outputs.yaml }} + env-vars-for-codebuild: | + QUALITY_BASE, + CORTEX_SOURCE_SHA, + CORTEX_GITHUB_REPOSITORY + env: + CORTEX_SOURCE_SHA: ${{ steps.rev.outputs.sha }} + - name: Post final status + if: always() + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SHA: ${{ steps.rev.outputs.sha }} + OUTCOME: ${{ job.status }} + run: | + if [ "$OUTCOME" = "success" ]; then + state=success + desc="AWS CodeBuild passed" + else + state=failure + desc="AWS CodeBuild failed" + fi + gh api "repos/${{ github.repository }}/statuses/${SHA}" \ + --field state="$state" \ + --field context="${STATUS_CONTEXT}" \ + --field description="$desc" \ + --field target_url="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" diff --git a/AGENTS.md b/AGENTS.md index 2fd3dacb..5a29449f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,6 +23,7 @@ Working branch: **`main`**. Version bumps land on `main` via PR; `.github/workfl | `docs/` | User and plugin docs | | `.rules/` | Engineering rules (security, errors, TUI, tests, …) | | `scripts/` | Version bump / consistency / release helpers | +| `deploy/aws/codebuild/` | Public-safe CodeBuild buildspecs and OIDC IAM ([README](deploy/aws/codebuild/README.md)) | ## Non-negotiables diff --git a/README.md b/README.md index c68c8b28..1e1c5b8f 100644 --- a/README.md +++ b/README.md @@ -199,6 +199,8 @@ via [`publish-r2.yml`](.github/workflows/publish-r2.yml). This repository does not invent cloud accounts. The secret *names* CI expects are listed in [docs/CI_SECRETS.md](./docs/CI_SECRETS.md). Values never go in git. +Linux CI can additionally run on AWS CodeBuild via GitHub OIDC; the one-time +IAM steps are in [deploy/aws/codebuild/README.md](./deploy/aws/codebuild/README.md). ## Contributing diff --git a/deploy/aws/codebuild/README.md b/deploy/aws/codebuild/README.md new file mode 100644 index 00000000..ccaf83cb --- /dev/null +++ b/deploy/aws/codebuild/README.md @@ -0,0 +1,148 @@ +# AWS CodeBuild CI for CortexLM/cli + +Public-repo CodeBuild integration for Linux **x64** and **arm64**. GitHub +Actions assumes a dedicated IAM role with **OIDC** (no long-lived AWS keys) +and starts the projects. Each project posts a commit status comparable to +the CortexLM/backend checks `cortex-gha-x64` / `cortex-gha-arm64`. + +Marker: `CLI_CODEBUILD_CI_READY` + +Existing workflows stay in place: `ci.yml`, `release.yml`, `publish-r2.yml`, +`homebrew.yml`, `winget.yml`, `version-bump.yml`, `test-stability.yml`. +CodeBuild **extends** them. It does not replace R2 publishing, version +bumps, or macOS/Windows release jobs. Staging/prod app deploy remains +unchanged (prod HOLD). + +Windows CodeBuild is **out of scope**. Compliance treats Windows CI as +outside the production gate; keep `windows-latest` on GitHub-hosted runners +in `ci.yml` / `release.yml` until a separate follow-up. + +Do not commit AWS account IDs, access keys, PATs, or internal hostnames. + +## Status checks (branch protection) + +After the one-time AWS setup below, add these **required** checks on `main`: + +| Context | Project | Arch | +|---------|---------|------| +| `cortex-cli-gha-x64` | `cortex-cli-gha-x64` | Linux x86_64 | +| `cortex-cli-gha-arm64` | `cortex-cli-gha-arm64` | Linux aarch64 | + +Keep the existing `ci.yml` checks (`Format`, `Clippy`, `Test`, `TUI checks`, +`Security Audit`, `Source and dependency policy`, `Changed-line coverage`, +`CLI Version and Distribution`, `CI Success`). Do not remove them in this +change. After CodeBuild is required and stable, a later PR can slim the +duplicate GitHub-hosted Linux cargo jobs. + +Same-repo PRs and pushes to `main` start CodeBuild. Fork PRs keep using +GitHub-hosted `ci.yml` only (OIDC is not granted to forks). + +## Prefer existing org projects? + +If this AWS account already hosts backend projects `cortex-gha-x64` / +`cortex-gha-arm64`, **reuse the GitHub OIDC provider** and the account, not +the projects. A CodeBuild project has one source/buildspec; do not point +backend projects at this public CLI repo. Create dedicated +`cortex-cli-gha-*` projects. Override names only via GitHub **variables** +if an admin already created equivalent CLI projects. + +## One-time admin setup + +### 1. Reuse or create the GitHub OIDC provider + +In the AWS account that already runs CortexLM/backend CodeBuild (or a new +account dedicated to public CLI CI): + +1. IAM → Identity providers → `token.actions.githubusercontent.com`. +2. If it exists, **do not recreate it**. Continue to the role. +3. If it does not exist, create it: + - Provider URL: `https://token.actions.githubusercontent.com` + - Audience: `sts.amazonaws.com` + - Or pass `CreateGithubOidcProvider=true` to the stack below. + +### 2. Deploy the stack (recommended) + +From a workstation that can assume an admin role (never from this repo's +CI, and never with keys committed here): + +```bash +aws cloudformation deploy \ + --stack-name cortex-cli-codebuild \ + --template-file deploy/aws/codebuild/cloudformation.yaml \ + --capabilities CAPABILITY_NAMED_IAM \ + --parameter-overrides \ + GitHubOrgRepo=CortexLM/cli \ + ProjectNameX64=cortex-cli-gha-x64 \ + ProjectNameArm64=cortex-cli-gha-arm64 \ + GhaRoleName=cortex-cli-codebuild-gha \ + CreateGithubOidcProvider=false +``` + +Copy the `GithubActionsRoleArn` output. It contains the account ID; store +it as a GitHub **variable**, not in git. + +### 3. Manual IAM if you do not use CloudFormation + +1. Create role `cortex-cli-codebuild-gha`. +2. Trust policy: `iam-trust-policy.json` with `ACCOUNT_ID` replaced at + deploy time. Subject must be `repo:CortexLM/cli:*` only. +3. Permissions: `iam-gha-permissions.json` with `ACCOUNT_ID` and `REGION` + replaced. Actions are only `codebuild:StartBuild`, + `codebuild:BatchGetBuilds`, and `logs:GetLogEvents` on the two CLI + projects. +4. Create CodeBuild projects `cortex-cli-gha-x64` (Linux x86, + `aws/codebuild/standard:7.0`, `BUILD_GENERAL1_LARGE`) and + `cortex-cli-gha-arm64` (Linux ARM, + `aws/codebuild/amazonlinux-aarch64-standard:3.0`, + `BUILD_GENERAL1_LARGE`). Source type **NO_SOURCE**. S3 cache on a + private bucket. Build timeout 90 minutes. +5. CodeBuild service role: CloudWatch Logs for those projects plus + read/write on the cache bucket. No deploy, no R2, no production secrets. + +### 4. GitHub repository variables (not secrets) + +On `CortexLM/cli` → Settings → Secrets and variables → Actions → Variables: + +| Variable | Value | +|----------|--------| +| `AWS_CODEBUILD_ROLE_ARN` | `GithubActionsRoleArn` stack output | +| `AWS_REGION` | Region of the stack (default in the workflow is `us-east-1`) | +| `AWS_CODEBUILD_PROJECT_X64` | Optional override; default `cortex-cli-gha-x64` | +| `AWS_CODEBUILD_PROJECT_ARM64` | Optional override; default `cortex-cli-gha-arm64` | + +Do **not** add `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY`. Do not put +staging or production app secrets on these projects. + +Until `AWS_CODEBUILD_ROLE_ARN` is set, `.github/workflows/codebuild.yml` +validates the in-repo assets and **skips** StartBuild. It does not post a +green `cortex-cli-gha-*` status for that skip (no mock-success). + +### 5. Require the checks + +Branch protection / ruleset on `main`: require +`cortex-cli-gha-x64` and `cortex-cli-gha-arm64` in addition to the +existing `ci.yml` jobs. Require these only after a successful StartBuild +has been observed on a test PR. + +## What CodeBuild runs + +`buildspec-ci.yml` clones the public `CortexLM/cli` commit over HTTPS +(no PAT) and runs `run-ci.sh`: + +- `cargo fmt --all -- --check` +- `./scripts/clippy.sh` +- `./scripts/check-cli-version.sh` +- `python3 scripts/readiness/tests.py` +- `cargo test --locked --workspace --doc` +- `python3 scripts/readiness/schema.py` +- `cargo build --locked -p cortex-cli -p cortex-app-server` +- `python3 scripts/readiness/qa.py` +- headless TUI / snapshot packages (same set as `ci.yml`) +- changed-line coverage against the real PR base SHA + +Cargo registry, git, rustup, and `target/` are cached in S3. + +## Follow-up (Windows) + +Not in this change. If Windows CodeBuild is added later, use a separate +project and a non-required check. Do not block production on it. diff --git a/deploy/aws/codebuild/buildspec-ci.yml b/deploy/aws/codebuild/buildspec-ci.yml new file mode 100644 index 00000000..5ab6d685 --- /dev/null +++ b/deploy/aws/codebuild/buildspec-ci.yml @@ -0,0 +1,59 @@ +# Linux CI for CortexLM/cli on AWS CodeBuild (x64 and arm64). +# Marker: CLI_CODEBUILD_CI_READY +# +# This file is the source of truth. GitHub Actions passes it as an inline +# buildspec override so the project can use NO_SOURCE (no GitHub token). +# The install phase clones the public repo over HTTPS when the workspace +# is empty, then runs scripts from that commit. +version: 0.2 + +env: + variables: + CARGO_TERM_COLOR: always + CARGO_INCREMENTAL: "0" + CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse + RUST_BACKTRACE: "1" + GIT_TERMINAL_PROMPT: "0" + +phases: + install: + runtime-versions: + python: 3.12 + nodejs: 22 + commands: + - | + set -euo pipefail + REPO="${CORTEX_GITHUB_REPOSITORY:-${GITHUB_REPOSITORY:-CortexLM/cli}}" + SHA="${CORTEX_SOURCE_SHA:-${GITHUB_SHA:-}}" + if [ -z "$SHA" ]; then + echo "Missing CORTEX_SOURCE_SHA / GITHUB_SHA" >&2 + exit 1 + fi + if [ -f "${CODEBUILD_SRC_DIR:-}/Cargo.toml" ]; then + SRC="$CODEBUILD_SRC_DIR" + else + SRC=/tmp/cortex-cli-src + rm -rf "$SRC" + git clone --no-tags "https://github.com/${REPO}.git" "$SRC" + git -C "$SRC" checkout --detach "$SHA" + fi + printf 'export CORTEX_CLI_SRC=%q\n' "$SRC" > /tmp/cortex-cli-env.sh + # shellcheck disable=SC1091 + . /tmp/cortex-cli-env.sh + bash "$CORTEX_CLI_SRC/deploy/aws/codebuild/install-deps.sh" + + build: + commands: + - | + set -euo pipefail + # shellcheck disable=SC1091 + . /tmp/cortex-cli-env.sh + bash "$CORTEX_CLI_SRC/deploy/aws/codebuild/run-ci.sh" + +cache: + paths: + - /root/.cargo/registry/**/* + - /root/.cargo/git/**/* + - /root/.rustup/toolchains/**/* + - /tmp/cortex-cli-src/target/**/* + - target/**/* diff --git a/deploy/aws/codebuild/cloudformation.yaml b/deploy/aws/codebuild/cloudformation.yaml new file mode 100644 index 00000000..5e371da3 --- /dev/null +++ b/deploy/aws/codebuild/cloudformation.yaml @@ -0,0 +1,247 @@ +# One-time AWS resources for CortexLM/cli CodeBuild CI. +# Public-safe: no secrets, no hardcoded account IDs, no long-lived keys. +# Substitute ACCOUNT_ID / REGION only at deploy time (never commit values). +# +# Marker: CLI_CODEBUILD_CI_READY +AWSTemplateFormatVersion: "2010-09-09" +Description: > + Cortex CLI CodeBuild projects (cortex-cli-gha-x64 / cortex-cli-gha-arm64) + plus a GitHub Actions OIDC role. Reuse an existing GitHub OIDC provider + when the account already runs CortexLM/backend CodeBuild. + +Parameters: + GitHubOrgRepo: + Type: String + Default: CortexLM/cli + Description: GitHub repository allowed to assume the GHA role (org/name). + ProjectNameX64: + Type: String + Default: cortex-cli-gha-x64 + Description: CodeBuild project name and GitHub status-check context (x64). + ProjectNameArm64: + Type: String + Default: cortex-cli-gha-arm64 + Description: CodeBuild project name and GitHub status-check context (arm64). + GhaRoleName: + Type: String + Default: cortex-cli-codebuild-gha + Description: IAM role assumed by GitHub Actions via OIDC. + CreateGithubOidcProvider: + Type: String + Default: "false" + AllowedValues: ["true", "false"] + Description: > + Set true only if this account has no token.actions.githubusercontent.com + OIDC provider yet. Backend accounts should leave this false and reuse + the existing provider. + ComputeType: + Type: String + Default: BUILD_GENERAL1_LARGE + AllowedValues: + - BUILD_GENERAL1_MEDIUM + - BUILD_GENERAL1_LARGE + - BUILD_GENERAL1_XLARGE + Description: CodeBuild compute size (LARGE = 8 vCPU / 15 GiB). + +Conditions: + ShouldCreateOidcProvider: !Equals [!Ref CreateGithubOidcProvider, "true"] + +Resources: + GithubOidcProvider: + Type: AWS::IAM::OIDCProvider + Condition: ShouldCreateOidcProvider + Properties: + Url: https://token.actions.githubusercontent.com + ClientIdList: + - sts.amazonaws.com + ThumbprintList: + - 6938fd4d98bab03faadb97b34396831e3780aea1 + + CacheBucket: + Type: AWS::S3::Bucket + Properties: + BucketName: !Sub "cortex-cli-codebuild-cache-${AWS::AccountId}-${AWS::Region}" + BucketEncryption: + ServerSideEncryptionConfiguration: + - ServerSideEncryptionByDefault: + SSEAlgorithm: AES256 + PublicAccessBlockConfiguration: + BlockPublicAcls: true + BlockPublicPolicy: true + IgnorePublicAcls: true + RestrictPublicBuckets: true + VersioningConfiguration: + Status: Enabled + LifecycleConfiguration: + Rules: + - Id: expire-cache + Status: Enabled + ExpirationInDays: 30 + + CodeBuildServiceRole: + Type: AWS::IAM::Role + Properties: + RoleName: cortex-cli-codebuild-service + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Principal: + Service: codebuild.amazonaws.com + Action: sts:AssumeRole + Policies: + - PolicyName: cortex-cli-codebuild-service + PolicyDocument: + Version: "2012-10-17" + Statement: + - Sid: CloudWatchLogs + Effect: Allow + Action: + - logs:CreateLogGroup + - logs:CreateLogStream + - logs:PutLogEvents + Resource: + - !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/${ProjectNameX64}" + - !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/${ProjectNameX64}:*" + - !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/${ProjectNameArm64}" + - !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/${ProjectNameArm64}:*" + - Sid: CargoCacheBucket + Effect: Allow + Action: + - s3:GetObject + - s3:GetObjectVersion + - s3:PutObject + - s3:DeleteObject + Resource: !Sub "${CacheBucket.Arn}/*" + - Sid: CargoCacheList + Effect: Allow + Action: + - s3:ListBucket + - s3:GetBucketLocation + Resource: !GetAtt CacheBucket.Arn + + GithubActionsRole: + Type: AWS::IAM::Role + Properties: + RoleName: !Ref GhaRoleName + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Sid: GitHubActionsOidc + Effect: Allow + Principal: + Federated: !If + - ShouldCreateOidcProvider + - !GetAtt GithubOidcProvider.Arn + - !Sub "arn:aws:iam::${AWS::AccountId}:oidc-provider/token.actions.githubusercontent.com" + Action: sts:AssumeRoleWithWebIdentity + Condition: + StringEquals: + token.actions.githubusercontent.com:aud: sts.amazonaws.com + StringLike: + token.actions.githubusercontent.com:sub: !Sub "repo:${GitHubOrgRepo}:*" + Policies: + - PolicyName: start-cli-codebuild + PolicyDocument: + Version: "2012-10-17" + Statement: + - Sid: StartAndObserve + Effect: Allow + Action: + - codebuild:StartBuild + - codebuild:BatchGetBuilds + Resource: + - !Sub "arn:aws:codebuild:${AWS::Region}:${AWS::AccountId}:project/${ProjectNameX64}" + - !Sub "arn:aws:codebuild:${AWS::Region}:${AWS::AccountId}:project/${ProjectNameArm64}" + - Sid: StreamLogs + Effect: Allow + Action: + - logs:GetLogEvents + Resource: + - !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/${ProjectNameX64}:*" + - !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/${ProjectNameArm64}:*" + + ProjectX64: + Type: AWS::CodeBuild::Project + Properties: + Name: !Ref ProjectNameX64 + Description: Cortex CLI Linux x64 CI (GitHub Actions OIDC trigger) + ServiceRole: !GetAtt CodeBuildServiceRole.Arn + TimeoutInMinutes: 90 + QueuedTimeoutInMinutes: 30 + BadgeEnabled: false + Artifacts: + Type: NO_ARTIFACTS + Cache: + Type: S3 + Location: !Sub "${CacheBucket}/x64" + Environment: + Type: LINUX_CONTAINER + ComputeType: !Ref ComputeType + Image: aws/codebuild/standard:7.0 + ImagePullCredentialsType: CODEBUILD + PrivilegedMode: false + LogsConfig: + CloudWatchLogs: + Status: ENABLED + GroupName: !Sub "/aws/codebuild/${ProjectNameX64}" + Source: + Type: NO_SOURCE + BuildSpec: | + version: 0.2 + phases: + build: + commands: + - echo "Start builds from .github/workflows/codebuild.yml (inline buildspec override)." + - exit 1 + + ProjectArm64: + Type: AWS::CodeBuild::Project + Properties: + Name: !Ref ProjectNameArm64 + Description: Cortex CLI Linux arm64 CI (GitHub Actions OIDC trigger) + ServiceRole: !GetAtt CodeBuildServiceRole.Arn + TimeoutInMinutes: 90 + QueuedTimeoutInMinutes: 30 + BadgeEnabled: false + Artifacts: + Type: NO_ARTIFACTS + Cache: + Type: S3 + Location: !Sub "${CacheBucket}/arm64" + Environment: + Type: ARM_CONTAINER + ComputeType: !Ref ComputeType + Image: aws/codebuild/amazonlinux-aarch64-standard:3.0 + ImagePullCredentialsType: CODEBUILD + PrivilegedMode: false + LogsConfig: + CloudWatchLogs: + Status: ENABLED + GroupName: !Sub "/aws/codebuild/${ProjectNameArm64}" + Source: + Type: NO_SOURCE + BuildSpec: | + version: 0.2 + phases: + build: + commands: + - echo "Start builds from .github/workflows/codebuild.yml (inline buildspec override)." + - exit 1 + +Outputs: + GithubActionsRoleArn: + Description: Set GitHub Actions variable AWS_CODEBUILD_ROLE_ARN to this value + Value: !GetAtt GithubActionsRole.Arn + ProjectX64Name: + Description: Required GitHub status-check context (x64) + Value: !Ref ProjectNameX64 + ProjectArm64Name: + Description: Required GitHub status-check context (arm64) + Value: !Ref ProjectNameArm64 + CacheBucketName: + Description: Private cargo cache bucket + Value: !Ref CacheBucket + Region: + Description: Set GitHub Actions variable AWS_REGION if not us-east-1 + Value: !Ref AWS::Region diff --git a/deploy/aws/codebuild/iam-gha-permissions.json b/deploy/aws/codebuild/iam-gha-permissions.json new file mode 100644 index 00000000..649c22e5 --- /dev/null +++ b/deploy/aws/codebuild/iam-gha-permissions.json @@ -0,0 +1,28 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "StartAndObserveCliCodeBuild", + "Effect": "Allow", + "Action": [ + "codebuild:StartBuild", + "codebuild:BatchGetBuilds" + ], + "Resource": [ + "arn:aws:codebuild:REGION:ACCOUNT_ID:project/cortex-cli-gha-x64", + "arn:aws:codebuild:REGION:ACCOUNT_ID:project/cortex-cli-gha-arm64" + ] + }, + { + "Sid": "StreamCliCodeBuildLogs", + "Effect": "Allow", + "Action": [ + "logs:GetLogEvents" + ], + "Resource": [ + "arn:aws:logs:REGION:ACCOUNT_ID:log-group:/aws/codebuild/cortex-cli-gha-x64:*", + "arn:aws:logs:REGION:ACCOUNT_ID:log-group:/aws/codebuild/cortex-cli-gha-arm64:*" + ] + } + ] +} diff --git a/deploy/aws/codebuild/iam-trust-policy.json b/deploy/aws/codebuild/iam-trust-policy.json new file mode 100644 index 00000000..bddb43d7 --- /dev/null +++ b/deploy/aws/codebuild/iam-trust-policy.json @@ -0,0 +1,21 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "GitHubActionsOidcCortexLmCli", + "Effect": "Allow", + "Principal": { + "Federated": "arn:aws:iam::ACCOUNT_ID:oidc-provider/token.actions.githubusercontent.com" + }, + "Action": "sts:AssumeRoleWithWebIdentity", + "Condition": { + "StringEquals": { + "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" + }, + "StringLike": { + "token.actions.githubusercontent.com:sub": "repo:CortexLM/cli:*" + } + } + } + ] +} diff --git a/deploy/aws/codebuild/install-deps.sh b/deploy/aws/codebuild/install-deps.sh new file mode 100755 index 00000000..16a71635 --- /dev/null +++ b/deploy/aws/codebuild/install-deps.sh @@ -0,0 +1,143 @@ +#!/usr/bin/env bash +# System + Rust toolchain for CodeBuild Linux CI. Fail closed; no mock-success. +set -euo pipefail + +if [[ -z "${CORTEX_CLI_SRC:-}" ]]; then + echo "CORTEX_CLI_SRC is required" >&2 + exit 1 +fi +cd "$CORTEX_CLI_SRC" + +export DEBIAN_FRONTEND=noninteractive +export CARGO_HOME="${CARGO_HOME:-$HOME/.cargo}" +export RUSTUP_HOME="${RUSTUP_HOME:-$HOME/.rustup}" +mkdir -p "$CARGO_HOME/bin" +export PATH="$CARGO_HOME/bin:$PATH" + +install_apt() { + apt-get update + apt-get install -y --no-install-recommends \ + build-essential \ + ca-certificates \ + curl \ + git \ + libasound2-dev \ + libssl-dev \ + pkg-config \ + python3 \ + python3-pip \ + python3-venv \ + ripgrep +} + +install_dnf() { + dnf install -y \ + alsa-lib-devel \ + ca-certificates \ + curl \ + gcc \ + gcc-c++ \ + git \ + make \ + openssl-devel \ + pkgconf-pkg-config \ + python3 \ + python3-pip \ + tar \ + gzip + dnf install -y ripgrep || true +} + +install_ripgrep_tarball() { + local target url tmp + case "$(uname -m)" in + x86_64) target="x86_64-unknown-linux-musl" ;; + aarch64 | arm64) target="aarch64-unknown-linux-gnu" ;; + *) + echo "Unsupported architecture for ripgrep fallback: $(uname -m)" >&2 + return 1 + ;; + esac + url="https://github.com/BurntSushi/ripgrep/releases/download/14.1.1/ripgrep-14.1.1-${target}.tar.gz" + tmp="$(mktemp -d)" + curl -LsSf "$url" | tar zxf - -C "$tmp" + install -m 0755 "$tmp"/ripgrep-*/rg /usr/local/bin/rg + rm -rf "$tmp" +} + +if command -v apt-get >/dev/null 2>&1; then + install_apt +elif command -v dnf >/dev/null 2>&1; then + install_dnf +else + echo "Unsupported CodeBuild image: need apt-get or dnf" >&2 + exit 1 +fi + +if ! command -v rg >/dev/null 2>&1; then + install_ripgrep_tarball +fi +if ! command -v rg >/dev/null 2>&1; then + echo "ripgrep (rg) is required for readiness tests" >&2 + exit 1 +fi + +channel="1.98.0" +if [[ -f rust-toolchain.toml ]]; then + channel="$(sed -n 's/^channel = "\([^"]*\)"/\1/p' rust-toolchain.toml | head -n1)" +fi +if [[ -z "$channel" ]]; then + echo "Could not determine Rust toolchain channel" >&2 + exit 1 +fi + +if ! command -v rustup >/dev/null 2>&1; then + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \ + sh -s -- -y --default-toolchain "$channel" --profile minimal \ + --component rustfmt --component clippy --component llvm-tools-preview +fi +# shellcheck disable=SC1091 +. "$CARGO_HOME/env" +rustup toolchain install "$channel" --profile minimal --component rustfmt,clippy,llvm-tools-preview +rustup default "$channel" + +arch="$(uname -m)" +case "$arch" in + x86_64) + nextest_dist="linux" + llvm_cov_target="x86_64-unknown-linux-musl" + ;; + aarch64 | arm64) + nextest_dist="linux-arm" + llvm_cov_target="aarch64-unknown-linux-musl" + ;; + *) + echo "Unsupported architecture: $arch" >&2 + exit 1 + ;; +esac + +if ! cargo nextest --version 2>/dev/null | grep -q '0.9.102'; then + curl -LsSf "https://get.nexte.st/0.9.102/${nextest_dist}" | tar zxf - -C "$CARGO_HOME/bin" +fi +if ! cargo llvm-cov --version 2>/dev/null | grep -q '0.6.21'; then + tmp="$(mktemp -d)" + curl -LsSf \ + "https://github.com/taiki-e/cargo-llvm-cov/releases/download/v0.6.21/cargo-llvm-cov-${llvm_cov_target}.tar.gz" \ + | tar zxf - -C "$tmp" + install -m 0755 "$tmp/cargo-llvm-cov" "$CARGO_HOME/bin/cargo-llvm-cov" + rm -rf "$tmp" +fi + +python3 -m pip install --disable-pip-version-check -r scripts/readiness/requirements.txt + +command -v rustc >/dev/null +command -v cargo >/dev/null +command -v python3 >/dev/null +command -v node >/dev/null +command -v git >/dev/null +command -v rg >/dev/null +cargo nextest --version +cargo llvm-cov --version +node --version +echo "CodeBuild CI dependencies ready (toolchain $channel, arch $arch)" diff --git a/deploy/aws/codebuild/run-ci.sh b/deploy/aws/codebuild/run-ci.sh new file mode 100755 index 00000000..02a99e3c --- /dev/null +++ b/deploy/aws/codebuild/run-ci.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# Heavy Linux CI suite for CodeBuild. Mirrors .github/workflows/ci.yml +# clippy / test / tui / coverage / schema / QA. Fail closed. +set -euo pipefail + +if [[ -z "${CORTEX_CLI_SRC:-}" ]]; then + echo "CORTEX_CLI_SRC is required" >&2 + exit 1 +fi +cd "$CORTEX_CLI_SRC" + +export CARGO_HOME="${CARGO_HOME:-$HOME/.cargo}" +export RUSTUP_HOME="${RUSTUP_HOME:-$HOME/.rustup}" +# shellcheck disable=SC1091 +. "$CARGO_HOME/env" +export PATH="$CARGO_HOME/bin:$PATH" +export CARGO_TERM_COLOR="${CARGO_TERM_COLOR:-always}" +export CARGO_INCREMENTAL="${CARGO_INCREMENTAL:-0}" +export CARGO_REGISTRIES_CRATES_IO_PROTOCOL="${CARGO_REGISTRIES_CRATES_IO_PROTOCOL:-sparse}" +export RUST_BACKTRACE="${RUST_BACKTRACE:-1}" + +QUALITY_BASE="${QUALITY_BASE:-}" +if [[ -z "$QUALITY_BASE" || "$QUALITY_BASE" == "0000000000000000000000000000000000000000" ]]; then + git fetch --no-tags origin main + QUALITY_BASE="$(git rev-parse --verify origin/main)" +fi +git fetch --no-tags origin "$QUALITY_BASE" +QUALITY_BASE="$(git rev-parse --verify "${QUALITY_BASE}^{commit}")" + +echo "==> format" +cargo fmt --all -- --check + +echo "==> clippy" +./scripts/clippy.sh + +echo "==> CLI version" +./scripts/check-cli-version.sh + +echo "==> tests" +python3 scripts/readiness/tests.py + +echo "==> doctests" +cargo test --locked --workspace --doc + +echo "==> API contracts" +python3 scripts/readiness/schema.py + +echo "==> local QA binaries" +cargo build --locked -p cortex-cli -p cortex-app-server + +echo "==> local functional QA" +python3 scripts/readiness/qa.py + +echo "==> TUI / snapshot tests" +cargo test -p cortex-tui -p cortex-tui-capture -p cortex-tui-components \ + -p cortex-tui-framework -p cortex-tui-core -p cortex-tui-buffer \ + -p cortex-tui-widgets -p cortex-tui-layout -p cortex-tui-text \ + -p cortex-tui-input -p cortex-tui-terminal -p cortex-tui-syntax + +echo "==> changed-line coverage" +mkdir -p target/readiness +cargo llvm-cov nextest --locked -p cortex-cli -p cortex-app-server -p cortex-common \ + --profile ci --lcov --output-path target/readiness/lcov.info +python3 scripts/readiness/coverage.py --base "$QUALITY_BASE" + +echo "CodeBuild Linux CI passed" diff --git a/docs/CI_SECRETS.md b/docs/CI_SECRETS.md index cd3efc6b..4cc93c80 100644 --- a/docs/CI_SECRETS.md +++ b/docs/CI_SECRETS.md @@ -8,6 +8,25 @@ None of these values belong in git. Do not add AWS access keys or an IAM user fo No secrets. `fmt`, `clippy`, `test`, `audit`, and TUI jobs use the public crates.io index and `GITHUB_TOKEN`. +## Optional Linux acceleration (`.github/workflows/codebuild.yml`) + +OIDC only. **Variables**, not secrets. Do not add `AWS_ACCESS_KEY_ID` or +`AWS_SECRET_ACCESS_KEY`. One-time IAM is in +[`deploy/aws/codebuild/README.md`](../deploy/aws/codebuild/README.md). +Marker: `CLI_CODEBUILD_CI_READY`. + +| Variable | Used for | +|----------|----------| +| `AWS_CODEBUILD_ROLE_ARN` | IAM role assumed by GitHub Actions (`repo:CortexLM/cli:*`) | +| `AWS_REGION` | CodeBuild region (workflow default `us-east-1`) | +| `AWS_CODEBUILD_PROJECT_X64` | Optional; default project `cortex-cli-gha-x64` | +| `AWS_CODEBUILD_PROJECT_ARM64` | Optional; default project `cortex-cli-gha-arm64` | + +Until `AWS_CODEBUILD_ROLE_ARN` is set, the workflow validates in-repo +buildspecs and skips StartBuild. It does not post a green +`cortex-cli-gha-*` status for that skip. Staging/prod app secrets stay +out of this repository and off these projects. + ## Version bump / tag (`.github/workflows/version-bump.yml`) | Secret | Used for | diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 11416df5..30173510 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -62,6 +62,12 @@ Common types: `feat`, `fix`, `docs`, `refactor`, `test`, `chore`. ## The gates +PRs to `main` also grow two CodeBuild status checks once an admin has +applied the one-time IAM in +[`deploy/aws/codebuild/README.md`](../deploy/aws/codebuild/README.md): +`cortex-cli-gha-x64` and `cortex-cli-gha-arm64`. Until that role variable +is set, GitHub-hosted `ci.yml` remains the merge gate. + These are exactly what CI runs, so run them before you push: ```bash diff --git a/docs/README.md b/docs/README.md index 2c807b0c..4a54741b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -64,6 +64,7 @@ New here? Start with **[Getting started](guides/getting-started.md)**, then keep | [Troubleshooting](troubleshooting.md) | Common failures and how to diagnose them | | [Contributing](CONTRIBUTING.md) | Filing issues, PR conventions, required checks | | [CI secrets](CI_SECRETS.md) | Secret *names* the release workflows expect | +| [CodeBuild CI](../deploy/aws/codebuild/README.md) | OIDC + AWS CodeBuild Linux x64/arm64 status checks | | [Local operations](guides/operations.md) | Alerts, error investigation, deployment comparison and profiling | | [Maintenance](guides/maintenance.md) | Ownership, labels, backlog triage and release review | | [Readiness remediation](guides/readiness-remediation.md) | Local validation snapshot and explicit remaining limits | diff --git a/docs/guides/development.md b/docs/guides/development.md index 8be3963e..4a35c731 100644 --- a/docs/guides/development.md +++ b/docs/guides/development.md @@ -95,6 +95,12 @@ The append regression test checks immediate visibility after Tokio 1.53.1 file writes. An awaited `flush` finishes the pending write; it is not an `fsync` durability guarantee. Do not replace this check with sleeps or retries. +Linux clippy, tests, TUI, schema, local QA, and changed-line coverage also run +on AWS CodeBuild (`cortex-cli-gha-x64` / `cortex-cli-gha-arm64`) when +`AWS_CODEBUILD_ROLE_ARN` is configured. Setup is +[CodeBuild CI](../../deploy/aws/codebuild/README.md). That path uses GitHub +OIDC; it does not add AWS keys to this repository. + ## Verification MCP (hidden) `cortex mcp-server --verify` is a hidden stdio JSON-RPC server (`hide = true` diff --git a/docs/guides/quality.md b/docs/guides/quality.md index 00886e9c..5e01c34b 100644 --- a/docs/guides/quality.md +++ b/docs/guides/quality.md @@ -49,3 +49,5 @@ registry dates, or remove `--locked`. The existing formatting, Clippy, audit, version, and TUI gates remain required. CI Success also depends on source policy and changed-line coverage. Test and coverage artifacts are retained for 14–30 days, not sent to a third-party service. +Linux CodeBuild status checks (`cortex-cli-gha-x64` / `cortex-cli-gha-arm64`) +are documented in [CodeBuild CI](../../deploy/aws/codebuild/README.md). diff --git a/scripts/readiness/test_codebuild.py b/scripts/readiness/test_codebuild.py new file mode 100644 index 00000000..cdd22238 --- /dev/null +++ b/scripts/readiness/test_codebuild.py @@ -0,0 +1,163 @@ +"""Public-safe CodeBuild CI wiring. No live AWS calls.""" + +import json +import re +import unittest +from pathlib import Path + +import yaml + +ROOT = Path(__file__).resolve().parents[2] +DEPLOY = ROOT / "deploy/aws/codebuild" +WORKFLOWS = ROOT / ".github/workflows" + +ACCOUNT_ID = re.compile(r"(? Date: Mon, 7 Sep 2026 22:13:09 +0000 Subject: [PATCH 2/5] fix(codebuild): isolate PR builds from the shared cache Load the buildspec from the PR base, send pull requests to logs-only NO_CACHE projects, and keep Cargo artifacts in CARGO_TARGET_DIR so a source refresh cannot wipe the restored target cache. Co-authored-by: Mathis --- .github/workflows/codebuild.yml | 18 ++- deploy/aws/codebuild/README.md | 37 +++--- deploy/aws/codebuild/buildspec-ci.yml | 19 +-- deploy/aws/codebuild/cloudformation.yaml | 118 +++++++++++++++++- deploy/aws/codebuild/iam-gha-permissions.json | 8 +- deploy/aws/codebuild/iam-trust-policy.json | 7 +- deploy/aws/codebuild/run-ci.sh | 2 + docs/CI_SECRETS.md | 6 +- scripts/readiness/test_codebuild.py | 30 ++++- 9 files changed, 208 insertions(+), 37 deletions(-) diff --git a/.github/workflows/codebuild.yml b/.github/workflows/codebuild.yml index ad70fc97..77852e1b 100644 --- a/.github/workflows/codebuild.yml +++ b/.github/workflows/codebuild.yml @@ -80,15 +80,24 @@ jobs: - context: cortex-cli-gha-x64 project_var: AWS_CODEBUILD_PROJECT_X64 project_default: cortex-cli-gha-x64 + pr_project_var: AWS_CODEBUILD_PROJECT_X64_PR + pr_project_default: cortex-cli-gha-x64-pr - context: cortex-cli-gha-arm64 project_var: AWS_CODEBUILD_PROJECT_ARM64 project_default: cortex-cli-gha-arm64 + pr_project_var: AWS_CODEBUILD_PROJECT_ARM64_PR + pr_project_default: cortex-cli-gha-arm64-pr env: STATUS_CONTEXT: ${{ matrix.context }} QUALITY_BASE: ${{ github.event.pull_request.base.sha || github.event.before }} CORTEX_GITHUB_REPOSITORY: ${{ github.repository }} steps: - - uses: actions/checkout@v7 + - name: Checkout trusted buildspec revision + uses: actions/checkout@v7 + with: + # Same-repo PRs are not trusted. Load buildspec from the PR base + # (or the pushed main SHA). The commit under test is CORTEX_SOURCE_SHA. + ref: ${{ github.event.pull_request.base.sha || github.sha }} - name: Resolve head SHA id: rev run: echo "sha=${{ github.event.pull_request.head.sha || github.sha }}" >> "$GITHUB_OUTPUT" @@ -108,9 +117,10 @@ jobs: role-to-assume: ${{ vars.AWS_CODEBUILD_ROLE_ARN }} aws-region: ${{ vars.AWS_REGION || 'us-east-1' }} role-session-name: ${{ matrix.context }} - - name: Read buildspec + - name: Read trusted buildspec id: spec run: | + test -f deploy/aws/codebuild/buildspec-ci.yml { echo "yaml<&2 exit 1 fi + TARGET="${CARGO_TARGET_DIR:-/tmp/cortex-cli-target}" + mkdir -p "$TARGET" if [ -f "${CODEBUILD_SRC_DIR:-}/Cargo.toml" ]; then SRC="$CODEBUILD_SRC_DIR" else @@ -37,7 +40,10 @@ phases: git clone --no-tags "https://github.com/${REPO}.git" "$SRC" git -C "$SRC" checkout --detach "$SHA" fi - printf 'export CORTEX_CLI_SRC=%q\n' "$SRC" > /tmp/cortex-cli-env.sh + { + printf 'export CORTEX_CLI_SRC=%q\n' "$SRC" + printf 'export CARGO_TARGET_DIR=%q\n' "$TARGET" + } > /tmp/cortex-cli-env.sh # shellcheck disable=SC1091 . /tmp/cortex-cli-env.sh bash "$CORTEX_CLI_SRC/deploy/aws/codebuild/install-deps.sh" @@ -55,5 +61,4 @@ cache: - /root/.cargo/registry/**/* - /root/.cargo/git/**/* - /root/.rustup/toolchains/**/* - - /tmp/cortex-cli-src/target/**/* - - target/**/* + - /tmp/cortex-cli-target/**/* diff --git a/deploy/aws/codebuild/cloudformation.yaml b/deploy/aws/codebuild/cloudformation.yaml index 5e371da3..de0c1983 100644 --- a/deploy/aws/codebuild/cloudformation.yaml +++ b/deploy/aws/codebuild/cloudformation.yaml @@ -22,6 +22,14 @@ Parameters: Type: String Default: cortex-cli-gha-arm64 Description: CodeBuild project name and GitHub status-check context (arm64). + ProjectNameX64Pr: + Type: String + Default: cortex-cli-gha-x64-pr + Description: Pull-request x64 project (no S3 cache; logs-only service role). + ProjectNameArm64Pr: + Type: String + Default: cortex-cli-gha-arm64-pr + Description: Pull-request arm64 project (no S3 cache; logs-only service role). GhaRoleName: Type: String Default: cortex-cli-codebuild-gha @@ -120,6 +128,34 @@ Resources: - s3:GetBucketLocation Resource: !GetAtt CacheBucket.Arn + CodeBuildPrServiceRole: + Type: AWS::IAM::Role + Properties: + RoleName: cortex-cli-codebuild-service-pr + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Principal: + Service: codebuild.amazonaws.com + Action: sts:AssumeRole + Policies: + - PolicyName: cortex-cli-codebuild-service-pr + PolicyDocument: + Version: "2012-10-17" + Statement: + - Sid: CloudWatchLogs + Effect: Allow + Action: + - logs:CreateLogGroup + - logs:CreateLogStream + - logs:PutLogEvents + Resource: + - !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/${ProjectNameX64Pr}" + - !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/${ProjectNameX64Pr}:*" + - !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/${ProjectNameArm64Pr}" + - !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/${ProjectNameArm64Pr}:*" + GithubActionsRole: Type: AWS::IAM::Role Properties: @@ -138,8 +174,10 @@ Resources: Condition: StringEquals: token.actions.githubusercontent.com:aud: sts.amazonaws.com - StringLike: - token.actions.githubusercontent.com:sub: !Sub "repo:${GitHubOrgRepo}:*" + ForAnyValue:StringEquals: + token.actions.githubusercontent.com:sub: + - !Sub "repo:${GitHubOrgRepo}:ref:refs/heads/main" + - !Sub "repo:${GitHubOrgRepo}:pull_request" Policies: - PolicyName: start-cli-codebuild PolicyDocument: @@ -153,6 +191,8 @@ Resources: Resource: - !Sub "arn:aws:codebuild:${AWS::Region}:${AWS::AccountId}:project/${ProjectNameX64}" - !Sub "arn:aws:codebuild:${AWS::Region}:${AWS::AccountId}:project/${ProjectNameArm64}" + - !Sub "arn:aws:codebuild:${AWS::Region}:${AWS::AccountId}:project/${ProjectNameX64Pr}" + - !Sub "arn:aws:codebuild:${AWS::Region}:${AWS::AccountId}:project/${ProjectNameArm64Pr}" - Sid: StreamLogs Effect: Allow Action: @@ -160,6 +200,8 @@ Resources: Resource: - !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/${ProjectNameX64}:*" - !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/${ProjectNameArm64}:*" + - !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/${ProjectNameX64Pr}:*" + - !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/${ProjectNameArm64Pr}:*" ProjectX64: Type: AWS::CodeBuild::Project @@ -229,6 +271,72 @@ Resources: - echo "Start builds from .github/workflows/codebuild.yml (inline buildspec override)." - exit 1 + ProjectX64Pr: + Type: AWS::CodeBuild::Project + Properties: + Name: !Ref ProjectNameX64Pr + Description: Cortex CLI Linux x64 CI for unapproved PRs (no shared cache) + ServiceRole: !GetAtt CodeBuildPrServiceRole.Arn + TimeoutInMinutes: 90 + QueuedTimeoutInMinutes: 30 + BadgeEnabled: false + Artifacts: + Type: NO_ARTIFACTS + Cache: + Type: NO_CACHE + Environment: + Type: LINUX_CONTAINER + ComputeType: !Ref ComputeType + Image: aws/codebuild/standard:7.0 + ImagePullCredentialsType: CODEBUILD + PrivilegedMode: false + LogsConfig: + CloudWatchLogs: + Status: ENABLED + GroupName: !Sub "/aws/codebuild/${ProjectNameX64Pr}" + Source: + Type: NO_SOURCE + BuildSpec: | + version: 0.2 + phases: + build: + commands: + - echo "Start builds from .github/workflows/codebuild.yml (inline buildspec override)." + - exit 1 + + ProjectArm64Pr: + Type: AWS::CodeBuild::Project + Properties: + Name: !Ref ProjectNameArm64Pr + Description: Cortex CLI Linux arm64 CI for unapproved PRs (no shared cache) + ServiceRole: !GetAtt CodeBuildPrServiceRole.Arn + TimeoutInMinutes: 90 + QueuedTimeoutInMinutes: 30 + BadgeEnabled: false + Artifacts: + Type: NO_ARTIFACTS + Cache: + Type: NO_CACHE + Environment: + Type: ARM_CONTAINER + ComputeType: !Ref ComputeType + Image: aws/codebuild/amazonlinux-aarch64-standard:3.0 + ImagePullCredentialsType: CODEBUILD + PrivilegedMode: false + LogsConfig: + CloudWatchLogs: + Status: ENABLED + GroupName: !Sub "/aws/codebuild/${ProjectNameArm64Pr}" + Source: + Type: NO_SOURCE + BuildSpec: | + version: 0.2 + phases: + build: + commands: + - echo "Start builds from .github/workflows/codebuild.yml (inline buildspec override)." + - exit 1 + Outputs: GithubActionsRoleArn: Description: Set GitHub Actions variable AWS_CODEBUILD_ROLE_ARN to this value @@ -239,6 +347,12 @@ Outputs: ProjectArm64Name: Description: Required GitHub status-check context (arm64) Value: !Ref ProjectNameArm64 + ProjectX64PrName: + Description: Pull-request x64 project (logs-only role, no S3 cache) + Value: !Ref ProjectNameX64Pr + ProjectArm64PrName: + Description: Pull-request arm64 project (logs-only role, no S3 cache) + Value: !Ref ProjectNameArm64Pr CacheBucketName: Description: Private cargo cache bucket Value: !Ref CacheBucket diff --git a/deploy/aws/codebuild/iam-gha-permissions.json b/deploy/aws/codebuild/iam-gha-permissions.json index 649c22e5..b7f3a103 100644 --- a/deploy/aws/codebuild/iam-gha-permissions.json +++ b/deploy/aws/codebuild/iam-gha-permissions.json @@ -10,7 +10,9 @@ ], "Resource": [ "arn:aws:codebuild:REGION:ACCOUNT_ID:project/cortex-cli-gha-x64", - "arn:aws:codebuild:REGION:ACCOUNT_ID:project/cortex-cli-gha-arm64" + "arn:aws:codebuild:REGION:ACCOUNT_ID:project/cortex-cli-gha-arm64", + "arn:aws:codebuild:REGION:ACCOUNT_ID:project/cortex-cli-gha-x64-pr", + "arn:aws:codebuild:REGION:ACCOUNT_ID:project/cortex-cli-gha-arm64-pr" ] }, { @@ -21,7 +23,9 @@ ], "Resource": [ "arn:aws:logs:REGION:ACCOUNT_ID:log-group:/aws/codebuild/cortex-cli-gha-x64:*", - "arn:aws:logs:REGION:ACCOUNT_ID:log-group:/aws/codebuild/cortex-cli-gha-arm64:*" + "arn:aws:logs:REGION:ACCOUNT_ID:log-group:/aws/codebuild/cortex-cli-gha-arm64:*", + "arn:aws:logs:REGION:ACCOUNT_ID:log-group:/aws/codebuild/cortex-cli-gha-x64-pr:*", + "arn:aws:logs:REGION:ACCOUNT_ID:log-group:/aws/codebuild/cortex-cli-gha-arm64-pr:*" ] } ] diff --git a/deploy/aws/codebuild/iam-trust-policy.json b/deploy/aws/codebuild/iam-trust-policy.json index bddb43d7..4f6c1a71 100644 --- a/deploy/aws/codebuild/iam-trust-policy.json +++ b/deploy/aws/codebuild/iam-trust-policy.json @@ -12,8 +12,11 @@ "StringEquals": { "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" }, - "StringLike": { - "token.actions.githubusercontent.com:sub": "repo:CortexLM/cli:*" + "ForAnyValue:StringEquals": { + "token.actions.githubusercontent.com:sub": [ + "repo:CortexLM/cli:ref:refs/heads/main", + "repo:CortexLM/cli:pull_request" + ] } } } diff --git a/deploy/aws/codebuild/run-ci.sh b/deploy/aws/codebuild/run-ci.sh index 02a99e3c..18ba33a0 100755 --- a/deploy/aws/codebuild/run-ci.sh +++ b/deploy/aws/codebuild/run-ci.sh @@ -11,6 +11,8 @@ cd "$CORTEX_CLI_SRC" export CARGO_HOME="${CARGO_HOME:-$HOME/.cargo}" export RUSTUP_HOME="${RUSTUP_HOME:-$HOME/.rustup}" +export CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-/tmp/cortex-cli-target}" +mkdir -p "$CARGO_TARGET_DIR" # shellcheck disable=SC1091 . "$CARGO_HOME/env" export PATH="$CARGO_HOME/bin:$PATH" diff --git a/docs/CI_SECRETS.md b/docs/CI_SECRETS.md index 4cc93c80..251602b3 100644 --- a/docs/CI_SECRETS.md +++ b/docs/CI_SECRETS.md @@ -19,8 +19,10 @@ Marker: `CLI_CODEBUILD_CI_READY`. |----------|----------| | `AWS_CODEBUILD_ROLE_ARN` | IAM role assumed by GitHub Actions (`repo:CortexLM/cli:*`) | | `AWS_REGION` | CodeBuild region (workflow default `us-east-1`) | -| `AWS_CODEBUILD_PROJECT_X64` | Optional; default project `cortex-cli-gha-x64` | -| `AWS_CODEBUILD_PROJECT_ARM64` | Optional; default project `cortex-cli-gha-arm64` | +| `AWS_CODEBUILD_PROJECT_X64` | Optional; default project `cortex-cli-gha-x64` (push to `main`, S3 cache) | +| `AWS_CODEBUILD_PROJECT_ARM64` | Optional; default project `cortex-cli-gha-arm64` (push to `main`, S3 cache) | +| `AWS_CODEBUILD_PROJECT_X64_PR` | Optional; default `cortex-cli-gha-x64-pr` (PRs, no cache) | +| `AWS_CODEBUILD_PROJECT_ARM64_PR` | Optional; default `cortex-cli-gha-arm64-pr` (PRs, no cache) | Until `AWS_CODEBUILD_ROLE_ARN` is set, the workflow validates in-repo buildspecs and skips StartBuild. It does not post a green diff --git a/scripts/readiness/test_codebuild.py b/scripts/readiness/test_codebuild.py index cdd22238..6a5a1416 100644 --- a/scripts/readiness/test_codebuild.py +++ b/scripts/readiness/test_codebuild.py @@ -87,6 +87,14 @@ def test_status_check_names_match_backend_style_projects(self): self.assertIn("state=success", text) self.assertIn("state=failure", text) + def test_pull_requests_use_trusted_buildspec_and_uncached_projects(self): + text = read(WORKFLOWS / "codebuild.yml") + self.assertIn("github.event.pull_request.base.sha", text) + self.assertIn("cortex-cli-gha-x64-pr", text) + self.assertIn("cortex-cli-gha-arm64-pr", text) + self.assertIn("pull_request", text) + self.assertIn("logs-only", text) + def test_start_build_is_skipped_without_role_variable(self): gate = read(WORKFLOWS / "codebuild.yml") self.assertIn("AWS_CODEBUILD_ROLE_ARN is unset", gate) @@ -96,8 +104,14 @@ def test_start_build_is_skipped_without_role_variable(self): def test_buildspec_caches_cargo_and_runs_real_gates(self): cache = "\n".join(self.buildspec["cache"]["paths"]) + spec = read(DEPLOY / "buildspec-ci.yml") self.assertIn(".cargo/registry", cache) - self.assertIn("target/", cache) + self.assertIn("/tmp/cortex-cli-target", cache) + self.assertNotIn("/tmp/cortex-cli-src/target", cache) + self.assertIn("CARGO_TARGET_DIR", spec) + self.assertIn('SRC=/tmp/cortex-cli-src', spec) + self.assertIn('rm -rf "$SRC"', spec) + self.assertIn("mkdir -p \"$TARGET\"", spec) runner = read(DEPLOY / "run-ci.sh") for token in ( "./scripts/clippy.sh", @@ -116,11 +130,15 @@ def test_buildspec_caches_cargo_and_runs_real_gates(self): def test_iam_templates_use_placeholders_and_cli_trust_only(self): trust_text = read(DEPLOY / "iam-trust-policy.json") perm_text = read(DEPLOY / "iam-gha-permissions.json") - self.assertIn("repo:CortexLM/cli:*", trust_text) + self.assertIn("repo:CortexLM/cli:ref:refs/heads/main", trust_text) + self.assertIn("repo:CortexLM/cli:pull_request", trust_text) + self.assertNotIn("repo:CortexLM/cli:*", trust_text) self.assertIn("ACCOUNT_ID", trust_text) self.assertIn("sts:AssumeRoleWithWebIdentity", trust_text) self.assertIn("cortex-cli-gha-x64", perm_text) self.assertIn("cortex-cli-gha-arm64", perm_text) + self.assertIn("cortex-cli-gha-x64-pr", perm_text) + self.assertIn("cortex-cli-gha-arm64-pr", perm_text) self.assertEqual( {"codebuild:StartBuild", "codebuild:BatchGetBuilds"}, set(self.gha_policy["Statement"][0]["Action"]), @@ -146,7 +164,11 @@ def test_cloudformation_projects_and_oidc_role(self): self.assertIn("Type: NO_SOURCE", self.template) self.assertIn("Type: S3", self.template) self.assertIn("BUILD_GENERAL1_LARGE", self.template) - self.assertIn("repo:${GitHubOrgRepo}:*", self.template) + self.assertIn("repo:${GitHubOrgRepo}:ref:refs/heads/main", self.template) + self.assertIn("repo:${GitHubOrgRepo}:pull_request", self.template) + self.assertNotIn("repo:${GitHubOrgRepo}:*", self.template) + self.assertIn("cortex-cli-codebuild-service-pr", self.template) + self.assertIn("Type: NO_CACHE", self.template) self.assertIn("token.actions.githubusercontent.com", self.template) self.assertIn("cortex-cli-codebuild-gha", self.template) self.assertNotIn("R2_", self.template) @@ -156,7 +178,7 @@ def test_docs_name_variables_not_secret_values(self): docs = read(DEPLOY / "README.md") + read(ROOT / "docs/CI_SECRETS.md") self.assertIn("AWS_CODEBUILD_ROLE_ARN", docs) self.assertIn("cortex-cli-gha-x64", docs) - self.assertIn("repo:CortexLM/cli:*", docs) + self.assertIn("repo:CortexLM/cli:pull_request", docs) self.assertIn("CLI_CODEBUILD_CI_READY", docs) self.assertIn("Windows", docs) self.assertNotIn("AKIA", docs) From 63cc8783f83fbb6088a6c1397e5e9bff60b96aae Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 8 Sep 2026 00:31:23 +0000 Subject: [PATCH 3/5] fix(codebuild): stop untrusted PR workflows from assuming oidc StartBuild no longer runs on pull_request (untrusted workflow file). OIDC trusts only main plus this workflow ref. pull_request_target uses the base buildspec and logs-only *-pr projects. Push to main still uses the cached projects. Co-authored-by: Mathis --- .github/workflows/codebuild.yml | 23 ++++++++++++----- deploy/aws/codebuild/README.md | 30 ++++++++++++++-------- deploy/aws/codebuild/buildspec-ci.yml | 3 +++ deploy/aws/codebuild/cloudformation.yaml | 7 +++-- deploy/aws/codebuild/iam-trust-policy.json | 10 +++----- docs/CI_SECRETS.md | 2 +- scripts/readiness/test_codebuild.py | 18 ++++++++++--- 7 files changed, 60 insertions(+), 33 deletions(-) diff --git a/.github/workflows/codebuild.yml b/.github/workflows/codebuild.yml index 77852e1b..c96241b3 100644 --- a/.github/workflows/codebuild.yml +++ b/.github/workflows/codebuild.yml @@ -10,11 +10,16 @@ name: CodeBuild CI -on: +"on": push: branches: [main] + # Unprivileged wiring only. StartBuild is skipped for this event because + # the workflow file would come from the unapproved head. pull_request: branches: [main] + # Privileged StartBuild. Workflow YAML and OIDC come from main. + pull_request_target: + branches: [main] workflow_dispatch: concurrency: @@ -36,6 +41,9 @@ jobs: same_repo: ${{ steps.gate.outputs.same_repo }} steps: - uses: actions/checkout@v7 + with: + persist-credentials: false + ref: ${{ github.event.pull_request.base.sha || github.sha }} - uses: actions/setup-python@v5 with: python-version: '3.12' @@ -70,7 +78,7 @@ jobs: codebuild: name: ${{ matrix.context }} needs: wiring - if: needs.wiring.outputs.enabled == 'true' && needs.wiring.outputs.same_repo == 'true' + if: needs.wiring.outputs.enabled == 'true' && needs.wiring.outputs.same_repo == 'true' && github.event_name != 'pull_request' runs-on: ubuntu-latest timeout-minutes: 120 strategy: @@ -95,8 +103,9 @@ jobs: - name: Checkout trusted buildspec revision uses: actions/checkout@v7 with: - # Same-repo PRs are not trusted. Load buildspec from the PR base - # (or the pushed main SHA). The commit under test is CORTEX_SOURCE_SHA. + persist-credentials: false + # pull_request_target already runs on the base. Pin the ref so a + # later step cannot silently check out the unapproved head. ref: ${{ github.event.pull_request.base.sha || github.sha }} - name: Resolve head SHA id: rev @@ -129,9 +138,9 @@ jobs: - name: Run CodeBuild uses: aws-actions/aws-codebuild-run-build@v1 with: - # Pull requests use *-pr projects: no S3 cache and a logs-only - # service role, so unapproved PR code cannot read or poison cache. - project-name: ${{ github.event_name == 'pull_request' && (vars[matrix.pr_project_var] || matrix.pr_project_default) || (vars[matrix.project_var] || matrix.project_default) }} + # Only push/workflow_dispatch on main use the S3-cached projects. + # pull_request_target always uses *-pr (logs-only, NO_CACHE). + project-name: ${{ (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (vars[matrix.project_var] || matrix.project_default) || (vars[matrix.pr_project_var] || matrix.pr_project_default) }} disable-source-override: true buildspec-override: ${{ steps.spec.outputs.yaml }} env-vars-for-codebuild: | diff --git a/deploy/aws/codebuild/README.md b/deploy/aws/codebuild/README.md index f040353c..da5e1d26 100644 --- a/deploy/aws/codebuild/README.md +++ b/deploy/aws/codebuild/README.md @@ -28,11 +28,16 @@ After the one-time AWS setup below, add these **required** checks on `main`: | `cortex-cli-gha-x64` | `cortex-cli-gha-x64` | `cortex-cli-gha-x64-pr` | Linux x86_64 | | `cortex-cli-gha-arm64` | `cortex-cli-gha-arm64` | `cortex-cli-gha-arm64-pr` | Linux aarch64 | -Unapproved same-repository pull requests start the `*-pr` projects only. -Those projects use a **logs-only** service role and `NO_CACHE`. They cannot -read, write, or delete the shared cargo cache. The workflow also loads -`buildspec-ci.yml` from the PR **base** (or the pushed `main` SHA), not -from the unapproved head. +Unapproved same-repository pull requests are handled with +`pull_request_target` so this workflow file and the OIDC token come from +`main`, not from the unapproved head. Those runs start the `*-pr` projects +only (logs-only service role, `NO_CACHE`). They cannot read, write, or +delete the shared cargo cache. The runner checks out the PR **base** for +`buildspec-ci.yml` and passes the head SHA into CodeBuild as +`CORTEX_SOURCE_SHA`. OIDC trust is only +`repo:CortexLM/cli:ref:refs/heads/main` plus +`job_workflow_ref` for `.github/workflows/codebuild.yml` on `main`. +Untrusted `pull_request` workflows cannot assume the role. Keep the existing `ci.yml` checks (`Format`, `Clippy`, `Test`, `TUI checks`, `Security Audit`, `Source and dependency policy`, `Changed-line coverage`, @@ -40,9 +45,10 @@ Keep the existing `ci.yml` checks (`Format`, `Clippy`, `Test`, `TUI checks`, change. After CodeBuild is required and stable, a later PR can slim the duplicate GitHub-hosted Linux cargo jobs. -Same-repo PRs start the `*-pr` projects. Pushes to `main` start the -cached projects. Fork PRs keep using GitHub-hosted `ci.yml` only (OIDC -is not granted to forks). +Same-repo PRs start the `*-pr` projects via `pull_request_target`. Pushes +to `main` start the cached projects. Fork PRs keep using GitHub-hosted +`ci.yml` only (StartBuild is skipped when the head repo is not this +repository). ## Prefer existing org projects? @@ -92,9 +98,11 @@ it as a GitHub **variable**, not in git. 1. Create role `cortex-cli-codebuild-gha`. 2. Trust policy: `iam-trust-policy.json` with `ACCOUNT_ID` replaced at - deploy time. Subjects must be only - `repo:CortexLM/cli:ref:refs/heads/main` and - `repo:CortexLM/cli:pull_request`. + deploy time. The subject must be only + `repo:CortexLM/cli:ref:refs/heads/main`. Also require + `job_workflow_ref` `CortexLM/cli/.github/workflows/codebuild.yml@refs/heads/main`. + Do not trust `repo:CortexLM/cli:pull_request` — that would let an + unapproved `pull_request` workflow assume the role. 3. Permissions: `iam-gha-permissions.json` with `ACCOUNT_ID` and `REGION` replaced. Actions are only `codebuild:StartBuild`, `codebuild:BatchGetBuilds`, and `logs:GetLogEvents` on the two CLI diff --git a/deploy/aws/codebuild/buildspec-ci.yml b/deploy/aws/codebuild/buildspec-ci.yml index 33536861..3d487d6e 100644 --- a/deploy/aws/codebuild/buildspec-ci.yml +++ b/deploy/aws/codebuild/buildspec-ci.yml @@ -36,6 +36,9 @@ phases: SRC="$CODEBUILD_SRC_DIR" else SRC=/tmp/cortex-cli-src + # Wipe only the source clone. Cargo artifacts live in + # CARGO_TARGET_DIR (/tmp/cortex-cli-target), which cache restores + # and this command must not delete. rm -rf "$SRC" git clone --no-tags "https://github.com/${REPO}.git" "$SRC" git -C "$SRC" checkout --detach "$SHA" diff --git a/deploy/aws/codebuild/cloudformation.yaml b/deploy/aws/codebuild/cloudformation.yaml index de0c1983..a56ba0a7 100644 --- a/deploy/aws/codebuild/cloudformation.yaml +++ b/deploy/aws/codebuild/cloudformation.yaml @@ -174,10 +174,9 @@ Resources: Condition: StringEquals: token.actions.githubusercontent.com:aud: sts.amazonaws.com - ForAnyValue:StringEquals: - token.actions.githubusercontent.com:sub: - - !Sub "repo:${GitHubOrgRepo}:ref:refs/heads/main" - - !Sub "repo:${GitHubOrgRepo}:pull_request" + token.actions.githubusercontent.com:sub: !Sub "repo:${GitHubOrgRepo}:ref:refs/heads/main" + StringLike: + token.actions.githubusercontent.com:job_workflow_ref: !Sub "${GitHubOrgRepo}/.github/workflows/codebuild.yml@refs/heads/main" Policies: - PolicyName: start-cli-codebuild PolicyDocument: diff --git a/deploy/aws/codebuild/iam-trust-policy.json b/deploy/aws/codebuild/iam-trust-policy.json index 4f6c1a71..3bc9e554 100644 --- a/deploy/aws/codebuild/iam-trust-policy.json +++ b/deploy/aws/codebuild/iam-trust-policy.json @@ -10,13 +10,11 @@ "Action": "sts:AssumeRoleWithWebIdentity", "Condition": { "StringEquals": { - "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" + "token.actions.githubusercontent.com:aud": "sts.amazonaws.com", + "token.actions.githubusercontent.com:sub": "repo:CortexLM/cli:ref:refs/heads/main" }, - "ForAnyValue:StringEquals": { - "token.actions.githubusercontent.com:sub": [ - "repo:CortexLM/cli:ref:refs/heads/main", - "repo:CortexLM/cli:pull_request" - ] + "StringLike": { + "token.actions.githubusercontent.com:job_workflow_ref": "CortexLM/cli/.github/workflows/codebuild.yml@refs/heads/main" } } } diff --git a/docs/CI_SECRETS.md b/docs/CI_SECRETS.md index 251602b3..633b93f5 100644 --- a/docs/CI_SECRETS.md +++ b/docs/CI_SECRETS.md @@ -17,7 +17,7 @@ Marker: `CLI_CODEBUILD_CI_READY`. | Variable | Used for | |----------|----------| -| `AWS_CODEBUILD_ROLE_ARN` | IAM role assumed by GitHub Actions (`repo:CortexLM/cli:*`) | +| `AWS_CODEBUILD_ROLE_ARN` | IAM role assumed by GitHub Actions (`repo:CortexLM/cli:ref:refs/heads/main` only; this workflow on `main`) | | `AWS_REGION` | CodeBuild region (workflow default `us-east-1`) | | `AWS_CODEBUILD_PROJECT_X64` | Optional; default project `cortex-cli-gha-x64` (push to `main`, S3 cache) | | `AWS_CODEBUILD_PROJECT_ARM64` | Optional; default project `cortex-cli-gha-arm64` (push to `main`, S3 cache) | diff --git a/scripts/readiness/test_codebuild.py b/scripts/readiness/test_codebuild.py index 6a5a1416..33233574 100644 --- a/scripts/readiness/test_codebuild.py +++ b/scripts/readiness/test_codebuild.py @@ -89,10 +89,16 @@ def test_status_check_names_match_backend_style_projects(self): def test_pull_requests_use_trusted_buildspec_and_uncached_projects(self): text = read(WORKFLOWS / "codebuild.yml") + triggers = self.workflow["on"] + self.assertIn("pull_request_target", triggers) + self.assertIn("pull_request", triggers) + self.assertIn("push", triggers) self.assertIn("github.event.pull_request.base.sha", text) + self.assertIn("persist-credentials: false", text) self.assertIn("cortex-cli-gha-x64-pr", text) self.assertIn("cortex-cli-gha-arm64-pr", text) - self.assertIn("pull_request", text) + self.assertIn("github.event_name != 'pull_request'", text) + self.assertIn("github.event_name == 'push'", text) self.assertIn("logs-only", text) def test_start_build_is_skipped_without_role_variable(self): @@ -131,8 +137,10 @@ def test_iam_templates_use_placeholders_and_cli_trust_only(self): trust_text = read(DEPLOY / "iam-trust-policy.json") perm_text = read(DEPLOY / "iam-gha-permissions.json") self.assertIn("repo:CortexLM/cli:ref:refs/heads/main", trust_text) - self.assertIn("repo:CortexLM/cli:pull_request", trust_text) + self.assertNotIn("repo:CortexLM/cli:pull_request", trust_text) self.assertNotIn("repo:CortexLM/cli:*", trust_text) + self.assertIn("job_workflow_ref", trust_text) + self.assertIn("codebuild.yml@refs/heads/main", trust_text) self.assertIn("ACCOUNT_ID", trust_text) self.assertIn("sts:AssumeRoleWithWebIdentity", trust_text) self.assertIn("cortex-cli-gha-x64", perm_text) @@ -165,8 +173,9 @@ def test_cloudformation_projects_and_oidc_role(self): self.assertIn("Type: S3", self.template) self.assertIn("BUILD_GENERAL1_LARGE", self.template) self.assertIn("repo:${GitHubOrgRepo}:ref:refs/heads/main", self.template) - self.assertIn("repo:${GitHubOrgRepo}:pull_request", self.template) + self.assertNotIn("repo:${GitHubOrgRepo}:pull_request", self.template) self.assertNotIn("repo:${GitHubOrgRepo}:*", self.template) + self.assertIn("job_workflow_ref", self.template) self.assertIn("cortex-cli-codebuild-service-pr", self.template) self.assertIn("Type: NO_CACHE", self.template) self.assertIn("token.actions.githubusercontent.com", self.template) @@ -178,7 +187,8 @@ def test_docs_name_variables_not_secret_values(self): docs = read(DEPLOY / "README.md") + read(ROOT / "docs/CI_SECRETS.md") self.assertIn("AWS_CODEBUILD_ROLE_ARN", docs) self.assertIn("cortex-cli-gha-x64", docs) - self.assertIn("repo:CortexLM/cli:pull_request", docs) + self.assertIn("repo:CortexLM/cli:ref:refs/heads/main", docs) + self.assertIn("pull_request_target", docs) self.assertIn("CLI_CODEBUILD_CI_READY", docs) self.assertIn("Windows", docs) self.assertNotIn("AKIA", docs) From 37690a837575561e850974ca966ff7dc439d3726 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 8 Sep 2026 00:41:08 +0000 Subject: [PATCH 4/5] fix(codebuild): validate pr tree in unprivileged wiring The wiring job was pinned to the PR base, so pull_request CI ran main and discovered zero tests. Checkout the PR sha on pull_request; keep the base pin only for pull_request_target and StartBuild. Co-authored-by: Mathis --- .github/workflows/codebuild.yml | 4 +++- scripts/readiness/test_codebuild.py | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/codebuild.yml b/.github/workflows/codebuild.yml index c96241b3..d151bccc 100644 --- a/.github/workflows/codebuild.yml +++ b/.github/workflows/codebuild.yml @@ -43,7 +43,9 @@ jobs: - uses: actions/checkout@v7 with: persist-credentials: false - ref: ${{ github.event.pull_request.base.sha || github.sha }} + # pull_request must validate the PR tree (github.sha). pin the base + # only for pull_request_target, which must not execute unapproved code. + ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha }} - uses: actions/setup-python@v5 with: python-version: '3.12' diff --git a/scripts/readiness/test_codebuild.py b/scripts/readiness/test_codebuild.py index 33233574..80798857 100644 --- a/scripts/readiness/test_codebuild.py +++ b/scripts/readiness/test_codebuild.py @@ -98,6 +98,7 @@ def test_pull_requests_use_trusted_buildspec_and_uncached_projects(self): self.assertIn("cortex-cli-gha-x64-pr", text) self.assertIn("cortex-cli-gha-arm64-pr", text) self.assertIn("github.event_name != 'pull_request'", text) + self.assertIn("github.event_name == 'pull_request_target'", text) self.assertIn("github.event_name == 'push'", text) self.assertIn("logs-only", text) From 0a377eda90a7e4587f1ace9cf0f3c390f3c32c98 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 8 Sep 2026 00:51:47 +0000 Subject: [PATCH 5/5] test(cli): accept interrupted abort frames under llvm-cov Coverage instrumentation can delay the first turn so cancel arrives as turn_aborted / interrupted instead of a typed Error event. Co-authored-by: Mathis --- src/cortex-cli/tests/exec_runtime.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/cortex-cli/tests/exec_runtime.rs b/src/cortex-cli/tests/exec_runtime.rs index 7d8d2dbc..3e80b5cb 100644 --- a/src/cortex-cli/tests/exec_runtime.rs +++ b/src/cortex-cli/tests/exec_runtime.rs @@ -342,14 +342,18 @@ fn a_second_turn_is_refused_while_one_is_running_and_the_budget_is_enforced() { assert_eq!(answer(3)["result"]["cancellation_requested"], true); // The failing turn surfaces as an error/abort event, never as a completion. + // llvm-cov slows the first turn so cancel can land as method `turn_aborted` + // / reason `interrupted` before a typed Error event is emitted. assert!( frames.iter().any(|frame| { let event = &frame["params"]["event"]; - event["type"] == "Error" || event["type"] == "TurnAborted" + event["type"] == "Error" + || event["type"] == "TurnAborted" + || event["reason"] == "interrupted" }) || frames.iter().any(|frame| { let msg = &frame["params"]["msg"]; msg["type"] == "error" || msg["type"] == "turn_aborted" - }), + }) || frames.iter().any(|frame| frame["method"] == "turn_aborted"), "the unreachable service must produce a failure event: {frames:#?}" ); assert!(