Skip to content

Commit b5b8417

Browse files
authored
Merge pull request #13 from cloudengine-labs/copilot/add-setup-instructions
Add --dir option to `devopsos init` command
2 parents 7631a62 + 70a40f5 commit b5b8417

3 files changed

Lines changed: 130 additions & 19 deletions

File tree

.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,13 @@ docs/test-report.html
1717
hugo-docs/public/
1818
hugo-docs/resources/
1919
hugo-docs/.hugo_build.lock
20+
21+
# Scaffold-generated output files (created by `python -m cli.devopsos scaffold *`)
22+
.gitlab-ci.yml
23+
Jenkinsfile
24+
argocd/
25+
sre/
26+
27+
# GHA workflow generated by scaffold (not the repo's own CI workflows)
28+
# Only exclude the scaffold-generated workflow name to avoid masking real CI files
29+
.github/workflows/devops-os-complete.yml

cli/devopsos.py

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import enum
2+
import sys
23
import typer
34
from InquirerPy import inquirer
45
import json
@@ -26,7 +27,9 @@ class ProcessFirstSection(str, enum.Enum):
2627
app = typer.Typer(help="Unified DevOps-OS CLI tool")
2728

2829
@app.command()
29-
def init():
30+
def init(
31+
directory: str = typer.Option(".", "--dir", help="Target directory in which the .devcontainer folder will be created (defaults to the current directory)"),
32+
):
3033
"""Interactive project initializer."""
3134
typer.echo("Welcome to DevOps-OS Init Wizard!")
3235

@@ -72,8 +75,8 @@ def init():
7275
raise typer.Exit(1)
7376

7477
# Write to .devcontainer/devcontainer.env.json
75-
devcontainer_dir = Path(".devcontainer")
76-
devcontainer_dir.mkdir(exist_ok=True)
78+
devcontainer_dir = Path(directory) / ".devcontainer"
79+
devcontainer_dir.mkdir(parents=True, exist_ok=True)
7780
env_json_path = devcontainer_dir / "devcontainer.env.json"
7881
with open(env_json_path, "w") as f:
7982
json.dump(config, f, indent=2)
@@ -144,22 +147,33 @@ def scaffold(
144147
tool: str = typer.Option(None, help="Tool type (e.g., github, jenkins, argo, flux)"),
145148
):
146149
"""Scaffold CI/CD or K8s resources."""
147-
if target == "cicd":
148-
scaffold_cicd.main()
149-
elif target == "gha":
150-
scaffold_gha.main()
151-
elif target == "gitlab":
152-
scaffold_gitlab.main()
153-
elif target == "jenkins":
154-
scaffold_jenkins.main()
155-
elif target == "argocd":
156-
scaffold_argocd.main()
157-
elif target == "sre":
158-
scaffold_sre.main()
159-
elif target == "devcontainer":
160-
scaffold_devcontainer.main()
161-
else:
162-
typer.echo("Unknown scaffold target.")
150+
# Each scaffold module uses argparse internally and calls parse_args() which
151+
# reads sys.argv. When invoked via `python -m cli.devopsos scaffold <target>`
152+
# sys.argv still contains the parent CLI tokens (e.g. ['devopsos.py', 'scaffold',
153+
# 'gha']). Temporarily strip those extra tokens so argparse inside each
154+
# scaffold module only sees the program name and falls back to env-var / default
155+
# values, then restore sys.argv afterwards.
156+
_saved_argv = sys.argv[:]
157+
sys.argv = sys.argv[:1]
158+
try:
159+
if target == "cicd":
160+
scaffold_cicd.main()
161+
elif target == "gha":
162+
scaffold_gha.main()
163+
elif target == "gitlab":
164+
scaffold_gitlab.main()
165+
elif target == "jenkins":
166+
scaffold_jenkins.main()
167+
elif target == "argocd":
168+
scaffold_argocd.main()
169+
elif target == "sre":
170+
scaffold_sre.main()
171+
elif target == "devcontainer":
172+
scaffold_devcontainer.main()
173+
else:
174+
typer.echo("Unknown scaffold target.")
175+
finally:
176+
sys.argv = _saved_argv
163177

