Add docs.publish RPC method #106
Labels
No labels
prio_critical
prio_low
type_bug
type_contact
type_issue
type_lead
type_question
type_story
type_task
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
lhumina_code/hero_books#106
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Parent: #101
What to add
Method:
docs.publishParams:
{ path: string, name: string }Returns:
{ job_id: string }Wraps:
DocSite::publish()atlib.rs:130— rsyncs to productionImplementation steps
hero_docs publishsubcommand existshandle_docs_publish(id, params, config) -> RpcResponsehandle_docs_job_statusrather than emitting a bogusoutput_path"docs.publish"atrpc.rs:155openrpc.json, bumpinfo.versionrpc.rs:9-28Acceptance Criteria
hero_docs publishsubcommand exists{ job_id }rawdaGastan referenced this issue2026-04-26 16:20:59 +00:00
Implementation Spec for Issue #106
Objective
Expose the rsync-based publish capability over JSON-RPC as
docs.publish. The handler submits ahero_procjob that shells out to a newhero_docs publishsubcommand. Per #101's "build directory" convention (consistent with #102-#105),pathis the Docusaurus build directory andnameis the site_name passed to the rsync destination — not a heroscript path. The CLI callsbuild::publish(&path, &[], &name)directly, which uses the defaultPublishDest(production rsync host) with the suppliednameas the site_name fallback.Requirements
docs.publishis dispatchable fromcrates/hero_books_server/src/web/rpc.rsand reaches a new handlerhandle_docs_publish.crates/hero_books_server/openrpc.jsonand the inlinerpc_spec.rsschema. Summary documents the rsync side-effect, theRSYNCD_SECRETenv var requirement, and the no-output_pathpolicy.info.versionunchanged at0.1.6(one bump per rolling PR).## Docsdoc-header gainsdocs.publish.hero_docs(src/bin/hero_docs.rs) gains apublishsubcommand:hero_docs publish --path <path> --name <name>. It callshero_books_docusaurus::build::publish(&path, &[], &name)directly — no heroscript loading. Empty destinations slice triggers the defaultPublishDest(production rsync host) withnameas thesite_name.config.hero_docs_bin, neverDocSitein-process.docs_publish_<input_hash>whereinput_hash = calculate_docs_input_hash(&[&path, &name]). Hash-based name (not base64-path like #104) becauseoutput_pathis intentionally not surfaced fordocs.publish.derive_docs_output_pathdoc-comment updated to mentiondocs_publish_*explicitly. No code change in the helper — unknown prefixes already returnNone, which is the desired behaviour for publish (the side-effect is a remote rsync push, not a local build directory).RpcResponse::invalid_params(-32602); internal failures:RpcResponse::error(id, -32000, ...).pathandnameare required; neither may be empty.Files to Modify/Create
src/bin/hero_docs.rs—Publish(PublishArgs)variant, args struct (pathandname, both required), runner.crates/hero_books_server/src/web/rpc.rshandle_docs_publishhandler.derive_docs_output_pathdoc-comment update.path/name.crates/hero_books_server/src/web/rpc_spec.rs— inlinedocs.publishentry.crates/hero_books_server/openrpc.json—docs.publishmethod entry.No new files.
Implementation Plan
Step 1 — Add
publishsubcommand tohero_docsFiles:
src/bin/hero_docs.rsCommandsenum withPublish(PublishArgs).&[]for destinations means the helper falls back to the defaultPublishDest(51.195.61.5:30873, useratlas, modulesites), substitutingargs.nameas the site_name. The rsync password is read from theRSYNCD_SECRETenv var insidebuild::publish.main().Dependencies: none.
Step 2 — Add
handle_docs_publishhandlerFiles:
crates/hero_books_server/src/web/rpc.rshandle_docs_devwith signaturefn handle_docs_publish(id: Option<Value>, params: Option<Value>, config: &ServerConfig) -> RpcResponse.path(required, non-empty) andname(required, non-empty) from the params object.-32602. The error message should be specific:"missing or empty 'path' parameter"or"missing or empty 'name' parameter".format!("{} publish --path {} --name {}", hero_docs, path_q, name_q)usingshell_quotefor both interpolated strings.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.rsdocs.devand beforedocs.jobStatus:Dependencies: Step 2.
Step 4 — Update doc-header and
derive_docs_output_pathdoc-commentFiles:
crates/hero_books_server/src/web/rpc.rs## Docsdoc-header: insertdocs.publishafterdocs.dev:derive_docs_output_path's bucket-3 comment sodocs_publish_*appears next todocs_dev_*(no longer "upcoming"):Dependencies: none.
Step 5 — Add
docs.publishentries (openrpc.json + rpc_spec.rs)Files:
crates/hero_books_server/openrpc.json,crates/hero_books_server/src/web/rpc_spec.rsdocs.devanddocs.jobStatus:info.versionunchanged.Dependencies: Step 3.
Step 6 — Add unit test
Files:
crates/hero_books_server/src/web/rpc.rstest_docs_publish_missing_params— covers:path: returns-32602mentioning"missing or empty 'path'".path: same.name(with validpath): returns-32602mentioning"missing or empty 'name'".name(with validpath): same.paramsobject at all:-32602.The existing
test_derive_docs_output_path_returns_none_for_install_update_templatetest from #104 (which exercisesdocs_dev_xxxetc. as None-returning prefixes) doesn't need changing —docs_publish_xxxwill fall through the same way. We can add a quick assertion fordocs_publish_xxxto that test, or add a new dedicated test. I'll just extend the existing test with one additionalassert!(... is_none())line to lock the behaviour.Dependencies: Step 2.
Step 7 — Verify and live-test
cargo check,cargo clippy,cargo test --releaseall green.cargo run --bin hero_docs -- publish --help.rpc.discoverlistsdocs.publishwith two required params.pathand missingnameeach return-32602with the right message.{"job_id": "<n>"}.(path, name)returns the same id.name(or differentpath) produces a different id.docs.jobStatusreturns nooutput_pathfield.docs_publish_<hash>;timeout_ms = 600000(default 10-min cap, same as one-shot methods).RSYNCD_SECRETand 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 --helpprints usage with--path(required) and--name(required).crates/hero_books_server/src/web/rpc.rsdefineshandle_docs_publish.docs.publishis wired into the dispatcher betweendocs.devanddocs.jobStatus.docs_publish_<input_hash>where the hash includes bothpathandname.docs.jobStatusreturns nooutput_pathfordocs_publish_*jobs in any state.## Docsdoc-header listsdocs.publish.crates/hero_books_server/openrpc.jsonandrpc_spec.rsinline schema both containdocs.publishwith the same shape.info.versionis unchanged at"0.1.6".cargo check,cargo clippy,cargo testall pass — including newtest_docs_publish_missing_params.docs_publish_<hash>inhero_proc job list(or the SQLitejobstable).Notes
build/subdirectory with the rendered static site —build::publisherrors 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 todocs.generate. Out of scope for #106.namesemantics: passed directly into the defaultPublishDest'ssite_name. The destination's other fields (host51.195.61.5, port30873, useratlas, modulesites) come fromPublishDest::default()inmodel.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 insidebuild::publishfrom the env var. Not exposed via the RPC params (correct — secrets shouldn't go through JSON-RPC bodies). The hero_books_server process must haveRSYNCD_SECRETin its environment for production publishes to authenticate.docs.publishis a remote side-effect (rsync push); there's no local "output_path" to surface.derive_docs_output_pathalready returnsNonefor unknown prefixes; the doc-comment is updated so future readers see why.info.versionstays at0.1.6.submit_or_dedup_docs_jobalready calls.no_retry()— important for publish, since rsync side-effects might not be idempotent under arbitrary retry. (Same as the other one-shot methods.)<path>/build/from a priordocs.build, andRSYNCD_SECRETmust be set to a real password for the rsync target. Without these, the job lands instate: failedwith a clear error frombuild::publish. The RPC chain still works correctly — the failure is environmental, same shape as #102-#105's bun-not-installed failures.Test Results
Suite:
cargo test -p hero_books_server --libTotal: 25 — Passed: 25 — Failed: 0
New test (1) and one existing test extended:
test_docs_publish_missing_params— covers missing/emptypath, missing/emptyname(with validpath), and no-paramscases. Each returns-32602with the appropriate field-specific error message.test_derive_docs_output_path_returns_none_for_install_update_template— extended to assertderive_docs_output_path("docs_publish_yyy", cache).is_none()andderive_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— OKcargo check --bin hero_docs— OKcargo clippy -p hero_books_server --lib --no-deps— silent (zero warnings)cargo clippy --bin hero_docs --no-deps— silentcargo test --release— 25/25Spec/impl parity
INSTRUCTIONS_OPENRPC.md §Verification diff is empty.
CLI smoke test
Live end-to-end RPC test (against running hero_books_server + hero_proc)
rpc.discoverlistsdocs.publishwith bothpath(required) andname(required)pathreturns-32602 "missing or empty 'path' parameter"namereturns-32602 "missing or empty 'name' parameter"{"job_id":"110"}for(path=/tmp/test_publish, name=test_site)(path, name)dedupes to the samejob_id: "110"name(other_site) produces a differentjob_id: "111"docs.jobStatusfor the failed job returnsstate: failed, error tail"Build directory does not exist. Run build() first.", and nooutput_pathfieldaction_id = docs_publish_<hash>,timeout_ms = 600000(default 10-min cap, same as one-shot methods)The job correctly failed with the expected validation error from
build::publishwhen the build directory has nobuild/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_docsCLI: strictly additive. Existing methods/subcommands unchanged. No version bump.info.versionUnchanged at
0.1.6— single bump per rolling PR.Implementation Summary
docs.publishis now exposed over JSON-RPC. The handler shells out to a newhero_docs publishsubcommand which callshero_books_docusaurus::build::publish(&path, &[], &name)directly. The empty destinations slice triggers the helper's defaultPublishDest(production rsync host51.195.61.5:30873, useratlas, modulesites), substituting the suppliednameas the site_name. The rsync password is read from theRSYNCD_SECRETenv var on the server side.Files changed (this iteration)
src/bin/hero_docs.rs— newPublish(PublishArgs)subcommand with required--pathand--nameflags. Runner callsbuild::publish(&path, &[], &name).crates/hero_books_server/src/web/rpc.rshandle_docs_publishhandler — validates both required params (-32602with field-specific messages), hashes(path, name)for dedup, shells out tohero_docs publish. Uses the default 10-min timeout (rsync of typical doc sizes is bounded)."docs.publish"betweendocs.devanddocs.jobStatus.## Docsdoc-header gainsdocs.publish.derive_docs_output_pathdoc-comment updated:docs_publish_*returnsNone(output is a remote rsync push, not a local directory).test_docs_publish_missing_params(5 assertions covering all validation paths).test_derive_docs_output_path_returns_none_for_install_update_templateextended to also assertdocs_publish_yyy → None.crates/hero_books_server/src/web/rpc_spec.rs— inline schema gains adocs.publishentry.crates/hero_books_server/openrpc.json— new method entry.info.versionunchanged at0.1.6.crates/hero_books_server/openrpc.client.generated.rs— auto-regenerated.Tests
cargo clippysilent on both crates.docs.jobStatus, and nooutput_pathfordocs_publish_*jobs.Backwards compatibility
docs.publish. All existing methods unchanged.hero_docsCLI: only addspublishsubcommand. Existing subcommands unchanged.info.version: unchanged at0.1.6.Notes
build/subdirectory with the rendered static site —build::publisherrors otherwise. We do not load heroscript here.namesemantics: passed as thesite_nameto the defaultPublishDest. The destination's other fields (host, port, user, module) come fromPublishDest::default(). If the user needs different rsync targets, that's a heroscript-mode concern (out of scope for #106).RSYNCD_SECRET: read insidebuild::publishfrom the env var; not exposed via JSON-RPC params (correct — secrets shouldn't go through RPC bodies). The hero_books_server process needsRSYNCD_SECRETset to authenticate against production rsync.derive_docs_output_pathreturnsNonefordocs_publish_*(locked by extended unit test).<path>/build/from a priordocs.build, andRSYNCD_SECRETmust be set to a real password. Without these, the job lands instate: failedwith a clear error frombuild::publish.rawdaGastan referenced this issue2026-04-27 09:23:12 +00:00