From 80bc18925f952069f599a3be3348dd4c4437df58 Mon Sep 17 00:00:00 2001 From: Yashchoure Date: Wed, 23 Sep 2026 16:28:11 +0530 Subject: [PATCH 1/8] feat: add JSON output to scaffold commands --- README.md | 16 ++++++++++++++ cli/devopsos.py | 39 +++++++++++++++++++++++---------- cli/test_cli.py | 58 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 102 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 96c967b..ebddd18 100644 --- a/README.md +++ b/README.md @@ -156,8 +156,24 @@ python -m cli.devopsos scaffold gha --name my-app --languages python --type comp # With Kubernetes deployment via Kustomize python -m cli.devopsos scaffold gha --name my-app --languages python --kubernetes --k8s-method kustomize + +# Return a machine-readable result for automation pipelines +python -m cli.devopsos scaffold gha --name my-app --json ``` +With `--json`, the command keeps the generated files unchanged and writes only +the result object to standard output, for example: + +```json +{ + "workflow": ".github/workflows/my-app-complete.yml", + "type": "github_actions" +} +``` + +The flag is available on every `scaffold` target. Multi-file generators return +the generated paths in a `files` array. + --- ### 4 — Generate other pipelines & configs diff --git a/cli/devopsos.py b/cli/devopsos.py index c6909be..23a3a3e 100644 --- a/cli/devopsos.py +++ b/cli/devopsos.py @@ -1,4 +1,5 @@ import enum +import io import sys import typer from InquirerPy import inquirer @@ -6,6 +7,7 @@ import os from pathlib import Path from typing import Optional +from contextlib import redirect_stdout # Import scaffold modules — used as libraries by the unified scaffold sub-commands import cli.scaffold_cicd as scaffold_cicd @@ -90,7 +92,7 @@ def main( app.add_typer(scaffold_app, name="scaffold") -def _run_scaffold(module_main, flags: list): +def _run_scaffold(module_main, flags: list, json_output: bool = False): """Call *module_main()* with the given CLI flag list via sys.argv. Each scaffold module uses argparse internally. We temporarily replace @@ -100,9 +102,15 @@ def _run_scaffold(module_main, flags: list): _saved = sys.argv[:] sys.argv = sys.argv[:1] + flags try: - module_main() + if json_output: + with redirect_stdout(io.StringIO()): + result = module_main() + typer.echo(json.dumps(result, indent=2)) + else: + result = module_main() finally: sys.argv = _saved + return result def _show_help_if_no_opts(ctx: typer.Context) -> None: @@ -123,6 +131,7 @@ def _show_help_if_no_opts(ctx: typer.Context) -> None: @scaffold_app.command("gha") def scaffold_gha_cmd( ctx: typer.Context, + json_output: bool = typer.Option(False, "--json", help="Print the generated result as JSON"), name: str = typer.Option("DevOps-OS", envvar="DEVOPS_OS_GHA_NAME", help="Workflow name"), workflow_type: str = typer.Option("complete", "--type", envvar="DEVOPS_OS_GHA_TYPE", @@ -181,7 +190,7 @@ def scaffold_gha_cmd( flags += ["--custom-values", custom_values] if env_file: flags += ["--env-file", env_file] - _run_scaffold(scaffold_gha.main, flags) + _run_scaffold(scaffold_gha.main, flags, json_output) # ── scaffold jenkins ──────────────────────────────────────────────────────── @@ -189,6 +198,7 @@ def scaffold_gha_cmd( @scaffold_app.command("jenkins") def scaffold_jenkins_cmd( ctx: typer.Context, + json_output: bool = typer.Option(False, "--json", help="Print the generated result as JSON"), name: str = typer.Option("DevOps-OS", envvar="DEVOPS_OS_JENKINS_NAME", help="Pipeline name"), pipeline_type: str = typer.Option("complete", "--type", envvar="DEVOPS_OS_JENKINS_TYPE", @@ -242,7 +252,7 @@ def scaffold_jenkins_cmd( flags += ["--custom-values", custom_values] if env_file: flags += ["--env-file", env_file] - _run_scaffold(scaffold_jenkins.main, flags) + _run_scaffold(scaffold_jenkins.main, flags, json_output) # ── scaffold gitlab ───────────────────────────────────────────────────────── @@ -250,6 +260,7 @@ def scaffold_jenkins_cmd( @scaffold_app.command("gitlab") def scaffold_gitlab_cmd( ctx: typer.Context, + json_output: bool = typer.Option(False, "--json", help="Print the generated result as JSON"), name: str = typer.Option("my-app", envvar="DEVOPS_OS_GITLAB_NAME", help="Application / pipeline name"), pipeline_type: str = typer.Option("complete", "--type", envvar="DEVOPS_OS_GITLAB_TYPE", @@ -295,7 +306,7 @@ def scaffold_gitlab_cmd( flags += ["--kube-namespace", kube_namespace] if custom_values: flags += ["--custom-values", custom_values] - _run_scaffold(scaffold_gitlab.main, flags) + _run_scaffold(scaffold_gitlab.main, flags, json_output) # ── scaffold argocd ───────────────────────────────────────────────────────── @@ -303,6 +314,7 @@ def scaffold_gitlab_cmd( @scaffold_app.command("argocd") def scaffold_argocd_cmd( ctx: typer.Context, + json_output: bool = typer.Option(False, "--json", help="Print the generated result as JSON"), name: str = typer.Option("my-app", envvar="DEVOPS_OS_ARGOCD_NAME", help="Application name"), method: str = typer.Option("argocd", envvar="DEVOPS_OS_ARGOCD_METHOD", @@ -358,7 +370,7 @@ def scaffold_argocd_cmd( flags.append("--rollouts") if allow_any_source_repo: flags.append("--allow-any-source-repo") - _run_scaffold(scaffold_argocd.main, flags) + _run_scaffold(scaffold_argocd.main, flags, json_output) # ── scaffold sre ──────────────────────────────────────────────────────────── @@ -366,6 +378,7 @@ def scaffold_argocd_cmd( @scaffold_app.command("sre") def scaffold_sre_cmd( ctx: typer.Context, + json_output: bool = typer.Option(False, "--json", help="Print the generated result as JSON"), name: str = typer.Option("my-app", envvar="DEVOPS_OS_SRE_NAME", help="Application / service name"), team: str = typer.Option("platform", envvar="DEVOPS_OS_SRE_TEAM", @@ -414,7 +427,7 @@ def scaffold_sre_cmd( ] if pagerduty_key: flags += ["--pagerduty-key", pagerduty_key] - _run_scaffold(scaffold_sre.main, flags) + _run_scaffold(scaffold_sre.main, flags, json_output) # ── scaffold devcontainer ─────────────────────────────────────────────────── @@ -422,6 +435,7 @@ def scaffold_sre_cmd( @scaffold_app.command("devcontainer") def scaffold_devcontainer_cmd( ctx: typer.Context, + json_output: bool = typer.Option(False, "--json", help="Print the generated result as JSON"), languages: str = typer.Option("python", envvar="DEVOPS_OS_DEVCONTAINER_LANGUAGES", help="Comma-separated languages to enable (default: python)"), cicd_tools: str = typer.Option("docker,github_actions", "--cicd-tools", @@ -506,7 +520,7 @@ def scaffold_devcontainer_cmd( "--grafana-version", grafana_version, "--output-dir", output_dir, ] - _run_scaffold(scaffold_devcontainer.main, flags) + _run_scaffold(scaffold_devcontainer.main, flags, json_output) # ── scaffold cicd ─────────────────────────────────────────────────────────── @@ -514,6 +528,7 @@ def scaffold_devcontainer_cmd( @scaffold_app.command("cicd") def scaffold_cicd_cmd( ctx: typer.Context, + json_output: bool = typer.Option(False, "--json", help="Print the generated result as JSON"), name: str = typer.Option("DevOps-OS", help="CI/CD pipeline name"), cicd_type: str = typer.Option("complete", "--type", help="Pipeline type: build | test | deploy | complete"), @@ -569,7 +584,7 @@ def scaffold_cicd_cmd( flags.append("--all") if custom_values: flags += ["--custom-values", custom_values] - _run_scaffold(scaffold_cicd.main, flags) + _run_scaffold(scaffold_cicd.main, flags, json_output) # ── scaffold unittest ──────────────────────────────────────────────────────── @@ -577,6 +592,7 @@ def scaffold_cicd_cmd( @scaffold_app.command("unittest") def scaffold_unittest_cmd( ctx: typer.Context, + json_output: bool = typer.Option(False, "--json", help="Print the generated result as JSON"), name: str = typer.Option("my-app", envvar="DEVOPS_OS_UNITTEST_NAME", help="Project / application name"), languages: str = typer.Option("python", envvar="DEVOPS_OS_UNITTEST_LANGUAGES", @@ -636,7 +652,7 @@ def scaffold_unittest_cmd( if not coverage: # coverage defaults to True; only pass flag when False flags.append("--no-coverage") - _run_scaffold(scaffold_unittest.main, flags) + _run_scaffold(scaffold_unittest.main, flags, json_output) # ── scaffold hardening ────────────────────────────────────────────────────── @@ -644,6 +660,7 @@ def scaffold_unittest_cmd( @scaffold_app.command("hardening") def scaffold_hardening_cmd( ctx: typer.Context, + json_output: bool = typer.Option(False, "--json", help="Print the generated result as JSON"), standard: str = typer.Option("all", envvar="DEVOPS_OS_HARDENING_STANDARD", help=( "Hardening standard: cis-k8s, stig-k8s, nsa-k8s, " @@ -689,7 +706,7 @@ def scaffold_hardening_cmd( ] if compliance_framework: flags += ["--compliance-framework", compliance_framework] - _run_scaffold(scaffold_hardening.main, flags) + _run_scaffold(scaffold_hardening.main, flags, json_output) @app.command() diff --git a/cli/test_cli.py b/cli/test_cli.py index 5e2065d..0ca37b0 100644 --- a/cli/test_cli.py +++ b/cli/test_cli.py @@ -255,6 +255,24 @@ def test_scaffold_gha_via_cli(): assert result.returncode == 0, result.stderr assert "error: unrecognized arguments" not in result.stderr +def test_scaffold_gha_json_output(): + """--json returns only a structured GitHub Actions result.""" + with tempfile.TemporaryDirectory() as tmp: + output_dir = os.path.join(tmp, ".github/workflows") + result = _run([ + "-m", "cli.devopsos", "scaffold", "gha", + "--name", "my-app", "--type", "complete", + "--output", output_dir, "--json", + ]) + + assert result.returncode == 0, result.stderr + payload = json.loads(result.stdout) + assert payload == { + "workflow": os.path.normpath(os.path.join(output_dir, "my-app-complete.yml")), + "type": "github_actions", + } + assert result.stderr == "" + def test_scaffold_gitlab_via_cli(): """Regression: `python -m cli.devopsos scaffold gitlab` must not raise argparse error.""" with tempfile.TemporaryDirectory() as tmp: @@ -268,6 +286,46 @@ def test_scaffold_gitlab_via_cli(): assert result.returncode == 0, result.stderr assert "error: unrecognized arguments" not in result.stderr +def test_scaffold_gitlab_json_output(): + """--json returns only the structured GitLab CI result.""" + with tempfile.TemporaryDirectory() as tmp: + output_path = os.path.join(tmp, ".gitlab-ci.yml") + result = _run([ + "-m", "cli.devopsos", "scaffold", "gitlab", + "--name", "my-app", "--type", "build", + "--output", output_path, "--json", + ]) + + assert result.returncode == 0, result.stderr + payload = json.loads(result.stdout) + assert payload == { + "pipeline": os.path.normpath(output_path), + "type": "gitlab_ci", + } + assert result.stderr == "" + +def test_scaffold_sre_json_output_lists_generated_files(): + """Multi-file scaffold commands expose every generated file in JSON mode.""" + with tempfile.TemporaryDirectory() as tmp: + result = _run([ + "-m", "cli.devopsos", "scaffold", "sre", + "--name", "my-app", "--output-dir", tmp, "--json", + ]) + + assert result.returncode == 0, result.stderr + payload = json.loads(result.stdout) + assert payload["type"] == "sre" + assert {os.path.normpath(path) for path in payload["files"]} == { + os.path.normpath(os.path.join(tmp, filename)) + for filename in ( + "alert-rules.yaml", + "grafana-dashboard.json", + "slo.yaml", + "alertmanager-config.yaml", + ) + } + assert result.stderr == "" + def test_scaffold_argocd_via_cli(): """Regression: `python -m cli.devopsos scaffold argocd` must not raise argparse error.""" with tempfile.TemporaryDirectory() as tmp: From 6201c47187103807f64c8be8c4a9b5750c93ea4a Mon Sep 17 00:00:00 2001 From: Yash Date: Wed, 23 Sep 2026 16:57:12 +0530 Subject: [PATCH 2/8] dd JSON output to scaffold commands --- .github/workflows/my-app-complete.yml | 107 ++++++ cli/scaffold_argocd.py | 2 + cli/scaffold_cicd.py | 41 ++- cli/scaffold_devcontainer.py | 5 + cli/scaffold_gha.py | 5 + cli/scaffold_gitlab.py | 5 + cli/scaffold_hardening.py | 2 + cli/scaffold_jenkins.py | 5 + cli/scaffold_sre.py | 2 + cli/scaffold_unittest.py | 5 + ...l mcp\357\200\276=1.0.0,\357\200\2742.0.0" | 327 ++++++++++++++++++ 11 files changed, 497 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/my-app-complete.yml create mode 100644 "tall --force-reinstall mcp\357\200\276=1.0.0,\357\200\2742.0.0.venvScriptspython.exe -m pip install --force-reinstall mcp\357\200\276=1.0.0,\357\200\2742.0.0" diff --git a/.github/workflows/my-app-complete.yml b/.github/workflows/my-app-complete.yml new file mode 100644 index 0000000..2fc6bdb --- /dev/null +++ b/.github/workflows/my-app-complete.yml @@ -0,0 +1,107 @@ +name: my-app CI/CD +'on': + push: + branches: + - main + pull_request: + branches: + - main + workflow_dispatch: + inputs: + environment: + description: Environment to deploy to + required: true + default: dev + type: choice + options: + - dev + - test + - staging + - prod +jobs: + build: + runs-on: ubuntu-latest + container: + image: ghcr.io/yourorg/devops-os:latest + options: --user root + steps: + - name: Checkout code + uses: actions/checkout@v3 + - name: Set up build environment + run: echo 'Setting up build environment for DevOps-OS' + - name: Install Python dependencies + run: if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + - name: Build Python package + run: if [ -f setup.py ]; then pip install -e .; elif [ -f pyproject.toml ]; + then pip install -e .; fi + - name: Install Node.js dependencies + run: if [ -f package.json ]; then npm ci; fi + - name: Build JavaScript/TypeScript + run: if [ -f package.json ]; then npm run build --if-present; fi + - name: Upload build artifacts + uses: actions/upload-artifact@v3 + with: + name: build-artifacts + path: dist/ + retention-days: 1 + test: + needs: + - build + runs-on: ubuntu-latest + container: + image: ghcr.io/yourorg/devops-os:latest + options: --user root + steps: + - name: Checkout code + uses: actions/checkout@v3 + - name: Set up test environment + run: echo 'Setting up test environment for DevOps-OS' + - name: Install Python dependencies + run: if [ -f requirements.txt ]; then pip install -r requirements.txt pytest + pytest-cov; fi + - name: Run Python tests + run: if [ -d tests ]; then python -m pytest --cov=./ --cov-report=xml; fi + - name: Run Pylint + run: if command -v pylint &> /dev/null; then pylint --disable=C0111 **/*.py; + fi + - name: Install Node.js dependencies + run: if [ -f package.json ]; then npm ci; fi + - name: Run JavaScript tests + run: if [ -f package.json ]; then npm test; fi + - name: Run ESLint + run: if [ -f package.json ] && grep -q eslint package.json; then npm run lint; + fi + - name: Upload test results + uses: actions/upload-artifact@v3 + with: + name: test-results + path: test-reports/ + retention-days: 1 + - name: Upload coverage reports + uses: codecov/codecov-action@v3 + with: + files: ./coverage.xml,./coverage/lcov.info + fail_ci_if_error: false + deploy: + needs: + - test + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + container: + image: ghcr.io/yourorg/devops-os:latest + options: --user root + steps: + - name: Checkout code + uses: actions/checkout@v3 + - name: Set up deployment environment + run: echo 'Setting up deployment environment for DevOps-OS' + - name: Build and Push Docker Image + if: github.ref == 'refs/heads/main' + run: 'echo "${{ secrets.REGISTRY_TOKEN }}" | docker login ghcr.io -u ${{ github.actor + }} --password-stdin + + docker build -t ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name + }}:latest . + + docker push ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name + }}:latest' diff --git a/cli/scaffold_argocd.py b/cli/scaffold_argocd.py index 05c5655..6a6ce65 100644 --- a/cli/scaffold_argocd.py +++ b/cli/scaffold_argocd.py @@ -306,6 +306,8 @@ def main(): for p in generated: print(f" {p}") + return {"files": generated, "type": args.method} + if __name__ == "__main__": main() diff --git a/cli/scaffold_cicd.py b/cli/scaffold_cicd.py index fb4e36a..86061f3 100755 --- a/cli/scaffold_cicd.py +++ b/cli/scaffold_cicd.py @@ -16,7 +16,7 @@ import argparse -def _run_module_main(module_main, flags: list) -> bool: +def _run_module_main(module_main, flags: list): """Call *module_main()* with the given CLI flag list via sys.argv swap. Returns True on success, False if the module exits with a non-zero code. @@ -24,8 +24,7 @@ def _run_module_main(module_main, flags: list) -> bool: saved = sys.argv[:] sys.argv = sys.argv[:1] + flags try: - module_main() - return True + return module_main() except SystemExit as exc: if exc.code not in (None, 0): return False @@ -136,12 +135,13 @@ def run_github_generator(args) -> bool: if args.custom_values: flags += ["--custom-values", args.custom_values] - ok = _run_module_main(scaffold_gha.main, flags) + result = _run_module_main(scaffold_gha.main, flags) + ok = bool(result) if ok: print("GitHub Actions workflow generated successfully!") else: print("Error generating GitHub Actions workflow. Check output above.") - return ok + return result if ok else False def run_jenkins_generator(args) -> bool: @@ -167,12 +167,13 @@ def run_jenkins_generator(args) -> bool: if args.custom_values: flags += ["--custom-values", args.custom_values] - ok = _run_module_main(scaffold_jenkins.main, flags) + result = _run_module_main(scaffold_jenkins.main, flags) + ok = bool(result) if ok: print("Jenkins pipeline generated successfully!") else: print("Error generating Jenkins pipeline. Check output above.") - return ok + return result if ok else False def create_readme(args): """Create a README.md file explaining the generated CI/CD files.""" @@ -227,14 +228,21 @@ def main(): args = parse_arguments() success = True + generated = [] if args.github: - if not run_github_generator(args): + result = run_github_generator(args) + if not result: success = False + else: + generated.append(result) if args.jenkins: - if not run_jenkins_generator(args): + result = run_jenkins_generator(args) + if not result: success = False + else: + generated.append(result) if success: create_readme(args) @@ -242,5 +250,20 @@ def main(): else: print("\nCI/CD generation completed with errors. Please check the output above.") + files = [] + for result in generated: + files.extend(result.get("files", [])) + for key in ("workflow", "pipeline"): + if result.get(key): + files.append(result[key]) + break + if success: + files.append(os.path.join(args.output_dir, "CICD-README.md")) + + return { + "files": files, + "type": "cicd", + } + if __name__ == "__main__": main() diff --git a/cli/scaffold_devcontainer.py b/cli/scaffold_devcontainer.py index 3f2df04..9c6e290 100644 --- a/cli/scaffold_devcontainer.py +++ b/cli/scaffold_devcontainer.py @@ -357,6 +357,11 @@ def main(): print(f" {env_json_path}") print(f" {dc_json_path}") + return { + "files": [str(env_json_path), str(dc_json_path)], + "type": "devcontainer", + } + if __name__ == "__main__": main() diff --git a/cli/scaffold_gha.py b/cli/scaffold_gha.py index 27957d0..237ae62 100644 --- a/cli/scaffold_gha.py +++ b/cli/scaffold_gha.py @@ -1085,5 +1085,10 @@ def main(): if args.kubernetes: print(f"Kubernetes deployment method: {args.k8s_method}") + return { + "workflow": str(Path(filepath)), + "type": "github_actions", + } + if __name__ == "__main__": main() diff --git a/cli/scaffold_gitlab.py b/cli/scaffold_gitlab.py index 6138559..b25b7d6 100644 --- a/cli/scaffold_gitlab.py +++ b/cli/scaffold_gitlab.py @@ -339,6 +339,11 @@ def main(): if args.kubernetes: print(f"Kubernetes deployment method: {args.k8s_method}") + return { + "pipeline": str(output_path), + "type": "gitlab_ci", + } + if __name__ == "__main__": main() diff --git a/cli/scaffold_hardening.py b/cli/scaffold_hardening.py index 4a4351c..d710c2f 100644 --- a/cli/scaffold_hardening.py +++ b/cli/scaffold_hardening.py @@ -1982,6 +1982,8 @@ def main(): "Check that the standard supports the requested output type." ) + return {"files": [str(path) for path in generated], "type": "hardening"} + if __name__ == "__main__": main() diff --git a/cli/scaffold_jenkins.py b/cli/scaffold_jenkins.py index 569c489..606fc4f 100644 --- a/cli/scaffold_jenkins.py +++ b/cli/scaffold_jenkins.py @@ -541,5 +541,10 @@ def main(): if args.parameters: print("Pipeline includes runtime parameters") + return { + "pipeline": str(Path(output_path)), + "type": "jenkins", + } + if __name__ == "__main__": main() diff --git a/cli/scaffold_sre.py b/cli/scaffold_sre.py index 6f406d0..cc86073 100644 --- a/cli/scaffold_sre.py +++ b/cli/scaffold_sre.py @@ -503,6 +503,8 @@ def main(): for p in generated: print(f" {p}") + return {"files": generated, "type": "sre"} + if __name__ == "__main__": main() diff --git a/cli/scaffold_unittest.py b/cli/scaffold_unittest.py index 1359099..74fc53b 100644 --- a/cli/scaffold_unittest.py +++ b/cli/scaffold_unittest.py @@ -604,6 +604,11 @@ def main(): print("Framework override:", args.framework) print("Coverage enabled:", args.coverage) + return { + "files": [str(path) for path, _ in written], + "type": "unit_tests", + } + if __name__ == "__main__": main() diff --git "a/tall --force-reinstall mcp\357\200\276=1.0.0,\357\200\2742.0.0.venvScriptspython.exe -m pip install --force-reinstall mcp\357\200\276=1.0.0,\357\200\2742.0.0" "b/tall --force-reinstall mcp\357\200\276=1.0.0,\357\200\2742.0.0.venvScriptspython.exe -m pip install --force-reinstall mcp\357\200\276=1.0.0,\357\200\2742.0.0" new file mode 100644 index 0000000..3bac49d --- /dev/null +++ "b/tall --force-reinstall mcp\357\200\276=1.0.0,\357\200\2742.0.0.venvScriptspython.exe -m pip install --force-reinstall mcp\357\200\276=1.0.0,\357\200\2742.0.0" @@ -0,0 +1,327 @@ + + SSUUMMMMAARRYY OOFF LLEESSSS CCOOMMMMAANNDDSS + + Commands marked with * may be preceded by a number, _N. + Notes in parentheses indicate the behavior if _N is given. + A key preceded by a caret indicates the Ctrl key; thus ^K is ctrl-K. + + h H Display this help. + q :q Q :Q ZZ Exit. + --------------------------------------------------------------------------- + + MMOOVVIINNGG + + e ^E j ^N CR * Forward one line (or _N lines). + y ^Y k ^K ^P * Backward one line (or _N lines). + ESC-j * Forward one file line (or _N file lines). + ESC-k * Backward one file line (or _N file lines). + f ^F ^V SPACE * Forward one window (or _N lines). + b ^B ESC-v * Backward one window (or _N lines). + z * Forward one window (and set window to _N). + w * Backward one window (and set window to _N). + ESC-SPACE * Forward one window, but don't stop at end-of-file. + ESC-b * Backward one window, but don't stop at beginning-of-file. + d ^D * Forward one half-window (and set half-window to _N). + u ^U * Backward one half-window (and set half-window to _N). + ESC-) RightArrow * Right one half screen width (or _N positions). + ESC-( LeftArrow * Left one half screen width (or _N positions). + ESC-} ^RightArrow Right to last column displayed. + ESC-{ ^LeftArrow Left to first column. + F Forward forever; like "tail -f". + ESC-F Like F but stop when search pattern is found. + ESC-f Like F but ring the bell when search pattern is found. + r ^R ^L Repaint screen. + R Repaint screen, discarding buffered input. + --------------------------------------------------- + Default "window" is the screen height. + Default "half-window" is half of the screen height. + --------------------------------------------------------------------------- + + SSEEAARRCCHHIINNGG + + /_p_a_t_t_e_r_n * Search forward for (_N-th) matching line. + ?_p_a_t_t_e_r_n * Search backward for (_N-th) matching line. + n * Repeat previous search (for _N-th occurrence). + N * Repeat previous search in reverse direction. + ESC-n * Repeat previous search, spanning files. + ESC-N * Repeat previous search, reverse dir. & spanning files. + ^O^N ^On * Search forward for (_N-th) OSC8 hyperlink. + ^O^P ^Op * Search backward for (_N-th) OSC8 hyperlink. + ^O^L ^Ol Jump to the currently selected OSC8 hyperlink. + ESC-u Undo (toggle) search highlighting. + ESC-U Clear search highlighting. + &_p_a_t_t_e_r_n * Display only matching lines. + --------------------------------------------------- + Search is case-sensitive unless changed with -i or -I. + A search pattern may begin with one or more of: + ^N or ! Search for NON-matching lines. + ^E or * Search multiple files (pass thru END OF FILE). + ^F or @ Start search at FIRST file (for /) or last file (for ?). + ^K Highlight matches, but don't move (KEEP position). + ^R Don't use REGULAR EXPRESSIONS. + ^S _n Search for match in _n-th parenthesized subpattern. + ^W WRAP search if no match found. + ^L Enter next character literally into pattern. + --------------------------------------------------------------------------- + + JJUUMMPPIINNGG + + g < ESC-< HOME * Go to first line in file (or line _N). + G > ESC-> END * Go to last line in file (or line _N). + p % * Go to beginning of file (or _N percent into file). + t * Go to the (_N-th) next tag. + T * Go to the (_N-th) previous tag. + { ( [ * Find close bracket } ) ]. + } ) ] * Find open bracket { ( [. + ESC-^F _<_c_1_> _<_c_2_> * Find close bracket _<_c_2_>. + ESC-^B _<_c_1_> _<_c_2_> * Find open bracket _<_c_1_>. + --------------------------------------------------- + Each "find close bracket" command goes forward to the close bracket + matching the (_N-th) open bracket in the top line. + Each "find open bracket" command goes backward to the open bracket + matching the (_N-th) close bracket in the bottom line. + + m_<_l_e_t_t_e_r_> Mark the current top line with . + M_<_l_e_t_t_e_r_> Mark the current bottom line with . + '_<_l_e_t_t_e_r_> Go to a previously marked position. + '' Go to the previous position. + ^X^X Same as '. + ESC-m_<_l_e_t_t_e_r_> Clear a mark. + --------------------------------------------------- + A mark is any upper-case or lower-case letter. + Certain marks are predefined: + ^ means beginning of the file + $ means end of the file + --------------------------------------------------------------------------- + + CCHHAANNGGIINNGG FFIILLEESS + + :e [_f_i_l_e] Examine a new file. + ^X^V Same as :e. + :n * Examine the (_N-th) next file from the command line. + :p * Examine the (_N-th) previous file from the command line. + :x * Examine the first (or _N-th) file from the command line. + ^O^O Open the currently selected OSC8 hyperlink. + :d Delete the current file from the command line list. + = ^G :f Print current file name. + --------------------------------------------------------------------------- + + MMIISSCCEELLLLAANNEEOOUUSS CCOOMMMMAANNDDSS + + -_<_f_l_a_g_> Toggle a command line option [see OPTIONS below]. + --_<_n_a_m_e_> Toggle a command line option, by name. + __<_f_l_a_g_> Display the setting of a command line option. + ___<_n_a_m_e_> Display the setting of an option, by name. + +_c_m_d Execute the less cmd each time a new file is examined. + + !_c_o_m_m_a_n_d Execute the shell command with $SHELL. + #_c_o_m_m_a_n_d Execute the shell command, expanded like a prompt. + |XX_c_o_m_m_a_n_d Pipe file between current pos & mark XX to shell command. + s _f_i_l_e Save input to a file. + v Edit the current file with $VISUAL or $EDITOR. + V Print version number of "less". + --------------------------------------------------------------------------- + + OOPPTTIIOONNSS + + Most options may be changed either on the command line, + or from within less by using the - or -- command. + Options may be given in one of two forms: either a single + character preceded by a -, or a name preceded by --. + + -? ........ --help + Display help (from command line). + -a ........ --search-skip-screen + Search skips current screen. + -A ........ --SEARCH-SKIP-SCREEN + Search starts just after target line. + -b [_N] .... --buffers=[_N] + Number of buffers. + -B ........ --auto-buffers + Don't automatically allocate buffers for pipes. + -c ........ --clear-screen + Repaint by clearing rather than scrolling. + -d ........ --dumb + Dumb terminal. + -D xx_c_o_l_o_r . --color=xx_c_o_l_o_r + Set screen colors. + -e -E .... --quit-at-eof --QUIT-AT-EOF + Quit at end of file. + -f ........ --force + Force open non-regular files. + -F ........ --quit-if-one-screen + Quit if entire file fits on first screen. + -g ........ --hilite-search + Highlight only last match for searches. + -G ........ --HILITE-SEARCH + Don't highlight any matches for searches. + -h [_N] .... --max-back-scroll=[_N] + Backward scroll limit. + -i ........ --ignore-case + Ignore case in searches that do not contain uppercase. + -I ........ --IGNORE-CASE + Ignore case in all searches. + -j [_N] .... --jump-target=[_N] + Screen position of target lines. + -J ........ --status-column + Display a status column at left edge of screen. + -k _f_i_l_e ... --lesskey-file=_f_i_l_e + Use a compiled lesskey file. + -K ........ --quit-on-intr + Exit less in response to ctrl-C. + -L ........ --no-lessopen + Ignore the LESSOPEN environment variable. + -m -M .... --long-prompt --LONG-PROMPT + Set prompt style. + -n ......... --line-numbers + Suppress line numbers in prompts and messages. + -N ......... --LINE-NUMBERS + Display line number at start of each line. + -o [_f_i_l_e] .. --log-file=[_f_i_l_e] + Copy to log file (standard input only). + -O [_f_i_l_e] .. --LOG-FILE=[_f_i_l_e] + Copy to log file (unconditionally overwrite). + -p _p_a_t_t_e_r_n . --pattern=[_p_a_t_t_e_r_n] + Start at pattern (from command line). + -P [_p_r_o_m_p_t] --prompt=[_p_r_o_m_p_t] + Define new prompt. + -q -Q .... --quiet --QUIET --silent --SILENT + Quiet the terminal bell. + -r -R .... --raw-control-chars --RAW-CONTROL-CHARS + Output "raw" control characters. + -s ........ --squeeze-blank-lines + Squeeze multiple blank lines. + -S ........ --chop-long-lines + Chop (truncate) long lines rather than wrapping. + -t _t_a_g .... --tag=[_t_a_g] + Find a tag. + -T [_t_a_g_s_f_i_l_e] --tag-file=[_t_a_g_s_f_i_l_e] + Use an alternate tags file. + -u -U .... --underline-special --UNDERLINE-SPECIAL + Change handling of backspaces, tabs and carriage returns. + -V ........ --version + Display the version number of "less". + -w ........ --hilite-unread + Highlight first new line after forward-screen. + -W ........ --HILITE-UNREAD + Highlight first new line after any forward movement. + -x [_N[,...]] --tabs=[_N[,...]] + Set tab stops. + -X ........ --no-init + Don't use termcap init/deinit strings. + -y [_N] .... --max-forw-scroll=[_N] + Forward scroll limit. + -z [_N] .... --window=[_N] + Set size of window. + -" [_c[_c]] . --quotes=[_c[_c]] + Set shell quote characters. + -~ ........ --tilde + Don't display tildes after end of file. + -# [_N] .... --shift=[_N] + Set horizontal scroll amount (0 = one half screen width). + + --autosave=[_m_/_!_*] + Actions which cause the history file to be saved. + --exit-follow-on-close + Exit F command on a pipe when writer closes pipe. + --file-size + Automatically determine the size of the input file. + --follow-name + The F command changes files if the input file is renamed. + --form-feed + Stop scrolling when a form feed character is reached. + --header=[_L[,_C[,_N]]] + Use _L lines (starting at line _N) and _C columns as headers. + --incsearch + Search file as each pattern character is typed in. + --intr=[_C] + Use _C instead of ^X to interrupt a read. + --lesskey-context=_t_e_x_t + Use lesskey source file contents. + --lesskey-src=_f_i_l_e + Use a lesskey source file. + --line-num-width=[_N] + Set the width of the -N line number field to _N characters. + --match-shift=[_N] + Show at least _N characters to the left of a search match. + --modelines=[_N] + Read _N lines from the input file and look for vim modelines. + --mouse + Enable mouse input. + --no-edit-warn + Don't warn when using v command on a file opened via LESSOPEN. + --no-keypad + Don't send termcap keypad init/deinit strings. + --no-histdups + Remove duplicates from command history. + --no-number-headers + Don't give line numbers to header lines. + --no-paste + Ignore pasted input. + --no-search-header-lines + Searches do not include header lines. + --no-search-header-columns + Searches do not include header columns. + --no-search-headers + Searches do not include header lines or columns. + --no-vbell + Disable the terminal's visual bell. + --redraw-on-quit + Redraw final screen when quitting. + --rscroll=[_C] + Set the character used to mark truncated lines. + --save-marks + Retain marks across invocations of less. + --search-options=[EFKNRW-] + Set default options for every search. + --show-preproc-errors + Display a message if preprocessor exits with an error status. + --proc-backspace + Process backspaces for bold/underline. + --PROC-BACKSPACE + Treat backspaces as control characters. + --proc-return + Delete carriage returns before newline. + --PROC-RETURN + Treat carriage returns as control characters. + --proc-tab + Expand tabs to spaces. + --PROC-TAB + Treat tabs as control characters. + --status-col-width=[_N] + Set the width of the -J status column to _N characters. + --status-line + Highlight or color the entire line containing a mark. + --use-backslash + Subsequent options use backslash as escape char. + --use-color + Enables colored text. + --wheel-lines=[_N] + Each click of the mouse wheel moves _N lines. + --wordwrap + Wrap lines at spaces. + + + --------------------------------------------------------------------------- + + LLIINNEE EEDDIITTIINNGG + + These keys can be used to edit text being entered + on the "command line" at the bottom of the screen. + + RightArrow ..................... ESC-l ... Move cursor right one character. + LeftArrow ...................... ESC-h ... Move cursor left one character. + ctrl-RightArrow ESC-RightArrow ESC-w ... Move cursor right one word. + ctrl-LeftArrow ESC-LeftArrow ESC-b ... Move cursor left one word. + HOME ........................... ESC-0 ... Move cursor to start of line. + END ............................ ESC-$ ... Move cursor to end of line. + BACKSPACE ................................ Delete char to left of cursor. + DELETE ......................... ESC-x ... Delete char under cursor. + ctrl-BACKSPACE ESC-BACKSPACE ........... Delete word to left of cursor. + ctrl-DELETE .... ESC-DELETE .... ESC-X ... Delete word under cursor. + ctrl-U ......... ESC (MS-DOS only) ....... Delete entire line. + UpArrow ........................ ESC-k ... Retrieve previous command line. + DownArrow ...................... ESC-j ... Retrieve next command line. + TAB ...................................... Complete filename & cycle. + SHIFT-TAB ...................... ESC-TAB Complete filename & reverse cycle. + ctrl-L ................................... Complete filename, list all. From 6cb379a6bc793b60b90fa5505b7e71203af99a9b Mon Sep 17 00:00:00 2001 From: Yash Date: Wed, 23 Sep 2026 22:44:06 +0530 Subject: [PATCH 3/8] Pin MCP to v1 API compatibility --- mcp_server/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mcp_server/requirements.txt b/mcp_server/requirements.txt index 94467aa..85ed510 100644 --- a/mcp_server/requirements.txt +++ b/mcp_server/requirements.txt @@ -1,4 +1,4 @@ -mcp>=1.0.0 +mcp>=1.0.0,<2.0.0 pyyaml>=6.0 typer>=0.9.0,<0.23.0 click>=8.0.0,<8.2 From 58a75d336bfafbbd1d2afd1cddb333bb4913018a Mon Sep 17 00:00:00 2001 From: Yash Date: Wed, 23 Sep 2026 22:58:31 +0530 Subject: [PATCH 4/8] Upgrade artifact upload action to v4 --- .github/workflows/my-app-complete.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/my-app-complete.yml b/.github/workflows/my-app-complete.yml index 2fc6bdb..f10eae2 100644 --- a/.github/workflows/my-app-complete.yml +++ b/.github/workflows/my-app-complete.yml @@ -39,7 +39,7 @@ jobs: - name: Build JavaScript/TypeScript run: if [ -f package.json ]; then npm run build --if-present; fi - name: Upload build artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: build-artifacts path: dist/ @@ -72,7 +72,7 @@ jobs: run: if [ -f package.json ] && grep -q eslint package.json; then npm run lint; fi - name: Upload test results - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: test-results path: test-reports/ From ea0be589b8fe56db021c8c3b7d127834490f0aaf Mon Sep 17 00:00:00 2001 From: Yash Date: Wed, 23 Sep 2026 22:59:15 +0530 Subject: [PATCH 5/8] Modernize workflow actions and artifact handling --- .github/workflows/my-app-complete.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/my-app-complete.yml b/.github/workflows/my-app-complete.yml index f10eae2..1134275 100644 --- a/.github/workflows/my-app-complete.yml +++ b/.github/workflows/my-app-complete.yml @@ -26,7 +26,7 @@ jobs: options: --user root steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up build environment run: echo 'Setting up build environment for DevOps-OS' - name: Install Python dependencies @@ -44,6 +44,7 @@ jobs: name: build-artifacts path: dist/ retention-days: 1 + if-no-files-found: warn test: needs: - build @@ -53,7 +54,7 @@ jobs: options: --user root steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up test environment run: echo 'Setting up test environment for DevOps-OS' - name: Install Python dependencies @@ -77,6 +78,7 @@ jobs: name: test-results path: test-reports/ retention-days: 1 + if-no-files-found: warn - name: Upload coverage reports uses: codecov/codecov-action@v3 with: @@ -92,7 +94,7 @@ jobs: options: --user root steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up deployment environment run: echo 'Setting up deployment environment for DevOps-OS' - name: Build and Push Docker Image From 4fc84cf129fd1fc5b44fec97279f4ea90473876f Mon Sep 17 00:00:00 2001 From: Yash Date: Wed, 23 Sep 2026 23:06:25 +0530 Subject: [PATCH 6/8] Remove unavailable job container from CI workflow --- .github/workflows/my-app-complete.yml | 48 +++++++++++++++++---------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/.github/workflows/my-app-complete.yml b/.github/workflows/my-app-complete.yml index 1134275..6ffbc3f 100644 --- a/.github/workflows/my-app-complete.yml +++ b/.github/workflows/my-app-complete.yml @@ -18,15 +18,23 @@ name: my-app CI/CD - test - staging - prod +permissions: + contents: read + packages: write jobs: build: runs-on: ubuntu-latest - container: - image: ghcr.io/yourorg/devops-os:latest - options: --user root steps: - name: Checkout code uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' - name: Set up build environment run: echo 'Setting up build environment for DevOps-OS' - name: Install Python dependencies @@ -49,12 +57,17 @@ jobs: needs: - build runs-on: ubuntu-latest - container: - image: ghcr.io/yourorg/devops-os:latest - options: --user root steps: - name: Checkout code uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' - name: Set up test environment run: echo 'Setting up test environment for DevOps-OS' - name: Install Python dependencies @@ -89,21 +102,22 @@ jobs: - test if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest - container: - image: ghcr.io/yourorg/devops-os:latest - options: --user root steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up deployment environment run: echo 'Setting up deployment environment for DevOps-OS' + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Build and Push Docker Image if: github.ref == 'refs/heads/main' - run: 'echo "${{ secrets.REGISTRY_TOKEN }}" | docker login ghcr.io -u ${{ github.actor - }} --password-stdin - - docker build -t ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name - }}:latest . - - docker push ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name - }}:latest' + env: + IMAGE_NAME: ghcr.io/${{ github.repository }} + run: | + docker build -t "$IMAGE_NAME:latest" -t "$IMAGE_NAME:${{ github.sha }}" . + docker push "$IMAGE_NAME:latest" + docker push "$IMAGE_NAME:${{ github.sha }}" From e8637d646c5c326edfbbd9085ab0dfff1dc36b09 Mon Sep 17 00:00:00 2001 From: Yash Date: Wed, 23 Sep 2026 23:08:56 +0530 Subject: [PATCH 7/8] Install pytest independently in test job --- .github/workflows/my-app-complete.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/my-app-complete.yml b/.github/workflows/my-app-complete.yml index 6ffbc3f..b54fe1e 100644 --- a/.github/workflows/my-app-complete.yml +++ b/.github/workflows/my-app-complete.yml @@ -71,8 +71,9 @@ jobs: - name: Set up test environment run: echo 'Setting up test environment for DevOps-OS' - name: Install Python dependencies - run: if [ -f requirements.txt ]; then pip install -r requirements.txt pytest - pytest-cov; fi + run: | + python -m pip install -r requirements.txt || true + python -m pip install pytest pytest-cov - name: Run Python tests run: if [ -d tests ]; then python -m pytest --cov=./ --cov-report=xml; fi - name: Run Pylint From 1dfde6fbbc69a32ab03a5dbc09cc3e4317bd0214 Mon Sep 17 00:00:00 2001 From: Yash Date: Wed, 23 Sep 2026 23:11:33 +0530 Subject: [PATCH 8/8] Install project dependency manifests in CI --- .github/workflows/my-app-complete.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/my-app-complete.yml b/.github/workflows/my-app-complete.yml index b54fe1e..9141ac9 100644 --- a/.github/workflows/my-app-complete.yml +++ b/.github/workflows/my-app-complete.yml @@ -72,7 +72,7 @@ jobs: run: echo 'Setting up test environment for DevOps-OS' - name: Install Python dependencies run: | - python -m pip install -r requirements.txt || true + python -m pip install -r cli/requirements.txt -r mcp_server/requirements.txt python -m pip install pytest pytest-cov - name: Run Python tests run: if [ -d tests ]; then python -m pytest --cov=./ --cov-report=xml; fi