From 4b6bf69eba7e9ecf98ab43c6eb36fc63ace5e88e Mon Sep 17 00:00:00 2001 From: Will Cullen Date: Fri, 15 May 2026 00:30:19 +1000 Subject: [PATCH 1/4] feat: develop inside a dev container with nested docker daemon Move the inner loop (Postgres, Vite, Vitest, Playwright, testcontainers) into a per-workspace dev container running docker-in-docker. The root docker-compose.yml stays the single source of truth for app services and is brought up against the container's nested daemon, so multiple Conductor workspaces no longer collide on host port 5432, the host docker daemon, or shared node_modules/pnpm state. CI migrates every job to devcontainers/ci@v0.4 so it runs against the same image used locally. --- .devcontainer/devcontainer.json | 21 +++++++ .devcontainer/docker-compose.yml | 17 ++++++ .devcontainer/post-create.sh | 14 +++++ .github/workflows/ci.yml | 96 ++++++++++---------------------- AGENTS.md | 2 +- README.md | 10 +++- 6 files changed, 91 insertions(+), 69 deletions(-) create mode 100644 .devcontainer/devcontainer.json create mode 100644 .devcontainer/docker-compose.yml create mode 100755 .devcontainer/post-create.sh diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..c31dd5b --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,21 @@ +{ + "name": "starter", + "dockerComposeFile": "docker-compose.yml", + "service": "workspace", + "workspaceFolder": "/workspace", + + "forwardPorts": [5173, 4983], + "portsAttributes": { + "5173": { "label": "Vite dev", "onAutoForward": "notify" }, + "4983": { "label": "Drizzle Studio", "onAutoForward": "silent" } + }, + + "features": { + "ghcr.io/devcontainers/features/docker-in-docker:2": {} + }, + + "postCreateCommand": ".devcontainer/post-create.sh", + "postStartCommand": "docker compose -f /workspace/docker-compose.yml up -d --wait", + + "remoteUser": "node" +} diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml new file mode 100644 index 0000000..61d1fd9 --- /dev/null +++ b/.devcontainer/docker-compose.yml @@ -0,0 +1,17 @@ +services: + workspace: + image: mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm + command: sleep infinity + privileged: true + environment: + TESTCONTAINERS_RYUK_DISABLED: 'true' + volumes: + - ..:/workspace:cached + - node-modules:/workspace/node_modules + - pnpm-store:/home/node/.local/share/pnpm/store + - dind-storage:/var/lib/docker + +volumes: + node-modules: + pnpm-store: + dind-storage: diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh new file mode 100755 index 0000000..f9b9446 --- /dev/null +++ b/.devcontainer/post-create.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -euo pipefail + +corepack enable +corepack prepare pnpm@10.32.1 --activate + +pnpm config set store-dir /home/node/.local/share/pnpm/store + +pnpm install + +pnpm exec playwright install --with-deps + +docker version >/dev/null +docker compose -f /workspace/docker-compose.yml up -d --wait diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bf63890..5284eae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,101 +13,65 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4 + - uses: devcontainers/ci@v0.4 with: - node-version: 22 - cache: pnpm - - run: pnpm install --frozen-lockfile - - name: Lint - run: pnpm oxlint - - name: Format check - run: pnpm oxfmt --check . + imageName: ghcr.io/${{ github.repository }}/devcontainer + cacheFrom: ghcr.io/${{ github.repository }}/devcontainer + push: never + runCmd: pnpm oxlint && pnpm oxfmt --check . test-unit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4 + - uses: devcontainers/ci@v0.4 with: - node-version: 22 - cache: pnpm - - run: pnpm install --frozen-lockfile - - name: Run unit tests - run: pnpm vitest run --project unit --passWithNoTests + imageName: ghcr.io/${{ github.repository }}/devcontainer + cacheFrom: ghcr.io/${{ github.repository }}/devcontainer + push: never + runCmd: pnpm vitest run --project unit --passWithNoTests test-browser: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4 + - uses: devcontainers/ci@v0.4 with: - node-version: 22 - cache: pnpm - - run: pnpm install --frozen-lockfile - - name: Install Playwright browsers - run: npx playwright install --with-deps chromium - - name: Run browser tests - run: pnpm vitest run --project browser --passWithNoTests + imageName: ghcr.io/${{ github.repository }}/devcontainer + cacheFrom: ghcr.io/${{ github.repository }}/devcontainer + push: never + runCmd: pnpm vitest run --project browser --passWithNoTests test-integration: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4 + - uses: devcontainers/ci@v0.4 with: - node-version: 22 - cache: pnpm - - run: pnpm install --frozen-lockfile - - name: Run integration tests - run: pnpm vitest run --project integration - env: - TESTCONTAINERS_RYUK_DISABLED: 'true' + imageName: ghcr.io/${{ github.repository }}/devcontainer + cacheFrom: ghcr.io/${{ github.repository }}/devcontainer + push: never + runCmd: pnpm vitest run --project integration build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4 + - uses: devcontainers/ci@v0.4 with: - node-version: 22 - cache: pnpm - - run: pnpm install --frozen-lockfile - - name: Build - run: pnpm vite build + imageName: ghcr.io/${{ github.repository }}/devcontainer + cacheFrom: ghcr.io/${{ github.repository }}/devcontainer + push: never + runCmd: pnpm vite build test-e2e: needs: [build] runs-on: ubuntu-latest - services: - postgres: - image: postgres:18 - env: - POSTGRES_USER: test - POSTGRES_PASSWORD: test - POSTGRES_DB: test - ports: - - 5432:5432 - options: >- - --health-cmd="pg_isready -U test" - --health-interval=10s - --health-timeout=5s - --health-retries=5 steps: - uses: actions/checkout@v6 - - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4 + - uses: devcontainers/ci@v0.4 with: - node-version: 22 - cache: pnpm - - run: pnpm install --frozen-lockfile - - name: Install Playwright browsers - run: npx playwright install --with-deps - - name: Run e2e tests - run: npx playwright test - env: - WRANGLER_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE: postgres://test:test@localhost:5432/test + imageName: ghcr.io/${{ github.repository }}/devcontainer + cacheFrom: ghcr.io/${{ github.repository }}/devcontainer + push: never + runCmd: npx playwright test diff --git a/AGENTS.md b/AGENTS.md index aaf261f..c28dc49 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,7 +15,7 @@ This project uses **Vite 8**, **Vitest 4**, **Stryker**, **oxlint**, **oxfmt**, - `pnpm test` - Run all tests (Vitest, watch mode) - `pnpm test:unit` - Run unit tests only - `pnpm test:browser` - Run browser tests only -- `pnpm test:integration` - Run integration tests (requires Docker) +- `pnpm test:integration` - Run integration tests (runs against the nested docker daemon inside the dev container; do not start docker on the host) - `pnpm test:e2e` - Run Playwright end-to-end tests - `pnpm test:mutate` - Run Stryker mutation tests against unit-tested source - `pnpm vitest run --project ` - Run a specific test project in CI mode (no watch) diff --git a/README.md b/README.md index 202d4d8..7fc284a 100644 --- a/README.md +++ b/README.md @@ -18,11 +18,17 @@ This README is both **descriptive** (what the repo enforces today) and **prescri ## Getting started +This repo is developed exclusively inside a dev container. The container owns the inner loop — Postgres, the Vite dev server, Vitest, Playwright browsers, and the testcontainers daemon all run inside it, so two workspaces side-by-side never collide on host ports or share state. Local `pnpm install` / `pnpm dev` on the host is unsupported. + +Open the workspace and **Reopen in Container** (VS Code / Cursor with the Dev Containers extension), or: + ```sh -pnpm install -pnpm dev +devcontainer up --workspace-folder . +devcontainer exec --workspace-folder . pnpm dev ``` +The first build runs `.devcontainer/post-create.sh`, which installs pnpm deps and Playwright browsers into per-workspace named volumes, then runs `docker compose up -d --wait` against the root `docker-compose.yml` on the container's nested docker daemon. Postgres (and any future app services) are reachable inside the container at `localhost:5432` and are not published to the real host. + Run the test suites: ```sh From 9f88d08602ee6f896c054b04c71bf1e9aa7ebc7c Mon Sep 17 00:00:00 2001 From: Will Cullen Date: Fri, 15 May 2026 00:40:42 +1000 Subject: [PATCH 2/4] docs: keep AGENTS.md integration-test note unchanged Revert the AGENTS.md tweak from the previous commit. The agent sees a docker host either way; it doesn't need to know about the dev container. --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index c28dc49..aaf261f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,7 +15,7 @@ This project uses **Vite 8**, **Vitest 4**, **Stryker**, **oxlint**, **oxfmt**, - `pnpm test` - Run all tests (Vitest, watch mode) - `pnpm test:unit` - Run unit tests only - `pnpm test:browser` - Run browser tests only -- `pnpm test:integration` - Run integration tests (runs against the nested docker daemon inside the dev container; do not start docker on the host) +- `pnpm test:integration` - Run integration tests (requires Docker) - `pnpm test:e2e` - Run Playwright end-to-end tests - `pnpm test:mutate` - Run Stryker mutation tests against unit-tested source - `pnpm vitest run --project ` - Run a specific test project in CI mode (no watch) From 28f2afc16d06b62710145c209d5ea29c5f1832a7 Mon Sep 17 00:00:00 2001 From: Will Cullen Date: Fri, 15 May 2026 13:44:05 +1000 Subject: [PATCH 3/4] fix(ci): pin devcontainers/ci to v0.3 v0.4 does not exist; v0.3 is the current moving tag. --- .github/workflows/ci.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5284eae..793ef6d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - - uses: devcontainers/ci@v0.4 + - uses: devcontainers/ci@v0.3 with: imageName: ghcr.io/${{ github.repository }}/devcontainer cacheFrom: ghcr.io/${{ github.repository }}/devcontainer @@ -24,7 +24,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - - uses: devcontainers/ci@v0.4 + - uses: devcontainers/ci@v0.3 with: imageName: ghcr.io/${{ github.repository }}/devcontainer cacheFrom: ghcr.io/${{ github.repository }}/devcontainer @@ -35,7 +35,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - - uses: devcontainers/ci@v0.4 + - uses: devcontainers/ci@v0.3 with: imageName: ghcr.io/${{ github.repository }}/devcontainer cacheFrom: ghcr.io/${{ github.repository }}/devcontainer @@ -46,7 +46,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - - uses: devcontainers/ci@v0.4 + - uses: devcontainers/ci@v0.3 with: imageName: ghcr.io/${{ github.repository }}/devcontainer cacheFrom: ghcr.io/${{ github.repository }}/devcontainer @@ -57,7 +57,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - - uses: devcontainers/ci@v0.4 + - uses: devcontainers/ci@v0.3 with: imageName: ghcr.io/${{ github.repository }}/devcontainer cacheFrom: ghcr.io/${{ github.repository }}/devcontainer @@ -69,7 +69,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - - uses: devcontainers/ci@v0.4 + - uses: devcontainers/ci@v0.3 with: imageName: ghcr.io/${{ github.repository }}/devcontainer cacheFrom: ghcr.io/${{ github.repository }}/devcontainer From 14e8e3358ad4fc44bc6f002aba8c6cd2ae9a6dd9 Mon Sep 17 00:00:00 2001 From: Will Cullen Date: Sat, 16 May 2026 10:26:25 +1000 Subject: [PATCH 4/4] Revert "ci: run all jobs in dev container via devcontainers/ci" Drop the CI migration to devcontainers/ci; keep CI on the original host-runner setup. The devcontainer changes for local dev remain. --- .github/workflows/ci.yml | 96 +++++++++++++++++++++++++++------------- 1 file changed, 66 insertions(+), 30 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 793ef6d..bf63890 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,65 +13,101 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - - uses: devcontainers/ci@v0.3 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 with: - imageName: ghcr.io/${{ github.repository }}/devcontainer - cacheFrom: ghcr.io/${{ github.repository }}/devcontainer - push: never - runCmd: pnpm oxlint && pnpm oxfmt --check . + node-version: 22 + cache: pnpm + - run: pnpm install --frozen-lockfile + - name: Lint + run: pnpm oxlint + - name: Format check + run: pnpm oxfmt --check . test-unit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - - uses: devcontainers/ci@v0.3 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 with: - imageName: ghcr.io/${{ github.repository }}/devcontainer - cacheFrom: ghcr.io/${{ github.repository }}/devcontainer - push: never - runCmd: pnpm vitest run --project unit --passWithNoTests + node-version: 22 + cache: pnpm + - run: pnpm install --frozen-lockfile + - name: Run unit tests + run: pnpm vitest run --project unit --passWithNoTests test-browser: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - - uses: devcontainers/ci@v0.3 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 with: - imageName: ghcr.io/${{ github.repository }}/devcontainer - cacheFrom: ghcr.io/${{ github.repository }}/devcontainer - push: never - runCmd: pnpm vitest run --project browser --passWithNoTests + node-version: 22 + cache: pnpm + - run: pnpm install --frozen-lockfile + - name: Install Playwright browsers + run: npx playwright install --with-deps chromium + - name: Run browser tests + run: pnpm vitest run --project browser --passWithNoTests test-integration: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - - uses: devcontainers/ci@v0.3 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 with: - imageName: ghcr.io/${{ github.repository }}/devcontainer - cacheFrom: ghcr.io/${{ github.repository }}/devcontainer - push: never - runCmd: pnpm vitest run --project integration + node-version: 22 + cache: pnpm + - run: pnpm install --frozen-lockfile + - name: Run integration tests + run: pnpm vitest run --project integration + env: + TESTCONTAINERS_RYUK_DISABLED: 'true' build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - - uses: devcontainers/ci@v0.3 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 with: - imageName: ghcr.io/${{ github.repository }}/devcontainer - cacheFrom: ghcr.io/${{ github.repository }}/devcontainer - push: never - runCmd: pnpm vite build + node-version: 22 + cache: pnpm + - run: pnpm install --frozen-lockfile + - name: Build + run: pnpm vite build test-e2e: needs: [build] runs-on: ubuntu-latest + services: + postgres: + image: postgres:18 + env: + POSTGRES_USER: test + POSTGRES_PASSWORD: test + POSTGRES_DB: test + ports: + - 5432:5432 + options: >- + --health-cmd="pg_isready -U test" + --health-interval=10s + --health-timeout=5s + --health-retries=5 steps: - uses: actions/checkout@v6 - - uses: devcontainers/ci@v0.3 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 with: - imageName: ghcr.io/${{ github.repository }}/devcontainer - cacheFrom: ghcr.io/${{ github.repository }}/devcontainer - push: never - runCmd: npx playwright test + node-version: 22 + cache: pnpm + - run: pnpm install --frozen-lockfile + - name: Install Playwright browsers + run: npx playwright install --with-deps + - name: Run e2e tests + run: npx playwright test + env: + WRANGLER_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE: postgres://test:test@localhost:5432/test