Skip to content

Commit 7c84d62

Browse files
committed
feat: update CLI to manage all RHDH repos dynamically
Generalize status, doctor, and config commands to iterate over all repos from SUBMODULE_REPOS instead of hardcoding overlay/local/factory checks. Update SKILL.md config examples to show all available repo keys. Add .rhdh/ to .gitignore for the new config directory.
1 parent b384cbd commit 7c84d62

5 files changed

Lines changed: 119 additions & 179 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,5 @@ Thumbs.db
3131
.python-version
3232

3333
# Local config (project-specific settings)
34+
.rhdh/
3435
.rhdh-plugin/

skills/rhdh/SKILL.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -165,10 +165,17 @@ $RHDH doctor
165165
**Configuration:**
166166

167167
```bash
168-
$RHDH config init # Create config with auto-detection
169-
$RHDH config show # Show resolved paths
170-
$RHDH config set overlay /path # Set repo location
171-
$RHDH config set local /path # Set rhdh-local location
168+
$RHDH config init # Create config with auto-detection
169+
$RHDH config show # Show resolved paths
170+
$RHDH config set overlay /path # Set rhdh-plugin-export-overlays location
171+
$RHDH config set local /path # Set rhdh-local location
172+
$RHDH config set rhdh /path # Set main rhdh repo location
173+
$RHDH config set downstream /path # Set rhdh-downstream location
174+
$RHDH config set cli /path # Set rhdh-cli location
175+
$RHDH config set plugins /path # Set rhdh-plugins location
176+
$RHDH config set operator /path # Set rhdh-operator location
177+
$RHDH config set chart /path # Set rhdh-chart location
178+
$RHDH config set catalog /path # Set rhdh-plugin-catalog location
172179
```
173180

174181
**Workspace operations:**

skills/rhdh/rhdh/cli.py

