cherry-docs 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- app/__init__.py +0 -0
- app/repo_scope.py +24 -0
- app/services/__init__.py +0 -0
- app/services/agent_protocol.py +59 -0
- app/services/auto_promote_sessions.py +245 -0
- app/services/capture_adapters.py +89 -0
- app/services/capture_core.py +164 -0
- app/services/internal_memory_agent.py +214 -0
- app/services/memory_evidence.py +89 -0
- app/services/memory_extraction_normalize.py +134 -0
- app/services/memory_lifecycle.py +258 -0
- app/services/memory_profiles.py +88 -0
- app/services/memory_providers.py +113 -0
- app/services/memory_retrieval.py +327 -0
- app/services/memory_retrieval_scoring.py +106 -0
- app/services/memory_retrieval_text.py +113 -0
- app/services/memory_similarity.py +135 -0
- app/services/privacy.py +72 -0
- app/services/promoted_memory_answer.py +157 -0
- app/services/promoted_memory_pipeline.py +194 -0
- app/services/promoted_memory_store.py +57 -0
- cherry_docs-0.2.0.dist-info/METADATA +143 -0
- cherry_docs-0.2.0.dist-info/RECORD +42 -0
- cherry_docs-0.2.0.dist-info/WHEEL +5 -0
- cherry_docs-0.2.0.dist-info/entry_points.txt +4 -0
- cherry_docs-0.2.0.dist-info/top_level.txt +3 -0
- cherrydocs/__init__.py +3 -0
- cherrydocs/cli.py +213 -0
- cherrydocs/hook.py +27 -0
- cherrydocs/mcp.py +22 -0
- scripts/__init__.py +0 -0
- scripts/auto_promote_capture.py +63 -0
- scripts/check_size_limits.py +115 -0
- scripts/ci_auto_capture.py +289 -0
- scripts/claude_hooks/__init__.py +0 -0
- scripts/claude_hooks/state_manager.py +526 -0
- scripts/coverage_regression_gate.py +121 -0
- scripts/eval_projects.py +247 -0
- scripts/install.py +212 -0
- scripts/pr_gate_report.py +282 -0
- scripts/promptfoo_regression_gate.py +176 -0
- scripts/render_agent_prompts.py +57 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Code size guardrails:
|
|
4
|
+
- max 250 lines per Python file
|
|
5
|
+
- max 100 lines per function/method
|
|
6
|
+
|
|
7
|
+
Existing legacy violations can be listed in scripts/code_size_allowlist.txt.
|
|
8
|
+
New violations fail CI immediately.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import ast
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Iterable, List, Set, Tuple
|
|
17
|
+
|
|
18
|
+
MAX_FILE_LINES = 250
|
|
19
|
+
MAX_FUNCTION_LINES = 100
|
|
20
|
+
ALLOWLIST_PATH = Path("scripts/code_size_allowlist.txt")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _load_allowlist(path: Path) -> Set[str]:
|
|
24
|
+
if not path.exists():
|
|
25
|
+
return set()
|
|
26
|
+
return {
|
|
27
|
+
line.strip()
|
|
28
|
+
for line in path.read_text(encoding="utf-8").splitlines()
|
|
29
|
+
if line.strip() and not line.strip().startswith("#")
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _iter_python_files(root: Path) -> Iterable[Path]:
|
|
34
|
+
return sorted(p for p in root.rglob("*.py") if p.is_file())
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _collect_violations(root: Path) -> List[str]:
|
|
38
|
+
violations: List[str] = []
|
|
39
|
+
for file_path in _iter_python_files(root):
|
|
40
|
+
text = file_path.read_text(encoding="utf-8")
|
|
41
|
+
lines = text.splitlines()
|
|
42
|
+
rel = file_path.as_posix()
|
|
43
|
+
|
|
44
|
+
if len(lines) > MAX_FILE_LINES:
|
|
45
|
+
violations.append(f"file:{rel}:{len(lines)}")
|
|
46
|
+
|
|
47
|
+
tree = ast.parse(text)
|
|
48
|
+
for node in ast.walk(tree):
|
|
49
|
+
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
50
|
+
continue
|
|
51
|
+
if not getattr(node, "end_lineno", None):
|
|
52
|
+
continue
|
|
53
|
+
span = node.end_lineno - node.lineno + 1
|
|
54
|
+
if span > MAX_FUNCTION_LINES:
|
|
55
|
+
violations.append(f"func:{rel}:{node.name}:{node.lineno}:{span}")
|
|
56
|
+
return sorted(violations)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _split_violations(
|
|
60
|
+
violations: List[str],
|
|
61
|
+
allowlist: Set[str],
|
|
62
|
+
) -> Tuple[List[str], List[str]]:
|
|
63
|
+
unexpected = [v for v in violations if v not in allowlist]
|
|
64
|
+
resolved = sorted(item for item in allowlist if item not in violations)
|
|
65
|
+
return unexpected, resolved
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def main() -> int:
|
|
69
|
+
parser = argparse.ArgumentParser(description="Enforce code size limits.")
|
|
70
|
+
parser.add_argument("--root", default="app", help="Path to scan (default: app)")
|
|
71
|
+
parser.add_argument(
|
|
72
|
+
"--strict",
|
|
73
|
+
action="store_true",
|
|
74
|
+
help="Fail on all violations, including allowlisted legacy ones.",
|
|
75
|
+
)
|
|
76
|
+
args = parser.parse_args()
|
|
77
|
+
|
|
78
|
+
root = Path(args.root)
|
|
79
|
+
if not root.exists():
|
|
80
|
+
print(f"ERROR: root path not found: {root}")
|
|
81
|
+
return 2
|
|
82
|
+
|
|
83
|
+
violations = _collect_violations(root)
|
|
84
|
+
allowlist = _load_allowlist(ALLOWLIST_PATH)
|
|
85
|
+
|
|
86
|
+
if args.strict:
|
|
87
|
+
unexpected = violations
|
|
88
|
+
resolved = []
|
|
89
|
+
else:
|
|
90
|
+
unexpected, resolved = _split_violations(violations, allowlist)
|
|
91
|
+
|
|
92
|
+
if violations:
|
|
93
|
+
print("Code size violations:")
|
|
94
|
+
for item in violations:
|
|
95
|
+
print(f" - {item}")
|
|
96
|
+
else:
|
|
97
|
+
print("No code size violations found.")
|
|
98
|
+
|
|
99
|
+
if resolved:
|
|
100
|
+
print("\nAllowlist entries now resolved (remove these lines):")
|
|
101
|
+
for item in resolved:
|
|
102
|
+
print(f" - {item}")
|
|
103
|
+
|
|
104
|
+
if unexpected:
|
|
105
|
+
print("\nERROR: New/unallowlisted size violations found.")
|
|
106
|
+
if not args.strict:
|
|
107
|
+
print(f"Add legacy-only items to {ALLOWLIST_PATH} if unavoidable.")
|
|
108
|
+
return 1
|
|
109
|
+
|
|
110
|
+
print("\nOK: No new size violations.")
|
|
111
|
+
return 0
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
if __name__ == "__main__":
|
|
115
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Auto-capture GitHub CI/PR lifecycle events into CherryDocs memory."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import urllib.error
|
|
9
|
+
import urllib.request
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Dict
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def normalize_workspace_repo(raw: str | None) -> str:
|
|
16
|
+
value = (raw or "").strip()
|
|
17
|
+
if not value:
|
|
18
|
+
return "github.com/unknown/unknown"
|
|
19
|
+
if value.startswith("github.com/"):
|
|
20
|
+
return value
|
|
21
|
+
return f"github.com/{value}"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def read_event_context() -> Dict[str, Any]:
|
|
25
|
+
payload: Dict[str, Any] = {}
|
|
26
|
+
payload_path = os.getenv("GITHUB_EVENT_PATH")
|
|
27
|
+
if payload_path and Path(payload_path).exists():
|
|
28
|
+
try:
|
|
29
|
+
payload = json.loads(Path(payload_path).read_text())
|
|
30
|
+
except Exception:
|
|
31
|
+
payload = {}
|
|
32
|
+
|
|
33
|
+
pull_request = payload.get("pull_request") or {}
|
|
34
|
+
pr_number = pull_request.get("number")
|
|
35
|
+
pr_title = pull_request.get("title")
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
"event_name": os.getenv("GITHUB_EVENT_NAME", ""),
|
|
39
|
+
"event_action": payload.get("action", ""),
|
|
40
|
+
"workflow": os.getenv("GITHUB_WORKFLOW", ""),
|
|
41
|
+
"repository": os.getenv("GITHUB_REPOSITORY", ""),
|
|
42
|
+
"sha": os.getenv("GITHUB_SHA", ""),
|
|
43
|
+
"ref": os.getenv("GITHUB_REF_NAME", ""),
|
|
44
|
+
"run_id": os.getenv("GITHUB_RUN_ID", ""),
|
|
45
|
+
"run_attempt": os.getenv("GITHUB_RUN_ATTEMPT", ""),
|
|
46
|
+
"server_url": os.getenv("GITHUB_SERVER_URL", "https://github.com"),
|
|
47
|
+
"pr_number": pr_number,
|
|
48
|
+
"pr_title": pr_title,
|
|
49
|
+
"pr_base_ref": pull_request.get("base", {}).get("ref"),
|
|
50
|
+
"pr_head_ref": pull_request.get("head", {}).get("ref"),
|
|
51
|
+
"pr_changed_files": pull_request.get("changed_files"),
|
|
52
|
+
"pr_additions": pull_request.get("additions"),
|
|
53
|
+
"pr_deletions": pull_request.get("deletions"),
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def classify_failure_modes(statuses: Dict[str, str], gate_summary: Dict[str, Any] | None = None) -> list[str]:
|
|
58
|
+
classes: list[str] = []
|
|
59
|
+
if statuses.get("quality_fast") and statuses["quality_fast"] != "success":
|
|
60
|
+
classes.append("quality_fast_failure")
|
|
61
|
+
if statuses.get("quality_integration") and statuses["quality_integration"] != "success":
|
|
62
|
+
classes.append("integration_failure")
|
|
63
|
+
if statuses.get("security_scan") and statuses["security_scan"] == "failure":
|
|
64
|
+
classes.append("security_scan_failure")
|
|
65
|
+
if statuses.get("typecheck_targeted") and statuses["typecheck_targeted"] == "failure":
|
|
66
|
+
classes.append("typecheck_failure")
|
|
67
|
+
if statuses.get("duplication_check") and statuses["duplication_check"] == "failure":
|
|
68
|
+
classes.append("duplication_failure")
|
|
69
|
+
if statuses.get("pr_gate_report") and statuses["pr_gate_report"] == "failure":
|
|
70
|
+
classes.append("report_failure")
|
|
71
|
+
if statuses.get("eval_secret_check") and statuses["eval_secret_check"] == "failure":
|
|
72
|
+
classes.append("eval_secret_config_failure")
|
|
73
|
+
if statuses.get("eval_smoke") and statuses["eval_smoke"] == "failure":
|
|
74
|
+
classes.append("eval_smoke_failure")
|
|
75
|
+
if statuses.get("eval_golden_memory") and statuses["eval_golden_memory"] == "failure":
|
|
76
|
+
classes.append("eval_golden_memory_failure")
|
|
77
|
+
if statuses.get("eval_pr_local") and statuses["eval_pr_local"] == "failure":
|
|
78
|
+
classes.append("eval_pr_local_failure")
|
|
79
|
+
return classes
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _read_json(path: Path) -> Dict[str, Any] | None:
|
|
83
|
+
if not path.exists():
|
|
84
|
+
return None
|
|
85
|
+
try:
|
|
86
|
+
return json.loads(path.read_text())
|
|
87
|
+
except Exception:
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def load_gate_summary(artifacts_root: str) -> Dict[str, Any] | None:
|
|
92
|
+
root = Path(artifacts_root)
|
|
93
|
+
if not root.exists():
|
|
94
|
+
return None
|
|
95
|
+
return _read_json(root / "pr-gate-report" / "pr-gate-report.json")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def summarize_gate_summary(summary: Dict[str, Any] | None) -> str:
|
|
99
|
+
if not summary:
|
|
100
|
+
return "PR Gate Summary: unavailable"
|
|
101
|
+
|
|
102
|
+
lines: list[str] = []
|
|
103
|
+
coverage = summary.get("coverage") or {}
|
|
104
|
+
if coverage:
|
|
105
|
+
lines.append(
|
|
106
|
+
"Coverage: "
|
|
107
|
+
f"line {float(coverage.get('line_rate', 0.0)):.2f}% / "
|
|
108
|
+
f"branch {float(coverage.get('branch_rate', 0.0)):.2f}%"
|
|
109
|
+
)
|
|
110
|
+
coverage_regression = summary.get("coverage_regression") or {}
|
|
111
|
+
if coverage_regression:
|
|
112
|
+
lines.append(
|
|
113
|
+
"Coverage Regression: "
|
|
114
|
+
f"baseline {float(coverage_regression.get('baseline_line_rate', 0.0)):.2f}% -> "
|
|
115
|
+
f"candidate {float(coverage_regression.get('candidate_line_rate', 0.0)):.2f}% "
|
|
116
|
+
f"(drop {float(coverage_regression.get('line_rate_drop', 0.0)):.2f})"
|
|
117
|
+
)
|
|
118
|
+
duplication = summary.get("duplication") or {}
|
|
119
|
+
if duplication:
|
|
120
|
+
lines.append(
|
|
121
|
+
f"Duplication: {float(duplication.get('percentage', 0.0)):.2f}% "
|
|
122
|
+
f"({duplication.get('duplicated_lines', 0)} duplicated lines)"
|
|
123
|
+
)
|
|
124
|
+
promptfoo = summary.get("promptfoo") or {}
|
|
125
|
+
if promptfoo:
|
|
126
|
+
lines.append(
|
|
127
|
+
"Promptfoo: "
|
|
128
|
+
f"score {float(promptfoo.get('overall_score', 0.0)):.3f}, "
|
|
129
|
+
f"{promptfoo.get('passing_cases', 0)} passing / {promptfoo.get('failing_cases', 0)} failing"
|
|
130
|
+
)
|
|
131
|
+
security = summary.get("security") or {}
|
|
132
|
+
bandit = security.get("bandit") or {}
|
|
133
|
+
gitleaks = security.get("gitleaks") or {}
|
|
134
|
+
semgrep = security.get("semgrep") or {}
|
|
135
|
+
if bandit or gitleaks or semgrep:
|
|
136
|
+
lines.append(
|
|
137
|
+
"Security: "
|
|
138
|
+
f"bandit_high={bandit.get('high', 0)}, "
|
|
139
|
+
f"gitleaks={gitleaks.get('findings', 0)}, "
|
|
140
|
+
f"semgrep_error={semgrep.get('error', 0)}"
|
|
141
|
+
)
|
|
142
|
+
return "PR Gate Summary:\n" + ("\n".join(lines) if lines else "No aggregated artifact data available")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def build_capture_payload(args: argparse.Namespace, ctx: Dict[str, Any]) -> Dict[str, Any]:
|
|
146
|
+
workspace_repo = normalize_workspace_repo(args.workspace_repo or ctx.get("repository"))
|
|
147
|
+
run_url = f"{ctx['server_url']}/{ctx['repository']}/actions/runs/{ctx['run_id']}" if ctx.get("run_id") else ""
|
|
148
|
+
|
|
149
|
+
statuses = {
|
|
150
|
+
"quality_fast": args.quality_fast,
|
|
151
|
+
"quality_integration": args.quality_integration,
|
|
152
|
+
"security_scan": args.security_scan,
|
|
153
|
+
"typecheck_targeted": args.typecheck_targeted,
|
|
154
|
+
"duplication_check": args.duplication_check,
|
|
155
|
+
"pr_gate_report": args.pr_gate_report,
|
|
156
|
+
"eval_secret_check": args.eval_secret_check,
|
|
157
|
+
"eval_smoke": args.eval_smoke,
|
|
158
|
+
"eval_golden_memory": args.eval_golden_memory,
|
|
159
|
+
"eval_pr_local": args.eval_pr_local,
|
|
160
|
+
}
|
|
161
|
+
status_line = ", ".join(f"{k}={v}" for k, v in statuses.items() if v)
|
|
162
|
+
gate_summary = load_gate_summary(args.artifacts_root)
|
|
163
|
+
failure_classes = classify_failure_modes(statuses, gate_summary)
|
|
164
|
+
event_desc = f"{ctx.get('event_name')}:{ctx.get('event_action') or 'n/a'}"
|
|
165
|
+
pr_text = (
|
|
166
|
+
f"PR #{ctx['pr_number']} - {ctx['pr_title']}"
|
|
167
|
+
if ctx.get("pr_number")
|
|
168
|
+
else "No PR context"
|
|
169
|
+
)
|
|
170
|
+
pr_scope = ""
|
|
171
|
+
if ctx.get("pr_number"):
|
|
172
|
+
pr_scope = (
|
|
173
|
+
f"PR Base: {ctx.get('pr_base_ref')}\n"
|
|
174
|
+
f"PR Head: {ctx.get('pr_head_ref')}\n"
|
|
175
|
+
f"PR Changed Files: {ctx.get('pr_changed_files')}\n"
|
|
176
|
+
f"PR Additions/Deletions: +{ctx.get('pr_additions')} / -{ctx.get('pr_deletions')}\n"
|
|
177
|
+
)
|
|
178
|
+
gate_summary_text = summarize_gate_summary(gate_summary)
|
|
179
|
+
|
|
180
|
+
summary = (
|
|
181
|
+
f"CI auto-capture [{ctx.get('workflow', 'workflow')}] "
|
|
182
|
+
f"{ctx.get('event_name', 'event')} on {ctx.get('ref', '?')} ({args.capture_status})"
|
|
183
|
+
f"{f' [{failure_classes[0]}]' if failure_classes else ''}"
|
|
184
|
+
)
|
|
185
|
+
details = (
|
|
186
|
+
f"Captured GitHub workflow lifecycle automatically.\n"
|
|
187
|
+
f"Workflow: {ctx.get('workflow')}\n"
|
|
188
|
+
f"Event: {event_desc}\n"
|
|
189
|
+
f"Repository: {ctx.get('repository')}\n"
|
|
190
|
+
f"Ref: {ctx.get('ref')}\n"
|
|
191
|
+
f"SHA: {ctx.get('sha')}\n"
|
|
192
|
+
f"Run URL: {run_url}\n"
|
|
193
|
+
f"Job statuses: {status_line or 'none'}\n"
|
|
194
|
+
f"Failure classes: {', '.join(failure_classes) if failure_classes else 'none'}\n"
|
|
195
|
+
f"{gate_summary_text}\n"
|
|
196
|
+
f"PR Context: {pr_text}\n"
|
|
197
|
+
f"{pr_scope}"
|
|
198
|
+
f"Captured At (UTC): {datetime.now(timezone.utc).isoformat()}"
|
|
199
|
+
)
|
|
200
|
+
tags = [
|
|
201
|
+
"auto-capture",
|
|
202
|
+
"ci",
|
|
203
|
+
f"workflow:{ctx.get('workflow', 'unknown')}",
|
|
204
|
+
f"event:{ctx.get('event_name', 'unknown')}",
|
|
205
|
+
f"status:{args.capture_status}",
|
|
206
|
+
]
|
|
207
|
+
tags.extend(f"failure:{item}" for item in failure_classes)
|
|
208
|
+
|
|
209
|
+
arguments = {
|
|
210
|
+
"_workspace_repo": workspace_repo,
|
|
211
|
+
"type": "episodic",
|
|
212
|
+
"summary": summary,
|
|
213
|
+
"details": details,
|
|
214
|
+
"reasoning": "Manual logging of pipeline events is unreliable; automatic capture prevents knowledge gaps.",
|
|
215
|
+
"tags": tags,
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
"jsonrpc": "2.0",
|
|
219
|
+
"id": 1,
|
|
220
|
+
"method": "tools/call",
|
|
221
|
+
"params": {"name": "log_activity", "arguments": arguments},
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def post_jsonrpc(url: str, api_key: str, payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
226
|
+
body = json.dumps(payload).encode("utf-8")
|
|
227
|
+
req = urllib.request.Request(
|
|
228
|
+
url,
|
|
229
|
+
data=body,
|
|
230
|
+
headers={
|
|
231
|
+
"Content-Type": "application/json",
|
|
232
|
+
"Authorization": f"Bearer {api_key}",
|
|
233
|
+
},
|
|
234
|
+
method="POST",
|
|
235
|
+
)
|
|
236
|
+
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
237
|
+
raw = resp.read().decode("utf-8")
|
|
238
|
+
return json.loads(raw)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def parse_args() -> argparse.Namespace:
|
|
242
|
+
parser = argparse.ArgumentParser(description="Auto-capture CI events to CherryDocs")
|
|
243
|
+
parser.add_argument("--workspace-repo", default="", help="Workspace repo (github.com/owner/repo)")
|
|
244
|
+
parser.add_argument("--artifacts-root", default="artifacts")
|
|
245
|
+
parser.add_argument("--capture-status", default="success", choices=["success", "failure"])
|
|
246
|
+
parser.add_argument("--quality-fast", default="")
|
|
247
|
+
parser.add_argument("--quality-integration", default="")
|
|
248
|
+
parser.add_argument("--security-scan", default="")
|
|
249
|
+
parser.add_argument("--typecheck-targeted", default="")
|
|
250
|
+
parser.add_argument("--duplication-check", default="")
|
|
251
|
+
parser.add_argument("--pr-gate-report", default="")
|
|
252
|
+
parser.add_argument("--eval-secret-check", default="")
|
|
253
|
+
parser.add_argument("--eval-smoke", default="")
|
|
254
|
+
parser.add_argument("--eval-golden-memory", default="")
|
|
255
|
+
parser.add_argument("--eval-pr-local", default="")
|
|
256
|
+
parser.add_argument("--golden-workflow", default="")
|
|
257
|
+
parser.add_argument("--mcp-url", default=os.getenv("CHERRY_MCP_URL", "https://cherry-docs.onrender.com/mcp"))
|
|
258
|
+
parser.add_argument("--api-key", default=os.getenv("CHERRY_API_KEY", ""))
|
|
259
|
+
return parser.parse_args()
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def main() -> int:
|
|
263
|
+
args = parse_args()
|
|
264
|
+
if not args.api_key:
|
|
265
|
+
print("ci-auto-capture: CHERRY_API_KEY missing; skipping.")
|
|
266
|
+
return 0
|
|
267
|
+
|
|
268
|
+
ctx = read_event_context()
|
|
269
|
+
payload = build_capture_payload(args, ctx)
|
|
270
|
+
try:
|
|
271
|
+
response = post_jsonrpc(args.mcp_url, args.api_key, payload)
|
|
272
|
+
except (urllib.error.URLError, TimeoutError) as exc:
|
|
273
|
+
print(f"ci-auto-capture: network error while posting log: {exc}")
|
|
274
|
+
return 0
|
|
275
|
+
except Exception as exc:
|
|
276
|
+
print(f"ci-auto-capture: unexpected error while posting log: {exc}")
|
|
277
|
+
return 0
|
|
278
|
+
|
|
279
|
+
result = response.get("result", {})
|
|
280
|
+
if result.get("isError"):
|
|
281
|
+
print(f"ci-auto-capture: log rejected: {result.get('error', 'unknown error')}")
|
|
282
|
+
return 0
|
|
283
|
+
|
|
284
|
+
print("ci-auto-capture: log sent successfully.")
|
|
285
|
+
return 0
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
if __name__ == "__main__":
|
|
289
|
+
raise SystemExit(main())
|
|
File without changes
|