ACTIVITY-WP-0044: disabled repo-manager-bounded-freshness draft (RMGR-WP-0020-T03)
Register bounded operation repo_manager_freshness (single attempt, 300 s, code-fixed 'rmgr freshness run --limit N --max-seconds S [--dry-run]') and add the disabled cron definition (40 */2 * * * UTC, limit 6, max_seconds 240). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 692292@bnt-lap001 Assistant-Session: 89c7d8e8-e1b3-47eb-9411-a0ab227fca2f
This commit is contained in:
parent
c6df768ef0
commit
ba4ef00128
9 changed files with 449 additions and 2 deletions
|
|
@ -254,6 +254,10 @@ async def execute_bounded_operation(payload: dict[str, Any]) -> dict[str, Any]:
|
|||
from activity_core.context_resolvers.cnpg_backup import cnpg_option_a_backup
|
||||
|
||||
result = cnpg_option_a_backup(params)
|
||||
elif spec.operation_id == "repo_manager_freshness":
|
||||
from activity_core.context_resolvers.repo_manager import repo_manager_freshness
|
||||
|
||||
result = repo_manager_freshness(params)
|
||||
else:
|
||||
raise ApplicationError(
|
||||
f"bounded operation {spec.operation_id!r} uses a dedicated activity",
|
||||
|
|
|
|||
|
|
@ -60,6 +60,19 @@ _SPECS = (
|
|||
credential_route="railiance-cnpg-option-a-backup",
|
||||
evidence_mode="context-evidence-sink",
|
||||
),
|
||||
# RMGR-WP-0020-T03 / ACTIVITY-WP-0044: Repo Manager owns the freshness
|
||||
# engine; Activity Core only invokes its fixed CLI with bounded arguments.
|
||||
BoundedOperationSpec(
|
||||
operation_id="repo_manager_freshness",
|
||||
source_type="repo-manager",
|
||||
query="freshness_run",
|
||||
resolve_before_execute=False,
|
||||
max_timeout_seconds=300,
|
||||
temporal_max_attempts=1,
|
||||
idempotency="single Temporal attempt; per-commit projections are idempotent (RMGR-WP-0017)",
|
||||
credential_route="repo-manager-freshness-runner",
|
||||
evidence_mode="repo-manager-freshness-receipt",
|
||||
),
|
||||
)
|
||||
|
||||
BOUNDED_OPERATION_REGISTRY = {spec.operation_id: spec for spec in _SPECS}
|
||||
|
|
@ -80,6 +93,12 @@ _CANONICAL_SCRIPT_PATHS = {
|
|||
"forgejo_package_prune": "/opt/railiance-platform/tools/cmd/forgejo-package-prune",
|
||||
"cnpg_option_a_backup": "/opt/railiance-platform/tools/cmd/cnpg-option-a-backup",
|
||||
}
|
||||
# repo-manager.freshness-receipt.v1 outcomes (RMGR-WP-0020-T01).
|
||||
REPO_MANAGER_FRESHNESS_OUTCOMES = (
|
||||
"applied", "noop", "queued", "failed", "refused", "skipped",
|
||||
)
|
||||
REPO_MANAGER_FRESHNESS_MAX_LIMIT = 12
|
||||
REPO_MANAGER_FRESHNESS_MAX_SECONDS = 270
|
||||
_TARGET_RE = re.compile(r"^[A-Za-z0-9_.-]+$")
|
||||
|
||||
|
||||
|
|
@ -136,6 +155,26 @@ def normalize_operation_result(operation_id: str, raw: Any) -> dict[str, Any]:
|
|||
"failed": _safe_count(result.get("failed")),
|
||||
"script_exit_code": exit_code,
|
||||
}
|
||||
if operation_id == "repo_manager_freshness":
|
||||
receipts = result.get("receipts")
|
||||
outcomes = {name: 0 for name in REPO_MANAGER_FRESHNESS_OUTCOMES}
|
||||
if isinstance(receipts, list):
|
||||
for receipt in receipts:
|
||||
outcome = receipt.get("outcome") if isinstance(receipt, dict) else None
|
||||
if outcome in outcomes:
|
||||
outcomes[outcome] += 1
|
||||
exit_code = result.get("exit_code", 0)
|
||||
if isinstance(exit_code, bool) or not isinstance(exit_code, int):
|
||||
exit_code = 1
|
||||
return {
|
||||
"kind": operation_id,
|
||||
"apply": bool(result.get("apply")),
|
||||
"limit": _safe_count(result.get("limit")),
|
||||
"max_seconds": _safe_count(result.get("max_seconds")),
|
||||
"receipt_count": len(receipts) if isinstance(receipts, list) else 0,
|
||||
"outcomes": outcomes,
|
||||
"exit_code": exit_code,
|
||||
}
|
||||
raise ValueError(f"no result projection for bounded operation {operation_id!r}")
|
||||
|
||||
|
||||
|
|
@ -192,6 +231,8 @@ def validate_bounded_operations(
|
|||
_validate_prune(params)
|
||||
elif spec.operation_id == "cnpg_option_a_backup":
|
||||
_validate_backup(params)
|
||||
elif spec.operation_id == "repo_manager_freshness":
|
||||
_validate_repo_manager_freshness(params)
|
||||
|
||||
|
||||
def _validate_timeout(spec: BoundedOperationSpec, params: dict[str, Any]) -> None:
|
||||
|
|
@ -259,6 +300,36 @@ def _validate_backup(params: dict[str, Any]) -> None:
|
|||
_require_context_evidence(params, "cnpg_option_a_backup")
|
||||
|
||||
|
||||
def _validate_repo_manager_freshness(params: dict[str, Any]) -> None:
|
||||
_explicit_bool(params, "apply", "repo_manager_freshness")
|
||||
limit = params.get("limit")
|
||||
if (
|
||||
isinstance(limit, bool)
|
||||
or not isinstance(limit, int)
|
||||
or not 1 <= limit <= REPO_MANAGER_FRESHNESS_MAX_LIMIT
|
||||
):
|
||||
raise ValueError(
|
||||
"bounded operation repo_manager_freshness limit must be in "
|
||||
f"1..{REPO_MANAGER_FRESHNESS_MAX_LIMIT}"
|
||||
)
|
||||
max_seconds = params.get("max_seconds")
|
||||
if (
|
||||
isinstance(max_seconds, bool)
|
||||
or not isinstance(max_seconds, int)
|
||||
or not 1 <= max_seconds <= REPO_MANAGER_FRESHNESS_MAX_SECONDS
|
||||
):
|
||||
raise ValueError(
|
||||
"bounded operation repo_manager_freshness max_seconds must be in "
|
||||
f"1..{REPO_MANAGER_FRESHNESS_MAX_SECONDS}"
|
||||
)
|
||||
for forbidden in ("command", "argv", "rmgr_bin", "script"):
|
||||
if forbidden in params:
|
||||
raise ValueError(
|
||||
f"bounded operation repo_manager_freshness must not declare {forbidden}; "
|
||||
"the CLI invocation is code-fixed"
|
||||
)
|
||||
|
||||
|
||||
def _validate_canonical_script(
|
||||
params: dict[str, Any], field: str, operation_id: str
|
||||
) -> None:
|
||||
|
|
|
|||
63
src/activity_core/context_resolvers/repo_manager.py
Normal file
63
src/activity_core/context_resolvers/repo_manager.py
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
"""Repo Manager bounded freshness invocation (RMGR-WP-0020-T03, ACTIVITY-WP-0044).
|
||||
|
||||
Invokes the Repo Manager CLI ``rmgr freshness run --limit N --max-seconds S
|
||||
[--dry-run]`` (RMGR-WP-0020-T02 interface). The argv is code-fixed: a
|
||||
definition supplies only the validated ``limit``, ``max_seconds`` and ``apply``
|
||||
values. The executable is ``rmgr`` on PATH unless the worker sets
|
||||
``ACTIVITY_CORE_RMGR_BIN``; definitions cannot choose it. Stdout is expected to
|
||||
be a JSON object carrying ``receipts`` (``repo-manager.freshness-receipt.v1``);
|
||||
only the counted projection in ``bounded_operations`` is persisted.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
from typing import Any
|
||||
|
||||
# Grace on top of the CLI's own --max-seconds budget for start-up/teardown.
|
||||
_TIMEOUT_GRACE_SECONDS = 30
|
||||
|
||||
|
||||
def build_freshness_argv(params: dict[str, Any]) -> list[str]:
|
||||
limit = int(params["limit"])
|
||||
max_seconds = int(params["max_seconds"])
|
||||
argv = [
|
||||
os.environ.get("ACTIVITY_CORE_RMGR_BIN", "rmgr"),
|
||||
"freshness",
|
||||
"run",
|
||||
"--limit",
|
||||
str(limit),
|
||||
"--max-seconds",
|
||||
str(max_seconds),
|
||||
]
|
||||
if params.get("apply") is not True:
|
||||
argv.append("--dry-run")
|
||||
return argv
|
||||
|
||||
|
||||
def repo_manager_freshness(params: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Run one bounded freshness pass and return its JSON summary."""
|
||||
argv = build_freshness_argv(params)
|
||||
timeout = int(params["max_seconds"]) + _TIMEOUT_GRACE_SECONDS
|
||||
completed = subprocess.run(
|
||||
argv, capture_output=True, text=True, timeout=timeout, check=False
|
||||
)
|
||||
stdout = (completed.stdout or "").strip()
|
||||
if completed.returncode != 0 and not stdout:
|
||||
raise RuntimeError(
|
||||
f"repo_manager_freshness failed (exit {completed.returncode}): "
|
||||
f"{(completed.stderr or '').strip()[:300]}"
|
||||
)
|
||||
try:
|
||||
summary = json.loads(stdout)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise RuntimeError(f"repo_manager_freshness returned invalid JSON: {exc}") from exc
|
||||
if not isinstance(summary, dict):
|
||||
raise RuntimeError("repo_manager_freshness output must be a JSON object")
|
||||
summary.setdefault("apply", params.get("apply") is True)
|
||||
summary.setdefault("limit", int(params["limit"]))
|
||||
summary.setdefault("max_seconds", int(params["max_seconds"]))
|
||||
summary["exit_code"] = completed.returncode
|
||||
return summary
|
||||
|
|
@ -40,6 +40,7 @@ _AUDIT_BUFFER: deque[dict[str, Any]] = deque(maxlen=100)
|
|||
SIDE_EFFECT_MARKERS = (
|
||||
"forgejo_package_prune",
|
||||
"cnpg_option_a_backup",
|
||||
"repo_manager_freshness",
|
||||
"apply: true",
|
||||
'"apply": true',
|
||||
"'apply': true",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue