diff --git a/.github/workflows/README.md b/.github/workflows/README.md new file mode 100644 index 000000000..f1f7d5187 --- /dev/null +++ b/.github/workflows/README.md @@ -0,0 +1,71 @@ +# GitHub Workflows + +## Hierarchy + +Workflows are organized into two roles: + +``` +Orchestrators (e.g. ci.yml, release.yml) +Jobs (e.g. build.yml, unit-test.yml, publish-npm.yml) +``` + +**Orchestrators** respond to events and coordinate work. They define _when_ and +_in what order_ things happen, but contain minimal logic themselves. An orchestrator +is a composition of jobs. + +**Jobs** are self-contained, reusable units of work. They accept inputs (like a +`ref` to check out), do one thing, and report pass/fail. A job doesn't know or +care what triggered it. + +An orchestrator calls jobs via `workflow_call`. + +### Current example + +``` +ci.yml + ├── build.yml (lint, format, typecheck, audit, bundle, compile) + └── unit-test.yml (tests on Linux, Windows, macOS) +``` + +### Future examples + +``` +release.yml + ├── unit-test.yml + ├── build.yml + └── publish-npm.yml +``` + +Jobs like `unit-test.yml` and `build.yml` appear in multiple orchestrators. This +is the point — write once, compose freely. + +## Naming Convention + +- **Orchestrators** are named for their purpose (e.g. `ci`, `release`). +- **Jobs** are named as verbs or noun-verb pairs describing the work + (e.g. `build`, `unit-test`, `publish-npm`). + +## Key Choices + +### Explicit ref passing + +Every job accepts a `ref` input and passes it to `actions/checkout`. The +orchestrator resolves the correct commit once and threads it to each job. This +ensures all jobs check out the exact same commit — important for PRs where the +default `github.sha` points to a merge commit that may shift mid-workflow. + +### persist-credentials: false + +All checkout steps disable credential persistence. Jobs only need read access to +clone; dropping the token from the local git config avoids accidental credential +leakage in downstream steps. + +### Minimal permissions + +Every job declares the least privilege it needs. Most only require +`permissions: { contents: read }`. + +### Bun throughout + +All jobs use `oven-sh/setup-bun@v2` and `bun install --frozen-lockfile`. The +lockfile is enforced to keep CI deterministic. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 000000000..677beb62c --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,35 @@ +# Verifies lint, formatting, types, audit, and build all pass. +name: build +on: + workflow_call: + inputs: + ref: + required: true + type: string + +jobs: + check: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ inputs.ref }} + persist-credentials: false + - uses: oven-sh/setup-bun@v2 + - run: bun install --frozen-lockfile + - run: bun run lint:check + if: always() + - run: bun run format:check + if: always() + - run: bun run typecheck + if: always() + - run: bun audit + if: always() + - run: bun run build + if: always() + - run: bun pm pack + if: always() + - run: bun run compile + if: always() diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..91dcaf999 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,27 @@ +# Orchestrates all CI checks on PRs and pushes to main. +name: CI +on: + # TODO: remove refactor from below once this makes it to main. + push: + branches: [main, refactor] + pull_request: + branches: [main, refactor] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + build: + uses: ./.github/workflows/build.yml + permissions: + contents: read + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + unit-test: + uses: ./.github/workflows/unit-test.yml + permissions: + contents: read + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml new file mode 100644 index 000000000..58394b8b9 --- /dev/null +++ b/.github/workflows/unit-test.yml @@ -0,0 +1,34 @@ +# Verifies unit tests pass on all platforms (Linux, Windows, macOS). +name: unit-test +on: + workflow_call: + inputs: + ref: + required: true + type: string + +jobs: + test: + name: Test (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + permissions: + contents: read + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ inputs.ref }} + persist-credentials: false + - uses: oven-sh/setup-bun@v2 + - run: bun install --frozen-lockfile + # TODO: investigate sharding as repo grows: https://bun.com/blog/release-notes/bun-v1.3.13#bun-test-shard-m-n-for-splitting-tests-across-ci-jobs + - run: bun test --coverage --coverage-reporter=lcov + + - name: Upload to CodeCov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: false diff --git a/package.json b/package.json index d5dce8f49..d3c31bcc9 100644 --- a/package.json +++ b/package.json @@ -21,12 +21,13 @@ "compile:windows-arm64": "bun build --compile --minify --target=bun-windows-arm64 ./src/index.ts --outfile dist/bin/agentcore-windows-arm64", "start": "bun run src/index.ts", "test": "bun test", + "typecheck": "tsc --noEmit", "lint": "oxlint --fix", "lint:check": "oxlint", "format": "prettier --write .", "format:check": "prettier --check .", "prepublishOnly": "bun run build", - "prepare": "husky" + "prepare": "husky || true" }, "lint-staged": { "*.{ts,tsx}": [ diff --git a/src/handlers/harness/exec/exec.screen.test.tsx b/src/handlers/harness/exec/exec.screen.test.tsx index 256fd05f9..66892af8e 100644 --- a/src/handlers/harness/exec/exec.screen.test.tsx +++ b/src/handlers/harness/exec/exec.screen.test.tsx @@ -11,6 +11,7 @@ import { cleanupScreens, StreamController, TestCoreClient, + waitFor, } from "../../../testing"; afterEach(cleanupScreens); diff --git a/src/handlers/harness/exec/exec.test.tsx b/src/handlers/harness/exec/exec.test.tsx index ad520df2c..bb548a98d 100644 --- a/src/handlers/harness/exec/exec.test.tsx +++ b/src/handlers/harness/exec/exec.test.tsx @@ -5,7 +5,7 @@ import type { } from "@aws-sdk/client-bedrock-agentcore"; import type { GetHarnessResponse } from "@aws-sdk/client-bedrock-agentcore-control"; import { createRootHandler } from "../../index"; -import { TestCoreClient, testIO } from "../../../testing"; +import { createSilentLogger, TestCoreClient, testIO } from "../../../testing"; // Command-flow tests for `harness exec`, driven through the real root handler. // Like the invoke suite, these use a TestCoreClient because the command @@ -31,7 +31,7 @@ async function run(args: string[], configure?: (core: TestCoreClient) => void) { core.harness.setExecEvents(...EXEC_EVENTS); configure?.(core); const io = testIO(); - const root = createRootHandler(core, io.io); + const root = createRootHandler(core, { io: io.io, logger: createSilentLogger() }); await root.route(["node", "agentcore", ...args, "--region", "us-west-2"]); return { core, stdout: io.stdout() }; }