morphit/.forgejo/workflows/ci.yml
Morphit Team 586271574d
Some checks failed
morphit-release / Build + publish release tarball (push) Has been cancelled
Morphit v1.11.0 — fix Tor-only playbook block + harden CI postgres readiness
2026-08-10 20:03:46 -07:00

329 lines
15 KiB
YAML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Morphit CI — runs on every push and pull request.
#
# Four gates, in order of cheapness:
# 1. typecheck-sweep — fast, catches type errors across all
# backend + library workspaces (apps/web excluded — it
# extends the generated .svelte-kit/tsconfig.json, which
# this sweep doesn't materialize)
# 2. web-check — fast, runs `svelte-kit sync` then
# `svelte-check` against apps/web; catches Svelte-template
# + .svelte-aware TypeScript errors the workspace sweep
# can't see
# 3. ansible-lint — fast, catches playbook quality drift
# 4. integration — the indexer integration suite (vitest)
# against a REAL Postgres 16, so cross-table SQL + migration
# regressions surface in CI instead of only in manual runs.
# (Added cp426: the suite was green but CI-invisible, so it
# silently drifted — see the two stale tests fixed at cp423.)
# 5. run-smokes — the full smoke suite (~4,400+ scenarios
# across ~190 runners, triple-pulsed for flakes)
#
# If any gate fails, the run fails. Gates 14 run in parallel;
# the smokes gate is the longest-running and effectively serial.
# The web-check job intentionally runs `svelte-kit sync &&
# svelte-check` directly (not via run-smokes →
# workspace-typecheck-smoke) so the protection is legible in
# the CI surface and a sandbox-broken smoke runner can't hide
# behind a SKIP exit code.
#
# Forgejo Actions has GitHub-Actions-compatible syntax; this
# workflow runs unchanged on either as long as the runner is a
# Debian/Ubuntu host.
#
# AUDIT-CI-2 (cp18 deep-deep): third-party actions are SHA-pinned,
# not tag-pinned. Tags like `@v4` can be re-pointed at attacker-
# controlled commits if an action repo is compromised; SHAs are
# immutable. The version comment after each SHA (`# v4.2.2`) lets
# Dependabot/Renovate auto-suggest updates. When bumping, ALSO
# audit the diff from the previous SHA before merging.
name: morphit-ci
on:
push:
branches:
- main
pull_request:
# Newer pushes to the same branch cancel any still-running CI on
# that branch. Saves CI minutes on rapid-fire commit + amend.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
typecheck:
name: TypeScript typecheck (sweep all workspaces)
runs-on: ubuntu-24.04
# cp145 — every job gets an explicit wall-clock ceiling.
# cp143 put a per-smoke timeout inside scripts/run-smokes.sh;
# this job-level timeout is the complement for the other CI
# steps (npm ci, tsc invocations) that no smoke wraps.
# Current runtime is <2 minutes; 10× headroom for slow runners.
timeout-minutes: 10
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Install Node.js 22 LTS
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3
with:
node-version: 22
cache: npm
- name: Install workspace dependencies
# --ignore-scripts skips native-build steps (better-sqlite3
# needs nodejs.org headers which may be restricted). The
# typecheck doesn't actually run any binaries so this is
# fine here — but the run-smokes job needs a full install.
run: npm ci --ignore-scripts --no-audit --no-fund
- name: Run typecheck-sweep
run: bash scripts/typecheck-sweep.sh
web-check:
name: apps/web svelte-check (svelte-kit sync + svelte-aware tsc)
runs-on: ubuntu-24.04
# cp145 — current runtime is ~3 minutes; 10 minutes is roomy.
timeout-minutes: 10
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Install Node.js 22 LTS
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3
with:
node-version: 22
cache: npm
- name: Install workspace dependencies
# --ignore-scripts is safe here — svelte-check doesn't
# need any native postinstall steps. Workspace links
# to @morphit/* must materialize, which they do under
# the standard `npm ci` workspace install.
run: npm ci --ignore-scripts --no-audit --no-fund
- name: svelte-kit sync (generate .svelte-kit/tsconfig.json)
working-directory: apps/web
# The web tsconfig extends `.svelte-kit/tsconfig.json`,
# generated by svelte-kit sync from the project config.
# Without this step, svelte-check would run against a
# stale or missing extended tsconfig.
run: npx svelte-kit sync
- name: svelte-check (strict, errors fail the build)
working-directory: apps/web
# `--threshold error` excludes warnings from the fail
# signal; `--fail-on-warnings false` is the documented
# default but stated explicitly here for clarity.
# The script in apps/web/package.json (`npm run check`)
# does the same thing; invoking the binary directly
# keeps the CI step legible without an indirection.
run: npx svelte-check --tsconfig ./tsconfig.json --threshold error
integration:
name: Integration tests (real Postgres 16)
runs-on: ubuntu-24.04
# cp145 — explicit wall-clock ceiling. The indexer integration
# suite is ~10 files / ~92 tests and runs in well under a minute
# against a warm DB; 15 minutes is generous headroom for the
# postgres service to become healthy + npm ci on a cold cache.
timeout-minutes: 15
# ── Why an explicit `docker run` instead of a `services:` block (cp426) ──
# This project's Forgejo runner uses act_runner's HOST executor: jobs run
# directly on the host, not in an ephemeral container (see `hostexecutor`
# in the run logs). On a host executor a GitHub-style `services:` block does
# NOT hand the job an isolated Postgres on localhost:5432 with our creds —
# the first attempt (task 895) reached *a* Postgres on localhost:5432 that
# had no `morphit_test` role and failed with 28P01 (either the host's own
# Postgres, or a service container that never received our env). So we start
# our OWN throwaway container explicitly, pass credentials directly as -e,
# and bind it to a NON-standard loopback port (55432) so it can never
# collide with a Postgres already running on the host's 5432.
#
# Requires the runner's job steps to be able to invoke `docker` (this host
# already runs its Docker stack, so the CLI is present). If a future run
# fails at the "Start throwaway Postgres" step with a docker permission /
# not-found error, add the runner's user to the `docker` group on the host
# (a one-time host change, not a repo change) — the tests themselves are
# unaffected.
#
# The harness (apps/indexer/test/integration/harness.ts) CREATEs a random
# schema per suite, runs the PRODUCTION migration path (runMigrations), and
# DROP SCHEMA … CASCADE on teardown — so an EMPTY database is all we need.
# Tests SKIP when TEST_DATABASE_URL is unset (INTEGRATION_ENABLED); setting
# it in the run step is what turns them on.
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Install Node.js 22 LTS
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3
with:
node-version: 22
cache: npm
- name: Start throwaway Postgres 16 (own container, loopback port 55432)
run: |
# Remove any leftover from a previous run on this persistent host.
docker rm -f morphit-ci-pg >/dev/null 2>&1 || true
docker run -d --name morphit-ci-pg \
-e POSTGRES_USER=morphit_test \
-e POSTGRES_PASSWORD=test \
-e POSTGRES_DB=morphit_test \
-p 127.0.0.1:55432:5432 \
postgres:16
# Wait until it accepts connections (pg_isready INSIDE the container,
# so the host needs no psql client). Fail loudly if it never comes up
# rather than letting the tests fail later with a vague error.
#
# `-h 127.0.0.1` forces a TCP check. During its first boot the
# postgres image runs a TEMPORARY socket-only server (listen_addresses='')
# to execute initdb, then stops it and starts the real server. A
# socket pg_isready (no -h) gives a false "ready" against that temp
# server, and the very next check lands in the stop/restart window
# with "no response" (task 1505). TCP is only ever answered by the
# real server, so it can't false-positive on the initdb phase.
for i in $(seq 1 30); do
if docker exec morphit-ci-pg pg_isready -h 127.0.0.1 -p 5432 -U morphit_test -d morphit_test >/dev/null 2>&1; then
echo "postgres ready after ${i}s"
break
fi
echo "waiting for postgres (${i}/30)..."
sleep 1
done
docker exec morphit-ci-pg pg_isready -h 127.0.0.1 -p 5432 -U morphit_test -d morphit_test
- name: Install workspace dependencies
# --ignore-scripts is SAFE here: the indexer integration tests use
# `pg`, not better-sqlite3, so no native build is required — and
# skipping postinstall dodges the better-sqlite3 native build (which
# the smokes job needs build-essential for) entirely. The @morphit/*
# workspace links still materialize under the standard workspace ci.
run: npm ci --ignore-scripts --no-audit --no-fund
- name: Run indexer integration tests
working-directory: apps/indexer
env:
# 127.0.0.1:55432 → our own container above (NOT the host's 5432).
TEST_DATABASE_URL: postgres://morphit_test:test@127.0.0.1:55432/morphit_test
run: npm run test:integration
- name: Tear down Postgres
if: always()
run: docker rm -f morphit-ci-pg >/dev/null 2>&1 || true
ansible-lint:
name: ansible-lint (playbook quality gate)
runs-on: ubuntu-24.04
# cp145 — current runtime is <1 minute; 5 minutes is roomy.
timeout-minutes: 5
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Install Python 3.12 + ansible-lint
run: |
# cp190 — update ONLY the base Ubuntu repos, not the
# third-party repos in /etc/apt/sources.list.d/ that the
# runner base image ships (e.g. repo.zabbix.com). We
# install nothing from those repos, and a transient
# Hash-Sum-mismatch on one of them (CI runs 523/524 died
# exactly this way) must not fail an unrelated apt install.
# `Dir::Etc::sourceparts=-` makes apt ignore sources.list.d;
# the retry loop rides out a brief base-mirror hiccup.
for i in 1 2 3; do
sudo apt-get update -qq \
-o Dir::Etc::sourceparts=- \
-o APT::Get::List-Cleanup=0 && break
echo "apt-get update attempt $i failed; retrying in 5s..."
sleep 5
done
sudo apt-get install -y --no-install-recommends \
python3 python3-pip python3-venv
# Use --break-system-packages because Ubuntu 24.04 marks
# the system Python as externally managed. This is a
# CI runner; the host is ephemeral.
pip3 install --break-system-packages --quiet \
ansible ansible-lint
- name: Install required ansible collections
run: |
ansible-galaxy collection install -r \
ops/ansible/collections/requirements.yml
- name: Run ansible-lint
working-directory: ops/ansible
run: ansible-lint --offline --strict playbook.yml
smokes:
name: Smoke suite (run-smokes.sh, triple-pulse)
runs-on: ubuntu-24.04
# cp145 — triple-pulse currently takes ~18 minutes (6 minutes
# per pulse × 3). cp143's per-smoke 240s timeout caps any
# single hung smoke; this job-level ceiling caps the whole
# battery in case a non-smoke step (npm ci, build, ansible-lint
# install, ansible-galaxy collection install) hangs, or in
# case the outer `for i in 1 2 3` loop itself wedges.
# 45 minutes gives 2.5× headroom over the current observed
# runtime.
timeout-minutes: 45
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Install Node.js 22 LTS
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3
with:
node-version: 22
cache: npm
- name: Install build tools (for better-sqlite3 native build)
run: |
# cp190 — see the ansible-lint job: scope apt update to the
# base Ubuntu repos so a flaky third-party repo in the runner
# image (e.g. repo.zabbix.com) can't fail this install.
for i in 1 2 3; do
sudo apt-get update -qq \
-o Dir::Etc::sourceparts=- \
-o APT::Get::List-Cleanup=0 && break
echo "apt-get update attempt $i failed; retrying in 5s..."
sleep 5
done
sudo apt-get install -y --no-install-recommends \
build-essential python3
- name: Install workspace dependencies (with native build)
# The smoke suite includes a matrix-bot deps-pin-check
# which needs node_modules populated with REAL packages.
# Don't pass --ignore-scripts here.
run: npm ci --no-audit --no-fund
- name: Install ansible-lint (for ansible-lint-smoke)
run: |
sudo apt-get install -y --no-install-recommends \
python3-pip
pip3 install --break-system-packages --quiet \
ansible ansible-lint
ansible-galaxy collection install -r \
ops/ansible/collections/requirements.yml
- name: Build workspaces that ship compiled artifacts
# `apps/mcp-server` is the only workspace whose `bin`
# points into `dist/` and therefore needs `tsc -p
# tsconfig.build.json` to run before its smoke spawns
# `node dist/main.js`. The smoke is self-healing as of
# cp142 (will lazy-build if missing), but doing the
# build here makes the failure mode legible — a build
# break surfaces as this step failing, not as a
# mystery hang inside the smoke runner.
#
# If a future workspace adds a similar dist-bin, add
# it here and the matching `spawn-dist-prebuild-coverage-smoke`
# (cp142) will keep this list and the smokes in sync.
run: npm run build -w apps/mcp-server
- name: Run smokes (triple-pulse)
# We run the suite three times to catch flakes. Each
# invocation is ~30s in CI; the parallel typecheck +
# ansible-lint jobs finish well before this anyway.
run: |
for i in 1 2 3; do
echo "=== Pulse $i ==="
bash scripts/run-smokes.sh
done