Skip to content

feat(runner): isolate structured workflows in one-attempt Kubernetes Pods #738

feat(runner): isolate structured workflows in one-attempt Kubernetes Pods

feat(runner): isolate structured workflows in one-attempt Kubernetes Pods #738

Workflow file for this run

# Quality gates only. No image build, no release.
#
# Triggers:
# - pull_request: fast feedback on every PR
# - push to main: post-merge sanity (catches squash-regressions). Releases
# are cut by merging the release-please Release PR, which these same PR
# checks gate.
# - workflow_call: retained for reuse; release-please.yml relies on the PR
# checks above rather than calling this workflow.
name: CI
on:
pull_request:
push:
branches: [main]
workflow_call:
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
lint-and-test:
name: Lint & Test
# `head_commit.message` is only present on `push` events. On
# `pull_request` and `workflow_call` it is null and contains() against
# null can behave unexpectedly. Restrict the skip-ci optimization to
# push events so PRs always run CI.
if: ${{ github.event_name != 'push' ||
!contains(github.event.head_commit.message, 'skip ci') }}
runs-on: ubuntu-24.04
timeout-minutes: 10
# Postgres + Valkey are provisioned as service containers so the
# DB-backed integration suites (test/db/migrate.test.ts,
# test/integration/repo-knowledge.test.ts,
# test/integration/telemetry-aggregates.test.ts) actually execute in
# CI instead of `describe.skipIf`-ing themselves to green. Image tags
# and healthcheck cadence mirror docker-compose.dev.yml.
services:
postgres:
image: pgvector/pgvector:pg17
env:
POSTGRES_USER: bot
POSTGRES_PASSWORD: bot
POSTGRES_DB: github_app
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U bot -d github_app"
--health-interval 5s
--health-timeout 3s
--health-retries 5
valkey:
image: valkey/valkey:9
ports:
- 6379:6379
options: >-
--health-cmd "valkey-cli ping"
--health-interval 5s
--health-timeout 3s
--health-retries 5
steps:
- name: Checkout source code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
# Full history kept so release-please can walk tags/commits for
# accurate version + changelog inference. Secret scanning lives
# in .github/workflows/secrets-scan.yml now.
fetch-depth: 0
- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version-file: .tool-versions
- name: Cache Bun install dir
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.bun/install/cache
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }}
restore-keys: |
${{ runner.os }}-bun-
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Typecheck
run: bun run typecheck
- name: Lint
run: bun run lint
- name: Format check
run: bun run format
- name: Dockerfile base-sync guard
run: bun run check:dockerfile-base-sync
- name: GitHub Actions SHA-pin guard
# Fails if any third-party `uses:` reference is on a mutable tag
# instead of a 40-char commit SHA. Prevents a hand-edited workflow
# from reintroducing the supply-chain vector closed in issue #137.
run: bun run check:action-pins
- name: Runner pin guard
# Fails if any `runs-on:` is on a `*-latest` rolling alias instead of
# an explicit image (e.g. ubuntu-24.04). Prevents a hand-edited
# workflow from reintroducing the drift closed in issue #173.
run: bun run check:runner-pins
- name: Test-glob drift guard
# Fails if any `*.test.ts` file is outside the canonical `test/` tree
# consumed by scripts/test-isolated.sh.
run: bun run check:test-globs
- name: Destructive-action guard (FR-009)
# Fails if the ship workflow or a daemon scoped executor contains a
# destructive git/merge call (force-push, reset --hard, gh pr merge,
# merge mutations). Previously local-only; wired into CI so the guard
# is actually enforced. See issue #203.
run: bun run check:no-destructive
- name: Env-contract guard
# Fails on a stale env-contract.json (vs src/config.ts's loadConfig),
# an env var undocumented in docs/operate/configuration.md, or a
# secret-shaped name not classified in SECRET_ENV_VARS. chrisleekr/helm-charts
# consumes env-contract.json (at v<appVersion>) for its ConfigMap/Secret
# parity gate, so this keeps config.ts, the contract, and the docs aligned.
run: bun run check:env-contract
- name: Config-schema guard
# Fails when schema/github-app.schema.json drifts from
# src/repo-config/schema.ts. That file is what editors consume via the
# `# yaml-language-server: $schema=` modeline, so a stale copy would
# advertise a config surface the runtime no longer accepts.
run: bun run check:config-schema
- name: Docs-sync guard (bot workflows, FR-019)
if: github.event_name == 'pull_request'
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: bun run check:docs-sync
- name: Dependency audit
# Wraps `bun audit --json` to gate on severity. `bun audit` itself
# exits 1 on ANY advisory regardless of --audit-level, so the
# wrapper restores severity-based gating + an inline GHSA allowlist.
run: bun run audit:ci
- name: Seed test database
# The `services:` Postgres container does NOT execute
# /docker-entrypoint-initdb.d/, so the github_app_test database
# mounted by docker-compose.dev.yml does not exist here. Create
# it explicitly before the test step. `psql` ships with
# postgresql-client, which is preinstalled on ubuntu-24.04
# runners (apt-get install kept as a fallback in case the image
# changes upstream).
env:
PGPASSWORD: bot
run: |
if ! command -v psql >/dev/null 2>&1; then
sudo apt-get update
sudo apt-get install -y --no-install-recommends postgresql-client
fi
psql -h localhost -U bot -d github_app -v ON_ERROR_STOP=1 -f scripts/init-test-db.sql
- name: Test
env:
TEST_DATABASE_URL: postgres://bot:bot@localhost:5432/github_app_test
# `VALKEY_URL` (no `TEST_` prefix) is what the test code actually
# reads: `test/preload.ts` defaults it and `liveness-reaper.test.ts`
# references `process.env["VALKEY_URL"]`. Setting it explicitly here
# makes CI independent of the preload default so a future preload
# tweak (or port-mapping change) cannot silently regress this job.
VALKEY_URL: redis://localhost:6379
run: bun run test
- name: Build
run: bun run build
- name: MCP bundle smoke check
# Validates that every MCP server name registered in src/mcp/registry.ts
# resolves to an existing dist/mcp/servers/<name>.js when consumed from
# both bundle locations (dist/app.js + dist/daemon/main.js). Catches the
# build/resolve drift class that shipped in 001990d before the daemon
# image hits production. Must run AFTER `bun run build`.
run: bun run check:mcp-bundle
admission-policy:
name: Workflow runner admission
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- name: Checkout source code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
# This job runs repository-controlled scripts (bun install, the kind
# admission harness). Nothing here talks to git after checkout, so the
# token has no reason to sit in .git/config where those scripts read.
persist-credentials: false
- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version-file: .tool-versions
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Validate admission against Kubernetes 1.30
run: bun run test:admission