mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-15 08:22:24 -06:00
c0be383f99
* refactor(doctor): replace turnstone-bootstrap with turnstone-doctor turnstone-bootstrap was an LLM setup wizard for Day-0; run.sh now owns install. Repurpose its LLM/conversation plumbing into turnstone-doctor — a diagnose-only tool for a running cluster. - Preflight detects the install kind (docker-compose/systemd/pip/source) from config.toml + TURNSTONE_* env, with secret redaction. - Self-configuring brain resolves the cluster's own model from config/env/storage read-only (no migrations, no create_all), falling back to interactive selection; the attempt itself is the LLM-backend health check. - Deterministic version check: installed version, cluster drift via the console's authoritative /health, and latest upstream stable/experimental (offline-safe). - Read-only diagnostic tools (read_file, compose/systemd/journal, http_health, check_llm_backend, node_health, finish) behind one secret-scrubbing chokepoint; no generic shell, so read-only is structural. - node_health reaches a node the right way for the detected install kind (exec-into-container for compose, direct HTTP otherwise), overridable per node for mixed clusters. - mTLS-aware: forwards [database] SSL params and reports node-mesh mTLS instead of mislabelling healthy nodes "unreachable". init_storage gains a backward-compatible create_tables override for read-only opens. Entry point turnstone-bootstrap -> turnstone-doctor; README/QUICKSTART/ architecture/docker docs, the bundled compose header, run.sh, and the CI smoke updated. CHANGELOG deferred. * fix(doctor): address Copilot + CodeQL review findings on #718 Validated all seven review findings (none false positives) and fixed: - check_llm_backend now applies the same scheme / metadata-host guard as http_health (extracted to _assert_safe_http_url), so a model-supplied base_url can't be steered at the cloud metadata endpoint or a file:// URL. - node_health no longer double-appends the default port when the operator passes host:port (regression: 10.0.0.5:8081 -> http://10.0.0.5:8081:8080). - node_health install_type enum uses "git-source" to match the label the rest of the module and the prompt/report show the model (a schema-strict provider would otherwise reject the value the model is told to use). - _read_api_creds takes base_url + api_key as a unit from the first config source that defines either field, then env-fills, instead of splicing the two across different config files into a pair that exists in no real config. - _mask_secrets masks assignment-shaped content inside comment lines, so a commented-out real secret can't leak through read_file / the report; prose comments (no KEY=value shape) still pass through untouched. - drop the mixed import styles CodeQL flagged in doctor.py and test_doctor.py. Adds 5 tests; ruff + mypy clean; full doctor suite passes (129).
197 lines
7.3 KiB
YAML
197 lines
7.3 KiB
YAML
name: CI
|
|
|
|
on:
|
|
push:
|
|
branches: [main, "stable/*"]
|
|
tags: ["v*"]
|
|
pull_request:
|
|
branches: [main, "stable/*"]
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
lint:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
|
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
|
|
with:
|
|
python-version: "3.14"
|
|
- run: pip install pre-commit
|
|
# mypy runs separately in typecheck job with full project deps
|
|
- run: SKIP=mypy pre-commit run --all-files
|
|
|
|
typecheck:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
|
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
|
|
with:
|
|
python-version: "3.14"
|
|
- run: pip install mypy
|
|
- run: pip install -e ".[all]"
|
|
- run: mypy turnstone/
|
|
|
|
test:
|
|
runs-on: ubuntu-latest
|
|
# Cap a hung run at 20 min instead of riding GitHub's 6-hour default
|
|
# (a flaky-hang run otherwise streams -v output for hours).
|
|
timeout-minutes: 20
|
|
strategy:
|
|
matrix:
|
|
python-version: ["3.11", "3.12", "3.13"]
|
|
steps:
|
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
|
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
|
|
with:
|
|
python-version: ${{ matrix.python-version }}
|
|
# Node is required by tests/test_renderer_js.py — without
|
|
# explicit setup, that suite silently skips if the runner
|
|
# image happens not to ship Node, masking regressions in
|
|
# the browser-side renderer.
|
|
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
|
with:
|
|
node-version: "24"
|
|
- run: pip install -e ".[test]"
|
|
# -v lists each test id as it starts (pytest prints the nodeid at
|
|
# logstart), so a hang names the culprit on the last line instead of
|
|
# riding the job timeout with only a trail of "..." dots.
|
|
- run: pytest tests/ -m "not live" --cov=turnstone --cov-report=term-missing --cov-report=xml -v
|
|
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
if: always()
|
|
with:
|
|
name: coverage-${{ matrix.python-version }}
|
|
path: coverage.xml
|
|
|
|
test-postgres:
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 20
|
|
services:
|
|
postgres:
|
|
image: postgres:18
|
|
env:
|
|
POSTGRES_USER: postgres
|
|
POSTGRES_PASSWORD: postgres
|
|
POSTGRES_DB: turnstone_test
|
|
ports:
|
|
- 5432:5432
|
|
options: >-
|
|
--health-cmd="pg_isready -U postgres"
|
|
--health-interval=10s
|
|
--health-timeout=5s
|
|
--health-retries=5
|
|
steps:
|
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
|
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
|
|
with:
|
|
python-version: "3.14"
|
|
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
|
with:
|
|
node-version: "24"
|
|
- run: pip install -e ".[test]"
|
|
- run: pytest tests/ -m "not live" --storage-backend=postgresql -v
|
|
env:
|
|
TURNSTONE_TEST_PG_URL: postgresql+psycopg://postgres:postgres@localhost:5432/turnstone_test
|
|
|
|
wheel-completeness:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
|
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
|
|
with:
|
|
python-version: "3.14"
|
|
- run: pip install build
|
|
- run: python -m build --wheel
|
|
- name: Check all data files are in wheel
|
|
run: |
|
|
SOURCE=$(find turnstone -type f \
|
|
! -name '*.py' ! -name '*.pyc' ! -path '*__pycache__*' \
|
|
| sort)
|
|
WHEEL=$(python -m zipfile -l dist/*.whl \
|
|
| awk '{print $1}' \
|
|
| grep -v '\.py$' | grep -v '\.dist-info' | grep -v '\.pyc' | grep -v '^File$' \
|
|
| sort)
|
|
|
|
# Files intentionally excluded from the wheel (one per line).
|
|
# The vllm-litellm/ deploy example ships in the repo, not the wheel
|
|
# (you clone the repo to run it; the package doesn't reference it).
|
|
ALLOW="
|
|
turnstone/core/storage/migrations/script.py.mako
|
|
turnstone/deploy/vllm-litellm/.env.example
|
|
turnstone/deploy/vllm-litellm/README.md
|
|
turnstone/deploy/vllm-litellm/docker-compose.yml
|
|
turnstone/deploy/vllm-litellm/gemma.Dockerfile
|
|
turnstone/deploy/vllm-litellm/litellm-config.yaml
|
|
"
|
|
|
|
MISSING=$(comm -23 <(echo "$SOURCE") <(echo "$WHEEL") \
|
|
| grep -vFxf <(echo "$ALLOW" | sed '/^[[:space:]]*$/d; s/^[[:space:]]*//' ) || true)
|
|
|
|
if [ -n "$MISSING" ]; then
|
|
echo "::error::Data files in source tree but missing from wheel:"
|
|
echo "$MISSING"
|
|
echo ""
|
|
echo "Add them to [tool.hatch.build.targets.wheel] in pyproject.toml"
|
|
echo "or to the ALLOW list in this job if intentionally excluded."
|
|
exit 1
|
|
fi
|
|
echo "All source data files present in wheel"
|
|
- name: Smoke-test entry points from installed wheel
|
|
run: |
|
|
python -m venv /tmp/smoke
|
|
/tmp/smoke/bin/pip install dist/*.whl
|
|
/tmp/smoke/bin/turnstone --help
|
|
/tmp/smoke/bin/turnstone-server --help
|
|
/tmp/smoke/bin/turnstone-console --help
|
|
/tmp/smoke/bin/turnstone-admin --help
|
|
/tmp/smoke/bin/turnstone-channel --help
|
|
/tmp/smoke/bin/turnstone-doctor --help
|
|
|
|
lock-check:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
|
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
|
with:
|
|
uv-version: "0.9.18"
|
|
- run: uv lock --check
|
|
|
|
security:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
|
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
|
with:
|
|
uv-version: "0.9.18"
|
|
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
|
|
with:
|
|
python-version: "3.14"
|
|
- run: uv sync --frozen --all-extras
|
|
- run: uv pip install pip-audit
|
|
- name: Security audit (dependencies)
|
|
# PYSEC-2025-183 (pyjwt): "weak encryption" — disputed by the
|
|
# supplier because the key length is chosen by the calling
|
|
# application, not the library. Turnstone generates its JWT
|
|
# signing keys via the standard ``secrets`` module at
|
|
# operator-controlled strength (see ``turnstone/core/auth.py``),
|
|
# so the advisory does not apply. pyjwt 2.12.1 is the current
|
|
# latest release; no fix version exists.
|
|
run: >-
|
|
uv export --no-emit-project --frozen
|
|
| uv run pip-audit --strict --desc -r /dev/stdin
|
|
--ignore-vuln PYSEC-2025-183
|
|
|
|
security-ts:
|
|
runs-on: ubuntu-latest
|
|
defaults:
|
|
run:
|
|
working-directory: sdk/typescript
|
|
steps:
|
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
|
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
|
with:
|
|
node-version: "24"
|
|
- run: npm ci
|
|
- run: npm audit --audit-level=moderate
|