Skip to content

feat: add phase-based CLI entry point with subcommands (CIV-013) - #616

Open
sshmulev wants to merge 2 commits into
refactor/civ-corefrom
civ-013-cli-entry-point
Open

sshmulev wants to merge 2 commits into
refactor/civ-corefrom
civ-013-cli-entry-point

Conversation

@sshmulev

@sshmulev sshmulev commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Add cli.py at repo root with 5 subcommands: provision, execute, collect, run, cleanup
  • Maps to phase-based execution flow: each subcommand calls the appropriate core/ module
  • civ run ties the full pipeline (provision → execute → collect → cleanup) and accepts all flags from cloud-image-val.py
  • cloud-image-val.py is not modified

Dependencies

Test plan

  • 39 unit tests pass (pytest test/test_cli.py -v)
  • 98% code coverage
  • flake8 passes with no warnings
  • All 5 subcommands tested: parser, routing, and behavior
  • cloud-image-val.py unchanged

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 3 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="cli.py" line_range="11-14" />
<code_context>
+import sys
+
+from core.config import CoreConfig
+from core.executor import execute
+from core.metadata import InstanceMetadata
+from core.provisioner import Provisioner
+from core.results import get_exit_code, merge_results
+
+
</code_context>
<issue_to_address>
**issue (bug_risk):** Importing `cli.py` raises `ModuleNotFoundError` because the repository contains no `core.provisioner` or `core.executor` modules, so the new CLI cannot start in the checked-in tree.

**Triggers:** When CIV-011 and CIV-012 have not already been merged into the same checkout.

**Suggested fix:** Add the dependency modules in this change or ensure they are present before exposing `cli.py`.
</issue_to_address>

### Comment 2
<location path="cli.py" line_range="227-253" />
<code_context>
+    p_run.add_argument("-r", "--resources-file",
</code_context>
<issue_to_address>
**issue (bug_risk):** The `run` subcommand does not define the existing `-a`/`--attach` flag from `cloud-image-val.py`, and `cmd_run` always calls `provision()` instead of attaching to the previously provisioned instances. Users migrating the full CLI cannot run the documented attach/recovery workflow.

**Triggers:** When a previous run left instances alive and the caller needs to resume them with `--attach`.

**Suggested fix:** Add the attach option and route it to the provisioner’s attach/reuse flow instead of unconditionally provisioning.
</issue_to_address>

### Comment 3
<location path="cli.py" line_range="163-164" />
<code_context>
+        print(f"Error: {exc}")
+        return 100
+    finally:
+        if not config.stop_cleanup:
+            provisioner.cleanup()
+
+
</code_context>
<issue_to_address>
**issue (bug_risk):** An exception raised by `provisioner.cleanup()` escapes the `finally` block and overrides the handled pipeline result, so `cmd_run` raises instead of returning its documented integer exit code when cleanup fails.

**Triggers:** When provisioning, execution, or result collection completes and infrastructure cleanup itself raises.

**Suggested fix:** Wrap cleanup in its own exception handler and return or report a cleanup-specific failure without allowing the `finally` block to replace the command result unexpectedly.

```suggestion
        if not config.stop_cleanup:
            try:
                provisioner.cleanup()
            except Exception as exc:
                print(f"Cleanup error: {exc}")
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread cli.py
Comment on lines +11 to +14
from core.executor import execute
from core.metadata import InstanceMetadata
from core.provisioner import Provisioner
from core.results import get_exit_code, merge_results

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): Importing cli.py raises ModuleNotFoundError because the repository contains no core.provisioner or core.executor modules, so the new CLI cannot start in the checked-in tree.

Triggers: When CIV-011 and CIV-012 have not already been merged into the same checkout.

Suggested fix: Add the dependency modules in this change or ensure they are present before exposing cli.py.

Comment thread cli.py
Comment thread cli.py
Comment on lines +163 to +164
if not config.stop_cleanup:
provisioner.cleanup()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): An exception raised by provisioner.cleanup() escapes the finally block and overrides the handled pipeline result, so cmd_run raises instead of returning its documented integer exit code when cleanup fails.

Triggers: When provisioning, execution, or result collection completes and infrastructure cleanup itself raises.

Suggested fix: Wrap cleanup in its own exception handler and return or report a cleanup-specific failure without allowing the finally block to replace the command result unexpectedly.

Suggested change
if not config.stop_cleanup:
provisioner.cleanup()
if not config.stop_cleanup:
try:
provisioner.cleanup()
except Exception as exc:
print(f"Cleanup error: {exc}")

@F-X64 F-X64 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Already looking good! There are some issues but we need to merge CIV-011 and CIV-012 first.

Comment thread cli.py
help="Output path for merged JUnit XML")
p_coll.set_defaults(func=cmd_collect)

# --- run ---

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Hm, this is missing the "--attach" flag that exists in the legacy cloud-image-val.py.
In the refactor, "civ execute --instances-json" partially covers this (if pointed at existing instances), but the behavior is a bit different "--attach" also reuses .tf.json and .tfstate files. Is the attach flag in use at the moment or should we skip it?

Comment thread cli.py
return get_exit_code(merge_result)


def cmd_run(args: argparse.Namespace) -> int:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

cmd_run (lines 120-161) reimplements the pipeline inline rather than calling cmd_provision, cmd_execute, and cmd_collect. This creates divergence risks.
E.g. the result collection logic diverges from cmd_collect: cmd_run uses !exec_result.instance_results[].result_file" (see lines 144 > ~146) while cmd_collect uses "glob.glob("*.xml") (see line 109)."

This does fulfill the task requirements but it might be solved more elegantly.

Comment thread cli.py
}

for attr in ("debug", "parallel", "stop_cleanup"):
val = getattr(args, attr, None)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The if val: check skips False values, which currenbtly works because CoreConfig defaults these to False. If a future flag has default=True and the user doesn't pass it in it'll break. Same could happen if someone adds a boolean config key that defaults to True or the CoreConfig defaults change.
You can fix that by using "if val is not None:" instead of "if val:".

Comment thread test/test_cli.py
Comment on lines +11 to +12
sys.modules.setdefault("core.provisioner", mock_provisioner_module)
sys.modules.setdefault("core.executor", mock_executor_module)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

A heads-up: setdefault only inserts if the key is absent. Once core/provisioner.py (CIV-011) and core/executor.py (CIV-012) are merged, Python's import system will load the real modules, setdefault won't override them, and the cli import will try to import actual Provisioner/execute symbols. If those modules have unresolvable imports of their own (OpenTofu, paramiko, etc.), every test in this file will fail at import time. Or in other owrds, lets get the other tasks merged and see what needs fixing-

Add cli.py with 5 subcommands (provision, execute, collect, run,
cleanup) mapping to the phase-based execution flow. Uses argparse
with subparsers, wires into core modules (provisioner, executor,
results). The run subcommand ties the full pipeline together.
39 unit tests at 98% coverage.
- Use 'if val is not None' instead of 'if val' for boolean flags
  to handle future default=True cases correctly
- Refactor cmd_run to delegate to cmd_execute and cmd_collect
  instead of reimplementing pipeline logic inline
- Add --attach flag to civ run for reusing existing infrastructure
@sshmulev
sshmulev force-pushed the civ-013-cli-entry-point branch from bb89261 to 47808b6 Compare September 9, 2026 12:07
@sshmulev
sshmulev requested a review from F-X64 September 9, 2026 12: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.

2 participants