Skip to content

feat: add distributed service scaffold commands#55

Open
patrickleet wants to merge 1 commit into
mainfrom
codex/hops-service-create-microsvc-scaffold
Open

feat: add distributed service scaffold commands#55
patrickleet wants to merge 1 commit into
mainfrom
codex/hops-service-create-microsvc-scaffold

Conversation

@patrickleet
Copy link
Copy Markdown
Collaborator

@patrickleet patrickleet commented Jun 2, 2026

Summary

  • add hops service scaffold for Distributed microsvc crates with handler/service/manifest modules; service create remains a hidden compatibility alias
  • add repeatable --model <name> for aggregate generation; no --model flags generate stateless handlers and no src/models module
  • generate each model in its own file, e.g. src/models/todo.rs, with src/models/mod.rs as the index/re-export plus shared CommandInput
  • make read-model scaffolding explicit with --read-models; default scaffolds do not generate src/read_models, do not register tables, and service schema emits no read-model SQL
  • add repeatable --command and repeatable --event
  • add --transport knative plus --knative; generated Knative services use Distributed's CloudEvents ingress and manifest transport knative
  • keep --transport for ingress shape (http/knative) and add --bus rabbitmq|kafka|psql|nats for bus backend selection
  • add --gitops to scaffold a Helm deploy chart under .gitops/deploy
  • add --gitops-promote argo|flux to scaffold .gitops/promote with an Argo CD Application or Flux GitRepository/HelmRelease pointed at .gitops/deploy
  • add --github <owner/repo> to configure release automation and create the target GitHub repo via gh repo create <owner/repo> --private when it does not already exist
  • --github generates .github/workflows/version.yaml using unbounded-tech/workflow-vnext-tag and .github/workflows/release.yaml using unbounded-tech/workflow-simple-release
  • add --github-preview <owner/repo> to generate a preview promotion workflow using unbounded-tech/workflows-gitops and a .gitops/preview/helm promotion chart
  • add --github-promote <owner/repo> to generate a permanent environment promotion workflow using unbounded-tech/workflows-gitops and a .gitops/promote/helm promotion chart
  • GitHub workflow flags auto-enable the deploy chart and set generated image references to ghcr.io/<owner>/<repo> when --github is provided
  • for Knative GitOps scaffolds, infer Brokers and Triggers from models, commands, and events:
    • model todo -> todo-commands and todo-events
    • command todo.create -> trigger on todo-commands
    • event checkout-saga.started -> trigger on checkout-saga-events
    • external event github-projects.issue.created -> trigger on github-projects-events
  • when --bus is provided, generated GitOps values include bus.kind and deploy resources expose HOPS_BUS
  • add --storage as a visible alias for --store, and --output as a visible alias for service schema --out
  • generated crates opt out of parent workspaces with an empty [workspace] table
  • cache describe/schema harness builds under each target service with mode-specific harness packages, so repeated runs reuse Cargo artifacts without parallel command collisions
  • document the expanded service command namespace

Tests

  • cargo check
  • cargo test commands::service
  • cargo test
  • rustfmt --check src/commands/service/mod.rs
  • git diff --check
  • help output verified: hops service --help lists scaffold and not create; hops service scaffold --help shows --model <MODEL>, --read-models, --bus <BUS>, --github <OWNER/REPO>, --github-preview <OWNER/REPO>, and --github-promote <OWNER/REPO>; --models, --no-models, and --no-read-models are absent
  • default fixture: hops service scaffold read-model-default --path .tmp/read-model-default --command todo.ping
  • default fixture: cargo check passed, no src/models, no src/read_models, service describe returns tables: [], and stdout service schema is empty
  • explicit read-model fixture: hops service scaffold read-model-explicit --path .tmp/read-model-explicit --model todo --read-models --command todo.create
  • explicit read-model fixture: cargo check passed, generated src/models/todo.rs and src/read_models/mod.rs, service describe returns todo_views, and service schema emits only todo_views
  • multi-model fixture: hops service scaffold model-file-multi --path .tmp/model-file-multi --model todo --model somethingelse --command todo.create --command somethingelse.complete --read-models
  • multi-model fixture: cargo check, service describe, and service schema passed; generated src/models/todo.rs, src/models/somethingelse.rs, and src/models/mod.rs
  • multi-model fixture schema emits todo_views and somethingelse_views
  • Knative/GitOps fixture: hops service scaffold model-file-knative --path .tmp/model-file-knative --transport knative --gitops --gitops-promote flux --model todo --command todo.create --event github-projects.issue.created --bus kafka
  • Knative/GitOps fixture: cargo check passed; .gitops/deploy contains Knative Service/Brokers/Triggers, .gitops/promote contains Flux HelmRelease, and generated values/env include kafka
  • GitHub workflow fixture with fake gh: hops service scaffold test-domain --path .tmp/github-workflows --github hops-ops/test-domain --github-preview hops-ops/test-previews --github-promote hops-ops/test-staging --transport knative --bus nats --command test-domain.create --event github-projects.issue.created
  • GitHub workflow fixture recorded gh repo view hops-ops/test-domain --json nameWithOwner then gh repo create hops-ops/test-domain --private
  • GitHub workflow fixture generated version.yaml, release.yaml, preview.yaml, promote.yaml, .gitops/preview/helm, .gitops/promote/helm, and .gitops/deploy; cargo check passed
  • GitHub workflow fixture service describe returns the Knative command/event manifest with tables: []; stdout service schema is empty
  • bus fixtures: --bus rabbitmq and --bus psql both compile and emit bus.kind plus HOPS_BUS
  • generated fixture: concurrent service describe, stdout service schema, and service schema --output /tmp/todo-custom.sql all succeeded
  • generated fixture schema emits only declared read-model SQL, with no outbox_messages

