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
37 changes: 37 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Mirrors the root package.json scripts step for step: keep the two in
# correspondence when adding a check to either side.
name: CI

on:
pull_request:
push:
branches: [main]

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm

- run: npm ci

- run: npm run typecheck

- run: npm test

- run: npm run build

- name: Version lockstep
run: npm run check-version
21 changes: 19 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,31 @@ npm test
npm run build
```

Releases bump every package to one version, then publish them all:
Releases bump every package to one version, then publish them all. `npm version`
fans the bump out through `scripts/set-version.mjs` (workspace manifests, internal
ranges, embedded `VERSION` constants) and refreshes the lockfile. It also creates
a release commit and a `v<version>` tag, and refuses to run on a dirty tree, so
commit or stash first. Pass `-m` to keep the `chore(release): vX.Y.Z` commit
convention:

```sh
npm run set-version -- 0.2.0
npm version 0.2.0 -m 'chore(release): v%s'
npm run build
npm publish --workspaces
```

Add `--no-git-tag-version` to skip the commit and tag; the bump is then left
Comment thread
cb-jeeves marked this conversation as resolved.
staged for you to commit yourself.

To bump without `npm version`, run `npm run set-version -- 0.2.0` followed by
`npm install --package-lock-only`; `check-version` does not inspect the lockfile,
so a bare `set-version` leaves `package-lock.json` behind unnoticed.

`npm run check-version` verifies the lockstep without changing anything:
Comment thread
cb-jeeves marked this conversation as resolved.
every manifest on the root version, every internal dependency range at
`^<version>`, every embedded `VERSION` constant matching. CI runs it alongside
typecheck, test, and build on every pull request and push to `main`.

## Getting started (operators)

```sh
Expand Down
51 changes: 51 additions & 0 deletions okf-bundle/decisions/cdk-tests-share-one-outdir.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
---
type: decision
title: CDK construct tests share one cloud-assembly outdir
description: Why construct tests build their App with test/support/test-app.ts
instead of new App(), and why vitest's timeout is 30 s.
tags:
- millwright
- testing
- cdk
timestamp: 2026-09-08T03:23:12.874Z
---

Every `Template.fromStack(...)` over the `Millwright` construct synthesizes a full stack, and that
stack bundles **ten `NodejsFunction` Lambdas with esbuild** (poller, launcher, sweep, three synth-job
functions, two run-executor functions, reporter, step-events writer) plus the synth tooling bundle
from `millwright-cli`. One synth costs roughly 1.5 s on a fast workstation and 5–11 s on a loaded
GitHub Actions runner — past vitest's 5 s default per-test budget. That is what made the first CI
workflow run (PR #36) fail with timeouts in `data-stores`, `millwright` and `run-executor-construct`.

## What was decided

- Construct tests build their App with `testApp()` from `packages/millwright-cdk/test/support/test-app.ts`,
never a bare `new App()`. The helper pins one cloud-assembly outdir per worker process.
- The root `vitest.config.ts` sets `testTimeout: 30_000`.

## Why the shared outdir works

`aws-cdk-lib`'s `AssetStaging` keeps a **process-wide cache** keyed on (outdir, source path, bundling
options). A bare `new App()` picks a fresh temp outdir every time, so the cache never hits and every
test in a file re-runs all the esbuild bundles. With one outdir per process the first synth in a file
bundles and every later synth reuses it: `data-stores.test.ts` dropped from ~21 s to ~4.5 s locally.

The outdir must be **per process**, not shared across vitest workers: each App writes
`manifest.json` and `Test.template.json` into it, and concurrent workers would race on those files.

## Why not `CDK_OUTDIR`

Setting the env var would reach every `new App()` without touching tests, but `App` treats a set
`CDK_OUTDIR` as a request for `autoSynth`, registering a `beforeExit` listener per App. Hundreds of
Apps per run means listener-leak warnings and exit-time synths of half-built trees from the
"throws at construct time" tests.

## Why not disable bundling

The `aws:cdk:bundling-stacks` context can skip bundling entirely, but the tests would then stop
exercising the real bundling configuration (workspace aliases, entry points, formats).