164178
@app.command("process-first")
165179
def process_first_cmd(

cli/test_cli.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import subprocess
22
import sys
3+
import re
34
import tempfile
45
import os
56
import yaml
@@ -15,12 +16,59 @@ def _run_module(module, extra_args=None):
1516
args = ["-m", module] + (extra_args or [])
1617
return _run(args)
1718

19+
_ANSI_ESCAPE = re.compile(r'\x1b\[[0-9;]*[mK]')
20+
21+
def _strip_ansi(s):
22+
"""Remove ANSI escape codes so plain-text assertions work regardless of terminal colour."""
23+
return _ANSI_ESCAPE.sub('', s)
24+
1825
# -- devopsos CLI ----------------------------------------------------------
1926

2027
def test_help():
2128
result = _run(["-m", "cli.devopsos", "--help"])
2229
assert "Unified DevOps-OS CLI tool" in result.stdout
2330

31+
def test_init_help_shows_dir_option():
32+
"""--dir option must appear in `devopsos init --help`."""
33+
result = subprocess.run(
34+
[sys.executable, "-m", "cli.devopsos", "init", "--help"],
35+
capture_output=True, text=True,
36+
cwd=os.path.dirname(os.path.dirname(__file__)),
37+
env={**os.environ, "NO_COLOR": "1"},
38+
)
39+
assert result.returncode == 0
40+
assert "--dir" in _strip_ansi(result.stdout)
41+
42+
def test_init_dir_option_creates_devcontainer_in_specified_dir():
43+
"""--dir places .devcontainer inside the given directory, not the cwd."""
44+
from unittest.mock import MagicMock, patch
45+
from typer.testing import CliRunner
46+
from cli.devopsos import app
47+
48+
checkbox_mock = MagicMock()
49+
checkbox_mock.execute.return_value = [] # nothing selected
50+
51+
# First confirm → Proceed = True
52+
# Second confirm → Generate Dockerfile = False (skip, we only need env.json)
53+
confirm_proceed = MagicMock()
54+
confirm_proceed.execute.return_value = True
55+
confirm_skip = MagicMock()
56+
confirm_skip.execute.return_value = False
57+
58+
with tempfile.TemporaryDirectory() as tmp:
59+
with patch("cli.devopsos.inquirer.checkbox", return_value=checkbox_mock), \
60+
patch("cli.devopsos.inquirer.confirm",
61+
side_effect=[confirm_proceed, confirm_skip]):
62+
runner = CliRunner()
63+
result = runner.invoke(app, ["init", "--dir", tmp])
64+
65+
assert result.exit_code == 0, result.output
66+
dc_dir = Path(tmp) / ".devcontainer"
67+
assert dc_dir.is_dir(), "Expected .devcontainer dir inside --dir target"
68+
assert (dc_dir / "devcontainer.env.json").exists(), (
69+
"Expected devcontainer.env.json inside .devcontainer"
70+
)
71+
2472
def test_scaffold_unknown():
2573
result = _run(["-m", "cli.devopsos", "scaffold", "unknown"])
2674
assert "Unknown scaffold target" in result.stdout
@@ -32,6 +80,45 @@ def test_scaffold_help_lists_new_targets():
3280
assert "argocd" in result.stdout
3381
assert "sre" in result.stdout
3482

83+
def test_scaffold_gha_via_cli():
84+
"""Regression: `python -m cli.devopsos scaffold gha` must not raise argparse error."""
85+
with tempfile.TemporaryDirectory() as tmp:
86+
env = {**os.environ, "DEVOPS_OS_GHA_OUTPUT": os.path.join(tmp, ".github/workflows")}
87+
result = subprocess.run(
88+
[sys.executable, "-m", "cli.devopsos", "scaffold", "gha"],
89+
capture_output=True, text=True,
90+
cwd=os.path.dirname(os.path.dirname(__file__)),
91+
env=env,
92+
)
93+
assert result.returncode == 0, result.stderr
94+
assert "error: unrecognized arguments" not in result.stderr
95+
96+
def test_scaffold_gitlab_via_cli():
97+
"""Regression: `python -m cli.devopsos scaffold gitlab` must not raise argparse error."""
98+
with tempfile.TemporaryDirectory() as tmp:
99+
env = {**os.environ, "DEVOPS_OS_GITLAB_OUTPUT": os.path.join(tmp, ".gitlab-ci.yml")}
100+
result = subprocess.run(
101+
[sys.executable, "-m", "cli.devopsos", "scaffold", "gitlab"],
102+
capture_output=True, text=True,
103+
cwd=os.path.dirname(os.path.dirname(__file__)),
104+
env=env,
105+
)
106+
assert result.returncode == 0, result.stderr
107+
assert "error: unrecognized arguments" not in result.stderr
108+
109+
def test_scaffold_argocd_via_cli():
110+
"""Regression: `python -m cli.devopsos scaffold argocd` must not raise argparse error."""
111+
with tempfile.TemporaryDirectory() as tmp:
112+
env = {**os.environ, "DEVOPS_OS_ARGOCD_OUTPUT_DIR": tmp}
113+
result = subprocess.run(
114+
[sys.executable, "-m", "cli.devopsos", "scaffold", "argocd"],
115+
capture_output=True, text=True,
116+
cwd=os.path.dirname(os.path.dirname(__file__)),
117+
env=env,
118+
)
119+
assert result.returncode == 0, result.stderr
120+
assert "error: unrecognized arguments" not in result.stderr
121+
35122
# -- GitLab CI generator ---------------------------------------------------
36123

37124
def test_scaffold_gitlab_build():

0 commit comments

Comments
 (0)