Notes

  • Companion framework PR: feat: add distributed project manifest primitives distributed#53
  • Operational schema tables such as outbox/aggregate event storage should be added as an explicit schema scope later if we want the CLI to render them.
  • The Distributed manifest schema does not currently have a bus field, so --bus is carried in generated GitOps config/runtime env for now.
  • The GitHub workflow scaffolds assume DEPLOY_KEY for vnext tagging and GH_ORG_ACTIONS_REPO_WRITE_PACKAGES for GitOps environment repo writes.
  • Repo-wide cargo fmt --check still reports unrelated formatting drift already present outside this service command change; this PR intentionally avoids that formatter churn.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 2, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a new top-level service CLI command with create, describe, and schema subcommands. Implements a scaffold generator, a manifest harness that runs a temporary Cargo project to emit JSON or SQL, crate discovery and entrypoint validation, unit tests, CLI wiring, and README updates.

Changes

Service Command Feature

Layer / File(s) Summary
Service CLI surface and arguments
src/commands/service/mod.rs (lines 1–204)
ServiceArgs/ServiceCommands and subcommand argument structs/enums plus the run entrypoint define the CLI surface and parsing types.
Create/Describe/Schema handlers
src/commands/service/mod.rs (lines 213–389)
Implements create, describe, and schema handlers, option validation, harness mode selection, and manifest/schema output handling.
Scaffold generator and templates
src/commands/service/mod.rs (lines 391–1204)
Scaffold writer that emits Cargo.toml, Rust source templates (manifest, service, handlers, models), selects distributed features for transports/stores, and generates optional GitOps/Knative YAML.
Scaffolding models and identifier utilities
src/commands/service/mod.rs (lines 1206–1450)
Model normalization, deduplication, Rust identifier validation/escaping, and Kubernetes-friendly naming helpers used by scaffolding.
Manifest harness and subprocess execution
src/commands/service/mod.rs (lines 1467–1624)
Temporary standalone Cargo workspace/main.rs harness, cached CARGO_TARGET_DIR behavior, and cargo run --manifest-path execution that emits manifest JSON or SQL.
Target resolution and distributed crate discovery
src/commands/service/mod.rs (lines 1625–1777)
Resolves target Cargo.toml and workspace package via cargo metadata; locates local distributed crate via CLI flag, DISTRIBUTED_PATH, or filesystem search.
Entrypoint validation and path utilities
src/commands/service/mod.rs (lines 1779–1896)
Qualifies and validates Rust manifest entrypoints, computes absolute/relative paths, and provides safe string/TOML escaping and case conversions.
Unit tests and validation
src/commands/service/mod.rs (lines 1897–2084)
Unit tests for scaffold name normalization, default read-model registration, handler wiring, GitOps/Knative YAML content, and harness workspace layout.
CLI integration and wiring
src/commands/mod.rs, src/main.rs
Exports new service module, adds Commands::Service(ServiceArgs) variant, and dispatches to commands::service::run() in main.
Documentation updates
README.md (lines 82, 99–100)
Adds hops service --help to Usage examples and a Command Areas bullet describing the service command.