## Citations

[1] [test-app.ts](../../packages/millwright-cdk/test/support/test-app.ts)
[2] [vitest.config.ts](../../vitest.config.ts)
1 change: 1 addition & 0 deletions okf-bundle/decisions/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# Concepts

* [BatchGetBuilds is authoritative for terminal job state](batchgetbuilds-authoritative.md)
* [CDK construct tests share one cloud-assembly outdir](cdk-tests-share-one-outdir.md) - Why construct tests build their App with test/support/test-app.ts instead of new App(), and why vitest's timeout is 30 s.
* [Caught-timeout wake instead of task heartbeats](no-heartbeat-wake.md)
* [Why polling instead of webhooks](no-webhooks.md)
* [The poller is non-VPC](non-vpc-poller.md)
Expand Down
10 changes: 9 additions & 1 deletion okf-bundle/interfaces/packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,19 @@ between "millwright's own deployment is CDK" and "your workflows are not CDK".
```sh
npm install && npm run typecheck && npm test && npm run build

npm run set-version -- 0.2.0 # bump every package to one version
npm version 0.2.0 -m 'chore(release): v%s' # bump every package, commit, tag v0.2.0
npm run build
npm publish --workspaces
```

`npm run check-version` (`scripts/set-version.mjs --check`) asserts the lockstep without writing:
every manifest on the root version, every `@copperbox/millwright-*` range at `^<version>`, every
`src/version.ts` `VERSION` matching. `.github/workflows/ci.yml` runs typecheck, test, build, and
this check on pull requests and pushes to `main`. The root `version` lifecycle script fans an
`npm version` bump through `set-version.mjs` and refreshes the lockfile. `npm version` then
commits and tags `v<version>`, and it refuses to run on a dirty tree; `--no-git-tag-version`
leaves the bump staged without a commit or tag. Publishing stays manual.

## Related

- [Deployment construct](deployment.md) · [Run model](../schemas/run-model.md) for the
Expand Down
3 changes: 3 additions & 0 deletions okf-bundle/log.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Update Log

## 2026-09-08
* Record why CDK construct tests share one outdir and why vitest's timeout is 30 s

## 2026-08-13
* **Update**: Updated [Deferred and out of scope for v1](/deferred-and-out-of-scope.md).

