Repository navigation
Merge pull request #38 from Kiln-AI/claude/festive-pascal-ngn8ls #114
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| jobs: | |
| check: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Install uv and Python 3.14 | |
| uses: astral-sh/setup-uv@v5 | |
| with: | |
| python-version: "3.14" | |
| enable-cache: true | |
| # `--extra serve` installs OpenEnv, which the `seahaven.openenv` module and | |
| # its tests need. Without it the type check and the transport tests would | |
| # be skipped here and run nowhere. `--extra mcp` cannot join it: the two | |
| # extras are declared as conflicting in `pyproject.toml`, because OpenEnv | |
| # pins the MCP SDK to 1.x and `seahaven.mcp` needs 2.x. The `mcp` job | |
| # below is that extra's environment. | |
| - name: Install the project | |
| run: uv sync --locked --extra serve | |
| # The suites guard the OpenEnv modules with `pytest.importorskip`, so an | |
| # extra that is installed but does not import would skip them and leave | |
| # CI green having tested none of `seahaven serve`, the environment or the | |
| # client. This is the one step that fails instead. | |
| - name: The serve extra imports | |
| run: uv run python -c "import seahaven.openenv" | |
| - name: Format | |
| run: uv run ruff format --check | |
| - name: Lint | |
| run: uv run ruff check | |
| # `ty` resolves imports against the environment it runs in, so neither | |
| # environment can check the whole tree: `seahaven.mcp` does not resolve | |
| # here, and `seahaven.openenv` does not resolve in the `mcp` job. The two | |
| # jobs split the tree between them -- what this run excludes is exactly | |
| # what the other one includes -- and `tests/test_ci_workflow.py` reads | |
| # both lines out of this file and fails when a path falls between them. | |
| - name: Types | |
| run: uv run ty check -c 'src.exclude=["src/seahaven/mcp", "tests/test_mcp_server.py", "tests/test_mcp_process.py"]' | |
| - name: Tests | |
| run: uv run pytest | |
| # The reference world is its own package with its own suite, and its own | |
| # pytest rootdir: a world is tested the way a world's author tests one. | |
| - name: Reference world tests | |
| run: uv run pytest worlds/projecttracker | |
| # The example extension is its own package with its own suite too, and its | |
| # world is a copy of ProjectTracker under its `tests/`. What it proves is | |
| # the extension contract: that a protocol the framework knows nothing about | |
| # rides on the published seams. | |
| - name: Example extension tests | |
| run: uv run pytest extensions/seahaven-xmlrpc | |
| # Seahaven is vendored into other people's products: nothing it installs | |
| # at runtime is copyleft. The gate is named the extra this job synced, and | |
| # reads the base closure plus that extra -- an extra whose licences cannot | |
| # be read is not cleared, it fails. Every declared extra is named by one | |
| # job or another, which `tests/test_licence_check.py` asserts against this | |
| # file. | |
| - name: Dependency licences | |
| run: uv run python scripts/check_licences.py serve | |
| # The second environment. `serve` and `mcp` are conflicting extras and no | |
| # environment can hold both, so the MCP extra gets a job of its own: the same | |
| # framework suite, which skips what needs OpenEnv, and the licence gate for | |
| # the tree this extra ships. | |
| mcp: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Install uv and Python 3.14 | |
| uses: astral-sh/setup-uv@v5 | |
| with: | |
| python-version: "3.14" | |
| enable-cache: true | |
| - name: Install the project | |
| run: uv sync --locked --extra mcp | |
| # Both modules, because they are two different claims. The suites skip on | |
| # `mcp.server.context`, which only the 2.x SDK has, so that is the | |
| # predicate the job has to assert; `seahaven.mcp` is the thing this | |
| # project ships, and what it happens to import is an implementation | |
| # detail rather than a promise. `tests/conftest.py` holds the guard, and | |
| # `tests/test_ci_workflow.py` holds this line to the same module so the | |
| # two cannot drift into a green run that tested none of this. | |
| - name: The mcp extra imports | |
| run: uv run python -c "import mcp.server.context, seahaven.mcp" | |
| # The other half of the type check: the paths the `check` job excludes, | |
| # named as arguments, because only a path argument narrows what `ty` | |
| # walks. They arrive in phases 2 and 3 and `ty` exits 2 on a path that is | |
| # not there yet, so the step checks the ones that exist and says so when | |
| # there are none. Phase 2 then needs no edit here. | |
| - name: Types | |
| run: | | |
| paths=$(ls -d src/seahaven/mcp tests/test_mcp_server.py tests/test_mcp_process.py 2>/dev/null || true) | |
| if [ -z "$paths" ]; then | |
| echo "nothing on the mcp side of the split yet" | |
| else | |
| uv run ty check $paths | |
| fi | |
| - name: Tests | |
| run: uv run pytest | |
| - name: Dependency licences | |
| run: uv run python scripts/check_licences.py mcp | |
| # The web console is a separate toolchain in `ui/`, and what ships is the | |
| # built file vendored at `src/seahaven/openenv/console/index.html`. Without | |
| # this job nothing type-checks the console, nothing runs its browser test, and | |
| # nothing notices when someone edits `ui/src` and forgets to re-vendor the | |
| # build. | |
| console: | |
| runs-on: ubuntu-latest | |
| defaults: | |
| run: | |
| working-directory: ui | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: "22" | |
| cache: npm | |
| cache-dependency-path: ui/package-lock.json | |
| - name: Install | |
| run: npm ci | |
| # `npm run build` is `tsc --noEmit && vite build`, so the types are | |
| # checked here and a type error fails the job before anything is built. | |
| - name: Build | |
| run: npm run build | |
| # The vendored copy is what `seahaven serve` answers `/console` with, and | |
| # it is a build artefact: a commit that changes `ui/src` without rebuilding | |
| # it ships the old page. The build is reproducible, so the copy and a fresh | |
| # build are the same bytes or the copy is stale. | |
| - name: The vendored console is the current build | |
| run: | | |
| cmp dist/index.html ../src/seahaven/openenv/console/index.html || { | |
| echo "::error::src/seahaven/openenv/console/index.html is not the current build of ui/." | |
| echo "Run: cd ui && npm run build && cp dist/index.html ../src/seahaven/openenv/console/index.html" | |
| exit 1 | |
| } | |
| # The build inlines its dependencies into the file that ships in the | |
| # wheel, so an npm dependency reaches a user the same way a PyPI one does. | |
| # `scripts/check_licences.py` is the no-copyleft rule for the Python | |
| # closure and cannot see this one. | |
| - name: Bundled licences | |
| run: npm run licences | |
| - name: Install Chromium | |
| run: npx playwright install --with-deps chromium | |
| # Two mock environments, because the console has two modes: a tool | |
| # environment and one whose actions are not tools. The test drives the | |
| # built file in a real browser and fails on any unexpected console error. | |
| - name: Browser test | |
| run: | | |
| node mock/server.mjs --port 8000 & | |
| node mock/server.mjs --port 8001 --plain & | |
| # Wait for both to answer rather than sleeping at them. A mock that | |
| # dies on startup otherwise surfaces as a locator timeout somewhere | |
| # in the middle of the run instead of as "the mock did not start". | |
| for port in 8000 8001; do | |
| for _ in $(seq 50); do | |
| curl -sf "http://127.0.0.1:$port/health" >/dev/null && break | |
| sleep 0.2 | |
| done | |
| curl -sf "http://127.0.0.1:$port/health" >/dev/null || { | |
| echo "::error::the mock on $port never came up" | |
| exit 1 | |
| } | |
| done | |
| node e2e/smoke.mjs |