code-mower 0.5.0b5__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.
- code_mower/__init__.py +3 -0
- code_mower/adapters/__init__.py +39 -0
- code_mower/adapters/_base.py +148 -0
- code_mower/adapters/cursor_bugbot.py +97 -0
- code_mower/adapters/gitar.py +111 -0
- code_mower/adapters/greptile.py +191 -0
- code_mower/adapters/qodo.py +140 -0
- code_mower/antigravity_cli_audit_pr.py +241 -0
- code_mower/audit_handoff_log.py +345 -0
- code_mower/audit_labeler_lib.py +311 -0
- code_mower/audit_progress.py +203 -0
- code_mower/blind_review_artifacts.py +562 -0
- code_mower/blind_review_coordinator.py +276 -0
- code_mower/bootstrap.py +524 -0
- code_mower/builder_experiment.py +539 -0
- code_mower/calibration/__init__.py +181 -0
- code_mower/calibration/arms.py +267 -0
- code_mower/calibration/auto_discovery.py +348 -0
- code_mower/calibration/commands.py +192 -0
- code_mower/calibration/context_inputs.py +199 -0
- code_mower/calibration/corpus.py +91 -0
- code_mower/calibration/evidence.py +20 -0
- code_mower/calibration/evidence_report.py +360 -0
- code_mower/calibration/identity.py +27 -0
- code_mower/calibration/metrics.py +18 -0
- code_mower/calibration/overlap.py +103 -0
- code_mower/calibration/planning.py +309 -0
- code_mower/calibration/policy.py +207 -0
- code_mower/calibration/results.py +315 -0
- code_mower/calibration/run_results.py +147 -0
- code_mower/calibration/run_status.py +64 -0
- code_mower/calibration/runner.py +360 -0
- code_mower/calibration/truth.py +188 -0
- code_mower/calibration/value_report.py +142 -0
- code_mower/checks.py +402 -0
- code_mower/claude_audit_pr.py +1126 -0
- code_mower/claude_cli_bounce.py +303 -0
- code_mower/claude_cli_environment.py +73 -0
- code_mower/clear_stale.py +374 -0
- code_mower/cli.py +537 -0
- code_mower/cloud.py +674 -0
- code_mower/cloud_client/__init__.py +173 -0
- code_mower/cloud_client/bundle.py +155 -0
- code_mower/cloud_client/doctor.py +206 -0
- code_mower/cloud_client/dogfood.py +78 -0
- code_mower/cloud_client/endpoints.py +114 -0
- code_mower/cloud_client/errors.py +7 -0
- code_mower/cloud_client/events.py +278 -0
- code_mower/cloud_client/export.py +272 -0
- code_mower/cloud_client/git_metadata.py +46 -0
- code_mower/cloud_client/manifest.py +39 -0
- code_mower/cloud_client/operations.py +448 -0
- code_mower/cloud_client/reports.py +45 -0
- code_mower/cloud_client/setup.py +205 -0
- code_mower/cloud_client/upload.py +97 -0
- code_mower/code_mower_calibration.py +598 -0
- code_mower/code_mower_context_packs.py +591 -0
- code_mower/code_mower_merge.py +227 -0
- code_mower/code_mower_telemetry.py +561 -0
- code_mower/coderabbit_cli_audit_pr.py +526 -0
- code_mower/codex_audit_env_preflight.py +220 -0
- code_mower/codex_audit_pr.py +1738 -0
- code_mower/codex_audit_schema_smoke.py +160 -0
- code_mower/codex_audit_verdict.schema.json +44 -0
- code_mower/config.py +655 -0
- code_mower/doctor.py +161 -0
- code_mower/doctor_checks/__init__.py +104 -0
- code_mower/doctor_checks/cloud.py +129 -0
- code_mower/doctor_checks/common.py +215 -0
- code_mower/doctor_checks/github.py +128 -0
- code_mower/doctor_checks/github_actions.py +11 -0
- code_mower/doctor_checks/github_actions_cost.py +99 -0
- code_mower/doctor_checks/github_actions_cost_summary.py +111 -0
- code_mower/doctor_checks/github_actions_failure_annotations.py +27 -0
- code_mower/doctor_checks/github_actions_failure_models.py +47 -0
- code_mower/doctor_checks/github_actions_failure_scan.py +200 -0
- code_mower/doctor_checks/github_actions_failure_selection.py +63 -0
- code_mower/doctor_checks/github_actions_failures.py +103 -0
- code_mower/doctor_checks/github_actions_permissions.py +55 -0
- code_mower/doctor_checks/github_api.py +79 -0
- code_mower/doctor_checks/github_branch.py +56 -0
- code_mower/doctor_checks/github_config.py +25 -0
- code_mower/doctor_checks/github_provider.py +61 -0
- code_mower/doctor_checks/github_repo.py +120 -0
- code_mower/doctor_checks/groups.py +36 -0
- code_mower/doctor_checks/models.py +96 -0
- code_mower/doctor_checks/output.py +86 -0
- code_mower/doctor_checks/presets.py +64 -0
- code_mower/doctor_checks/privacy.py +20 -0
- code_mower/doctor_checks/provider_api_model.py +138 -0
- code_mower/doctor_checks/provider_api_model_openai.py +29 -0
- code_mower/doctor_checks/provider_api_model_profiles.py +137 -0
- code_mower/doctor_checks/provider_env.py +113 -0
- code_mower/doctor_checks/provider_env_required.py +56 -0
- code_mower/doctor_checks/provider_env_tokens.py +100 -0
- code_mower/doctor_checks/provider_local_cli.py +162 -0
- code_mower/doctor_checks/provider_local_cli_commands.py +47 -0
- code_mower/doctor_checks/provider_local_cli_probe_config.py +70 -0
- code_mower/doctor_checks/provider_probe.py +20 -0
- code_mower/doctor_checks/provider_probe_auth.py +52 -0
- code_mower/doctor_checks/provider_probe_evaluation.py +109 -0
- code_mower/doctor_checks/provider_probe_json.py +45 -0
- code_mower/doctor_checks/provider_probe_remediation.py +39 -0
- code_mower/doctor_checks/providers.py +159 -0
- code_mower/doctor_checks/registry.py +69 -0
- code_mower/doctor_checks/runner.py +188 -0
- code_mower/doctor_checks/runtime.py +89 -0
- code_mower/doctor_checks/runtime_github_auth.py +148 -0
- code_mower/gemini_cli_audit_pr.py +897 -0
- code_mower/hermes_cli_audit_pr.py +436 -0
- code_mower/init.py +888 -0
- code_mower/lane_configs/__init__.py +37 -0
- code_mower/lane_configs/aider.py +32 -0
- code_mower/lane_configs/antigravity_cli.py +35 -0
- code_mower/lane_configs/claude.py +35 -0
- code_mower/lane_configs/codex.py +32 -0
- code_mower/lane_configs/devin.py +33 -0
- code_mower/lane_configs/gemini_cli.py +35 -0
- code_mower/lane_configs/hermes_cli.py +35 -0
- code_mower/lane_configs/local_llm.py +31 -0
- code_mower/local_llm_audit_pr.py +1364 -0
- code_mower/local_llm_bakeoff.py +458 -0
- code_mower/local_llm_calibration.py +441 -0
- code_mower/local_llm_profiles.py +66 -0
- code_mower/migration.py +508 -0
- code_mower/migration_install.py +292 -0
- code_mower/migration_mirror.py +392 -0
- code_mower/migration_readiness.py +237 -0
- code_mower/migration_rehearsal.py +718 -0
- code_mower/next_steps.py +441 -0
- code_mower/package.py +673 -0
- code_mower/package_content.py +444 -0
- code_mower/package_manifest.py +452 -0
- code_mower/package_paths.py +53 -0
- code_mower/package_rendering.py +90 -0
- code_mower/package_static.py +585 -0
- code_mower/prompts.py +267 -0
- code_mower/provider_registry.py +469 -0
- code_mower/provider_runners/__init__.py +60 -0
- code_mower/provider_runners/comments.py +31 -0
- code_mower/provider_runners/git.py +46 -0
- code_mower/provider_runners/github_auth.py +61 -0
- code_mower/provider_runners/github_pr.py +120 -0
- code_mower/provider_runners/process.py +58 -0
- code_mower/provider_runners/repo_paths.py +23 -0
- code_mower/provider_runners/text_schema.py +41 -0
- code_mower/provider_runners/verdict_artifacts.py +103 -0
- code_mower/provider_runners/workspace.py +57 -0
- code_mower/release_readiness.py +549 -0
- code_mower/reviewer_metrics.py +389 -0
- code_mower/saas_reviewer_labeler.py +809 -0
- code_mower/secrets.py +89 -0
- code_mower/templates/builder-experiment.example.json +55 -0
- code_mower/templates/calibration-corpus.example.json +129 -0
- code_mower/templates/calibration-corpus.json +129 -0
- code_mower/templates/code-mower.example.yml +423 -0
- code_mower/templates/context-packs.example.json +150 -0
- code_mower/templates/lane_prompts/base-audit.md +22 -0
- code_mower/templates/lane_prompts/calibration-policy.md +21 -0
- code_mower/templates/lane_prompts/context-driven-quality.md +21 -0
- code_mower/templates/lane_prompts/docs-design.md +12 -0
- code_mower/templates/lane_prompts/generic-programming.md +21 -0
- code_mower/templates/lane_prompts/operability.md +22 -0
- code_mower/templates/lane_prompts/package-runtime.md +12 -0
- code_mower/templates/lane_prompts/security-threat-model.md +22 -0
- code_mower/templates/product-support/code_mower +216 -0
- code_mower/templates/product-support/code_mower_standalone_pin.env +7 -0
- code_mower/templates/product-support/code_mower_standalone_shadow.sh +151 -0
- code_mower/templates/product-support/run_claude_audit_pr.sh +32 -0
- code_mower/templates/product-support/run_codex_audit_pr.sh +32 -0
- code_mower/templates/product-support/safe_gh_comment.py +96 -0
- code_mower/templates/providers.yml +454 -0
- code_mower/templates/reviewer-spend.example.json +28 -0
- code_mower/templates/reviewer-value-report.example.md +20 -0
- code_mower/templates/workflows/private-standalone-shadow.yml.j2 +106 -0
- code_mower/templates/workflows/review-clear-stale.yml.j2 +83 -0
- code_mower/trailer_comment_labeler.py +207 -0
- code_mower/versioning.py +32 -0
- code_mower-0.5.0b5.dist-info/METADATA +302 -0
- code_mower-0.5.0b5.dist-info/RECORD +185 -0
- code_mower-0.5.0b5.dist-info/WHEEL +5 -0
- code_mower-0.5.0b5.dist-info/entry_points.txt +2 -0
- code_mower-0.5.0b5.dist-info/licenses/LICENSE +202 -0
- code_mower-0.5.0b5.dist-info/licenses/NOTICE +10 -0
- code_mower-0.5.0b5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Safe Git metadata helpers for Code Mower Cloud uploads."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def run_git(repo_path: Path, args: list[str]) -> str:
|
|
10
|
+
"""Return stdout for a best-effort git command, or an empty string."""
|
|
11
|
+
try:
|
|
12
|
+
completed = subprocess.run(
|
|
13
|
+
["git", *args],
|
|
14
|
+
cwd=repo_path,
|
|
15
|
+
text=True,
|
|
16
|
+
capture_output=True,
|
|
17
|
+
check=False,
|
|
18
|
+
)
|
|
19
|
+
except OSError:
|
|
20
|
+
return ""
|
|
21
|
+
if completed.returncode != 0:
|
|
22
|
+
return ""
|
|
23
|
+
return completed.stdout.strip()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def repo_slug_from_remote(remote_url: str) -> str:
|
|
27
|
+
remote = remote_url.strip()
|
|
28
|
+
if not remote:
|
|
29
|
+
return ""
|
|
30
|
+
if remote.startswith("git@github.com:"):
|
|
31
|
+
remote = remote.removeprefix("git@github.com:")
|
|
32
|
+
elif remote.startswith("https://github.com/"):
|
|
33
|
+
remote = remote.removeprefix("https://github.com/")
|
|
34
|
+
elif remote.startswith("http://github.com/"):
|
|
35
|
+
remote = remote.removeprefix("http://github.com/")
|
|
36
|
+
else:
|
|
37
|
+
return ""
|
|
38
|
+
remote = remote.removesuffix(".git").strip("/")
|
|
39
|
+
parts = remote.split("/")
|
|
40
|
+
if len(parts) >= 2 and parts[0] and parts[1]:
|
|
41
|
+
return f"{parts[0]}/{parts[1]}"
|
|
42
|
+
return ""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def detect_repo_slug(repo_path: Path) -> str:
|
|
46
|
+
return repo_slug_from_remote(run_git(repo_path, ["config", "--get", "remote.origin.url"]))
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Bundle manifest loading and path validation helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from .bundle import BUNDLE_MANIFEST_FILENAME, is_bundle_manifest
|
|
10
|
+
from .errors import CloudBundleError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def load_bundle_manifest(bundle_dir: Path) -> dict[str, Any]:
|
|
14
|
+
manifest_path = bundle_dir / BUNDLE_MANIFEST_FILENAME
|
|
15
|
+
if not manifest_path.is_file():
|
|
16
|
+
raise CloudBundleError(f"bundle manifest not found: {manifest_path}")
|
|
17
|
+
try:
|
|
18
|
+
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
19
|
+
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
20
|
+
raise CloudBundleError(f"unable to read bundle manifest {manifest_path}: {exc}") from exc
|
|
21
|
+
if not is_bundle_manifest(manifest):
|
|
22
|
+
raise CloudBundleError(f"unsupported bundle manifest schema in {manifest_path}")
|
|
23
|
+
return manifest
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def report_path_from_manifest(bundle_dir: Path, target: str) -> Path:
|
|
27
|
+
if not target or target.startswith("/") or ".." in Path(target).parts:
|
|
28
|
+
raise CloudBundleError(f"unsafe report target in bundle manifest: {target!r}")
|
|
29
|
+
path = bundle_dir / target
|
|
30
|
+
try:
|
|
31
|
+
resolved = path.resolve()
|
|
32
|
+
bundle_resolved = bundle_dir.resolve()
|
|
33
|
+
except OSError as exc:
|
|
34
|
+
raise CloudBundleError(f"unable to resolve bundle report path {path}: {exc}") from exc
|
|
35
|
+
if not resolved.is_relative_to(bundle_resolved):
|
|
36
|
+
raise CloudBundleError(f"report target escapes bundle directory: {target!r}")
|
|
37
|
+
if not resolved.is_file():
|
|
38
|
+
raise CloudBundleError(f"bundle report file is missing: {target!r}")
|
|
39
|
+
return resolved
|
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
"""High-level CodeMower.com upload operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from .. import code_mower_telemetry
|
|
10
|
+
from .bundle import MAX_EVENT_COUNT
|
|
11
|
+
from .doctor import run_cloud_doctor
|
|
12
|
+
from .dogfood import build_dogfood_dry_run_preview, build_dogfood_plan, default_dogfood_reports
|
|
13
|
+
from .endpoints import is_local_http_endpoint
|
|
14
|
+
from .errors import CloudBundleError
|
|
15
|
+
from .events import (
|
|
16
|
+
build_dogfood_event,
|
|
17
|
+
build_workflow_run_event,
|
|
18
|
+
run_gh_run_list,
|
|
19
|
+
)
|
|
20
|
+
from .export import build_cloud_bundle
|
|
21
|
+
from .git_metadata import detect_repo_slug
|
|
22
|
+
from .setup import DEFAULT_INSTALL_ID_ENV, DEFAULT_TEAM_ID_ENV
|
|
23
|
+
from .upload import build_upload_payload, post_upload_payload
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def dogfood_upload(
|
|
27
|
+
*,
|
|
28
|
+
repo_path: Path,
|
|
29
|
+
output_dir: Path,
|
|
30
|
+
reports: list[tuple[Path, str]],
|
|
31
|
+
events: list[dict[str, Any]],
|
|
32
|
+
repo_slug: str,
|
|
33
|
+
team_id: str,
|
|
34
|
+
install_id: str,
|
|
35
|
+
source: str,
|
|
36
|
+
endpoint: str,
|
|
37
|
+
token_env: str,
|
|
38
|
+
include_reports: bool,
|
|
39
|
+
yes: bool,
|
|
40
|
+
timeout: float,
|
|
41
|
+
) -> dict[str, Any]:
|
|
42
|
+
repo_path = repo_path.expanduser().resolve()
|
|
43
|
+
detected_repo_slug = repo_slug or detect_repo_slug(repo_path)
|
|
44
|
+
if not detected_repo_slug:
|
|
45
|
+
raise CloudBundleError(
|
|
46
|
+
"unable to detect repo slug; pass --repo-slug OWNER/REPO"
|
|
47
|
+
)
|
|
48
|
+
resolved_team_id = team_id or os.environ.get(DEFAULT_TEAM_ID_ENV, "")
|
|
49
|
+
resolved_install_id = install_id or os.environ.get(DEFAULT_INSTALL_ID_ENV, "")
|
|
50
|
+
resolved_reports = reports or default_dogfood_reports(repo_path)
|
|
51
|
+
dogfood_plan = build_dogfood_plan(
|
|
52
|
+
repo_slug=detected_repo_slug,
|
|
53
|
+
team_id=resolved_team_id,
|
|
54
|
+
install_id=resolved_install_id,
|
|
55
|
+
source=source,
|
|
56
|
+
reports=resolved_reports,
|
|
57
|
+
events=events,
|
|
58
|
+
)
|
|
59
|
+
all_events = [
|
|
60
|
+
build_dogfood_event(
|
|
61
|
+
repo_path=repo_path,
|
|
62
|
+
plan=dogfood_plan,
|
|
63
|
+
),
|
|
64
|
+
*events,
|
|
65
|
+
]
|
|
66
|
+
export_result = build_cloud_bundle(
|
|
67
|
+
reports=resolved_reports,
|
|
68
|
+
events=all_events,
|
|
69
|
+
output_dir=output_dir,
|
|
70
|
+
repo_slug=detected_repo_slug,
|
|
71
|
+
team_id=resolved_team_id,
|
|
72
|
+
install_id=resolved_install_id,
|
|
73
|
+
anonymous=False,
|
|
74
|
+
)
|
|
75
|
+
doctor_result = run_cloud_doctor(
|
|
76
|
+
bundle_dir=output_dir,
|
|
77
|
+
endpoint=endpoint,
|
|
78
|
+
token_env=token_env,
|
|
79
|
+
require_token=yes,
|
|
80
|
+
)
|
|
81
|
+
if doctor_result["failures"]:
|
|
82
|
+
return {
|
|
83
|
+
"mode": "cloud-dogfood",
|
|
84
|
+
"status": "doctor_failed",
|
|
85
|
+
"export": export_result,
|
|
86
|
+
"doctor": doctor_result,
|
|
87
|
+
}
|
|
88
|
+
payload = build_upload_payload(
|
|
89
|
+
bundle_dir=output_dir,
|
|
90
|
+
include_reports=include_reports,
|
|
91
|
+
)
|
|
92
|
+
if not yes:
|
|
93
|
+
return {
|
|
94
|
+
"mode": "cloud-dogfood",
|
|
95
|
+
"status": "dry_run",
|
|
96
|
+
"export": export_result,
|
|
97
|
+
"doctor": doctor_result,
|
|
98
|
+
"upload": build_dogfood_dry_run_preview(endpoint=endpoint, payload=payload),
|
|
99
|
+
}
|
|
100
|
+
token = os.environ.get(token_env, "")
|
|
101
|
+
if not token and not is_local_http_endpoint(endpoint):
|
|
102
|
+
raise CloudBundleError(
|
|
103
|
+
f"{token_env} is not set; refusing non-local upload without a token"
|
|
104
|
+
)
|
|
105
|
+
return {
|
|
106
|
+
"mode": "cloud-dogfood",
|
|
107
|
+
"status": "uploaded",
|
|
108
|
+
"export": export_result,
|
|
109
|
+
"doctor": doctor_result,
|
|
110
|
+
"upload": post_upload_payload(
|
|
111
|
+
payload=payload,
|
|
112
|
+
endpoint=endpoint,
|
|
113
|
+
token=token,
|
|
114
|
+
timeout=timeout,
|
|
115
|
+
),
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def catch_up_upload(
|
|
120
|
+
*,
|
|
121
|
+
repo_path: Path,
|
|
122
|
+
output_dir: Path,
|
|
123
|
+
repo_slug: str,
|
|
124
|
+
team_id: str,
|
|
125
|
+
install_id: str,
|
|
126
|
+
source: str,
|
|
127
|
+
limit: int,
|
|
128
|
+
endpoint: str,
|
|
129
|
+
token_env: str,
|
|
130
|
+
yes: bool,
|
|
131
|
+
timeout: float,
|
|
132
|
+
include_git_ref: bool,
|
|
133
|
+
) -> dict[str, Any]:
|
|
134
|
+
if limit < 1 or limit > MAX_EVENT_COUNT:
|
|
135
|
+
raise CloudBundleError(f"--limit must be between 1 and {MAX_EVENT_COUNT}")
|
|
136
|
+
repo_path = repo_path.expanduser().resolve()
|
|
137
|
+
detected_repo_slug = repo_slug or detect_repo_slug(repo_path)
|
|
138
|
+
if not detected_repo_slug:
|
|
139
|
+
raise CloudBundleError(
|
|
140
|
+
"unable to detect repo slug; pass --repo-slug OWNER/REPO"
|
|
141
|
+
)
|
|
142
|
+
resolved_team_id = team_id or os.environ.get(DEFAULT_TEAM_ID_ENV, "")
|
|
143
|
+
resolved_install_id = install_id or os.environ.get(DEFAULT_INSTALL_ID_ENV, "")
|
|
144
|
+
runs = run_gh_run_list(
|
|
145
|
+
repo_slug=detected_repo_slug,
|
|
146
|
+
limit=limit,
|
|
147
|
+
repo_path=repo_path,
|
|
148
|
+
)
|
|
149
|
+
events = [
|
|
150
|
+
build_workflow_run_event(
|
|
151
|
+
repo_slug=detected_repo_slug,
|
|
152
|
+
team_id=resolved_team_id,
|
|
153
|
+
install_id=resolved_install_id,
|
|
154
|
+
source=source,
|
|
155
|
+
run=run,
|
|
156
|
+
include_git_ref=include_git_ref,
|
|
157
|
+
)
|
|
158
|
+
for run in runs
|
|
159
|
+
]
|
|
160
|
+
export_result = build_cloud_bundle(
|
|
161
|
+
reports=[],
|
|
162
|
+
events=events,
|
|
163
|
+
output_dir=output_dir,
|
|
164
|
+
repo_slug=detected_repo_slug,
|
|
165
|
+
team_id=resolved_team_id,
|
|
166
|
+
install_id=resolved_install_id,
|
|
167
|
+
anonymous=False,
|
|
168
|
+
)
|
|
169
|
+
doctor_result = run_cloud_doctor(
|
|
170
|
+
bundle_dir=output_dir,
|
|
171
|
+
endpoint=endpoint,
|
|
172
|
+
token_env=token_env,
|
|
173
|
+
require_token=yes,
|
|
174
|
+
)
|
|
175
|
+
if doctor_result["failures"]:
|
|
176
|
+
return {
|
|
177
|
+
"mode": "cloud-catch-up",
|
|
178
|
+
"status": "doctor_failed",
|
|
179
|
+
"repo_slug": detected_repo_slug,
|
|
180
|
+
"run_count": len(runs),
|
|
181
|
+
"export": export_result,
|
|
182
|
+
"doctor": doctor_result,
|
|
183
|
+
}
|
|
184
|
+
payload = build_upload_payload(bundle_dir=output_dir, include_reports=False)
|
|
185
|
+
if not yes:
|
|
186
|
+
return {
|
|
187
|
+
"mode": "cloud-catch-up",
|
|
188
|
+
"status": "dry_run",
|
|
189
|
+
"repo_slug": detected_repo_slug,
|
|
190
|
+
"run_count": len(runs),
|
|
191
|
+
"export": export_result,
|
|
192
|
+
"doctor": doctor_result,
|
|
193
|
+
"upload": build_dogfood_dry_run_preview(endpoint=endpoint, payload=payload),
|
|
194
|
+
}
|
|
195
|
+
token = os.environ.get(token_env, "")
|
|
196
|
+
if not token and not is_local_http_endpoint(endpoint):
|
|
197
|
+
raise CloudBundleError(
|
|
198
|
+
f"{token_env} is not set; refusing non-local upload without a token"
|
|
199
|
+
)
|
|
200
|
+
return {
|
|
201
|
+
"mode": "cloud-catch-up",
|
|
202
|
+
"status": "uploaded",
|
|
203
|
+
"repo_slug": detected_repo_slug,
|
|
204
|
+
"run_count": len(runs),
|
|
205
|
+
"export": export_result,
|
|
206
|
+
"doctor": doctor_result,
|
|
207
|
+
"upload": post_upload_payload(
|
|
208
|
+
payload=payload,
|
|
209
|
+
endpoint=endpoint,
|
|
210
|
+
token=token,
|
|
211
|
+
timeout=timeout,
|
|
212
|
+
),
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def reviewer_runs_upload(
|
|
217
|
+
*,
|
|
218
|
+
repo_path: Path,
|
|
219
|
+
verdicts: Path,
|
|
220
|
+
output_dir: Path,
|
|
221
|
+
repo_slug: str,
|
|
222
|
+
team_id: str,
|
|
223
|
+
install_id: str,
|
|
224
|
+
limit: int,
|
|
225
|
+
endpoint: str,
|
|
226
|
+
token_env: str,
|
|
227
|
+
yes: bool,
|
|
228
|
+
timeout: float,
|
|
229
|
+
include_git_ref: bool,
|
|
230
|
+
) -> dict[str, Any]:
|
|
231
|
+
if limit < 1 or limit > MAX_EVENT_COUNT:
|
|
232
|
+
raise CloudBundleError(f"--limit must be between 1 and {MAX_EVENT_COUNT}")
|
|
233
|
+
repo_path = repo_path.expanduser().resolve()
|
|
234
|
+
detected_repo_slug = repo_slug or detect_repo_slug(repo_path)
|
|
235
|
+
if not detected_repo_slug:
|
|
236
|
+
raise CloudBundleError(
|
|
237
|
+
"unable to detect repo slug; pass --repo-slug OWNER/REPO"
|
|
238
|
+
)
|
|
239
|
+
resolved_team_id = team_id or os.environ.get(DEFAULT_TEAM_ID_ENV, "")
|
|
240
|
+
resolved_install_id = install_id or os.environ.get(DEFAULT_INSTALL_ID_ENV, "")
|
|
241
|
+
try:
|
|
242
|
+
events = code_mower_telemetry.export_reviewer_run_events_from_verdicts(
|
|
243
|
+
verdicts,
|
|
244
|
+
repo=detected_repo_slug,
|
|
245
|
+
limit=limit,
|
|
246
|
+
include_git_ref=include_git_ref,
|
|
247
|
+
)
|
|
248
|
+
except ValueError as exc:
|
|
249
|
+
raise CloudBundleError(str(exc)) from exc
|
|
250
|
+
if not events:
|
|
251
|
+
return {
|
|
252
|
+
"mode": "cloud-reviewer-runs",
|
|
253
|
+
"status": "no_events",
|
|
254
|
+
"repo_slug": detected_repo_slug,
|
|
255
|
+
"event_count": 0,
|
|
256
|
+
"verdicts": str(verdicts.expanduser()),
|
|
257
|
+
"git_ref_included": include_git_ref,
|
|
258
|
+
}
|
|
259
|
+
export_result = build_cloud_bundle(
|
|
260
|
+
reports=[],
|
|
261
|
+
events=events,
|
|
262
|
+
output_dir=output_dir,
|
|
263
|
+
repo_slug=detected_repo_slug,
|
|
264
|
+
team_id=resolved_team_id,
|
|
265
|
+
install_id=resolved_install_id,
|
|
266
|
+
anonymous=False,
|
|
267
|
+
)
|
|
268
|
+
doctor_result = run_cloud_doctor(
|
|
269
|
+
bundle_dir=output_dir,
|
|
270
|
+
endpoint=endpoint,
|
|
271
|
+
token_env=token_env,
|
|
272
|
+
require_token=yes,
|
|
273
|
+
)
|
|
274
|
+
if doctor_result["failures"]:
|
|
275
|
+
return {
|
|
276
|
+
"mode": "cloud-reviewer-runs",
|
|
277
|
+
"status": "doctor_failed",
|
|
278
|
+
"repo_slug": detected_repo_slug,
|
|
279
|
+
"event_count": len(events),
|
|
280
|
+
"export": export_result,
|
|
281
|
+
"doctor": doctor_result,
|
|
282
|
+
}
|
|
283
|
+
payload = build_upload_payload(bundle_dir=output_dir, include_reports=False)
|
|
284
|
+
if not yes:
|
|
285
|
+
return {
|
|
286
|
+
"mode": "cloud-reviewer-runs",
|
|
287
|
+
"status": "dry_run",
|
|
288
|
+
"repo_slug": detected_repo_slug,
|
|
289
|
+
"event_count": len(events),
|
|
290
|
+
"export": export_result,
|
|
291
|
+
"doctor": doctor_result,
|
|
292
|
+
"upload": build_dogfood_dry_run_preview(endpoint=endpoint, payload=payload),
|
|
293
|
+
}
|
|
294
|
+
token = os.environ.get(token_env, "")
|
|
295
|
+
if not token and not is_local_http_endpoint(endpoint):
|
|
296
|
+
raise CloudBundleError(
|
|
297
|
+
f"{token_env} is not set; refusing non-local upload without a token"
|
|
298
|
+
)
|
|
299
|
+
return {
|
|
300
|
+
"mode": "cloud-reviewer-runs",
|
|
301
|
+
"status": "uploaded",
|
|
302
|
+
"repo_slug": detected_repo_slug,
|
|
303
|
+
"event_count": len(events),
|
|
304
|
+
"export": export_result,
|
|
305
|
+
"doctor": doctor_result,
|
|
306
|
+
"upload": post_upload_payload(
|
|
307
|
+
payload=payload,
|
|
308
|
+
endpoint=endpoint,
|
|
309
|
+
token=token,
|
|
310
|
+
timeout=timeout,
|
|
311
|
+
),
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def parse_repo_sync_spec(spec: str) -> tuple[str, Path]:
|
|
316
|
+
if "=" not in spec:
|
|
317
|
+
return "", Path(spec)
|
|
318
|
+
repo_slug, repo_path = spec.split("=", 1)
|
|
319
|
+
repo_slug = repo_slug.strip()
|
|
320
|
+
repo_path = repo_path.strip()
|
|
321
|
+
if not repo_slug or not repo_path:
|
|
322
|
+
raise CloudBundleError(
|
|
323
|
+
"--repo entries must be PATH or OWNER/REPO=PATH for repo-sync"
|
|
324
|
+
)
|
|
325
|
+
return repo_slug, Path(repo_path)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def repo_sync_output_name(repo_slug: str, repo_path: Path, index: int) -> str:
|
|
329
|
+
raw = repo_slug.replace("/", "__") if repo_slug else repo_path.name
|
|
330
|
+
cleaned = "".join(
|
|
331
|
+
ch.lower() if ch.isalnum() else "-" for ch in raw.strip()
|
|
332
|
+
).strip("-")
|
|
333
|
+
return f"{cleaned or 'repo'}-{index + 1}"
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def repo_sync_upload(
|
|
337
|
+
*,
|
|
338
|
+
repo_specs: list[str],
|
|
339
|
+
output_dir: Path,
|
|
340
|
+
modes: list[str],
|
|
341
|
+
team_id: str,
|
|
342
|
+
install_id: str,
|
|
343
|
+
source_prefix: str,
|
|
344
|
+
limit: int,
|
|
345
|
+
endpoint: str,
|
|
346
|
+
token_env: str,
|
|
347
|
+
include_reports: bool,
|
|
348
|
+
include_git_ref: bool,
|
|
349
|
+
yes: bool,
|
|
350
|
+
timeout: float,
|
|
351
|
+
) -> dict[str, Any]:
|
|
352
|
+
selected_modes = modes or ["dogfood", "reviewer-runs"]
|
|
353
|
+
repos: list[dict[str, Any]] = []
|
|
354
|
+
error_count = 0
|
|
355
|
+
step_statuses: list[str] = []
|
|
356
|
+
|
|
357
|
+
for index, spec in enumerate(repo_specs):
|
|
358
|
+
repo_slug, repo_path = parse_repo_sync_spec(spec)
|
|
359
|
+
repo_path = repo_path.expanduser().resolve()
|
|
360
|
+
repo_output_dir = output_dir / repo_sync_output_name(repo_slug, repo_path, index)
|
|
361
|
+
repo_result: dict[str, Any] = {
|
|
362
|
+
"repo_spec": spec,
|
|
363
|
+
"repo_slug": repo_slug,
|
|
364
|
+
"repo_path": str(repo_path),
|
|
365
|
+
"steps": [],
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
for mode in selected_modes:
|
|
369
|
+
try:
|
|
370
|
+
if mode == "dogfood":
|
|
371
|
+
step_result = dogfood_upload(
|
|
372
|
+
repo_path=repo_path,
|
|
373
|
+
output_dir=repo_output_dir / "dogfood",
|
|
374
|
+
reports=[],
|
|
375
|
+
events=[],
|
|
376
|
+
repo_slug=repo_slug,
|
|
377
|
+
team_id=team_id,
|
|
378
|
+
install_id=install_id,
|
|
379
|
+
source=f"{source_prefix}-dogfood",
|
|
380
|
+
endpoint=endpoint,
|
|
381
|
+
token_env=token_env,
|
|
382
|
+
include_reports=include_reports,
|
|
383
|
+
yes=yes,
|
|
384
|
+
timeout=timeout,
|
|
385
|
+
)
|
|
386
|
+
elif mode == "catch-up":
|
|
387
|
+
step_result = catch_up_upload(
|
|
388
|
+
repo_path=repo_path,
|
|
389
|
+
output_dir=repo_output_dir / "catch-up",
|
|
390
|
+
repo_slug=repo_slug,
|
|
391
|
+
team_id=team_id,
|
|
392
|
+
install_id=install_id,
|
|
393
|
+
source=f"{source_prefix}-catch-up",
|
|
394
|
+
limit=limit,
|
|
395
|
+
endpoint=endpoint,
|
|
396
|
+
token_env=token_env,
|
|
397
|
+
yes=yes,
|
|
398
|
+
timeout=timeout,
|
|
399
|
+
include_git_ref=include_git_ref,
|
|
400
|
+
)
|
|
401
|
+
elif mode == "reviewer-runs":
|
|
402
|
+
step_result = reviewer_runs_upload(
|
|
403
|
+
repo_path=repo_path,
|
|
404
|
+
verdicts=code_mower_telemetry.default_verdict_artifact_dir(),
|
|
405
|
+
output_dir=repo_output_dir / "reviewer-runs",
|
|
406
|
+
repo_slug=repo_slug,
|
|
407
|
+
team_id=team_id,
|
|
408
|
+
install_id=install_id,
|
|
409
|
+
limit=limit,
|
|
410
|
+
endpoint=endpoint,
|
|
411
|
+
token_env=token_env,
|
|
412
|
+
yes=yes,
|
|
413
|
+
timeout=timeout,
|
|
414
|
+
include_git_ref=include_git_ref,
|
|
415
|
+
)
|
|
416
|
+
else: # pragma: no cover - argparse constrains modes.
|
|
417
|
+
raise CloudBundleError(f"unsupported repo-sync mode: {mode}")
|
|
418
|
+
except CloudBundleError as exc:
|
|
419
|
+
step_result = {
|
|
420
|
+
"mode": f"cloud-{mode}",
|
|
421
|
+
"status": "error",
|
|
422
|
+
"error": str(exc),
|
|
423
|
+
}
|
|
424
|
+
repo_result["repo_slug"] = repo_result["repo_slug"] or str(
|
|
425
|
+
step_result.get("repo_slug") or ""
|
|
426
|
+
)
|
|
427
|
+
repo_result["steps"].append(step_result)
|
|
428
|
+
step_status = str(step_result.get("status") or "")
|
|
429
|
+
step_statuses.append(step_status)
|
|
430
|
+
if step_result.get("status") in {"error", "doctor_failed"}:
|
|
431
|
+
error_count += 1
|
|
432
|
+
|
|
433
|
+
repos.append(repo_result)
|
|
434
|
+
|
|
435
|
+
status = "dry_run" if not yes else "uploaded"
|
|
436
|
+
if error_count:
|
|
437
|
+
status = "partial"
|
|
438
|
+
elif yes and not any(step_status == "uploaded" for step_status in step_statuses):
|
|
439
|
+
status = "no_events"
|
|
440
|
+
return {
|
|
441
|
+
"mode": "cloud-repo-sync",
|
|
442
|
+
"status": status,
|
|
443
|
+
"repo_count": len(repos),
|
|
444
|
+
"step_count": sum(len(repo["steps"]) for repo in repos),
|
|
445
|
+
"error_count": error_count,
|
|
446
|
+
"modes": selected_modes,
|
|
447
|
+
"repos": repos,
|
|
448
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Bundle report payload construction helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .bundle import MAX_REPORT_UPLOAD_BYTES
|
|
9
|
+
from .errors import CloudBundleError
|
|
10
|
+
from .manifest import report_path_from_manifest
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def included_report_payloads(
|
|
14
|
+
manifest: dict[str, Any],
|
|
15
|
+
bundle_dir: Path,
|
|
16
|
+
*,
|
|
17
|
+
include_reports: bool,
|
|
18
|
+
) -> list[dict[str, Any]]:
|
|
19
|
+
payloads: list[dict[str, Any]] = []
|
|
20
|
+
for entry in manifest.get("included_reports", []):
|
|
21
|
+
if not isinstance(entry, dict):
|
|
22
|
+
raise CloudBundleError("bundle manifest has a non-object included_reports entry")
|
|
23
|
+
target = str(entry.get("target", ""))
|
|
24
|
+
report_payload = {
|
|
25
|
+
"kind": entry.get("kind", ""),
|
|
26
|
+
"target": target,
|
|
27
|
+
"bytes": entry.get("bytes", 0),
|
|
28
|
+
"source_basename": entry.get("source_basename", ""),
|
|
29
|
+
}
|
|
30
|
+
if include_reports:
|
|
31
|
+
path = report_path_from_manifest(bundle_dir, target)
|
|
32
|
+
size = path.stat().st_size
|
|
33
|
+
if size > MAX_REPORT_UPLOAD_BYTES:
|
|
34
|
+
raise CloudBundleError(
|
|
35
|
+
f"refusing to upload {target}: {size} bytes exceeds "
|
|
36
|
+
f"{MAX_REPORT_UPLOAD_BYTES} byte limit"
|
|
37
|
+
)
|
|
38
|
+
try:
|
|
39
|
+
report_payload["text"] = path.read_text(encoding="utf-8")
|
|
40
|
+
except UnicodeDecodeError as exc:
|
|
41
|
+
raise CloudBundleError(f"report is not UTF-8 text: {target}") from exc
|
|
42
|
+
except OSError as exc:
|
|
43
|
+
raise CloudBundleError(f"unable to read report {target}: {exc}") from exc
|
|
44
|
+
payloads.append(report_payload)
|
|
45
|
+
return payloads
|