Expand Down
22 changes: 11 additions & 11 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "millwright",
"version": "0.6.3",
"version": "0.7.0",
Comment thread
cb-jeeves marked this conversation as resolved.
"private": true,
"description": "Millwright monorepo — polling-driven CI/CD in your own AWS account",
"license": "MIT",
Expand All @@ -11,7 +11,9 @@
"build": "npm run build --workspace @copperbox/millwright-state --workspace @copperbox/millwright-workflows --workspace @copperbox/millwright-cdk --workspace @copperbox/millwright-cli",
"typecheck": "npm run typecheck --workspaces",
"test": "vitest run",
"set-version": "node scripts/set-version.mjs"
"set-version": "node scripts/set-version.mjs",
"check-version": "node scripts/set-version.mjs --check",
"version": "node scripts/set-version.mjs && npm install --package-lock-only && git add -u package.json packages package-lock.json"
},
"devDependencies": {
"@types/node": "^22.10.0",
Expand Down
8 changes: 4 additions & 4 deletions packages/millwright-cdk/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@copperbox/millwright-cdk",
"version": "0.6.2",
"version": "0.7.0",
"description": "The Millwright CDK construct — deploys the millwright control plane into your AWS account",
"license": "MIT",
"repository": {
Expand All @@ -23,9 +23,9 @@
"test": "vitest run"
},
"dependencies": {
"@copperbox/millwright-cli": "^0.6.2",
"@copperbox/millwright-state": "^0.6.2",
"@copperbox/millwright-workflows": "^0.6.2",
"@copperbox/millwright-cli": "^0.7.0",
"@copperbox/millwright-state": "^0.7.0",
"@copperbox/millwright-workflows": "^0.7.0",
"esbuild": "^0.28.0",
"ssh2": "^1.17.0"
},
Expand Down
2 changes: 1 addition & 1 deletion packages/millwright-cdk/src/version.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Kept in lockstep with package.json by scripts/set-version.mjs — do not edit by hand.
export const VERSION = '0.6.2';
export const VERSION = '0.7.0';

/**
* Highest run-model schemaVersion this control plane accepts. Synth fails
Expand Down
5 changes: 3 additions & 2 deletions packages/millwright-cdk/test/build-project.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { App, Stack } from 'aws-cdk-lib';
import { Stack } from 'aws-cdk-lib';
import { Match, Template } from 'aws-cdk-lib/assertions';
import * as logs from 'aws-cdk-lib/aws-logs';
import * as s3 from 'aws-cdk-lib/aws-s3';
import { describe, expect, it } from 'vitest';
import { BuildProject } from '../src';
import { testApp } from './support/test-app';

function synth(): { buildProject: BuildProject; template: Template } {
const stack = new Stack(new App(), 'Test');
const stack = new Stack(testApp(), 'Test');
const buildProject = new BuildProject(stack, 'BuildProject', {
deploymentName: 'ci',
artifactBucket: new s3.Bucket(stack, 'Artifacts'),
Expand Down
7 changes: 4 additions & 3 deletions packages/millwright-cdk/test/data-stores.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { App, Duration, Stack } from 'aws-cdk-lib';
import { Duration, Stack } from 'aws-cdk-lib';
import { Annotations, Match, Template } from 'aws-cdk-lib/assertions';
import { describe, expect, it } from 'vitest';
import { Millwright, MillwrightProps } from '../src';
import { testApp } from './support/test-app';

const BOUNDARY_ARN = 'arn:aws:iam::123456789012:policy/team-boundary';

Expand All @@ -10,7 +11,7 @@ function templateFor(props: Partial<MillwrightProps> = {}): {
millwright: Millwright;
template: Template;
} {
const stack = new Stack(new App(), 'Test');
const stack = new Stack(testApp(), 'Test');
const millwright = new Millwright(stack, 'Millwright', {
permissionsBoundary: BOUNDARY_ARN,
...props,
Expand Down Expand Up @@ -169,7 +170,7 @@ describe('build log group (C17)', () => {
});

it('rejects retention day counts CloudWatch does not support', () => {
const stack = new Stack(new App(), 'Test');
const stack = new Stack(testApp(), 'Test');
expect(
() =>
new Millwright(stack, 'Millwright', {
Expand Down
5 changes: 3 additions & 2 deletions packages/millwright-cdk/test/event-bus.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { App, Stack } from 'aws-cdk-lib';
import { Stack } from 'aws-cdk-lib';
import { Template } from 'aws-cdk-lib/assertions';
import { describe, expect, it } from 'vitest';
import { MillwrightEventBus } from '../src';
import { testApp } from './support/test-app';

function synth(deploymentName = 'millwright'): {
bus: MillwrightEventBus;
template: Template;
} {
const stack = new Stack(new App(), 'Test');
const stack = new Stack(testApp(), 'Test');
const bus = new MillwrightEventBus(stack, 'EventBus', { deploymentName });
return { bus, template: Template.fromStack(stack) };
}
Expand Down
5 changes: 3 additions & 2 deletions packages/millwright-cdk/test/launcher-construct.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { App, Duration, Stack } from 'aws-cdk-lib';
import { Duration, Stack } from 'aws-cdk-lib';
import { Match, Template } from 'aws-cdk-lib/assertions';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as s3 from 'aws-cdk-lib/aws-s3';
import { describe, expect, it } from 'vitest';
import { Launcher, MillwrightEventBus } from '../src';
import { testApp } from './support/test-app';

function synth(): { launcher: Launcher; template: Template } {
const stack = new Stack(new App(), 'Test');
const stack = new Stack(testApp(), 'Test');
const bus = new MillwrightEventBus(stack, 'EventBus', { deploymentName: 'ci' });
const launcher = new Launcher(stack, 'Launcher', {
deploymentName: 'ci',
Expand Down
Loading
Loading