Skip to content

Commit d8f9441

Browse files
Copilotchefgs
andcommitted
fix: forward CLI args through devopsos scaffold to underlying modules; clarify devopsos vs scaffold_* distinction
Co-authored-by: chefgs <7605658+chefgs@users.noreply.github.com>
1 parent ccbb8e2 commit d8f9441

2 files changed

Lines changed: 134 additions & 10 deletions

File tree

cli/devopsos.py

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -241,20 +241,59 @@ def _sel(group): return selected_by_group.get(group, [])
241241
# Optionally, update Dockerfile (not strictly needed if it uses build args)
242242
typer.echo("Dockerfile uses build args; ensure it references the correct ARGs.")
243243

244-
@app.command()
244+
@app.command(context_settings={"allow_extra_args": True, "ignore_unknown_options": True})
245245
def scaffold(
246+
ctx: typer.Context,
246247
target: str = typer.Argument(..., help="What to scaffold: cicd | gha | gitlab | jenkins | argocd | sre | devcontainer"),
247-
tool: str = typer.Option(None, help="Tool type (e.g., github, jenkins, argo, flux)"),
248248
):
249-
"""Scaffold CI/CD or K8s resources."""
250-
# Each scaffold module uses argparse internally and calls parse_args() which
251-
# reads sys.argv. When invoked via `python -m cli.devopsos scaffold <target>`
252-
# sys.argv still contains the parent CLI tokens (e.g. ['devopsos.py', 'scaffold',
253-
# 'gha']). Temporarily strip those extra tokens so argparse inside each
254-
# scaffold module only sees the program name and falls back to env-var / default
255-
# values, then restore sys.argv afterwards.
249+
"""Scaffold CI/CD or K8s resources.
250+
251+
\b
252+
┌─ Two ways to invoke scaffold commands ──────────────────────────────────┐
253+
│ │
254+
│ 1. Via the unified CLI (this command): │
255+
│ python -m cli.devopsos scaffold <TARGET> [OPTIONS] │
256+
│ │
257+
│ 2. Directly as a standalone module: │
258+
│ python -m cli.scaffold_<TARGET> [OPTIONS] │
259+
│ │
260+
│ Both forms are equivalent. Any OPTIONS you pass after TARGET are │
261+
│ forwarded verbatim to the underlying scaffold module. │
262+
└─────────────────────────────────────────────────────────────────────────┘
263+
264+
\b
265+
DIFFERENCE BETWEEN cli.devopsos AND cli.scaffold_*
266+
cli.devopsos — unified entry point; provides init, scaffold, and
267+
process-first commands under one roof. Use this
268+
when you want a single consistent CLI.
269+
270+
cli.scaffold_* — standalone, tool-specific generators (e.g.
271+
cli.scaffold_jenkins, cli.scaffold_argocd). Each
272+
module can be run on its own via `python -m` and
273+
supports the full set of argparse options for that
274+
tool. They are also the engine behind `devopsos
275+
scaffold <target>`.
276+
277+
\b
278+
EXAMPLES
279+
# Unified CLI — args forwarded to the Jenkins generator:
280+
python -m cli.devopsos scaffold jenkins --type complete --languages python
281+
282+
# Direct module invocation — identical result:
283+
python -m cli.scaffold_jenkins --type complete --languages python
284+
285+
# See all options available for a specific scaffold target:
286+
python -m cli.scaffold_jenkins --help
287+
python -m cli.scaffold_gha --help
288+
python -m cli.scaffold_argocd --help
289+
"""
290+
# Each scaffold module uses argparse internally and reads sys.argv.
291+
# We temporarily replace sys.argv so argparse sees only the program name
292+
# plus any extra arguments the user passed after the TARGET token.
293+
# This lets `devopsos scaffold jenkins --type complete` work identically
294+
# to `python -m cli.scaffold_jenkins --type complete`.
256295
_saved_argv = sys.argv[:]
257-
sys.argv = sys.argv[:1]
296+
sys.argv = sys.argv[:1] + ctx.args
258297
try:
259298
if target == "cicd":
260299
scaffold_cicd.main()

cli/test_cli.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -570,3 +570,88 @@ def test_process_first_specific_section_no_usage_footer():
570570
f"--section {section} should not show the usage footer"
571571
)
572572

