Skip to content

feat(review): schedule fleet escalation sweep with Discord notify (#6… #1686

feat(review): schedule fleet escalation sweep with Discord notify (#6…

feat(review): schedule fleet escalation sweep with Discord notify (#6… #1686

Workflow file for this run

# Self-host stack CI (#980/#982). Provides integration coverage the main CI can't:
# 1. Postgres integration test — needs a real PG service container
# 2. Self-host bundle build validation (build-selfhost.mjs)
# 3. Docker image build + container smoke test (/health, /ready, /metrics)
# Unit tests and typecheck are NOT duplicated here — the main CI validate job covers them.
name: self-host
on:
push:
branches: [main]
paths:
- "src/selfhost/**"
- "src/server.ts"
- "scripts/build-selfhost.mjs"
- "scripts/validate-selfhost-sourcemap.mjs"
- "Dockerfile"
- "docker-compose.yml"
- "migrations/**"
- "test/unit/selfhost-*"
- "test/integration/selfhost-pg*"
- ".github/workflows/selfhost.yml"
pull_request:
paths:
- "src/selfhost/**"
- "src/server.ts"
- "scripts/build-selfhost.mjs"
- "scripts/validate-selfhost-sourcemap.mjs"
- "Dockerfile"
- "docker-compose.yml"
- "migrations/**"
- "test/unit/selfhost-*"
- "test/integration/selfhost-pg*"
- ".github/workflows/selfhost.yml"
# Least privilege — the smoke test only reads the repo; no writes, no packages.
permissions:
contents: read
concurrency:
# Same push/pr split as ci.yml's group, and for the same reason: pull requests stay in one ref-scoped
# group so newer commits cancel superseded PR runs (this job boots a Postgres service container, does a
# full npm ci, builds the self-host bundle, and runs two docker buildx builds plus a boot smoke test --
# letting a superseded push run to completion wastes real minutes). github.sha for push runs keeps
# distinct main commits from cancelling each other's validation.
group: selfhost-${{ github.ref }}-${{ github.event_name == 'push' && github.sha || 'pr' }}
# NB: keep this a literal boolean, not an expression — see ci.yml for why (startup_failure).
cancel-in-progress: true
jobs:
build-boot:
name: build + boot smoke test
runs-on: ubuntu-latest
timeout-minutes: 20
services:
postgres:
image: postgres:18-alpine
env:
POSTGRES_PASSWORD: devpw
POSTGRES_DB: gittensory
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U postgres" --health-interval 5s --health-timeout 5s --health-retries 10
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: "24.18.0"
cache: "npm"
- name: Install deps
run: npm ci --ignore-scripts
# Same #ci-engine-build-order fix as ci.yml (see its "Build engine package" step): the Postgres
# integration test's import graph now reaches @loopover/engine transitively (#5117 moved
# local-write action specs there; src/mcp/local-write-tools.ts re-exports them). That package's
# dist/ is gitignored and only exists after this build step, so the test fails to resolve the
# package's exports without it -- this workflow never needed the engine package built before, so it
# never had this step; it does now.
- name: Build engine package
run: npm run build --workspace @loopover/engine
- name: Postgres integration test (real PG)
run: PG_TEST_URL=postgres://postgres:devpw@localhost:5432/gittensory npx vitest run test/integration/selfhost-pg.test.ts
- name: Build the self-host bundle
run: node scripts/build-selfhost.mjs
- name: Validate self-host source map
run: node scripts/validate-selfhost-sourcemap.mjs
- name: Validate docker-compose.yml
run: |
docker compose config --quiet
docker compose --profile workflows --profile storage config --quiet
- uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
- name: Build the Docker image
run: |
docker buildx build \
--cache-from type=gha \
--cache-to type=gha,mode=max \
--load \
-t gittensory:selfhost-ci .
- name: Smoke-test bundled AI CLIs
run: |
docker run --rm --entrypoint sh gittensory:selfhost-ci -c \
'command -v claude && claude --version && command -v codex && codex --version'
- name: Free disk before visual-review image build
run: |
docker image prune -f
docker builder prune -f --filter until=24h || true
- name: Build release target with visual review deps
run: |
docker buildx build \
--target runtime-prebuilt \
--build-context selfhost_dist=./dist \
--build-arg INSTALL_VISUAL_REVIEW=true \
--load \
-t gittensory:selfhost-prebuilt-visual-ci .
docker run --rm --entrypoint node gittensory:selfhost-prebuilt-visual-ci \
-e "import('puppeteer-core').then(() => console.log('puppeteer-core ok'))"
docker rmi gittensory:selfhost-prebuilt-visual-ci || true
docker image prune -f
- name: Boot the container + smoke-test /health, /ready, /metrics, migrations
run: |
docker network create gt-smoke
docker run -d --name gt-redis --network gt-smoke redis:7-alpine
trap 'docker rm -f gt gt-redis >/dev/null 2>&1 || true; docker network rm gt-smoke >/dev/null 2>&1 || true' EXIT
for _ in $(seq 1 30); do
if docker exec gt-redis redis-cli ping | grep -q PONG; then break; fi
sleep 1
done
docker run -d --name gt --network gt-smoke -p 8787:8787 \
-e REDIS_URL=redis://gt-redis:6379 \
-e SELFHOST_SETUP_TOKEN=selfhost-ci-setup-token \
-e PUBLIC_API_ORIGIN=https://selfhost-ci.example \
gittensory:selfhost-ci
ok=0
for _ in $(seq 1 30); do
if curl -sf http://127.0.0.1:8787/health >/dev/null; then ok=1; break; fi
sleep 2
done
if [ "$ok" != "1" ]; then echo "::error::container did not become healthy"; docker logs gt; exit 1; fi
curl -sf http://127.0.0.1:8787/health | grep -q '"status":"ok"'
curl -sf http://127.0.0.1:8787/ready | grep -q '"ok":true'
curl -sf http://127.0.0.1:8787/metrics | grep -q 'loopover_uptime_seconds'
docker logs gt 2>&1 | grep -q 'selfhost_migrations_applied'
echo "self-host smoke test passed"