Contributing¶
How to set up a development copy of Tuneline Studio, run the tests and linters, and follow the project's conventions when you change something.
Development setup¶
You need Python 3.10 or newer (3.12 recommended: it is what Vercel runs), Node.js 20.19+ or 22.12+ (for Vite), and Git. The setup scripts create a virtual environment in .venv with the API's packages, install the web app's packages with npm ci, build the web app once and create .env from .env.example. They are safe to run again.
Windows (PowerShell):
powershell -ExecutionPolicy Bypass -File .\scripts\setup-dev.ps1 # API + web app
powershell -ExecutionPolicy Bypass -File .\scripts\setup-dev.ps1 -Runner # + CPU PyTorch and the training stack
macOS, Linux or WSL:
bash scripts/setup-dev.sh # API + web app
bash scripts/setup-dev.sh --runner # + CPU PyTorch and the training stack (about 2 GB)
Without the runner option the tuneline package is installed in editable mode without its training dependencies; the runner tests need the --runner / -Runner setup (pip install -e ".[train,gguf,eval,dev]" after CPU PyTorch).
Tip
On Windows, QLoRA (bitsandbytes) and Unsloth are most reliable under WSL2. A few Python tests also assume Linux (see Tests), so WSL2 is the smoothest place to run the full suite.
Run the app locally¶
Start the API and the web dev server in two terminals:
# Terminal 1: the API on http://localhost:8000 (reads .env)
.venv/Scripts/python -m uvicorn app:app --reload --port 8000 # Windows
.venv/bin/python -m uvicorn app:app --reload --port 8000 # macOS / Linux
# Terminal 2: the web app on http://localhost:5173
cd web
npm run dev
Open http://localhost:5173. The Vite dev server proxies /api, /runner and /samples to the API on port 8000 (set TUNELINE_DEV_API to point it elsewhere). The local API also serves the last web build at http://localhost:8000.
By default both talk to the Supabase project in tuneline/config_defaults.py (and web/src/lib/config.ts). To create runs from a local API, put the server key in TUNELINE_SERVER_KEY in .env: it must equal the Vault secret tuneline_server_key of that Supabase project (select decrypted_secret from vault.decrypted_secrets where name = 'tuneline_server_key';). Never commit .env. For your own Supabase project, see Self-hosting.
In VS Code, Terminal → Run Task offers setup: local dev environment, dev: API (port 8000), dev: web (port 5173), dev: API + web, test: Python, test: web (unit), lint: ruff and deploy: Vercel production (.vscode/tasks.json). Run and Debug has API: debug (port 8000), Pytest: current file, Runner: run-local on the open pipeline YAML and Web app in Chrome (start the dev servers first) (.vscode/launch.json).
Tests¶
| Suite | Command | Notes |
|---|---|---|
| Python, all | .venv/Scripts/python -m pytest -q |
Several minutes. The GGUF tests clone llama.cpp and build llama-quantize (needs git, cmake and a C++ compiler). |
| Python, API only | .venv/Scripts/python -m pytest -q tests/test_api_*.py |
Fast; no training stack needed. |
| Web unit tests | cd web; npm test |
vitest. |
| Web types | cd web; npm run typecheck |
tsc -b. |
| Browser tests | cd web; npx playwright install chromium; npx playwright test |
Builds the app and runs it with vite preview on port 4173; Supabase, the API and Realtime are stubbed in web/e2e/mocks.ts, so no network is needed. |
(On macOS and Linux use .venv/bin/python.)
API tests never call Supabase: tests/test_api_support.py provides a fake Supabase (FakeSupabase) that answers the REST and RPC calls the API makes. When you add an RPC that the API calls, add an rpc_<name> handler to FakeSupabase (see rpc_server_create_run and the others) so the tests can exercise it. Provider SDKs and HTTP calls are faked the same way (for example FakeModal).
Web tests mirror Python. Parts of tuneline/spec.py (validation, YAML) and the API's response shapes are re-implemented in TypeScript. The vitest suite checks them against fixtures generated from the Python code, so regenerate the fixtures when you change that code (see Generated files).
Tests that assume Linux
A few Python tests run bash or set PATH to /usr/bin:/bin (for example the launch-script and installer tests in tests/test_api_launchers.py and tests/test_api_static.py, and the Ollama test in tests/test_runner_templates.py). They fail on native Windows; run the suite under WSL2 or rely on CI for them.
Lint¶
The rule selection is pinned in pyproject.toml (select = ["E4", "E7", "E9", "F"], ruff's defaults before 0.16). Newer ruff versions enable many more rules by default and would report hundreds of style findings; adopt more rules only on purpose, in their own change.
Generated files¶
Some files are generated from the Python code and committed. Regenerate them after changing the source:
| After changing | Run | Updates |
|---|---|---|
tuneline/registry.py |
python -m tuneline.registry > web/src/generated/registry.json |
Block registry bundled by the web app |
tuneline/templates.py |
python -m tuneline.templates > web/src/generated/templates.json |
Templates bundled by the web app |
tuneline/spec.py |
cd web; python scripts/gen_spec_fixtures.py |
web/src/lib/__fixtures__/spec-cases.json, yaml-cases.json |
tuneline/estimator.py, API response shapes |
cd web; python scripts/gen_api_fixtures.py |
web/src/lib/__fixtures__/api-shapes.json |
| Registry or templates JSON | python documentation/scripts/gen_reference.py |
documentation/docs/reference/blocks.md, templates.md |
Use the project's Python (.venv/Scripts/python or .venv/bin/python). npm run fixtures:spec and npm run fixtures:api do the same as the two fixture scripts but call python3, which Windows may not have.
Conventions¶
Contracts first. docs/CONTRACTS.md is the source of truth for everything that crosses a boundary: tables, RPCs, the run spec, runner events, API routes, artifact formats. Read it before changing one of them, and update it in the same change. Record decisions and open items in docs/DEVELOPMENT_NOTES.md.
Database migrations. Add a new file to supabase/migrations/ named <version>_<name>.sql, where the version is a timestamp (YYYYMMDDHHMMSS) that sorts after the last file; never edit an applied migration. Apply it in the Supabase SQL editor or with npx supabase db push. To try SQL without keeping it, run it inside a DO block that ends with raise exception, so everything rolls back (and don't put DDL you want to keep in that same call). New server_*, runner_* or worker_* functions must check the server key, run token or worker token first; give every new table row level security.
Registry and blocks. A block is defined in tuneline/registry.py (ports, params with show_if, semantic, presets, secrets) and implemented in tuneline/runner/nodes/. Mark a param semantic=False only if it cannot change the output (it is then left out of the cache key). A block's version is part of its cache key, so bumping it when the block's output changes for the same inputs keeps old cache entries from being reused. Regenerate the generated files afterwards.
Runner protocol. The runner version (tuneline/__init__.py, currently 0.1.0) is what the installers and jobs download. Bump it when the runner protocol (RPCs, events, spec fields the runner reads) changes. Installers force-reinstall the runner, so ordinary fixes reach users on their next install.
Secrets. Never log, print or return a key or token. The API installs log redaction at start-up (tuneline/api/errors.py); the runner redacts secrets and token-looking strings from its output. The web app only ever shows the last four characters of a saved key. Never commit .env.
Dependencies. The API runs on Vercel from requirements.txt pinned by constraints.txt (not from pyproject.toml). Keep both in sync; regenerate constraints.txt on Linux with Python 3.12 (pip freeze of a fresh virtual environment) and rerun the API tests. The runner's ranges live in pyproject.toml: transformers ≥5,<6, TRL ≥1,<2, PEFT ≥0.18, datasets ≥4.7, huggingface_hub ≥1.5,<2.
Wording in the UI and messages. Follow docs/UI_GUIDE.md: plain, specific, active voice; name things as users recognise them ("API keys", "Your computers", "Run on RunPod"), not by internals ("Vault secret", "launcher"). Errors say what went wrong and how to fix it. Keep the docs and server messages in step with the UI labels. New colours must meet WCAG AA contrast in both themes.
Public URLs. Don't use VERCEL_URL for anything public: deployment URLs sit behind Vercel sign-in. The address registered with the database comes only from configuration (settings.trusted_app_url()), never from request headers.
Documentation¶
This site lives in documentation/ and is built with MkDocs Material from documentation/docs/. The block and template references are generated; everything else is written by hand.
python documentation/scripts/gen_reference.py # regenerate reference/blocks.md and templates.md
python documentation/scripts/gen_reference.py --check # exit 1 if they are out of date
mkdocs serve # preview, from the folder that holds mkdocs.yml
The generator uses only the standard library and reads web/src/generated/registry.json and templates.json, so regenerate those first when the registry or templates change. Link pages with relative .md paths, and follow the same wording rules as the UI.
Continuous integration¶
.github/workflows/ci.yml runs on pushes to main, on pull requests and on demand, with two jobs on Ubuntu:
| Job | Steps |
|---|---|
| Python (API, runner, spec) | Python 3.12; CPU PyTorch; pip install -e ".[train,gguf,eval,dev]" -r requirements.txt "uvicorn[standard]" ruff; ruff check tuneline tests; pytest -q (45-minute limit) |
| Web app | Node 22; npm ci; npm run typecheck; npm test; npx playwright install --with-deps chromium; npx playwright test |
A newer push to the same branch cancels the running workflow. Make sure both jobs pass before merging.