feat: consolidate legacy workflows into gyxx-flow
This commit is contained in:
commit
c23b62a8c8
374 files changed
+132990
No files matched your search
@@ -0,0 +1,101 @@
|
||||
"""Built-in executable workflow steps."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Mapping
|
||||
|
||||
from gyxx_flow.core.context import RunContext
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class StepExecution:
|
||||
"""One action invocation outcome before retry aggregation."""
|
||||
|
||||
exit_code: int
|
||||
stdout: str = ""
|
||||
stderr: str = ""
|
||||
error: str | None = None
|
||||
skipped: bool = False
|
||||
reason: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class CommandStep:
|
||||
"""Execute an argv command without shell parsing or implicit process context."""
|
||||
|
||||
argv: tuple[str, ...]
|
||||
cwd: Path
|
||||
env: Mapping[str, str]
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
object.__setattr__(self, "argv", tuple(self.argv))
|
||||
object.__setattr__(self, "cwd", Path(self.cwd))
|
||||
object.__setattr__(self, "env", dict(self.env))
|
||||
if not self.argv or not all(
|
||||
isinstance(argument, str) and argument and "\x00" not in argument
|
||||
for argument in self.argv
|
||||
):
|
||||
raise ValueError("argv must contain non-empty strings")
|
||||
if not all(
|
||||
isinstance(key, str)
|
||||
and key
|
||||
and "\x00" not in key
|
||||
and isinstance(value, str)
|
||||
and "\x00" not in value
|
||||
for key, value in self.env.items()
|
||||
):
|
||||
raise ValueError("env must contain valid string keys and values")
|
||||
|
||||
def execute(
|
||||
self,
|
||||
*,
|
||||
context: RunContext,
|
||||
timeout_seconds: float | None,
|
||||
dry_run: bool,
|
||||
) -> StepExecution:
|
||||
del context
|
||||
if dry_run:
|
||||
return StepExecution(exit_code=0, skipped=True, reason="dry-run")
|
||||
try:
|
||||
completed = subprocess.run(
|
||||
list(self.argv),
|
||||
cwd=self.cwd,
|
||||
env=dict(self.env),
|
||||
shell=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout_seconds,
|
||||
check=False,
|
||||
)
|
||||
except subprocess.TimeoutExpired as exc:
|
||||
stdout = _to_text(exc.stdout)
|
||||
stderr = _to_text(exc.stderr)
|
||||
return StepExecution(
|
||||
exit_code=124,
|
||||
stdout=stdout,
|
||||
stderr=stderr,
|
||||
error=f"command timed out after {exc.timeout} seconds",
|
||||
)
|
||||
except OSError as exc:
|
||||
return StepExecution(exit_code=127, error=f"cannot start command: {exc}")
|
||||
|
||||
error = None
|
||||
if completed.returncode != 0:
|
||||
error = completed.stderr.strip() or f"command exited with code {completed.returncode}"
|
||||
return StepExecution(
|
||||
exit_code=completed.returncode,
|
||||
stdout=completed.stdout,
|
||||
stderr=completed.stderr,
|
||||
error=error,
|
||||
)
|
||||
|
||||
|
||||
def _to_text(value: str | bytes | None) -> str:
|
||||
if value is None:
|
||||
return ""
|
||||
if isinstance(value, bytes):
|
||||
return value.decode(errors="replace")
|
||||
return value
|
||||
Reference in new issue
Block a user