Add docs.publish RPC method #106

Closed
opened 2026-04-26 12:26:18 +00:00 by mahmoud · 3 comments
Owner

Parent: #101

What to add

Method: docs.publish
Params: { path: string, name: string }
Returns: { job_id: string }
Wraps: DocSite::publish() at lib.rs:130 — rsyncs to production

Implementation steps

  1. Verify hero_docs publish subcommand exists
  2. Add handle_docs_publish(id, params, config) -> RpcResponse
  3. Non-deterministic output — extend prefix list in handle_docs_job_status rather than emitting a bogus output_path
  4. Add dispatch arm "docs.publish" at rpc.rs:155
  5. Add method entry in openrpc.json, bump info.version
  6. Update doc-comment header at rpc.rs:9-28

Acceptance Criteria

  • hero_docs publish subcommand exists
  • Handler shells out via hero_docs
  • Returns { job_id }
  • spec/impl parity check passes
Parent: #101 ## What to add **Method:** `docs.publish` **Params:** `{ path: string, name: string }` **Returns:** `{ job_id: string }` **Wraps:** `DocSite::publish()` at `lib.rs:130` — rsyncs to production ## Implementation steps 1. Verify `hero_docs publish` subcommand exists 2. Add `handle_docs_publish(id, params, config) -> RpcResponse` 3. Non-deterministic output — extend prefix list in `handle_docs_job_status` rather than emitting a bogus `output_path` 4. Add dispatch arm `"docs.publish"` at `rpc.rs:155` 5. Add method entry in `openrpc.json`, bump `info.version` 6. Update doc-comment header at `rpc.rs:9-28` ## Acceptance Criteria - [x] `hero_docs publish` subcommand exists - [x] Handler shells out via hero_docs - [x] Returns `{ job_id }` - [x] spec/impl parity check passes
rawdaGastan added this to the ACTIVE project 2026-04-26 14:08:37 +00:00
Member

Implementation Spec for Issue #106

Objective

Expose the rsync-based publish capability over JSON-RPC as docs.publish. The handler submits a hero_proc job that shells out to a new hero_docs publish subcommand. Per #101's "build directory" convention (consistent with #102-#105), path is the Docusaurus build directory and name is the site_name passed to the rsync destination — not a heroscript path. The CLI calls build::publish(&path, &[], &name) directly, which uses the default PublishDest (production rsync host) with the supplied name as the site_name fallback.

