Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions cli/localci/cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,9 @@ def _coerce_value(raw: str) -> str | int | float | bool:
try:
return int(raw)
except ValueError:
pass
pass # fall through to float / plain string
try:
return float(raw)
except ValueError:
pass
pass # not numeric; return raw string below
return raw
19 changes: 14 additions & 5 deletions cli/localci/cli/images.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import yaml

from localci.core.registry import ImageRegistry
from localci.errors import DockerNotAvailableError
from localci.utils.docker import DockerManager
from localci.utils.output import (
console,
Expand Down Expand Up @@ -80,10 +81,14 @@ def images_list(ctx: click.Context, output_format: str, registry_path: Path | No
"""List available images."""
try:
registry = _get_registry(registry_path)
except Exception as exc: # noqa: BLE001
except FileNotFoundError as exc:
print_error(str(exc))
ctx.exit(1)
return
except (OSError, yaml.YAMLError, TypeError, ValueError) as exc:
print_error(f"Could not load image registry: {exc}")
ctx.exit(1)
return

if output_format == "json":
click.echo(json.dumps([e.to_dict() for e in registry.entries], indent=2))
Expand Down Expand Up @@ -122,10 +127,14 @@ def images_info(ctx: click.Context, image: str, registry_path: Path | None) -> N
"""Show detailed information about an image."""
try:
registry = _get_registry(registry_path)
except Exception as exc: # noqa: BLE001
except FileNotFoundError as exc:
print_error(str(exc))
ctx.exit(1)
return
except (OSError, yaml.YAMLError, TypeError, ValueError) as exc:
print_error(f"Could not load image registry: {exc}")
ctx.exit(1)
return

match = registry.find_by_name(image)
if not match:
Expand Down Expand Up @@ -212,7 +221,7 @@ def images_clean(

try:
dm = DockerManager()
except RuntimeError as exc:
except DockerNotAvailableError as exc:
print_error(str(exc))
ctx.exit(1)
return
Expand Down Expand Up @@ -258,7 +267,7 @@ def images_import(ctx: click.Context, tar_file: str) -> None:
"""Import a Docker image from a tar file."""
try:
dm = DockerManager()
except RuntimeError as exc:
except DockerNotAvailableError as exc:
print_error(str(exc))
ctx.exit(1)
return
Expand All @@ -285,7 +294,7 @@ def images_export(ctx: click.Context, image: str, output_path: str) -> None:
"""Export a Docker image to a tar file."""
try:
dm = DockerManager()
except RuntimeError as exc:
except DockerNotAvailableError as exc:
print_error(str(exc))
ctx.exit(1)
return
Expand Down
2 changes: 1 addition & 1 deletion cli/localci/cli/list.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ def list_cmd(
try:
analyzer = WorkflowAnalyzer()
wf = analyzer.analyze(wf_path)
except (WorkflowError, FileNotFoundError) as exc:
except WorkflowError as exc:
print_error(str(exc))
ctx.exit(1)
return
Expand Down
3 changes: 2 additions & 1 deletion cli/localci/cli/logs.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from __future__ import annotations

import json
from pathlib import Path

import click
Expand Down Expand Up @@ -95,7 +96,7 @@ def logs(

try:
summary = ExecutionSummary.load(results_file)
except Exception as exc:
except (OSError, json.JSONDecodeError, KeyError, TypeError, ValueError) as exc:
print_error(f"Failed to load results: {exc}")
ctx.exit(1)
return
Expand Down
21 changes: 18 additions & 3 deletions cli/localci/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@

from localci import __version__
from localci.core.config import load_config
from localci.errors import (
ConfigError,
ConfigFileNotFoundError,
ConfigIOError,
ConfigValidationError,
)
from localci.utils.output import configure_console, console, print_error

# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -58,12 +64,21 @@ def cli(
# Load configuration – surface any validation errors immediately.
try:
cfg = load_config(config_path)
except FileNotFoundError as exc:
except ConfigFileNotFoundError as exc:
print_error(str(exc))
ctx.exit(1)
return
except Exception as exc: # noqa: BLE001
print_error(f"Failed to load config: {exc}")
except ConfigIOError as exc:
print_error(f"Cannot read config file {exc.path}: {exc.cause}")
ctx.exit(1)
return
except ConfigValidationError as exc:
location = f" {exc.path}" if exc.path else ""
print_error(f"Invalid config{location}: {exc.cause}")
ctx.exit(1)
return
except ConfigError as exc:
print_error(str(exc))
ctx.exit(1)
return

Expand Down
11 changes: 7 additions & 4 deletions cli/localci/cli/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
DockerNotAvailableError,
JobExecutor,
)
from localci.errors import WorkflowError
from localci.core.models import JobEvent, JobEventType
from localci.core.orchestrator import (
OrchestratorConfig,
Expand Down Expand Up @@ -158,8 +159,8 @@ def run(
try:
analyzer = WorkflowAnalyzer()
wf = analyzer.analyze(workflow_path)
except Exception as exc:
print_error(f"Failed to parse workflow: {exc}")
except WorkflowError as exc:
print_error(str(exc))
ctx.exit(1)
return

Expand Down Expand Up @@ -210,7 +211,7 @@ def run(
seen.add((jid, e.index))
continue
except ValueError:
pass
pass # not a numeric job index; treat *j* as a name substring below
j_lower = j.lower()
for jid, e in selected:
if j_lower in e.name.lower() and (jid, e.index) not in seen:
Expand Down Expand Up @@ -387,8 +388,10 @@ def run(
summary.save(execution_file)
print_info(f"Results saved to {last_run_file}")
print_info(f"Execution ID: {summary.execution_id} (use with status -e or logs -e)")
except Exception as exc:
except OSError as exc:
print_warning(f"Could not save results: {exc}")
except (TypeError, ValueError) as exc:
print_warning(f"Could not serialize results for save: {exc}")

if not summary.all_passed:
ctx.exit(1)
Expand Down
4 changes: 2 additions & 2 deletions cli/localci/cli/status.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ def status(

try:
summary = ExecutionSummary.load(results_file)
except Exception as exc:
except (OSError, json.JSONDecodeError, KeyError, TypeError, ValueError) as exc:
print_error(f"Failed to load results: {exc}")
ctx.exit(1)
return
Expand Down Expand Up @@ -270,4 +270,4 @@ def _follow_status(status_file: Path, output_format: str) -> None:
else:
_print_status_table(last_data)
except KeyboardInterrupt:
pass
pass # intentional: quiet exit after follow mode
8 changes: 4 additions & 4 deletions cli/localci/core/__init__.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
"""Core modules for Local CI."""

from localci.core.workflow import (
from localci.errors import (
ActNotFoundError,
CyclicDependencyError,
DockerNotAvailableError,
MissingFieldError,
UnsupportedMatrixError,
WorkflowError,
WorkflowParseError,
)
from localci.core.executor import ( # noqa: F401
ActCommand,
ActNotFoundError,
DockerNotAvailableError,
JobExecutor,
JobResult,
JobStatus,
Expand All @@ -23,7 +24,6 @@
QueuedJobStatus,
)
from localci.core.queue import ( # noqa: F401
CyclicDependencyError,
DependencyResolver,
PriorityConfig,
PriorityJobQueue,
Expand Down
20 changes: 14 additions & 6 deletions cli/localci/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
from typing import Any, Optional

import yaml
from pydantic import BaseModel, Field, field_validator
from pydantic import BaseModel, Field, ValidationError, field_validator

from localci.errors import ConfigFileNotFoundError, ConfigIOError, ConfigValidationError

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -372,7 +374,7 @@ def load_config(path: Path | str | None = None) -> LocalCIConfig:
if path is not None:
config_path = Path(path)
if not config_path.is_file():
raise FileNotFoundError(f"Config file not found: {config_path}")
raise ConfigFileNotFoundError(config_path)
else:
config_path = find_config_file()

Expand All @@ -381,10 +383,16 @@ def load_config(path: Path | str | None = None) -> LocalCIConfig:
return LocalCIConfig()

logger.debug("Loading config from %s", config_path)
with open(config_path, "r", encoding="utf-8") as fh:
raw: dict[str, Any] = yaml.safe_load(fh) or {}

return LocalCIConfig.model_validate(raw)
try:
with open(config_path, "r", encoding="utf-8") as fh:
raw: dict[str, Any] = yaml.safe_load(fh) or {}
except (OSError, yaml.YAMLError) as exc:
raise ConfigIOError(config_path, exc) from exc

try:
return LocalCIConfig.model_validate(raw)
except ValidationError as exc:
raise ConfigValidationError(config_path, exc) from exc
Comment thread
coderabbitai[bot] marked this conversation as resolved.


def default_config_yaml() -> str:
Expand Down
28 changes: 2 additions & 26 deletions cli/localci/core/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
from pathlib import Path
from typing import IO, Callable, Optional

from localci.errors import ActNotFoundError, DockerNotAvailableError

logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -268,32 +270,6 @@ def __str__(self) -> str:
return self.display()


# =====================================================================
# Errors
# =====================================================================


class ActNotFoundError(RuntimeError):
"""``act`` is not installed."""

def __init__(self) -> None:
super().__init__(
"act is not installed.\n"
"Install with:\n"
" Windows: choco install act-cli\n"
" Linux: curl -s https://raw.githubusercontent.com/nektos/act/"
"master/install.sh | sudo bash\n"
" macOS: brew install act"
)


class DockerNotAvailableError(RuntimeError):
"""Docker daemon is not running or not installed."""

def __init__(self, detail: str = "Docker daemon is not running") -> None:
super().__init__(detail)


# =====================================================================
# JobExecutor
# =====================================================================
Expand Down
9 changes: 1 addition & 8 deletions cli/localci/core/queue.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
QueuedJob,
QueuedJobStatus,
)
from localci.errors import CyclicDependencyError

if TYPE_CHECKING:
from localci.core.config import LocalCIConfig
Expand Down Expand Up @@ -84,14 +85,6 @@ def from_config(cls, config: "LocalCIConfig") -> PriorityConfig:
# ---------------------------------------------------------------------------


class CyclicDependencyError(Exception):
"""Circular dependency detected in job graph."""

def __init__(self, job_id: str):
super().__init__(f"Cyclic dependency detected involving job: {job_id}")
self.job_id = job_id


class DependencyResolver:
"""Resolve job dependencies using topological sort.

Expand Down
Loading
Loading