Skip to content

feat(cli,ci): 'rocketride validate' subcommand (Python + TS) and validate-pipes GitHub Action#1573

Open
nihalnihalani wants to merge 5 commits into
rocketride-org:developfrom
nihalnihalani:feat/RR-1572-validate-cli-github-action
Open

feat(cli,ci): 'rocketride validate' subcommand (Python + TS) and validate-pipes GitHub Action#1573
nihalnihalani wants to merge 5 commits into
rocketride-org:developfrom
nihalnihalani:feat/RR-1572-validate-cli-github-action

Conversation

@nihalnihalani

Copy link
Copy Markdown
Contributor

#Hire-Me-RocketRide

Contribution Type

Featurerocketride validate CLI subcommand (Python + TypeScript) and a reusable GitHub Action to gate .pipe files in CI. Includes one server-side bug fix discovered during live verification.

Closes #1572

Summary

RocketRide's wedge is that pipelines are portable, git-versioned JSON — but until now nothing could check a .pipe file outside the IDE. Validation already existed end-to-end (rrext_validate → C++ validatePipeline) and both SDKs expose client.validate(); it was simply never surfaced in either CLI. This PR closes that loop:

rocketride validate examples/*.pipe --json
  • Python CLI: new ValidateCommand (cli/commands/validate.py) following the existing command pattern — same registration, connection flags (--uri/--apikey, ROCKETRIDE_* env fallbacks), and output style as start/status/list.
  • TypeScript CLI: same subcommand in src/cli/rocketride.ts, implemented with exported pure helpers (expandFilePatterns, loadPipelineFile, buildValidateReport, validateExitCode) so the logic is unit-testable without a server.
  • Identical contract in both CLIs (byte-aligned human output and --json shape, verified by running both binaries side by side):
    • files: literal paths and/or globs, expanded in-CLI (recursive, sorted, deduped — identical behavior on shells without globbing, e.g. Windows);
    • accepts the flat pipeline config or the {"pipeline": {...}} wrapper (unwrapped exactly like client.use()); non-object JSON is rejected locally before any server call;
    • --source <id> passes through to the SDK; --json prints a single machine-readable document with the engine's error/warning objects verbatim;
    • exit codes: 0 all valid · 1 ≥1 file invalid · 2 usage/connection error or no file received a server verdict.
  • Composite GitHub Action (.github/actions/validate-pipes): sets up Python, installs the rocketride CLI (fails fast with a clear message if the installed release lacks validate; cli-version input to pin), starts the engine image in Docker, waits for the public /version endpoint, validates .pipe files (changed-only vs the PR base by default), and always removes the container. No existing workflow files are touched.

Server bug found and fixed during live verification

Running the new CLI against the published engine image (v3.3.1.35) exposed a real bug: every rrext_validate call failed with ccode 40 "'pipeline' is missing or invalid", regardless of the config's validity.

Root cause: the DAP handler (cmd_misc.py on_rrext_validate) passed the config flat to rocketlib validatePipeline, but the C++ binding requires the config under a top-level 'pipeline' key — the envelope the HTTP path (modules/pipe pipe_Validate) already builds:

# modules/pipe/pipe_validate.py (correct)          # cmd_misc.py (before this PR)
data = validatePipeline({'pipeline': inner})       data = validatePipeline(inner)

This one-line divergence had gone unnoticed because nothing in the repo calls rrext_validate with real payloads — the VS Code extension does its own client-side config checks. Fixed by mirroring pipe_Validate, existing handler tests updated to assert the envelope, plus a regression test pinning the wire shape (test_on_rrext_validate_wraps_config_in_pipeline_envelope).

Design decisions (and why)

  • Engine-backed validation, not a local reimplementation — the 13 structural rules in pipeline_config.cpp stay the single source of truth; the CLI is a thin, honest client.
  • Strict json.load / JSON.parse for .pipe files (not the json5-tolerant loader start uses) so both CLIs reject exactly the same inputs. Called out in docs.
  • exit 2 = "no file received a server verdict" (all unreadable, or connection failed); mixed runs where some files validated exit 1. Documented in --help, docstrings, and the READMEs so CI semantics are unambiguous.
  • Action polls /version, not /ping/version is registered unconditionally public in server.py; /ping sits behind the standardEndpoints gate and 404s on published images (verified live; the Helm chart's /ping probes may deserve the same look, noted in feat(sdk,ci): 'rocketride validate' CLI subcommand + reusable GitHub Action to gate .pipe files in CI #1572).
  • changed-only works with default fetch-depth: 1 — the action fetches the PR base commit itself before git diff.
  • API key via env only (ROCKETRIDE_APIKEY), never argv; third-party actions pinned by full commit SHA.

Validation

Unit (no server required)

  • Python: pytest packages/client-python/tests/test_validate_cli.py14 passed (glob expansion, wrapper unwrap, non-object rejection, exit codes 0/1/2, --json shape, --source passthrough, connection failure).
  • Server: pytest .../test_cmd_misc.py (envelope assertions + regression test) → all passing inside the engine image (see below).
  • TypeScript: tsc -p tsconfig.cli.json and tsc -p tsconfig.json clean; jest tests/validate.test.ts27 passed.
  • ruff check + ruff format --check clean on all changed Python files; action.yml YAML-parses, all run blocks pass bash -n and shellcheck.

Live end-to-end (published engine image ghcr.io/rocketride-org/rocketride-engine, amd64 under QEMU)
All run against ghcr.io/rocketride-org/rocketride-engine:latest (v3.3.1.35, amd64 under QEMU):

  • Server handler tests inside the engine image (./engine -m pytest, the only environment with engLib): 24 passed, including the new test_on_rrext_validate_wraps_config_in_pipeline_envelope.
  • Patched engine (fix bind-mounted over the installed handler) + the CLI from this branch:
    • rocketride validate examples/agent-llamaindex.pipe → valid, exit 0 (likewise db_arango, butterbase-agent, xtrace-memory-agent, agent-workflow, llm-benchmark);
    • multi-source rag-pipeline.pipe with --source webhook_1 → valid, exit 0; without --source the engine rejects the request ("multiple source components") → exit 2 per the documented contract;
    • deliberately broken copies → exit 1 with the engine's real errors (Duplicate component agent_llamaindex_1; input references unknown component id: ghost_component_99);
    • validate <good> <broken> --json → exit 1, "summary": {"total": 2, "valid": 1, "invalid": 1} with per-file ccode errors.
  • Negative control — the same valid file against a fresh unpatched container: ✗ 'pipeline' is missing or invalid (ccode 40), proving the one-line server fix is necessary and sufficient.
  • TypeScript CLI (built dist/cli) against the patched engine: byte-identical JSON structure, summary, and exit codes vs the Python CLI.

One honest caveat: examples/n8n-roundtrip.pipe and examples/document-processor.pipe fail on this engine build with "references a provider with no registered service definition" (tool_n8n_1, anonymize_1) — engine-build limitations independent of this PR; CI configs asserting "all examples validate" should exclude them or use a fuller engine build.

Notes for reviewers

  • The first commit (fix(cli): use the Node process global…) is a small pre-existing bug fix isolated on request of our internal review: import * as process breaks SIGINT/SIGTERM registration on Node ≥ 24 because the CommonJS interop wrapper drops inherited EventEmitter members. The validate command's cleanup path needs working signal handlers, so it is a prerequisite, not drive-by churn.
  • The GitHub Action becomes fully usable for external consumers once a rocketride PyPI release containing validate is published and an engine image containing the cmd_misc.py fix is tagged — the README and the fail-fast install check both say so explicitly. Within this repo it can run from source immediately.
  • Scope: no changes to existing workflows, no new dependencies, no engine (C++) changes.

🤖 Generated with Claude Code

nihalnihalani and others added 5 commits July 14, 2026 15:01
…l handlers on Node 24+

The CommonJS interop wrapper (__importStar) only copies an object's own
properties. On Node >= 24, process.on/once/emit are inherited from
EventEmitter.prototype, so 'import * as process' yields a module object
without them and the TypeScript CLI crashes at startup when registering
SIGINT/SIGTERM handlers. The Node global 'process' carries the full
prototype chain.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e validatePipeline expects

The DAP rrext_validate handler passed the pipeline config flat to
rocketlib validatePipeline, but the C++ binding requires the config under
a top-level 'pipeline' key — the envelope the HTTP path (modules/pipe
pipe_Validate) already builds. As a result every rrext_validate call,
including SDK client.validate(), failed with ccode 40 "'pipeline' is
missing or invalid" regardless of the config's actual validity (verified
against the published engine image v3.3.1.35 and current source).

Wrap the resolved payload exactly like pipe_Validate does. Existing
handler tests updated to assert the envelope, plus a regression test
pinning the wire shape.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eScript CLIs (rocketride-org#1572)

Surfaces the existing SDK validate() (rrext_validate) in both CLIs so
.pipe files can be checked from a terminal or CI without executing them:

  rocketride validate <files...> [--source <id>] [--json]

- Files: literal paths and/or glob patterns, expanded in-CLI (recursive,
  sorted, deduped) so behavior is identical on shells without globbing.
- Accepts either the flat pipeline config or the { "pipeline": {...} }
  wrapper (unwrapped exactly like client.use()); non-object JSON is
  rejected locally before any server call.
- --json emits a single machine-readable document (per-file errors and
  warnings passed through verbatim from the engine, plus a summary).
- Exit codes: 0 all valid; 1 at least one file invalid; 2 usage error,
  connection failure, or no file received a server verdict.
- Connection handling, argument style, and output formatting mirror the
  existing subcommands in each CLI; no new dependencies.

Both CLIs implement the identical contract (byte-aligned human output
and JSON shape); covered by 14 pytest and 27 jest unit tests with a
mocked client — no server required.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reusable composite action that gates .pipe files in CI: sets up Python,
installs the rocketride CLI (fails fast with a clear message when the
installed release lacks the validate subcommand; cli-version input to
pin), starts the engine image in Docker, waits for the public /version
endpoint, validates the repo's .pipe files (changed-only vs the PR base
by default), and always removes the container.

Referencable as
rocketride-org/rocketride-server/.github/actions/validate-pipes@<ref>.
No existing workflow files are modified.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds the validate synopsis, flags, exit-code semantics, and examples to
the Python and TypeScript client READMEs and the CLI reference, with a
pointer to the validate-pipes GitHub Action.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions github-actions Bot added docs Documentation module:client-python Python SDK and MCP client module:ai AI/ML modules ci/cd CI/CD and build system module:client-typescript labels Jul 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor
🤖 Internal: Discord sync marker

Auto-managed by the Discord notification workflow. Stores the linked Discord message ID and forum thread ID. Do not edit or delete.

@nihalnihalani nihalnihalani left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Self-review from the author: inline notes on the four decisions most worth a reviewer's attention. Full design rationale and live-engine evidence (including the negative control against the unpatched image) are in the PR description.

# validatePipeline expects the config under a top-level 'pipeline'
# key — same envelope pipe_Validate (modules/pipe) builds; without
# it every config fails with "'pipeline' is missing or invalid"
data = validatePipeline({'pipeline': inner})

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the load-bearing line of the PR. rocketlib validatePipeline requires the config under a top-level 'pipeline' key; the HTTP path (modules/pipe/pipe_validate.py) already builds this envelope, but the DAP handler passed the config flat — so every rrext_validate call (including SDK client.validate()) failed with ccode 40 "'pipeline' is missing or invalid" on every published engine.

Evidence: against the unpatched ghcr.io/rocketride-org/rocketride-engine:latest (v3.3.1.35), examples/agent-llamaindex.pipe fails with ccode 40; with only this line changed (bind-mounted into the same image), it validates clean and deliberately-broken files return real errors (Duplicate component ..., input references unknown component id: ...). The regression test below pins the wire shape so the two paths can't diverge again.

raise ValueError(f'Invalid pipeline format in {file_path}: expected a JSON object')

# .pipe files wrap the config in { "pipeline": { ... } } - unwrap if present
inner = parsed.get('pipeline')

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrapper handling mirrors client.use() / mixins/execution.py: a .pipe file may be the flat config or {"pipeline": {...}}, and we unwrap only when the value under pipeline is a dict — byte-identical logic to the TS CLI's loadPipelineFile so the two CLIs accept exactly the same inputs. Non-object top-level JSON is rejected locally (before any server call) with the same message string in both CLIs.

One deliberate strictness choice: files are parsed with strict json.load, not the json5-tolerant loader start uses — a CI gate should not accept files the wire format doesn't. Flagged in the docs.

# registers it outside the standardEndpoints gate), so it works on
# every published engine image; /ping is gated and 404s on some.
deadline=$(( SECONDS + 120 ))
until curl -fsS "http://127.0.0.1:${ENGINE_PORT}/version" > /dev/null 2>&1; do

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Health probe is /version rather than /ping deliberately: server.py registers /version unconditionally public, while /ping sits behind the standardEndpoints gate — on the published engine image it 404s even with valid auth (verified live). The Helm chart's /ping liveness probes may deserve the same scrutiny; noted in #1572.

Also worth a look two steps down: changed-only fetches the PR base SHA explicitly before git diff, so the action works with actions/checkout's default fetch-depth: 1 — no fetch-depth requirement leaks onto consumers.

* ```
*/

