Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions .github/workflows/README.md
Original file line number Diff line number Diff line change
@@ -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.
35 changes: 35 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -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()
27 changes: 27 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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 }}
34 changes: 34 additions & 0 deletions .github/workflows/unit-test.yml
Original file line number Diff line number Diff line change
@@ -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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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}": [
Expand Down
1 change: 1 addition & 0 deletions src/handlers/harness/exec/exec.screen.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
cleanupScreens,
StreamController,
TestCoreClient,
waitFor,
} from "../../../testing";

afterEach(cleanupScreens);
Expand Down
4 changes: 2 additions & 2 deletions src/handlers/harness/exec/exec.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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() };
}
Expand Down
Loading