Skip to content

Commit 2cf6463

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 a7f5231 commit 2cf6463

76 files changed

Lines changed: 14136 additions & 449 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

.gitlab-ci.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,35 @@ test-unit:
119119
# No `coverage:` regex or artifact: test-isolated.sh runs one bun process per
120120
# file and prints output only on failure, so there is no aggregate to parse.
121121

122+
admission-policy:
123+
<<: *gate
124+
# kind requires privileged DinD. This installation has no isolated ephemeral
125+
# DinD runner, so never expose this job to branch-controlled pipelines.
126+
rules:
127+
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_COMMIT_REF_PROTECTED == "true"
128+
image: oven/bun:1.3.14-alpine@sha256:5acc90a93e91ff07bf72aa90a7c9f0fa189765aec90b47bdbf2152d2196383c0
129+
services:
130+
- name: docker:29-dind@sha256:12e683a161823b2a839aeea999b9d960e6e1f9a97b1679ad6b441982e2d9cf07
131+
alias: docker
132+
variables:
133+
DOCKER_HOST: tcp://docker:2376
134+
DOCKER_TLS_CERTDIR: /certs
135+
DOCKER_TLS_VERIFY: 1
136+
DOCKER_CERT_PATH: $DOCKER_TLS_CERTDIR/client
137+
before_script:
138+
- apk add --no-cache docker-cli
139+
script:
140+
# kind publishes its API on the DinD host loopback. Run the harness in the
141+
# remote daemon's host network so its kubeconfig points at the right host.
142+
- >-
143+
docker run --rm --network host
144+
--volume /var/run/docker.sock:/var/run/docker.sock
145+
--volume "$CI_PROJECT_DIR:$CI_PROJECT_DIR"
146+
--workdir "$CI_PROJECT_DIR"
147+
oven/bun:1.3.14-alpine@sha256:5acc90a93e91ff07bf72aa90a7c9f0fa189765aec90b47bdbf2152d2196383c0
148+
sh -ec 'apk add --no-cache bash ca-certificates coreutils curl docker-cli
149+
&& bun run test:admission'
150+
122151
# The invariant guards from .github/workflows/ci.yml. They catch stale generated
123152
# artifacts and drifted pins, which a build alone cannot see.
124153
guards:
@@ -184,6 +213,11 @@ publish-dev-npm:
184213
artifacts: false
185214
- job: test-unit
186215
artifacts: false
216+
# `optional: true`: this job's rule excludes it on non-protected refs, and
217+
# a required edge to an excluded job rejects pipeline creation outright.
218+
- job: admission-policy
219+
artifacts: false
220+
optional: true
187221
- job: guards
188222
artifacts: false
189223
- job: build

0 commit comments

Comments
 (0)