// NOTE: do not `import * as process from 'process'` here — the CommonJS

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This hunk is the isolated pre-existing bug fix (first commit, fix(cli): — kept separate from the feature on our internal devil's-advocate review). import * as process compiles to an __importStar wrapper that only copies own properties; on Node >= 24 process.on/once/emit are inherited from EventEmitter.prototype, so the wrapped module lacks them and the CLI crashes at startup when registering SIGINT/SIGTERM handlers. The validate command's engine-connection cleanup relies on those handlers, which is how we hit it.

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 491ae2cf-be76-44d5-b99e-e45884515309

📥 Commits

Reviewing files that changed from the base of the PR and between cbe5d12 and 32649c2.

📒 Files selected for processing (13)
  • .github/actions/validate-pipes/README.md
  • .github/actions/validate-pipes/action.yml
  • docs/README-python-client.md
  • docs/README-typescript-client.md
  • packages/ai/src/ai/modules/task/commands/cmd_misc.py
  • packages/ai/tests/ai/modules/task/commands/test_cmd_misc.py
  • packages/client-python/src/rocketride/cli/commands/__init__.py
  • packages/client-python/src/rocketride/cli/commands/validate.py
  • packages/client-python/src/rocketride/cli/main.py
  • packages/client-python/tests/test_validate_cli.py
  • packages/client-typescript/src/cli/rocketride.ts
  • packages/client-typescript/tests/validate.test.ts
  • packages/docs/content-static/cli.mdx
 ___________________________________________
< Transformers: Not just a movie franchise. >
 -------------------------------------------
  \
   \   \
        \ /\
        ( )
      .( o ).
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd CI/CD and build system docs Documentation module:ai AI/ML modules module:client-python Python SDK and MCP client module:client-typescript

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(sdk,ci): 'rocketride validate' CLI subcommand + reusable GitHub Action to gate .pipe files in CI

1 participant