Skip to content

Rewrite the README around what Seahaven does well #131

Rewrite the README around what Seahaven does well

Rewrite the README around what Seahaven does well #131

Workflow file for this run

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