Skip to content

docs(npm): ship real READMEs for core + skill-nanoclaw, fix client qu… #1588

docs(npm): ship real READMEs for core + skill-nanoclaw, fix client qu…

docs(npm): ship real READMEs for core + skill-nanoclaw, fix client qu… #1588

Workflow file for this run

name: CI
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
# Path filter — lets the heavy JS workspace jobs (client, mcp, app) run ONLY
# when their own code changes. Keeps the onnxruntime-fetch-prone client/mcp
# jobs off unrelated PRs (docs, contracts, relay, python) entirely, so a
# transient onnxruntime/nuget blip can't stall an unrelated PR's auto-merge.
# Skipped jobs report a "skipped" conclusion, which poseidon/wait-for-status-checks
# (auto-merge-l1.yml) treats as complete — so this does not block merges.
changes:
name: Detect changed areas
runs-on: ubuntu-latest
timeout-minutes: 5
# paths-filter lists a PR's files through the API; the graphprotocol org's
# default token is contents-only, so the job fails without this.
permissions:
contents: read
pull-requests: read
outputs:
client: ${{ steps.filter.outputs.client }}
mcp: ${{ steps.filter.outputs.mcp }}
app: ${{ steps.filter.outputs.app }}
plugin: ${{ steps.filter.outputs.plugin }}
docs: ${{ steps.filter.outputs.docs }}
enclave: ${{ steps.filter.outputs.enclave }}
steps:
- uses: actions/checkout@v5
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
client:
- 'client/**'
mcp:
- 'mcp/**'
app:
- 'app/**'
plugin:
- 'skill/**'
docs:
- '**/*.md'
- 'skill/plugin/skill.json'
- 'scripts/check-docs.sh'
# The enclave imports the in-repo Python client and core, so
# changes there must re-run its tests too.
enclave:
- 'enclave/**'
- 'python/**'
- 'rust/totalreclaw-core/**'
- '.github/workflows/ci.yml'
docs-check:
# Guards visitor-facing docs against retired-product drift: retired
# tokens (Base Sepolia-as-current, deleted tools, "unlimited" tiers,
# removed env vars, unfilled placeholders) + broken/root-absolute
# relative links. The 2026-07-15 docs audit found ~40 instances this
# would have caught at PR time. See scripts/check-docs.sh.
name: Docs Check
needs: changes
if: ${{ needs.changes.outputs.docs == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v5
- name: Run docs guardrail (retired tokens + link check)
run: bash scripts/check-docs.sh
phrase-safety:
# Static guard against accidental "display the recovery phrase" copy
# leaking into a SKILL.md / README / docs page. Caught regressing
# twice (rc.20 doc rewrite + an earlier slip); fast-fails the PR if
# any human-facing artifact contains a phrase-display pattern that
# isn't paired with a safety qualifier ("NEVER display", "do not
# show", etc) on the same line.
#
# The dist scan (added 2026-04-26 with the @totalreclaw/mcp-server@3.2.1
# SECURITY hotfix) is the source-code companion: it scans the COMPILED
# JavaScript shipped to npm for any tool-response payload that emits
# `recovery_phrase` or `mnemonic` as an object key. The deleted
# `totalreclaw_setup` MCP tool used to do exactly that.
#
# See: scripts/check-phrase-safety.sh + scripts/check-phrase-safety-dist.sh
# + tests/phrase-safety/.
name: Phrase Safety
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: '22'
- name: Run phrase-safety guard (markdown / SKILL.md)
run: bash scripts/check-phrase-safety.sh
- name: Run phrase-safety regression suite
run: bash tests/phrase-safety/run-regression.sh
- name: Build mcp/dist (required for dist scan)
run: cd mcp && rm -f package-lock.json && npm install --legacy-peer-deps && npm run build
- name: Run phrase-safety dist scan (compiled JS payloads)
run: bash scripts/check-phrase-safety-dist.sh
client-tests:
name: Client Tests (TypeScript)
needs: changes
if: ${{ needs.changes.outputs.client == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 25
strategy:
# The crypto/fingerprint golden-vector suites run production KDF params
# (~11 min locally, slower on CI runners) — shard so each job fits.
matrix:
shard: ["1/2", "2/2"]
# Skip the onnxruntime-node CUDA/GPU binary fetch from api.nuget.org — it
# hung CI 25-30 min repeatedly (ETIMEDOUT) and blocked auto-merge on PRs
# that didn't even touch client/. CPU inference is all CI needs.
env:
ONNXRUNTIME_NODE_INSTALL_CUDA: skip
steps:
- uses: actions/checkout@v5
- name: Set up Node.js
uses: actions/setup-node@v5
with:
node-version: '22'
- name: Install and test
run: cd client && rm -f package-lock.json && npm install --legacy-peer-deps && npm test -- --forceExit --shard=${{ matrix.shard }}
app-tests:
name: Web App Tests (Vitest)
needs: changes
if: ${{ needs.changes.outputs.app == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: '22'
- name: Install and test
run: cd app && npm ci && npm test
plugin-build:
name: Plugin Build Check
# Path-scoped (reliability #2 follow-up): the plugin install pulls
# onnxruntime-node, whose postinstall fetches from api.nuget.org and
# routinely ETIMEDOUTs on CI. client/mcp/app were gated in #295 but this
# job was missed, so it ran — and flaked — on python/docs-only PRs. Gate
# it on skill/** so unrelated PRs don't eat the onnxruntime network flake.
needs: changes
if: ${{ needs.changes.outputs.plugin == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 10
# Same as the client-tests job: skip onnxruntime-node's CUDA/GPU binary
# fetch from api.nuget.org. Since onnxruntime-node 1.31.1 that fetch fails
# outright ("Failed to find specific package versions for
# Microsoft.ML.OnnxRuntime.Gpu.Linux"), breaking npm install. CPU
# inference is all this job needs.
env:
ONNXRUNTIME_NODE_INSTALL_CUDA: skip
steps:
- uses: actions/checkout@v5
- name: Set up Node.js
uses: actions/setup-node@v5
with:
node-version: '20'
# We delete the lockfile because it was last regenerated against a local
# `file:` dep for `@totalreclaw/core`; the current `package.json` uses
# `^2.0.0` from npm and a fresh install resolves against the registry.
# Once the lockfile is regenerated after `@totalreclaw/core@2.x` is
# published, this can switch back to `npm ci`.
- name: Install dependencies
run: cd skill/plugin && rm -f package-lock.json && npm install --legacy-peer-deps
# Simulate OpenClaw's `env-harvesting` scanner rule so a JSDoc comment
# with "fetch" in a file that reads process.env never ships again.
# (The rule also gates `openclaw plugins install` from npm, so this
# stayed load-bearing after the ClawHub channel was retired.)
# See docs/notes/INVESTIGATION-OPENCLAW-SCANNER-*.
- name: Scanner-sim (env-harvesting false-positive check)
run: node skill/scripts/check-scanner.mjs
# Manifest shape regression guard (3.3.0-rc.6): asserts that
# openclaw.plugin.json does NOT have "kind":"memory" (startup registry fix)
# AND that index.ts still declares kind:'memory' as const (memory-slot
# matching preserved). Uses npx tsx — available without a devDependency.
# Includes import-time-smoke.test.ts (issue #110 fix 2) which asserts
# plugin module load completes in <5s with no eager network I/O.
- name: Plugin manifest shape + smoke tests
run: cd skill/plugin && npm test
# Issue #110 fix 1: build the plugin's TS to dist/.js, and verify the
# tarball would ship a complete set of `.js` artifacts. PR-time check
# so a broken build (e.g. missing import target, syntax error) doesn't
# reach the publish workflow.
- name: Build plugin (.ts -> dist/.js)
run: cd skill/plugin && npm run build
- name: Verify tarball ships built `.js` artifacts (issue #110)
run: cd skill/plugin && npm run verify-tarball
# Issue #124 (rc.20 ship-stopper): the published `dist/index.js` is
# ESM-only (`"type":"module"`) but had three runtime `require()` calls
# left over from an unfinished migration — every plugin entry point
# died with `require is not defined`. The pre-publish gate above only
# runs `node --check` (syntax-only) and missed it. This step actually
# imports the built dist in a clean Node ESM process AND grep-guards
# the dist files for any bare `require(` outside comments. Catches
# the regression at PR time, never publish-time.
- name: ESM-shape smoke (issue #124 — bare require regression)
run: cd skill/plugin && npm run smoke:dist
mcp-tests:
name: MCP Server Tests
needs: changes
if: ${{ needs.changes.outputs.mcp == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 10
# Same onnxruntime-node CUDA-fetch skip as client-tests (mcp depends on
# @huggingface/transformers → onnxruntime-node).
env:
ONNXRUNTIME_NODE_INSTALL_CUDA: skip
steps:
- uses: actions/checkout@v5
# `mcp` depends on `@totalreclaw/core@^2.5.x` from npm — `npm install`
# resolves it from the registry, so no local WASM build is needed here.
- uses: actions/setup-node@v5
with:
node-version: '22'
- name: Install, build, and test
run: cd mcp && rm -f package-lock.json && npm install --legacy-peer-deps && npm run build && npm test
rust-tests:
name: Rust Core Tests
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
- uses: actions/setup-python@v5
with:
python-version: '3.12'
# Source-level guard (issue #503): a #[pyfunction] split into
# src/python/bind_*.rs is invisible at runtime unless it is also
# wrap_pyfunction!-registered in python.rs's #[pymodule] (or a module's
# register_python_functions). A missed registration silently drops the
# function from the PyPI wheel -- same failure class as the Hermes rc10
# incident (#427: tool shipped with schema+handler but never
# register()-ed). Cheap source scan over the python that's already
# installed for cargo's pyo3 build -- no extra toolchain download
# (see #489/#491: no flaky toolchain fetches in test jobs).
- name: PyO3 pyfunction <-> registration parity
run: python scripts/check-pyfunction-parity.py
- name: Run tests
run: cd rust/totalreclaw-core && cargo test
python-tests:
name: Python Client Tests
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Build totalreclaw_core wheel from local source
uses: PyO3/maturin-action@v1
with:
working-directory: rust/totalreclaw-core
args: --release --features python-extension --out dist -i python3.12
- name: Install local totalreclaw_core wheel
run: pip install --force-reinstall rust/totalreclaw-core/dist/totalreclaw_core-*.whl
- name: Install and test
run: |
cd python
pip install -e ".[dev]"
python -m pytest tests/ -v --ignore=tests/test_staging_integration.py --ignore=tests/cross_client_e2e.py --ignore=tests/test_cross_client.py -x
- name: Outer protobuf fields 9/11 absent — Python calldata parity (PRD-04 F8 / DEP-6)
run: cd python && python -m pytest ../tests/parity/test_outer_protobuf_parity.py -v -p no:cacheprovider
# Hermes runs its own bundled Python 3.14. Install the client there with
# --only-binary :all:, reusing the abi3 core wheel built above, so any
# dependency (core included) that would need a compiler on 3.14 fails
# this PR instead of a user's install.
- uses: actions/setup-python@v5
id: py314
with:
python-version: '3.14'
- name: Install client binary-only on Python 3.14 (no compiler)
run: |
set -e
pip wheel --no-deps --wheel-dir /tmp/client-wheel ./python
"${{ steps.py314.outputs.python-path }}" -m venv /tmp/venv314
/tmp/venv314/bin/pip install --only-binary :all: \
rust/totalreclaw-core/dist/totalreclaw_core-*.whl /tmp/client-wheel/totalreclaw-*.whl
/tmp/venv314/bin/python -c "import sys, totalreclaw, totalreclaw_core; print('OK: totalreclaw imports on', sys.version.split()[0])"
enclave-tests:
# TotalReclaw Enclave service (enclave/, Python 3.12). Path-scoped like
# client/mcp/app: runs when enclave/, python/ or core change. Installs
# the in-repo core wheel and Python client first so the enclave is tested
# against the code in this PR, not the PyPI releases its floors allow.
name: Enclave Tests (Python)
needs: changes
if: ${{ needs.changes.outputs.enclave == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Build totalreclaw_core wheel from local source
uses: PyO3/maturin-action@v1
with:
working-directory: rust/totalreclaw-core
args: --release --features python-extension --out dist -i python3.12
- name: Install local totalreclaw_core wheel
run: pip install --force-reinstall rust/totalreclaw-core/dist/totalreclaw_core-*.whl
- name: Install the in-repo Python client, then the enclave
run: |
pip install -e ./python
pip install -e "./enclave[dev]"
- name: Lint (ruff check + format check)
run: |
cd enclave
ruff check src tests
ruff format --check src tests
- name: Test
run: cd enclave && python -m pytest tests/ -v
cross-language-parity:
name: Cross-Language Parity Tests
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
- uses: jetli/wasm-pack-action@v0.4.0
with:
version: 'v0.14.0'
- name: Build @totalreclaw/core (WASM)
run: |
cd rust/totalreclaw-core
wasm-pack build --target nodejs --out-dir pkg --features wasm
node -e "const p=require('./pkg/package.json'); p.name='@totalreclaw/core'; require('fs').writeFileSync('./pkg/package.json', JSON.stringify(p,null,2)+'\n')"
- uses: actions/setup-node@v5
with:
node-version: '22'
- name: Install parity-test deps
run: cd tests/parity && npm install --no-audit --no-fund
- name: Cross-language executeBatch calldata parity (TS/WASM side)
run: cd tests/parity && npx tsx userop-batch-parity.test.ts
- name: Session-key EIP-712 grant digest parity (TS/viem side, cred-9, #584)
run: cd tests/parity && npx tsx session-key-grant-roundtrip.ts
- name: Outer protobuf fields 9/11 absent — MCP + WASM calldata parity (PRD-04 F8 / DEP-6)
run: cd tests/parity && npx tsx outer-protobuf-parity.test.ts
# NOTE: Relay unit tests run in the private totalreclaw-relay repo CI.
# This job runs the E2E smoke test against a real relay + PostgreSQL
# using Docker Compose. Requires access to totalreclaw-relay repo.
# Uncomment when relay repo is accessible from CI via RELAY_REPO_TOKEN secret.
#
# e2e-relay:
# name: E2E Relay Smoke Tests
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v5
# - uses: actions/checkout@v5
# with:
# repository: graphprotocol/totalreclaw-relay
# path: totalreclaw-relay
# token: ${{ secrets.RELAY_REPO_TOKEN }}
# - name: Set up Node.js
# uses: actions/setup-node@v5
# with:
# node-version: '20'
# - name: Start relay + PostgreSQL
# run: cd tests/e2e-relay && docker compose up -d --wait
# - name: Run smoke tests
# run: cd tests/e2e-relay && npx tsx smoke-test.ts
# - name: Tear down
# if: always()
# run: cd tests/e2e-relay && docker compose down -v