Skip to content

Commit 7a67ea0

Browse files
chrisleekrclaude
andcommitted
feat(runner): isolate structured workflows in one-attempt Kubernetes Pods
Stack 3 of 3, base `feat/workflow-rail-outbox`. Stack 2 leased structured workflow attempts in the database and removed the shared-daemon executor. This adds the executor that claims those leases: one bare Kubernetes Pod and one capability Secret per attempt, for exactly one attempt. - `src/runner/`: the Pod-side process. One attempt, its own RPC client, its own output sanitizer, a token deadline, and a process boundary. - `src/orchestrator/workflow-runner-*`: payload preparation, per-attempt capability minting, dispatch, result settlement, reconciliation and resource teardown, each fenced by run id, attempt id, owner id, lease and command receipt. - `src/k8s/workflow-runner-spawner.ts`: creates the Pod, then makes the Secret a Kubernetes-owned dependent of that exact Pod UID. - `src/shared/workflow-runner-messages.ts` is a separate schema from `ws-messages.ts`, not a superset, so a workflow-run can never enter the shared-daemon protocol by accident. - Runner RPC output scanning is fail-CLOSED, unlike `safePostToGitHub`: scanner disablement, failure or timeout rejects the command. The Pod holds a repo-scoped write token and handles attacker-influenced content, so degrading to regex-only there is not an acceptable trade. - New `admission-policy` CI job validates the runner admission spec against Kubernetes 1.30 via `bun run test:admission`. Structured dispatch fails closed in PAT mode: a PAT cannot be narrowed to a single repository. Verified: typecheck, lint, format, all check gates, 213/213 test files against live Postgres 17 + Valkey. The resulting tree is byte-identical to the pre-split single-PR branch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KUPpJPtxAaHWrBsjytRGyM
1 parent 559d954 commit 7a67ea0

71 files changed

Lines changed: 14064 additions & 434 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,8 @@ jobs:
112112
run: bun run check:runner-pins
113113

114114
- name: Test-glob drift guard
115-
# Fails if any `*.test.ts` file is not reachable by the runner glob in
116-
# scripts/test-isolated.sh, so a colocated test outside the globbed
117-
# roots cannot go dark while CI stays green. See issue #201.
115+
# Fails if any `*.test.ts` file is outside the canonical `test/` tree
116+
# consumed by scripts/test-isolated.sh.
118117
run: bun run check:test-globs
119118

120119
- name: Destructive-action guard (FR-009)
@@ -190,3 +189,22 @@ jobs:
190189
# build/resolve drift class that shipped in 001990d before the daemon
191190
# image hits production. Must run AFTER `bun run build`.
192191
run: bun run check:mcp-bundle
192+
193+
admission-policy:
194+
name: Workflow runner admission
195+
runs-on: ubuntu-24.04
196+
timeout-minutes: 10
197+
steps:
198+
- name: Checkout source code
199+
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
200+
201+
- name: Setup Bun
202+
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
203+
with:
204+
bun-version-file: .tool-versions
205+
206+
- name: Install dependencies
207+
run: bun install --frozen-lockfile
208+
209+
- name: Validate admission against Kubernetes 1.30
210+
run: bun run test:admission

.github/workflows/docker-build.yml

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
# - chrisleekr/github-app:<tag>-daemon (rich, Claude worker)
77
# Orchestrator additionally publishes a bare `<tag>` alias so existing
88
# consumers that pull `chrisleekr/github-app:<version>` keep working.
9-
# `:latest` is published on prod releases only, on the orchestrator variant.
9+
# Each variant also gets a mutable `:latest-<variant>` alias, so `latest-daemon`
10+
# resolves on Docker Hub the same way it does on the GitLab registry. Both that
11+
# alias and the bare `:latest` (orchestrator only) are prod-release only: a beta
12+
# prerelease must not move a tag prod consumers follow.
1013
#
1114
# Multi-platform via the documented split-and-merge pattern:
1215
# https://docs.docker.com/build/ci/github-actions/multi-platform/#distribute-build-across-multiple-runners
@@ -185,6 +188,22 @@ jobs:
185188
GIT_HASH=${{ steps.meta.outputs.git_hash }}
186189
NODE_ENV=production
187190
191+
- name: Smoke daemon process boundary
192+
if: matrix.variant == 'daemon'
193+
env:
194+
DIGEST: ${{ steps.build.outputs.digest }}
195+
run: |
196+
docker pull "${IMAGE_NAME}@${DIGEST}"
197+
docker run --rm \
198+
--user 1000:1000 \
199+
--read-only \
200+
--network none \
201+
--cap-drop ALL \
202+
--security-opt no-new-privileges=true \
203+
--entrypoint bun \
204+
"${IMAGE_NAME}@${DIGEST}" \
205+
run dist/daemon/process-boundary-smoke.js
206+
188207
- name: Export digest
189208
env:
190209
DIGEST: ${{ steps.build.outputs.digest }}
@@ -256,6 +275,7 @@ jobs:
256275
type=raw,value=${{ steps.tag.outputs.variant_tag }}
257276
type=raw,value=${{ steps.tag.outputs.version }},enable=${{ matrix.variant == 'orchestrator' }}
258277
type=raw,value=latest,enable=${{ matrix.variant == 'orchestrator' && inputs.is-dev-release == false }}
278+
type=raw,value=latest-${{ matrix.variant }},enable=${{ inputs.is-dev-release == false }}
259279
260280
- name: Create manifest list and push
261281
working-directory: ${{ runner.temp }}/digests

CLAUDE.md

Lines changed: 29 additions & 20 deletions
Large diffs are not rendered by default.

CONTRIBUTING.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,9 @@ bun run test:watch # re-run on file changes
6363
bun run test:coverage # explicit coverage report (output in coverage/)
6464
```
6565

66-
Test files live under `test/` mirroring the `src/` directory structure, or
67-
colocated as `src/.../foo.test.ts`. Both are CI-gated: the runner globs
68-
`test/**/*.test.ts src/**/*.test.ts`, and `bun run check:test-globs` fails CI
69-
if any `*.test.ts` is unreachable by that glob set.
66+
Test files live under `test/`, mirroring the `src/` directory structure. The
67+
runner globs `test/**/*.test.ts`, and `bun run check:test-globs` fails CI if any
68+
`*.test.ts` file is placed outside that tree.
7069

7170
**Coverage threshold**: `bunfig.toml` enforces a per-file minimum of **90%
7271
lines and 90% functions** via Bun's native `coverageThreshold`. Any PR that

bunfig.toml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@
22
# Preload test environment setup before each test file
33
preload = ["./test/preload.ts"]
44

5-
# Test timeout (ms) -- prevent hung tests in CI
6-
timeout = 30000
7-
85
# Coverage configuration.
96
# coverageThreshold is applied per-file: every file must meet both minimums.
107
# See https://bun.sh/docs/test/code-coverage#coverage-thresholds

docs/build/architecture.md

Lines changed: 351 additions & 68 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)