573+
574+
# -- scaffold arg pass-through (cli.devopsos vs cli.scaffold_*) ------------
575+
576+
def test_scaffold_help_explains_dual_invocation():
577+
"""`devopsos scaffold --help` documents both invocation forms and the difference."""
578+
result = _run(["-m", "cli.devopsos", "scaffold", "--help"])
579+
assert result.returncode == 0
580+
text = _strip_ansi(result.stdout)
581+
assert "cli.devopsos" in text, "help should mention cli.devopsos"
582+
assert "cli.scaffold_" in text, "help should mention cli.scaffold_* modules"
583+
assert "forwarded" in text.lower() or "forward" in text.lower(), (
584+
"help should mention that extra args are forwarded"
585+
)
586+
587+
588+
def test_scaffold_jenkins_args_forwarded_via_cli():
589+
"""`devopsos scaffold jenkins --type build --languages python` forwards args to scaffold_jenkins."""
590+
with tempfile.TemporaryDirectory() as tmp:
591+
out = os.path.join(tmp, "Jenkinsfile")
592+
result = subprocess.run(
593+
[
594+
sys.executable, "-m", "cli.devopsos",
595+
"scaffold", "jenkins",
596+
"--type", "build",
597+
"--languages", "python",
598+
"--output", out,
599+
],
600+
capture_output=True, text=True,
601+
cwd=os.path.dirname(os.path.dirname(__file__)),
602+
)
603+
assert result.returncode == 0, result.stderr
604+
assert "error: unrecognized arguments" not in result.stderr
605+
assert os.path.isfile(out), "Jenkinsfile should have been created"
606+
content = Path(out).read_text()
607+
assert "pipeline" in content.lower() or "node" in content.lower(), (
608+
"Jenkinsfile should contain pipeline content"
609+
)
610+
611+
612+
def test_scaffold_gha_args_forwarded_via_cli():
613+
"""`devopsos scaffold gha --type build --name my-app` forwards args to scaffold_gha."""
614+
with tempfile.TemporaryDirectory() as tmp:
615+
out_dir = os.path.join(tmp, ".github", "workflows")
616+
result = subprocess.run(
617+
[
618+
sys.executable, "-m", "cli.devopsos",
619+
"scaffold", "gha",
620+
"--type", "build",
621+
"--name", "my-app",
622+
"--output", out_dir,
623+
],
624+
capture_output=True, text=True,
625+
cwd=os.path.dirname(os.path.dirname(__file__)),
626+
)
627+
assert result.returncode == 0, result.stderr
628+
assert "error: unrecognized arguments" not in result.stderr
629+
wf_files = list(Path(out_dir).glob("*.yml"))
630+
assert wf_files, "At least one workflow YAML should have been created"
631+
632+
633+
def test_scaffold_via_cli_matches_direct_module_output():
634+
"""Output from `devopsos scaffold gitlab` equals `python -m cli.scaffold_gitlab` (same defaults)."""
635+
with tempfile.TemporaryDirectory() as tmp1, tempfile.TemporaryDirectory() as tmp2:
636+
out1 = os.path.join(tmp1, ".gitlab-ci.yml")
637+
out2 = os.path.join(tmp2, ".gitlab-ci.yml")
638+
639+
base_args = ["--name", "test-app", "--type", "build"]
640+
641+
result_unified = subprocess.run(
642+
[sys.executable, "-m", "cli.devopsos", "scaffold", "gitlab"] + base_args + ["--output", out1],
643+
capture_output=True, text=True,
644+
cwd=os.path.dirname(os.path.dirname(__file__)),
645+
)
646+
result_direct = subprocess.run(
647+
[sys.executable, "-m", "cli.scaffold_gitlab"] + base_args + ["--output", out2],
648+
capture_output=True, text=True,
649+
cwd=os.path.dirname(os.path.dirname(__file__)),
650+
)
651+
652+
assert result_unified.returncode == 0, result_unified.stderr
653+
assert result_direct.returncode == 0, result_direct.stderr
654+
assert Path(out1).read_text() == Path(out2).read_text(), (
655+
"Unified CLI and direct module should produce identical output"
656+
)
657+

0 commit comments

Comments
 (0)