Lines changed: 83 additions & 150 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,9 @@
1919
from .config import (
2020
SUBMODULE_REPOS,
2121
get_data_dir,
22-
get_factory_repo,
2322
get_github_username,
24-
get_local_repo,
2523
get_overlay_repo,
24+
get_repo,
2625
list_submodule_repos,
2726
save_github_username,
2827
setup_submodule,
@@ -97,89 +96,45 @@ def cmd_status(fmt: OutputFormatter, _args: argparse.Namespace) -> int:
9796
next_steps: list[str] = []
9897
needs_setup = False
9998

100-
# Check overlay repo
101-
overlay_repo = get_overlay_repo()
102-
if overlay_repo:
103-
branch = get_git_branch(overlay_repo)
104-
status = "uncommitted" if has_uncommitted_changes(overlay_repo) else "clean"
105-
checks.append(
106-
{
107-
"name": "overlay_repo",
108-
"status": "pass",
109-
"message": f"{overlay_repo} ({branch}, {status})",
110-
"path": str(overlay_repo),
111-
"branch": branch,
112-
"clean": status == "clean",
113-
}
114-
)
115-
fmt.log_ok(f"overlay repo: {overlay_repo} ({branch}, {status})")
116-
else:
117-
checks.append(
118-
{
119-
"name": "overlay_repo",
120-
"status": "fail",
121-
"message": "not found",
122-
}
123-
)
124-
fmt.log_fail("overlay repo: not found")
125-
needs_setup = True
126-
127-
# Check rhdh-local
128-
local_repo = get_local_repo()
129-
if local_repo:
130-
# Check if running (podman)
131-
running = False
132-
if check_tool("podman"):
133-
rc, stdout, _ = run_command(["podman", "ps", "--format", "{{.Names}}"])
134-
running = rc == 0 and "rhdh" in stdout
135-
136-
status_msg = "running" if running else "not running"
137-
check_status = "pass" if running else "warn"
138-
checks.append(
139-
{
140-
"name": "rhdh_local",
141-
"status": check_status,
142-
"message": f"{local_repo} ({status_msg})",
143-
"path": str(local_repo),
144-
"running": running,
145-
}
146-
)
147-
if running:
148-
fmt.log_ok(f"rhdh-local: {local_repo} (running)")
149-
else:
150-
fmt.log_warn(f"rhdh-local: {local_repo} (not running)")
151-
else:
152-
checks.append(
153-
{
154-
"name": "rhdh_local",
155-
"status": "fail",
156-
"message": "not found",
157-
}
158-
)
159-
fmt.log_fail("rhdh-local: not found")
160-
needs_setup = True
99+
# Check all configured repos
100+
for repo_name, info in SUBMODULE_REPOS.items():
101+
config_key = info["config_key"]
102+
required = info["required"]
103+
repo_path = get_repo(config_key)
161104

162-
# Check factory (optional)
163-
factory_repo = get_factory_repo()
164-
if factory_repo:
165-
checks.append(
166-
{
167-
"name": "factory",
168-
"status": "pass",
169-
"message": str(factory_repo),
170-
"path": str(factory_repo),
171-
}
172-
)
173-
fmt.log_ok(f"factory: {factory_repo}")
174-
else:
175-
checks.append(
176-
{
177-
"name": "factory",
178-
"status": "info",
179-
"message": "not configured (optional)",
180-
}
181-
)
182-
fmt.log_info("factory: not configured (optional)")
105+
if repo_path:
106+
branch = get_git_branch(repo_path)
107+
status = "uncommitted" if has_uncommitted_changes(repo_path) else "clean"
108+
checks.append(
109+
{
110+
"name": config_key,
111+
"status": "pass",
112+
"message": f"{repo_path} ({branch}, {status})",
113+
"path": str(repo_path),
114+
"branch": branch,
115+
"clean": status == "clean",
116+
}
117+
)
118+
fmt.log_ok(f"{config_key}: {repo_path} ({branch}, {status})")
119+
elif required:
120+
checks.append(
121+
{
122+
"name": config_key,
123+
"status": "fail",
124+
"message": "not found",
125+
}
126+
)
127+
fmt.log_fail(f"{config_key}: not found")
128+
needs_setup = True
129+
else:
130+
checks.append(
131+
{
132+
"name": config_key,
133+
"status": "info",
134+
"message": "not configured (optional)",
135+
}
136+
)
137+
fmt.log_info(f"{config_key}: not configured (optional)")
183138

184139
# Check tools
185140
fmt.header("Tools")
@@ -279,56 +234,38 @@ def cmd_doctor(fmt: OutputFormatter, _args: argparse.Namespace) -> int:
279234
checks: list[dict[str, Any]] = []
280235
issues: list[str] = []
281236

282-
# Check repos
283-
overlay_repo = get_overlay_repo()
284-
if overlay_repo:
285-
checks.append({"name": "overlay_repo", "status": "pass", "message": str(overlay_repo)})
286-
fmt.log_ok(f"Overlay repo found: {overlay_repo}")
237+
# Check all repos
238+
for repo_name, info in SUBMODULE_REPOS.items():
239+
config_key = info["config_key"]
240+
required = info["required"]
241+
repo_path = get_repo(config_key)
287242

288-
# Check it's a git repo
289-
rc, _, _ = run_command(["git", "rev-parse", "--git-dir"], cwd=overlay_repo)
290-
if rc == 0:
291-
checks.append({"name": "overlay_git", "status": "pass", "message": "valid"})
292-
fmt.log_ok(" Git repository valid")
293-
else:
294-
checks.append({"name": "overlay_git", "status": "fail", "message": "invalid"})
295-
fmt.log_fail(" Not a valid git repository")
296-
issues.append("Overlay repo is not a git repository")
243+
if repo_path:
244+
checks.append({"name": config_key, "status": "pass", "message": str(repo_path)})
245+
fmt.log_ok(f"{config_key} found: {repo_path}")
297246

298-
# Check remote
299-
rc, stdout, _ = run_command(["git", "remote", "get-url", "origin"], cwd=overlay_repo)
300-
if rc == 0:
301-
remote = stdout.strip()
302-
if "rhdh-plugin-export-overlays" in remote:
303-
checks.append({"name": "overlay_remote", "status": "pass", "message": remote})
304-
fmt.log_ok(f" Remote: {remote}")
247+
# Check it's a git repo
248+
rc, _, _ = run_command(["git", "rev-parse", "--git-dir"], cwd=repo_path)
249+
if rc == 0:
250+
checks.append(
251+
{"name": f"{config_key}_git", "status": "pass", "message": "valid"}
252+
)
253+
fmt.log_ok(" Git repository valid")
305254
else:
306-
checks.append({"name": "overlay_remote", "status": "warn", "message": remote})
307-
fmt.log_warn(f" Remote may be incorrect: {remote}")
308-
else:
309-
checks.append({"name": "overlay_repo", "status": "fail", "message": "not found"})
310-
fmt.log_fail("Overlay repo not found")
311-
issues.append("Configure overlay repo: rhdh config set overlay /path/to/repo")
312-
313-
local_repo = get_local_repo()
314-
if local_repo:
315-
checks.append({"name": "rhdh_local", "status": "pass", "message": str(local_repo)})
316-
fmt.log_ok(f"rhdh-local found: {local_repo}")
317-
318-
# Check compose file exists
319-
has_compose = (local_repo / "compose.yaml").exists() or (
320-
local_repo / "docker-compose.yaml"
321-
).exists()
322-
if has_compose:
323-
checks.append({"name": "compose_file", "status": "pass", "message": "found"})
324-
fmt.log_ok(" Compose file found")
255+
checks.append(
256+
{"name": f"{config_key}_git", "status": "fail", "message": "invalid"}
257+
)
258+
fmt.log_fail(" Not a valid git repository")
259+
issues.append(f"{config_key} is not a git repository")
260+
elif required:
261+
checks.append({"name": config_key, "status": "fail", "message": "not found"})
262+
fmt.log_fail(f"{config_key} not found")
263+
issues.append(f"Configure {config_key}: rhdh config set {config_key} /path/to/repo")
325264
else:
326-
checks.append({"name": "compose_file", "status": "warn", "message": "not found"})
327-
fmt.log_warn(" No compose file found")
328-
else:
329-
checks.append({"name": "rhdh_local", "status": "fail", "message": "not found"})
330-
fmt.log_fail("rhdh-local not found")
331-
issues.append("Configure rhdh-local: rhdh config set local /path/to/repo")
265+
checks.append(
266+
{"name": config_key, "status": "info", "message": "not configured (optional)"}
267+
)
268+
fmt.log_info(f"{config_key}: not configured (optional)")
332269

333270
fmt.header("GitHub CLI")
334271

@@ -479,14 +416,14 @@ def cmd_config_init(fmt: OutputFormatter, args: argparse.Namespace) -> int:
479416
fmt.log_ok(f"Created: {data.get('created', '')}")
480417
config = data.get("config", {})
481418
repos = config.get("repos", {})
482-
if repos.get("overlay"):
483-
fmt.log_ok(f"Auto-detected overlay: {repos['overlay']}")
484-
else:
485-
fmt.log_info("overlay: not found (configure with: rhdh config set repos.overlay /path)")
486-
if repos.get("local"):
487-
fmt.log_ok(f"Auto-detected local: {repos['local']}")
488-
else:
489-
fmt.log_info("local: not found (configure with: rhdh config set repos.local /path)")
419+
for info in SUBMODULE_REPOS.values():
420+
key = info["config_key"]
421+
if repos.get(key):
422+
fmt.log_ok(f"Auto-detected {key}: {repos[key]}")
423+
elif info["required"]:
424+
fmt.log_info(
425+
f"{key}: not found (configure with: rhdh config set {key} /path)"
426+
)
490427
fmt.success(data, next_steps=next_steps)
491428
return 0
492429
else:
@@ -511,18 +448,14 @@ def cmd_config_show(fmt: OutputFormatter, args: argparse.Namespace) -> int:
511448

512449
fmt.header("Resolved Paths")
513450
resolved = data.get("resolved", {})
514-
if resolved.get("overlay"):
515-
fmt.log_ok(f"overlay: {resolved['overlay']}")
516-
else:
517-
fmt.log_fail("overlay: not found")
518-
if resolved.get("local"):
519-
fmt.log_ok(f"local: {resolved['local']}")
520-
else:
521-
fmt.log_fail("local: not found")
522-
if resolved.get("factory"):
523-
fmt.log_ok(f"factory: {resolved['factory']}")
524-
else:
525-
fmt.log_info("factory: not configured")
451+
for info in SUBMODULE_REPOS.values():
452+
key = info["config_key"]
453+
if resolved.get(key):
454+
fmt.log_ok(f"{key}: {resolved[key]}")
455+
elif info["required"]:
456+
fmt.log_fail(f"{key}: not found")
457+
else:
458+
fmt.log_info(f"{key}: not configured")
526459

527460
fmt.success(data, next_steps=next_steps)
528461
return 0

tests/conftest.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -224,12 +224,12 @@ def unconfigured_cli(isolated_env, monkeypatch):
224224
"""Fixture providing CLI with no repos configured.
225225
226226
Use this to test the "needs setup" state without manual monkeypatching.
227-
Mocks get_overlay_repo and get_local_repo to return None.
227+
Mocks get_repo to return None for all repos.
228228
"""
229229
from rhdh import cli as cli_module
230230

231231
monkeypatch.setattr(cli_module, "get_overlay_repo", lambda: None)
232-
monkeypatch.setattr(cli_module, "get_local_repo", lambda: None)
232+
monkeypatch.setattr(cli_module, "get_repo", lambda key: None)
233233

234234
def _run_cli(*args, env=None):
235235
full_env = {}

tests/e2e/test_cli.py

Lines changed: 22 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -56,21 +56,15 @@ def test_status_shows_next_steps(self, cli, isolated_env):
5656
assert response is not None
5757
assert "next_steps" in response
5858

59-
def test_status_needs_setup_points_to_doctor(self, unconfigured_cli):
60-
"""When needs_setup is true, status should point to doctor command.
61-
62-
Doctor command has all the setup guidance - status just redirects there.
63-
"""
59+
def test_status_unconfigured_is_valid(self, unconfigured_cli):
60+
"""When no repos are configured, needs_setup should be false (all repos are optional)."""
6461
result = unconfigured_cli()
6562

6663
response = parse_response(result)
6764
assert response is not None
68-
assert response["data"]["needs_setup"] is True
69-
70-
# next_steps should include doctor command
71-
assert "rhdh doctor" in response["next_steps"]
65+
assert response["data"]["needs_setup"] is False
7266

73-
# Should NOT include setup_options or doctor_workflow (that's doctor's job)
67+
# Should NOT include setup_options or doctor_workflow
7468
assert "setup_options" not in response["data"]
7569
assert "doctor_workflow" not in response["data"]
7670

@@ -106,26 +100,31 @@ def test_doctor_returns_all_passed(self, cli, isolated_env):
106100
assert response is not None
107101
assert "all_passed" in response["data"]
108102

109-
def test_doctor_points_to_workflow_when_issues_found(self, unconfigured_cli):
110-
"""When doctor finds issues, it should point to the workflow file.
103+
def test_doctor_checks_all_repos(self, unconfigured_cli):
104+
"""Doctor should include a check for every configured repository."""
105+
from rhdh.config import SUBMODULE_REPOS
111106

112-
This enables agents to read the workflow for remediation steps.
113-
"""
114107
result = unconfigured_cli("doctor")
115-
116108
response = parse_response(result)
117109
assert response is not None
118-
assert response["data"]["all_passed"] is False
119110

120-
# Should include workflow path for agentic discovery
121-
assert "workflow" in response["data"]
122-
assert response["data"]["workflow"] == "workflows/doctor.md"
111+
check_names = [c["name"] for c in response["data"]["checks"]]
112+
for info in SUBMODULE_REPOS.values():
113+
config_key = info["config_key"]
114+
assert config_key in check_names, (
115+
f"Doctor missing check for repo '{config_key}'"
116+
)
117+
118+
def test_doctor_unconfigured_repos_no_issues(self, unconfigured_cli):
119+
"""When no repos are configured, doctor should not report repo issues (all optional)."""
120+
result = unconfigured_cli("doctor")
123121

124-
# Should include instruction for agent
125-
assert "agent_instruction" in response["data"]
122+
response = parse_response(result)
123+
assert response is not None
126124

127-
# next_steps should point to reading the workflow
128-
assert any("workflow" in step.lower() for step in response["next_steps"])
125+
# No repo-related issues should appear (all repos are optional)
126+
repo_issues = [i for i in response["data"]["issues"] if "config set" in i]
127+
assert repo_issues == [], f"Unexpected repo issues: {repo_issues}"
129128

130129

131130
class TestCliConfig:

0 commit comments

Comments
 (0)