Requirements

  • docs.publish is dispatchable from crates/hero_books_server/src/web/rpc.rs and reaches a new handler handle_docs_publish.
  • Matching entry in crates/hero_books_server/openrpc.json and the inline rpc_spec.rs schema. Summary documents the rsync side-effect, the RSYNCD_SECRET env var requirement, and the no-output_path policy.
  • info.version unchanged at 0.1.6 (one bump per rolling PR).
  • ## Docs doc-header gains docs.publish.
  • hero_docs (src/bin/hero_docs.rs) gains a publish subcommand: hero_docs publish --path <path> --name <name>. It calls hero_books_docusaurus::build::publish(&path, &[], &name) directly — no heroscript loading. Empty destinations slice triggers the default PublishDest (production rsync host) with name as the site_name.
  • Handler shells out via config.hero_docs_bin, never DocSite in-process.
  • Action name format: docs_publish_<input_hash> where input_hash = calculate_docs_input_hash(&[&path, &name]). Hash-based name (not base64-path like #104) because output_path is intentionally not surfaced for docs.publish.
  • derive_docs_output_path doc-comment updated to mention docs_publish_* explicitly. No code change in the helper — unknown prefixes already return None, which is the desired behaviour for publish (the side-effect is a remote rsync push, not a local build directory).
  • Invalid-params: RpcResponse::invalid_params (-32602); internal failures: RpcResponse::error(id, -32000, ...).
  • Both path and name are required; neither may be empty.
  • INSTRUCTIONS_OPENRPC.md §Verification diff produces empty output.

Files to Modify/Create

  • src/bin/hero_docs.rsPublish(PublishArgs) variant, args struct (path and name, both required), runner.
  • crates/hero_books_server/src/web/rpc.rs
    • handle_docs_publish handler.
    • Dispatch arm.
    • Doc-header bullet.
    • derive_docs_output_path doc-comment update.
    • New unit test for missing/empty path/name.
  • crates/hero_books_server/src/web/rpc_spec.rs — inline docs.publish entry.
  • crates/hero_books_server/openrpc.jsondocs.publish method entry.

No new files.

Implementation Plan

Step 1 — Add publish subcommand to hero_docs

Files: src/bin/hero_docs.rs

  • Extend the Commands enum with Publish(PublishArgs).
  • Args struct:
    #[derive(Args)]
    struct PublishArgs {
        /// Path to the Docusaurus build directory (must contain a `build/` subdirectory)
        #[arg(long)]
        path: String,
        /// Site name used as the rsync destination's site_name
        #[arg(long)]
        name: String,
    }
    
  • Runner:
    fn run_publish(args: PublishArgs) -> Result<(), Box<dyn std::error::Error>> {
        let path = std::path::PathBuf::from(&args.path);
        log::info!("Publishing site '{}' from {} to production rsync destination", args.name, path.display());
        hero_books_docusaurus::build::publish(&path, &[], &args.name)?;
        log::info!("Published.");
        Ok(())
    }
    
    Note: empty &[] for destinations means the helper falls back to the default PublishDest (51.195.61.5:30873, user atlas, module sites), substituting args.name as the site_name. The rsync password is read from the RSYNCD_SECRET env var inside build::publish.
  • Dispatch from main().

Dependencies: none.

Step 2 — Add handle_docs_publish handler

Files: crates/hero_books_server/src/web/rpc.rs

  • Add the function below handle_docs_dev with signature fn handle_docs_publish(id: Option<Value>, params: Option<Value>, config: &ServerConfig) -> RpcResponse.
  • Body:
    • Extract path (required, non-empty) and name (required, non-empty) from the params object.
    • Reject empty/missing with the conventional -32602. The error message should be specific: "missing or empty 'path' parameter" or "missing or empty 'name' parameter".
    • Hash both raw inputs:
      let input_hash = calculate_docs_input_hash(&[&path, &name]);
      let action_name = format!("docs_publish_{}", input_hash);
      
    • Build script: format!("{} publish --path {} --name {}", hero_docs, path_q, name_q) using shell_quote for both interpolated strings.
    • Submit via submit_or_dedup_docs_job(id, config, &action_name, &script) — uses the default 10-min timeout (rsync of a built site rarely exceeds this for typical doc sizes).

Dependencies: Step 1 (subcommand exists at runtime).

Step 3 — Wire dispatch arm

Files: crates/hero_books_server/src/web/rpc.rs

  • Add immediately after docs.dev and before docs.jobStatus:
    "docs.publish" => handle_docs_publish(request.id, request.params, config),
    

Dependencies: Step 2.

Step 4 — Update doc-header and derive_docs_output_path doc-comment

Files: crates/hero_books_server/src/web/rpc.rs

  • ## Docs doc-header: insert docs.publish after docs.dev:
    //! - `docs.publish` - Publish the built site to production rsync destinations
    
  • Update derive_docs_output_path's bucket-3 comment so docs_publish_* appears next to docs_dev_* (no longer "upcoming"):
    ///    `docs_dev_*` (long-running; output is a live HTTP server) and
    ///    `docs_publish_*` (output is a remote rsync push, not a local
    ///    directory), upcoming `docs_publish_dev_*`) — `None`.
    

Dependencies: none.

Step 5 — Add docs.publish entries (openrpc.json + rpc_spec.rs)

Files: crates/hero_books_server/openrpc.json, crates/hero_books_server/src/web/rpc_spec.rs

  • Both schemas: insert new method object between docs.dev and docs.jobStatus:
    {
      "name": "docs.publish",
      "summary": "Submit a hero_proc job that publishes the built Docusaurus site at the given build path to the production rsync destination. The `name` parameter is used as the rsync destination's site_name. Reads the rsync password from the RSYNCD_SECRET env var on the server. Returns the hero_proc job id as a string; poll via docs.jobStatus. docs.jobStatus does not surface output_path for this method (the side-effect is a remote rsync push, not a local directory).",
      "params": [
        { "name": "path", "schema": { "type": "string" }, "required": true },
        { "name": "name", "schema": { "type": "string" }, "required": true }
      ],
      "result": {
        "name": "jobRef",
        "schema": {
          "type": "object",
          "properties": { "job_id": { "type": "string" } }
        }
      }
    }
    
  • info.version unchanged.

Dependencies: Step 3.

Step 6 — Add unit test

Files: crates/hero_books_server/src/web/rpc.rs

  • test_docs_publish_missing_params — covers:
    • Missing path: returns -32602 mentioning "missing or empty 'path'".
    • Empty path: same.
    • Missing name (with valid path): returns -32602 mentioning "missing or empty 'name'".
    • Empty name (with valid path): same.
    • No params object at all: -32602.

The existing test_derive_docs_output_path_returns_none_for_install_update_template test from #104 (which exercises docs_dev_xxx etc. as None-returning prefixes) doesn't need changing — docs_publish_xxx will fall through the same way. We can add a quick assertion for docs_publish_xxx to that test, or add a new dedicated test. I'll just extend the existing test with one additional assert!(... is_none()) line to lock the behaviour.

Dependencies: Step 2.

Step 7 — Verify and live-test

  • INSTRUCTIONS_OPENRPC.md §Verification diff empty.
  • cargo check, cargo clippy, cargo test --release all green.
  • Smoke: cargo run --bin hero_docs -- publish --help.
  • Live RPC:
    • rpc.discover lists docs.publish with two required params.
    • Validation: missing path and missing name each return -32602 with the right message.
    • Submit: returns {"job_id": "<n>"}.
    • Idempotent dedup with same (path, name) returns the same id.
    • Different name (or different path) produces a different id.
    • docs.jobStatus returns no output_path field.
    • DB inspection: action_id = docs_publish_<hash>; timeout_ms = 600000 (default 10-min cap, same as one-shot methods).
    • The job will likely fail without a real RSYNCD_SECRET and without a built site at <path>/build — that's expected. We're verifying the RPC chain, not actually publishing to production.

Dependencies: Steps 1-6.

Acceptance Criteria

  • hero_docs publish --help prints usage with --path (required) and --name (required).
  • crates/hero_books_server/src/web/rpc.rs defines handle_docs_publish.
  • docs.publish is wired into the dispatcher between docs.dev and docs.jobStatus.
  • Action name format is docs_publish_<input_hash> where the hash includes both path and name.
  • docs.jobStatus returns no output_path for docs_publish_* jobs in any state.
  • ## Docs doc-header lists docs.publish.
  • crates/hero_books_server/openrpc.json and rpc_spec.rs inline schema both contain docs.publish with the same shape.
  • info.version is unchanged at "0.1.6".
  • INSTRUCTIONS_OPENRPC.md §Verification diff empty.
  • cargo check, cargo clippy, cargo test all pass — including new test_docs_publish_missing_params.
  • Live: dev server reports the job as docs_publish_<hash> in hero_proc job list (or the SQLite jobs table).

Notes

  • Path semantics: build directory (consistent with #102-#105). The directory must contain a build/ subdirectory with the rendered static site — build::publish errors otherwise. We do not load heroscript here; that means the user's heroscript-configured destinations are NOT honoured. If a future child issue (or follow-up PR) wants heroscript-aware publish, it would take a heroscript path instead, similar to docs.generate. Out of scope for #106.
  • name semantics: passed directly into the default PublishDest's site_name. The destination's other fields (host 51.195.61.5, port 30873, user atlas, module sites) come from PublishDest::default() in model.rs:128. If the user needs different rsync destinations, they currently cannot override them via this RPC — that's a heroscript-mode concern and out of scope.
  • RSYNCD_SECRET: read inside build::publish from the env var. Not exposed via the RPC params (correct — secrets shouldn't go through JSON-RPC bodies). The hero_books_server process must have RSYNCD_SECRET in its environment for production publishes to authenticate.
  • Output_path policy: docs.publish is a remote side-effect (rsync push); there's no local "output_path" to surface. derive_docs_output_path already returns None for unknown prefixes; the doc-comment is updated so future readers see why.
  • No version bump: per the rolling PR rule, info.version stays at 0.1.6.
  • No-retry semantics: submit_or_dedup_docs_job already calls .no_retry() — important for publish, since rsync side-effects might not be idempotent under arbitrary retry. (Same as the other one-shot methods.)
  • Pre-condition for live success: the build path must contain <path>/build/ from a prior docs.build, and RSYNCD_SECRET must be set to a real password for the rsync target. Without these, the job lands in state: failed with a clear error from build::publish. The RPC chain still works correctly — the failure is environmental, same shape as #102-#105's bun-not-installed failures.
## Implementation Spec for Issue #106 ### Objective Expose the rsync-based publish capability over JSON-RPC as `docs.publish`. The handler submits a `hero_proc` job that shells out to a new `hero_docs publish` subcommand. Per #101's "build directory" convention (consistent with #102-#105), `path` is the Docusaurus build directory and `name` is the site_name passed to the rsync destination — not a heroscript path. The CLI calls `build::publish(&path, &[], &name)` directly, which uses the default `PublishDest` (production rsync host) with the supplied `name` as the site_name fallback. ### Requirements - `docs.publish` is dispatchable from `crates/hero_books_server/src/web/rpc.rs` and reaches a new handler `handle_docs_publish`. - Matching entry in `crates/hero_books_server/openrpc.json` and the inline `rpc_spec.rs` schema. Summary documents the rsync side-effect, the `RSYNCD_SECRET` env var requirement, and the no-`output_path` policy. - `info.version` unchanged at `0.1.6` (one bump per rolling PR). - `## Docs` doc-header gains `docs.publish`. - `hero_docs` (`src/bin/hero_docs.rs`) gains a `publish` subcommand: `hero_docs publish --path <path> --name <name>`. It calls `hero_books_docusaurus::build::publish(&path, &[], &name)` directly — no heroscript loading. Empty destinations slice triggers the default `PublishDest` (production rsync host) with `name` as the `site_name`. - Handler shells out via `config.hero_docs_bin`, never `DocSite` in-process. - Action name format: `docs_publish_<input_hash>` where `input_hash = calculate_docs_input_hash(&[&path, &name])`. Hash-based name (not base64-path like #104) because `output_path` is intentionally not surfaced for `docs.publish`. - `derive_docs_output_path` doc-comment updated to mention `docs_publish_*` explicitly. No code change in the helper — unknown prefixes already return `None`, which is the desired behaviour for publish (the side-effect is a remote rsync push, not a local build directory). - Invalid-params: `RpcResponse::invalid_params` (-32602); internal failures: `RpcResponse::error(id, -32000, ...)`. - Both `path` and `name` are required; neither may be empty. - INSTRUCTIONS_OPENRPC.md §Verification diff produces empty output. ### Files to Modify/Create - `src/bin/hero_docs.rs` — `Publish(PublishArgs)` variant, args struct (`path` and `name`, both required), runner. - `crates/hero_books_server/src/web/rpc.rs` - `handle_docs_publish` handler. - Dispatch arm. - Doc-header bullet. - `derive_docs_output_path` doc-comment update. - New unit test for missing/empty `path`/`name`. - `crates/hero_books_server/src/web/rpc_spec.rs` — inline `docs.publish` entry. - `crates/hero_books_server/openrpc.json` — `docs.publish` method entry. No new files. ### Implementation Plan #### Step 1 — Add `publish` subcommand to `hero_docs` Files: `src/bin/hero_docs.rs` - Extend the `Commands` enum with `Publish(PublishArgs)`. - Args struct: ```rust #[derive(Args)] struct PublishArgs { /// Path to the Docusaurus build directory (must contain a `build/` subdirectory) #[arg(long)] path: String, /// Site name used as the rsync destination's site_name #[arg(long)] name: String, } ``` - Runner: ```rust fn run_publish(args: PublishArgs) -> Result<(), Box<dyn std::error::Error>> { let path = std::path::PathBuf::from(&args.path); log::info!("Publishing site '{}' from {} to production rsync destination", args.name, path.display()); hero_books_docusaurus::build::publish(&path, &[], &args.name)?; log::info!("Published."); Ok(()) } ``` Note: empty `&[]` for destinations means the helper falls back to the default `PublishDest` (`51.195.61.5:30873`, user `atlas`, module `sites`), substituting `args.name` as the site_name. The rsync password is read from the `RSYNCD_SECRET` env var inside `build::publish`. - Dispatch from `main()`. Dependencies: none. #### Step 2 — Add `handle_docs_publish` handler Files: `crates/hero_books_server/src/web/rpc.rs` - Add the function below `handle_docs_dev` with signature `fn handle_docs_publish(id: Option<Value>, params: Option<Value>, config: &ServerConfig) -> RpcResponse`. - Body: - Extract `path` (required, non-empty) and `name` (required, non-empty) from the params object. - Reject empty/missing with the conventional `-32602`. The error message should be specific: `"missing or empty 'path' parameter"` or `"missing or empty 'name' parameter"`. - Hash both raw inputs: ```rust let input_hash = calculate_docs_input_hash(&[&path, &name]); let action_name = format!("docs_publish_{}", input_hash); ``` - Build script: `format!("{} publish --path {} --name {}", hero_docs, path_q, name_q)` using `shell_quote` for both interpolated strings. - Submit via `submit_or_dedup_docs_job(id, config, &action_name, &script)` — uses the default 10-min timeout (rsync of a built site rarely exceeds this for typical doc sizes). Dependencies: Step 1 (subcommand exists at runtime). #### Step 3 — Wire dispatch arm Files: `crates/hero_books_server/src/web/rpc.rs` - Add immediately after `docs.dev` and before `docs.jobStatus`: ```rust "docs.publish" => handle_docs_publish(request.id, request.params, config), ``` Dependencies: Step 2. #### Step 4 — Update doc-header and `derive_docs_output_path` doc-comment Files: `crates/hero_books_server/src/web/rpc.rs` - `## Docs` doc-header: insert `docs.publish` after `docs.dev`: ```rust //! - `docs.publish` - Publish the built site to production rsync destinations ``` - Update `derive_docs_output_path`'s bucket-3 comment so `docs_publish_*` appears next to `docs_dev_*` (no longer "upcoming"): ``` /// `docs_dev_*` (long-running; output is a live HTTP server) and /// `docs_publish_*` (output is a remote rsync push, not a local /// directory), upcoming `docs_publish_dev_*`) — `None`. ``` Dependencies: none. #### Step 5 — Add `docs.publish` entries (openrpc.json + rpc_spec.rs) Files: `crates/hero_books_server/openrpc.json`, `crates/hero_books_server/src/web/rpc_spec.rs` - Both schemas: insert new method object between `docs.dev` and `docs.jobStatus`: ```json { "name": "docs.publish", "summary": "Submit a hero_proc job that publishes the built Docusaurus site at the given build path to the production rsync destination. The `name` parameter is used as the rsync destination's site_name. Reads the rsync password from the RSYNCD_SECRET env var on the server. Returns the hero_proc job id as a string; poll via docs.jobStatus. docs.jobStatus does not surface output_path for this method (the side-effect is a remote rsync push, not a local directory).", "params": [ { "name": "path", "schema": { "type": "string" }, "required": true }, { "name": "name", "schema": { "type": "string" }, "required": true } ], "result": { "name": "jobRef", "schema": { "type": "object", "properties": { "job_id": { "type": "string" } } } } } ``` - `info.version` unchanged. Dependencies: Step 3. #### Step 6 — Add unit test Files: `crates/hero_books_server/src/web/rpc.rs` - `test_docs_publish_missing_params` — covers: - Missing `path`: returns `-32602` mentioning `"missing or empty 'path'"`. - Empty `path`: same. - Missing `name` (with valid `path`): returns `-32602` mentioning `"missing or empty 'name'"`. - Empty `name` (with valid `path`): same. - No `params` object at all: `-32602`. The existing `test_derive_docs_output_path_returns_none_for_install_update_template` test from #104 (which exercises `docs_dev_xxx` etc. as None-returning prefixes) doesn't need changing — `docs_publish_xxx` will fall through the same way. We can add a quick assertion for `docs_publish_xxx` to that test, or add a new dedicated test. I'll just extend the existing test with one additional `assert!(... is_none())` line to lock the behaviour. Dependencies: Step 2. #### Step 7 — Verify and live-test - INSTRUCTIONS_OPENRPC.md §Verification diff empty. - `cargo check`, `cargo clippy`, `cargo test --release` all green. - Smoke: `cargo run --bin hero_docs -- publish --help`. - Live RPC: - `rpc.discover` lists `docs.publish` with two required params. - Validation: missing `path` and missing `name` each return `-32602` with the right message. - Submit: returns `{"job_id": "<n>"}`. - Idempotent dedup with same `(path, name)` returns the same id. - Different `name` (or different `path`) produces a different id. - `docs.jobStatus` returns no `output_path` field. - DB inspection: action_id = `docs_publish_<hash>`; `timeout_ms = 600000` (default 10-min cap, same as one-shot methods). - The job will likely fail without a real `RSYNCD_SECRET` and without a built site at `<path>/build` — that's expected. We're verifying the RPC chain, not actually publishing to production. Dependencies: Steps 1-6. ### Acceptance Criteria - [ ] `hero_docs publish --help` prints usage with `--path` (required) and `--name` (required). - [ ] `crates/hero_books_server/src/web/rpc.rs` defines `handle_docs_publish`. - [ ] `docs.publish` is wired into the dispatcher between `docs.dev` and `docs.jobStatus`. - [ ] Action name format is `docs_publish_<input_hash>` where the hash includes both `path` and `name`. - [ ] `docs.jobStatus` returns no `output_path` for `docs_publish_*` jobs in any state. - [ ] `## Docs` doc-header lists `docs.publish`. - [ ] `crates/hero_books_server/openrpc.json` and `rpc_spec.rs` inline schema both contain `docs.publish` with the same shape. - [ ] `info.version` is unchanged at `"0.1.6"`. - [ ] INSTRUCTIONS_OPENRPC.md §Verification diff empty. - [ ] `cargo check`, `cargo clippy`, `cargo test` all pass — including new `test_docs_publish_missing_params`. - [ ] Live: dev server reports the job as `docs_publish_<hash>` in `hero_proc job list` (or the SQLite `jobs` table). ### Notes - **Path semantics**: build directory (consistent with #102-#105). The directory must contain a `build/` subdirectory with the rendered static site — `build::publish` errors otherwise. We do not load heroscript here; that means the user's heroscript-configured destinations are NOT honoured. If a future child issue (or follow-up PR) wants heroscript-aware publish, it would take a heroscript path instead, similar to `docs.generate`. Out of scope for #106. - **`name` semantics**: passed directly into the default `PublishDest`'s `site_name`. The destination's other fields (host `51.195.61.5`, port `30873`, user `atlas`, module `sites`) come from `PublishDest::default()` in `model.rs:128`. If the user needs different rsync destinations, they currently cannot override them via this RPC — that's a heroscript-mode concern and out of scope. - **`RSYNCD_SECRET`**: read inside `build::publish` from the env var. Not exposed via the RPC params (correct — secrets shouldn't go through JSON-RPC bodies). The hero_books_server process must have `RSYNCD_SECRET` in its environment for production publishes to authenticate. - **Output_path policy**: `docs.publish` is a remote side-effect (rsync push); there's no local "output_path" to surface. `derive_docs_output_path` already returns `None` for unknown prefixes; the doc-comment is updated so future readers see why. - **No version bump**: per the rolling PR rule, `info.version` stays at `0.1.6`. - **No-retry semantics**: `submit_or_dedup_docs_job` already calls `.no_retry()` — important for publish, since rsync side-effects might not be idempotent under arbitrary retry. (Same as the other one-shot methods.) - **Pre-condition for live success**: the build path must contain `<path>/build/` from a prior `docs.build`, and `RSYNCD_SECRET` must be set to a real password for the rsync target. Without these, the job lands in `state: failed` with a clear error from `build::publish`. The RPC chain still works correctly — the failure is environmental, same shape as #102-#105's bun-not-installed failures.
Member

Test Results

Suite: cargo test -p hero_books_server --lib
Total: 25 — Passed: 25 — Failed: 0

New test (1) and one existing test extended:

  • test_docs_publish_missing_params — covers missing/empty path, missing/empty name (with valid path), and no-params cases. Each returns -32602 with the appropriate field-specific error message.
  • test_derive_docs_output_path_returns_none_for_install_update_template — extended to assert derive_docs_output_path("docs_publish_yyy", cache).is_none() and derive_docs_output_path("docs_dev_xxx", cache).is_none(). Locks the no-output-path policy for the side-effect family of methods.

Build & lint

  • cargo check -p hero_books_server — OK
  • cargo check --bin hero_docs — OK
  • cargo clippy -p hero_books_server --lib --no-deps — silent (zero warnings)
  • cargo clippy --bin hero_docs --no-deps — silent
  • cargo test --release — 25/25

Spec/impl parity

INSTRUCTIONS_OPENRPC.md §Verification diff is empty.

CLI smoke test

$ hero_docs publish --help
Publish the built site to production rsync destinations

Usage: hero_docs publish --path <PATH> --name <NAME>

Options:
      --path <PATH>  Path to the Docusaurus build directory (must contain a `build/` subdirectory)
      --name <NAME>  Site name used as the rsync destination's site_name
  -h, --help         Print help

Live end-to-end RPC test (against running hero_books_server + hero_proc)

Step Result
rpc.discover lists docs.publish with both path (required) and name (required) pass
Validation: missing path returns -32602 "missing or empty 'path' parameter" pass
Validation: missing name returns -32602 "missing or empty 'name' parameter" pass
Submit returns {"job_id":"110"} for (path=/tmp/test_publish, name=test_site) pass
Re-submit with same (path, name) dedupes to the same job_id: "110" pass
Different name (other_site) produces a different job_id: "111" pass
docs.jobStatus for the failed job returns state: failed, error tail "Build directory does not exist. Run build() first.", and no output_path field pass
DB: action_id = docs_publish_<hash>, timeout_ms = 600000 (default 10-min cap, same as one-shot methods) pass

The job correctly failed with the expected validation error from build::publish when the build directory has no build/ subdirectory — this is the right behaviour bubbled all the way up through the RPC chain. The full pipeline (dispatch → handler → hero_proc → hero_docs publishbuild::publish → status reporting) executed end-to-end.

Backwards compatibility

Public RPC surface and hero_docs CLI: strictly additive. Existing methods/subcommands unchanged. No version bump.

info.version

Unchanged at 0.1.6 — single bump per rolling PR.

## Test Results **Suite:** `cargo test -p hero_books_server --lib` **Total:** 25 — Passed: 25 — Failed: 0 New test (1) and one existing test extended: - `test_docs_publish_missing_params` — covers missing/empty `path`, missing/empty `name` (with valid `path`), and no-`params` cases. Each returns `-32602` with the appropriate field-specific error message. - `test_derive_docs_output_path_returns_none_for_install_update_template` — extended to assert `derive_docs_output_path("docs_publish_yyy", cache).is_none()` and `derive_docs_output_path("docs_dev_xxx", cache).is_none()`. Locks the no-output-path policy for the side-effect family of methods. ### Build & lint - `cargo check -p hero_books_server` — OK - `cargo check --bin hero_docs` — OK - `cargo clippy -p hero_books_server --lib --no-deps` — silent (zero warnings) - `cargo clippy --bin hero_docs --no-deps` — silent - `cargo test --release` — 25/25 ### Spec/impl parity INSTRUCTIONS_OPENRPC.md §Verification diff is empty. ### CLI smoke test ``` $ hero_docs publish --help Publish the built site to production rsync destinations Usage: hero_docs publish --path <PATH> --name <NAME> Options: --path <PATH> Path to the Docusaurus build directory (must contain a `build/` subdirectory) --name <NAME> Site name used as the rsync destination's site_name -h, --help Print help ``` ### Live end-to-end RPC test (against running hero_books_server + hero_proc) | Step | Result | | --- | --- | | `rpc.discover` lists `docs.publish` with both `path` (required) and `name` (required) | pass | | Validation: missing `path` returns `-32602 "missing or empty 'path' parameter"` | pass | | Validation: missing `name` returns `-32602 "missing or empty 'name' parameter"` | pass | | Submit returns `{"job_id":"110"}` for `(path=/tmp/test_publish, name=test_site)` | pass | | Re-submit with same `(path, name)` dedupes to the same `job_id: "110"` | pass | | Different `name` (other_site) produces a different `job_id: "111"` | pass | | `docs.jobStatus` for the failed job returns `state: failed`, error tail `"Build directory does not exist. Run build() first."`, **and no `output_path` field** | pass | | DB: `action_id = docs_publish_<hash>`, `timeout_ms = 600000` (default 10-min cap, same as one-shot methods) | pass | The job correctly failed with the expected validation error from `build::publish` when the build directory has no `build/` subdirectory — this is the right behaviour bubbled all the way up through the RPC chain. The full pipeline (dispatch → handler → hero_proc → `hero_docs publish` → `build::publish` → status reporting) executed end-to-end. ### Backwards compatibility Public RPC surface and `hero_docs` CLI: strictly additive. Existing methods/subcommands unchanged. No version bump. ### `info.version` Unchanged at `0.1.6` — single bump per rolling PR.
Member

Implementation Summary

docs.publish is now exposed over JSON-RPC. The handler shells out to a new hero_docs publish subcommand which calls hero_books_docusaurus::build::publish(&path, &[], &name) directly. The empty destinations slice triggers the helper's default PublishDest (production rsync host 51.195.61.5:30873, user atlas, module sites), substituting the supplied name as the site_name. The rsync password is read from the RSYNCD_SECRET env var on the server side.

Files changed (this iteration)

  • src/bin/hero_docs.rs — new Publish(PublishArgs) subcommand with required --path and --name flags. Runner calls build::publish(&path, &[], &name).
  • crates/hero_books_server/src/web/rpc.rs
    • handle_docs_publish handler — validates both required params (-32602 with field-specific messages), hashes (path, name) for dedup, shells out to hero_docs publish. Uses the default 10-min timeout (rsync of typical doc sizes is bounded).
    • Dispatch arm "docs.publish" between docs.dev and docs.jobStatus.
    • ## Docs doc-header gains docs.publish.
    • derive_docs_output_path doc-comment updated: docs_publish_* returns None (output is a remote rsync push, not a local directory).
    • New unit test test_docs_publish_missing_params (5 assertions covering all validation paths).
    • Existing test_derive_docs_output_path_returns_none_for_install_update_template extended to also assert docs_publish_yyy → None.
  • crates/hero_books_server/src/web/rpc_spec.rs — inline schema gains a docs.publish entry.
  • crates/hero_books_server/openrpc.json — new method entry. info.version unchanged at 0.1.6.
  • crates/hero_books_server/openrpc.client.generated.rs — auto-regenerated.

Tests

  • 25/25 lib tests pass (debug + release) — 1 new + 1 extended.
  • cargo clippy silent on both crates.
  • Spec/impl parity diff empty.
  • Live RPC test confirmed: discovery, both validation paths, submit, idempotent dedup, name-driven dedup distinction, hero_proc dispatch, error-tail surfacing through docs.jobStatus, and no output_path for docs_publish_* jobs.

Backwards compatibility

  • Public RPC API: only adds docs.publish. All existing methods unchanged.
  • hero_docs CLI: only adds publish subcommand. Existing subcommands unchanged.
  • info.version: unchanged at 0.1.6.
  • ActionSpec wire format for existing methods: byte-identical to before.

Notes

  • Path semantics: build directory (consistent with #102-#105). The directory must contain a build/ subdirectory with the rendered static site — build::publish errors otherwise. We do not load heroscript here.
  • name semantics: passed as the site_name to the default PublishDest. The destination's other fields (host, port, user, module) come from PublishDest::default(). If the user needs different rsync targets, that's a heroscript-mode concern (out of scope for #106).
  • RSYNCD_SECRET: read inside build::publish from the env var; not exposed via JSON-RPC params (correct — secrets shouldn't go through RPC bodies). The hero_books_server process needs RSYNCD_SECRET set to authenticate against production rsync.
  • Output_path policy: a remote rsync push has no recoverable local "output_path"; derive_docs_output_path returns None for docs_publish_* (locked by extended unit test).
  • Pre-condition for live success: build path must contain <path>/build/ from a prior docs.build, and RSYNCD_SECRET must be set to a real password. Without these, the job lands in state: failed with a clear error from build::publish.
## Implementation Summary `docs.publish` is now exposed over JSON-RPC. The handler shells out to a new `hero_docs publish` subcommand which calls `hero_books_docusaurus::build::publish(&path, &[], &name)` directly. The empty destinations slice triggers the helper's default `PublishDest` (production rsync host `51.195.61.5:30873`, user `atlas`, module `sites`), substituting the supplied `name` as the site_name. The rsync password is read from the `RSYNCD_SECRET` env var on the server side. ### Files changed (this iteration) - `src/bin/hero_docs.rs` — new `Publish(PublishArgs)` subcommand with required `--path` and `--name` flags. Runner calls `build::publish(&path, &[], &name)`. - `crates/hero_books_server/src/web/rpc.rs` - `handle_docs_publish` handler — validates both required params (`-32602` with field-specific messages), hashes `(path, name)` for dedup, shells out to `hero_docs publish`. Uses the default 10-min timeout (rsync of typical doc sizes is bounded). - Dispatch arm `"docs.publish"` between `docs.dev` and `docs.jobStatus`. - `## Docs` doc-header gains `docs.publish`. - `derive_docs_output_path` doc-comment updated: `docs_publish_*` returns `None` (output is a remote rsync push, not a local directory). - New unit test `test_docs_publish_missing_params` (5 assertions covering all validation paths). - Existing `test_derive_docs_output_path_returns_none_for_install_update_template` extended to also assert `docs_publish_yyy → None`. - `crates/hero_books_server/src/web/rpc_spec.rs` — inline schema gains a `docs.publish` entry. - `crates/hero_books_server/openrpc.json` — new method entry. `info.version` unchanged at `0.1.6`. - `crates/hero_books_server/openrpc.client.generated.rs` — auto-regenerated. ### Tests - 25/25 lib tests pass (debug + release) — 1 new + 1 extended. - `cargo clippy` silent on both crates. - Spec/impl parity diff empty. - Live RPC test confirmed: discovery, both validation paths, submit, idempotent dedup, name-driven dedup distinction, hero_proc dispatch, error-tail surfacing through `docs.jobStatus`, and no `output_path` for `docs_publish_*` jobs. ### Backwards compatibility - Public RPC API: only adds `docs.publish`. All existing methods unchanged. - `hero_docs` CLI: only adds `publish` subcommand. Existing subcommands unchanged. - `info.version`: unchanged at `0.1.6`. - ActionSpec wire format for existing methods: byte-identical to before. ### Notes - **Path semantics**: build directory (consistent with #102-#105). The directory must contain a `build/` subdirectory with the rendered static site — `build::publish` errors otherwise. We do not load heroscript here. - **`name` semantics**: passed as the `site_name` to the default `PublishDest`. The destination's other fields (host, port, user, module) come from `PublishDest::default()`. If the user needs different rsync targets, that's a heroscript-mode concern (out of scope for #106). - **`RSYNCD_SECRET`**: read inside `build::publish` from the env var; not exposed via JSON-RPC params (correct — secrets shouldn't go through RPC bodies). The hero_books_server process needs `RSYNCD_SECRET` set to authenticate against production rsync. - **Output_path policy**: a remote rsync push has no recoverable local "output_path"; `derive_docs_output_path` returns `None` for `docs_publish_*` (locked by extended unit test). - **Pre-condition for live success**: build path must contain `<path>/build/` from a prior `docs.build`, and `RSYNCD_SECRET` must be set to a real password. Without these, the job lands in `state: failed` with a clear error from `build::publish`.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lhumina_code/hero_books#106
No description provided.