Sequence Diagram

sequenceDiagram
  participant CLI as CLI
  participant Harness as ManifestHarness
  participant Cargo as Cargo(subprocess)
  participant Distributed as LocalDistributedCrate

  CLI->>Harness: prepare harness options (target, entrypoint, mode)
  Harness->>Distributed: resolve local distributed crate path
  Harness->>Cargo: write temp workspace + main.rs and run `cargo run --manifest-path`
  Cargo->>Harness: stdout JSON manifest or SQL schema
  Harness->>CLI: return parsed manifest JSON or written SQL output
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • hops-ops/hops-cli#33: Also extends CLI routing by adding a new Commands variant and wiring a subcommand into main.rs.

Poem

I nibble code and hum a tune,
Scaffolds bloom under the moon,
Manifests printed, schemas spun,
A little rabbit-run has begun. 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: add distributed service scaffold commands' directly summarizes the main change—introducing new scaffold commands for Distributed microservices. It is clear, concise, and specific enough for scanning commit history.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/hops-service-create-microsvc-scaffold

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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/commands/service/mod.rs`:
- Around line 547-567: ScaffoldNames::new must reject inputs that produce
invalid Rust identifiers: after computing package_name, crate_ident
(package_name.replace('-', "_")) and model_struct
(to_pascal_case(&package_name)), call the existing is_rust_ident helper to
validate both crate_ident and model_struct (and view_struct if you want) and
return an Err if any are invalid; update the function (ScaffoldNames::new) to
perform these checks before constructing Self and include a clear error message,
referencing the symbols package_name, crate_ident, model_struct, view_struct,
and the is_rust_ident helper so generated scaffolds won't produce identifiers
starting with digits or otherwise invalid Rust names.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a86c273d-0559-4f8f-8cf1-d000bdbc844c

📥 Commits

Reviewing files that changed from the base of the PR and between 256a0b4 and 6d7ba4e.

📒 Files selected for processing (4)
  • README.md
  • src/commands/mod.rs
  • src/commands/service/mod.rs
  • src/main.rs

Comment on lines +547 to +567
fn new(input: &str) -> Result<Self, Box<dyn Error>> {
let package_name = to_kebab_case(input);
if package_name.is_empty() {
return Err("service name must contain at least one ASCII letter or digit".into());
}
let crate_ident = package_name.replace('-', "_");
let model_struct = to_pascal_case(&package_name);
let view_struct = format!("{model_struct}View");
let command_name = format!("{crate_ident}.create");
let created_event = format!("{crate_ident}.created");
let table_name = format!("{crate_ident}_views");

Ok(Self {
package_name,
crate_ident,
model_struct,
view_struct,
command_name,
created_event,
table_name,
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

What are Rust's identifier rules for crate and type names? Can they start with a digit?

💡 Result:

In Rust, identifiers—which are used for naming items such as types and crates—cannot start with a digit [1][2]. The syntactic rules for identifiers are defined as follows: 1. Valid Start: An identifier must start with either a Unicode XID_Start character or an underscore () [1][2]. 2. Subsequent Characters: Following the initial character, an identifier can consist of any number of Unicode XID_Continue characters [1][2]. 3. Crates: Crate names follow these general identifier rules but have an additional restriction: they must only contain Unicode alphanumeric characters or the underscore () [3][4]. 4. Types: While type names must follow the general identifier rules, the community-standard API guidelines recommend using UpperCamelCase for types [5][6]. Because identifiers must begin with XID_Start or an underscore, they cannot start with a digit [1][2]. This restriction helps maintain clear syntax, as digits at the start of a token are reserved for numeric literals (e.g., integer or floating-point numbers) [7]. If you need an identifier that incorporates a number, you must prefix it with an underscore (e.g., _1_example) [7].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the surrounding code where ScaffoldNames::new constructs crate_ident/model_struct
rg -n "struct ScaffoldNames|fn new\(input: &str\)|to_kebab_case|to_pascal_case|crate_ident|model_struct" src/commands/service/mod.rs
echo "----"
# Print the relevant section (bounded) for context
sed -n '520,610p' src/commands/service/mod.rs

echo "----"
# Find where crate_ident/model_struct are interpolated into generated Rust code
rg -n "crate_ident|model_struct|view_struct|command_name|created_event|table_name" src/commands/service/mod.rs

echo "----"
# Search templates / codegen modules for these identifiers being emitted
rg -n "package_name|crate_ident|model_struct" src | head -n 200

Repository: hops-ops/hops-cli

Length of output: 12439


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Print the to_kebab_case and to_pascal_case implementations and nearby tests
sed -n '900,1040p' src/commands/service/mod.rs

# Search for any existing helper that validates Rust identifiers / paths
rg -n "is_rust_ident|rust_ident|validate_rust_ident|identifier|Rust identifier" src/commands/service/mod.rs src | head -n 200

Repository: hops-ops/hops-cli

Length of output: 3862


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '840,920p' src/commands/service/mod.rs

Repository: hops-ops/hops-cli

Length of output: 2448


Reject service names that produce invalid Rust identifiers in the generated scaffold

ScaffoldNames::new currently turns "3d-checkout" into crate_ident = "3d_checkout" and model_struct = "3dCheckout", and those values are emitted as Rust identifiers (e.g., use crate::models::{model_struct};, pub struct {model_struct} { ... }, and {crate_ident}::service::in_memory()). Rust identifiers can’t start with a digit, so these scaffolds won’t compile. The file already has an is_rust_ident helper (used for validate_rust_path), but ScaffoldNames::new doesn’t use it.

Suggested fix
         let crate_ident = package_name.replace('-', "_");
         let model_struct = to_pascal_case(&package_name);
+        if !is_rust_ident(&crate_ident) || !is_rust_ident(&model_struct) {
+            return Err("service name must produce valid Rust identifiers".into());
+        }
         let view_struct = format!("{model_struct}View");
         let command_name = format!("{crate_ident}.create");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fn new(input: &str) -> Result<Self, Box<dyn Error>> {
let package_name = to_kebab_case(input);
if package_name.is_empty() {
return Err("service name must contain at least one ASCII letter or digit".into());
}
let crate_ident = package_name.replace('-', "_");
let model_struct = to_pascal_case(&package_name);
let view_struct = format!("{model_struct}View");
let command_name = format!("{crate_ident}.create");
let created_event = format!("{crate_ident}.created");
let table_name = format!("{crate_ident}_views");
Ok(Self {
package_name,
crate_ident,
model_struct,
view_struct,
command_name,
created_event,
table_name,
})
fn new(input: &str) -> Result<Self, Box<dyn Error>> {
let package_name = to_kebab_case(input);
if package_name.is_empty() {
return Err("service name must contain at least one ASCII letter or digit".into());
}
let crate_ident = package_name.replace('-', "_");
let model_struct = to_pascal_case(&package_name);
if !is_rust_ident(&crate_ident) || !is_rust_ident(&model_struct) {
return Err("service name must produce valid Rust identifiers".into());
}
let view_struct = format!("{model_struct}View");
let command_name = format!("{crate_ident}.create");
let created_event = format!("{crate_ident}.created");
let table_name = format!("{crate_ident}_views");
Ok(Self {
package_name,
crate_ident,
model_struct,
view_struct,
command_name,
created_event,
table_name,
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/commands/service/mod.rs` around lines 547 - 567, ScaffoldNames::new must
reject inputs that produce invalid Rust identifiers: after computing
package_name, crate_ident (package_name.replace('-', "_")) and model_struct
(to_pascal_case(&package_name)), call the existing is_rust_ident helper to
validate both crate_ident and model_struct (and view_struct if you want) and
return an Err if any are invalid; update the function (ScaffoldNames::new) to
perform these checks before constructing Self and include a clear error message,
referencing the symbols package_name, crate_ident, model_struct, view_struct,
and the is_rust_ident helper so generated scaffolds won't produce identifiers
starting with digits or otherwise invalid Rust names.

@patrickleet patrickleet force-pushed the codex/hops-service-create-microsvc-scaffold branch 5 times, most recently from 5a55335 to de56772 Compare June 3, 2026 03:55
Implements [[tasks/hops-service-create-microsvc-scaffold]].

Updates [[customize-hops-service-scaffold-and-schema-output]].

Updates [[gitops-knative-service-scaffold]].

Updates [[replace-model-booleans-with-repeatable-model]].

Updates [[add-service-bus-flag]].

Updates [[make-service-read-models-opt-in]].

Updates [[rename-service-create-to-scaffold]].

Updates [[add-service-scaffold-github-workflows]].
@patrickleet patrickleet force-pushed the codex/hops-service-create-microsvc-scaffold branch from de56772 to 8bdd30d Compare June 3, 2026 04:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant