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,549 @@
|
|
|
1
|
+
"""Static release-readiness checks for Code Mower package promotion."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import tempfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import yaml
|
|
11
|
+
|
|
12
|
+
from . import package as package_module
|
|
13
|
+
from . import versioning as code_mower_versioning
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
RELEASE_DOC_PATHS = (
|
|
17
|
+
"README.md",
|
|
18
|
+
"docs/quickstart.md",
|
|
19
|
+
"docs/try-in-10-minutes.md",
|
|
20
|
+
"docs/first-user-install-rehearsal.md",
|
|
21
|
+
"docs/pypi-release.md",
|
|
22
|
+
"docs/public-release-checklist.md",
|
|
23
|
+
)
|
|
24
|
+
REQUIRED_RELEASE_TAG_DOC_PATHS = (
|
|
25
|
+
"README.md",
|
|
26
|
+
"docs/quickstart.md",
|
|
27
|
+
"docs/try-in-10-minutes.md",
|
|
28
|
+
"docs/first-user-install-rehearsal.md",
|
|
29
|
+
"docs/public-release-checklist.md",
|
|
30
|
+
)
|
|
31
|
+
PUBLIC_HYGIENE_DOC_PATHS = (
|
|
32
|
+
"CODE_OF_CONDUCT.md",
|
|
33
|
+
"CONTRIBUTING.md",
|
|
34
|
+
"SECURITY.md",
|
|
35
|
+
"SUPPORT.md",
|
|
36
|
+
".github/ISSUE_TEMPLATE/bug_report.yml",
|
|
37
|
+
".github/ISSUE_TEMPLATE/feature_request.yml",
|
|
38
|
+
".github/PULL_REQUEST_TEMPLATE.md",
|
|
39
|
+
".github/dependabot.yml",
|
|
40
|
+
)
|
|
41
|
+
PACKAGE_INDEX_SETUP_URLS = {
|
|
42
|
+
"github_environments": (
|
|
43
|
+
"https://github.com/codemower-ai/code-mower/settings/environments"
|
|
44
|
+
),
|
|
45
|
+
"release_workflow": (
|
|
46
|
+
"https://github.com/codemower-ai/code-mower/actions/workflows/release.yml"
|
|
47
|
+
),
|
|
48
|
+
"testpypi_project": "https://test.pypi.org/project/code-mower/",
|
|
49
|
+
"testpypi_trusted_publishers": (
|
|
50
|
+
"https://test.pypi.org/manage/project/code-mower/settings/publishing/"
|
|
51
|
+
),
|
|
52
|
+
"pypi_project": "https://pypi.org/project/code-mower/",
|
|
53
|
+
"pypi_trusted_publishers": (
|
|
54
|
+
"https://pypi.org/manage/project/code-mower/settings/publishing/"
|
|
55
|
+
),
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _release_check(
|
|
60
|
+
*,
|
|
61
|
+
check_id: str,
|
|
62
|
+
title: str,
|
|
63
|
+
status: str,
|
|
64
|
+
evidence: str,
|
|
65
|
+
detail: dict[str, Any] | None = None,
|
|
66
|
+
) -> dict[str, Any]:
|
|
67
|
+
check: dict[str, Any] = {
|
|
68
|
+
"id": check_id,
|
|
69
|
+
"title": title,
|
|
70
|
+
"status": status,
|
|
71
|
+
"evidence": evidence,
|
|
72
|
+
}
|
|
73
|
+
if detail:
|
|
74
|
+
check["detail"] = detail
|
|
75
|
+
return check
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _read_text_if_exists(path: Path) -> str:
|
|
79
|
+
try:
|
|
80
|
+
return path.read_text(encoding="utf-8")
|
|
81
|
+
except OSError:
|
|
82
|
+
return ""
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _python_package_version(repo_path: Path) -> str:
|
|
86
|
+
init_text = _read_text_if_exists(repo_path / "src" / "code_mower" / "__init__.py")
|
|
87
|
+
match = re.search(r"__version__\s*=\s*[\"']([^\"']+)[\"']", init_text)
|
|
88
|
+
return match.group(1) if match else ""
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _pyproject_version(repo_path: Path) -> str:
|
|
92
|
+
pyproject_text = _read_text_if_exists(repo_path / "pyproject.toml")
|
|
93
|
+
match = re.search(r"^version\s*=\s*[\"']([^\"']+)[\"']", pyproject_text, re.MULTILINE)
|
|
94
|
+
return match.group(1) if match else ""
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _materialized_package_versions(repo_path: Path) -> dict[str, Any]:
|
|
98
|
+
try:
|
|
99
|
+
plan = package_module.render_package_plan(
|
|
100
|
+
package_module.load_config(
|
|
101
|
+
repo_path / "src" / "code_mower" / "templates" / "code-mower.example.yml"
|
|
102
|
+
),
|
|
103
|
+
package_module.load_provider_templates(
|
|
104
|
+
repo_path / "src" / "code_mower" / "templates" / "providers.yml"
|
|
105
|
+
),
|
|
106
|
+
)
|
|
107
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
108
|
+
output_dir = Path(tmp)
|
|
109
|
+
package_module.materialize_package_plan(
|
|
110
|
+
plan,
|
|
111
|
+
output_dir=output_dir,
|
|
112
|
+
repo_root=repo_path,
|
|
113
|
+
force=True,
|
|
114
|
+
)
|
|
115
|
+
return {
|
|
116
|
+
"error": "",
|
|
117
|
+
"init_version": _python_package_version(output_dir),
|
|
118
|
+
"pyproject_version": _pyproject_version(output_dir),
|
|
119
|
+
}
|
|
120
|
+
except Exception as exc: # pragma: no cover - exercised through status output.
|
|
121
|
+
return {
|
|
122
|
+
"error": str(exc),
|
|
123
|
+
"init_version": "",
|
|
124
|
+
"pyproject_version": "",
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _release_tag_for_version(version: str) -> str:
|
|
129
|
+
return code_mower_versioning.release_tag_for_version(version)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _release_docs(repo_path: Path) -> dict[str, str]:
|
|
133
|
+
return {
|
|
134
|
+
relative_path: _read_text_if_exists(repo_path / relative_path)
|
|
135
|
+
for relative_path in RELEASE_DOC_PATHS
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _workflow_jobs(workflow: str) -> dict[str, Any]:
|
|
140
|
+
try:
|
|
141
|
+
parsed = yaml.safe_load(workflow) if workflow.strip() else {}
|
|
142
|
+
except yaml.YAMLError:
|
|
143
|
+
return {}
|
|
144
|
+
if not isinstance(parsed, dict):
|
|
145
|
+
return {}
|
|
146
|
+
jobs = parsed.get("jobs")
|
|
147
|
+
return jobs if isinstance(jobs, dict) else {}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _needs_job(job: Any, required: str) -> bool:
|
|
151
|
+
if not isinstance(job, dict):
|
|
152
|
+
return False
|
|
153
|
+
needs = job.get("needs")
|
|
154
|
+
if isinstance(needs, str):
|
|
155
|
+
return needs == required
|
|
156
|
+
if isinstance(needs, list):
|
|
157
|
+
return required in needs
|
|
158
|
+
return False
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _permissions_include(job: Any, key: str, value: str) -> bool:
|
|
162
|
+
if not isinstance(job, dict):
|
|
163
|
+
return False
|
|
164
|
+
permissions = job.get("permissions")
|
|
165
|
+
return isinstance(permissions, dict) and permissions.get(key) == value
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _job_uses_environment(job: Any, environment: str) -> bool:
|
|
169
|
+
if not isinstance(job, dict):
|
|
170
|
+
return False
|
|
171
|
+
value = job.get("environment")
|
|
172
|
+
if isinstance(value, str):
|
|
173
|
+
return value == environment
|
|
174
|
+
if isinstance(value, dict):
|
|
175
|
+
return value.get("name") == environment
|
|
176
|
+
return False
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _job_uses_action(job: Any, action_prefix: str) -> bool:
|
|
180
|
+
if not isinstance(job, dict):
|
|
181
|
+
return False
|
|
182
|
+
steps = job.get("steps")
|
|
183
|
+
if not isinstance(steps, list):
|
|
184
|
+
return False
|
|
185
|
+
for step in steps:
|
|
186
|
+
if not isinstance(step, dict):
|
|
187
|
+
continue
|
|
188
|
+
uses = step.get("uses")
|
|
189
|
+
if isinstance(uses, str) and uses.startswith(action_prefix):
|
|
190
|
+
return True
|
|
191
|
+
return False
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _job_text(job: Any) -> str:
|
|
195
|
+
return yaml.safe_dump(job, sort_keys=True) if isinstance(job, dict) else ""
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def render_release_readiness(repo_path: Path) -> dict[str, Any]:
|
|
199
|
+
"""Inspect whether the standalone package is ready for package-index promotion."""
|
|
200
|
+
|
|
201
|
+
repo_path = repo_path.expanduser().resolve()
|
|
202
|
+
workflow_path = repo_path / ".github" / "workflows" / "release.yml"
|
|
203
|
+
workflow = _read_text_if_exists(workflow_path)
|
|
204
|
+
ci_workflow_path = repo_path / ".github" / "workflows" / "ci.yml"
|
|
205
|
+
ci_workflow = _read_text_if_exists(ci_workflow_path)
|
|
206
|
+
workflow_jobs = _workflow_jobs(workflow)
|
|
207
|
+
testpypi_job = workflow_jobs.get("publish-testpypi")
|
|
208
|
+
pypi_job = workflow_jobs.get("publish-pypi")
|
|
209
|
+
testpypi_job_text = _job_text(testpypi_job)
|
|
210
|
+
pypi_job_text = _job_text(pypi_job)
|
|
211
|
+
docs = _release_docs(repo_path)
|
|
212
|
+
public_hygiene_docs = {
|
|
213
|
+
relative_path: _read_text_if_exists(repo_path / relative_path)
|
|
214
|
+
for relative_path in PUBLIC_HYGIENE_DOC_PATHS
|
|
215
|
+
}
|
|
216
|
+
init_version = _python_package_version(repo_path)
|
|
217
|
+
pyproject_version = _pyproject_version(repo_path)
|
|
218
|
+
version = init_version or pyproject_version
|
|
219
|
+
materialized_versions = _materialized_package_versions(repo_path)
|
|
220
|
+
release_tag = _release_tag_for_version(version) if version else ""
|
|
221
|
+
package_index_spec = f"code-mower=={version}" if version else ""
|
|
222
|
+
doc_blob = "\n".join(docs.values())
|
|
223
|
+
public_hygiene_blobs = {
|
|
224
|
+
relative_path: text.lower()
|
|
225
|
+
for relative_path, text in public_hygiene_docs.items()
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
version_docs = [
|
|
229
|
+
relative_path
|
|
230
|
+
for relative_path, text in docs.items()
|
|
231
|
+
if release_tag and release_tag in text
|
|
232
|
+
]
|
|
233
|
+
missing_release_docs = [
|
|
234
|
+
relative_path
|
|
235
|
+
for relative_path in REQUIRED_RELEASE_TAG_DOC_PATHS
|
|
236
|
+
if release_tag and release_tag not in docs.get(relative_path, "")
|
|
237
|
+
]
|
|
238
|
+
package_index_docs = [
|
|
239
|
+
relative_path
|
|
240
|
+
for relative_path, text in docs.items()
|
|
241
|
+
if package_index_spec and package_index_spec in text
|
|
242
|
+
]
|
|
243
|
+
missing_public_hygiene_docs = [
|
|
244
|
+
relative_path
|
|
245
|
+
for relative_path, text in public_hygiene_docs.items()
|
|
246
|
+
if not text
|
|
247
|
+
]
|
|
248
|
+
redaction_terms = (
|
|
249
|
+
"tokens",
|
|
250
|
+
"private source",
|
|
251
|
+
"raw diffs",
|
|
252
|
+
"raw model transcripts",
|
|
253
|
+
"auth output",
|
|
254
|
+
"security.md",
|
|
255
|
+
)
|
|
256
|
+
public_redaction_docs = ("SUPPORT.md", "CODE_OF_CONDUCT.md")
|
|
257
|
+
missing_redaction_terms = {
|
|
258
|
+
relative_path: [
|
|
259
|
+
term
|
|
260
|
+
for term in redaction_terms
|
|
261
|
+
if term not in public_hygiene_blobs.get(relative_path, "")
|
|
262
|
+
]
|
|
263
|
+
for relative_path in public_redaction_docs
|
|
264
|
+
}
|
|
265
|
+
missing_redaction_terms = {
|
|
266
|
+
relative_path: terms
|
|
267
|
+
for relative_path, terms in missing_redaction_terms.items()
|
|
268
|
+
if terms
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
checks = [
|
|
272
|
+
_release_check(
|
|
273
|
+
check_id="package-version-consistency",
|
|
274
|
+
title="Package versions agree",
|
|
275
|
+
status=(
|
|
276
|
+
"pass"
|
|
277
|
+
if init_version and pyproject_version and init_version == pyproject_version
|
|
278
|
+
else "fail"
|
|
279
|
+
),
|
|
280
|
+
evidence=(
|
|
281
|
+
f"src/code_mower/__init__.py={init_version or 'missing'}, "
|
|
282
|
+
f"pyproject.toml={pyproject_version or 'missing'}"
|
|
283
|
+
),
|
|
284
|
+
detail={"init_version": init_version, "pyproject_version": pyproject_version},
|
|
285
|
+
),
|
|
286
|
+
_release_check(
|
|
287
|
+
check_id="materialized-package-version-consistency",
|
|
288
|
+
title="Materialized package versions agree with source",
|
|
289
|
+
status=(
|
|
290
|
+
"pass"
|
|
291
|
+
if (
|
|
292
|
+
version
|
|
293
|
+
and not materialized_versions["error"]
|
|
294
|
+
and materialized_versions["init_version"] == version
|
|
295
|
+
and materialized_versions["pyproject_version"] == version
|
|
296
|
+
)
|
|
297
|
+
else "fail"
|
|
298
|
+
),
|
|
299
|
+
evidence=(
|
|
300
|
+
f"generated src/code_mower/__init__.py="
|
|
301
|
+
f"{materialized_versions['init_version'] or 'missing'}, "
|
|
302
|
+
f"generated pyproject.toml="
|
|
303
|
+
f"{materialized_versions['pyproject_version'] or 'missing'}"
|
|
304
|
+
),
|
|
305
|
+
detail={
|
|
306
|
+
"source_version": version,
|
|
307
|
+
"generated_init_version": materialized_versions["init_version"],
|
|
308
|
+
"generated_pyproject_version": materialized_versions[
|
|
309
|
+
"pyproject_version"
|
|
310
|
+
],
|
|
311
|
+
"error": materialized_versions["error"],
|
|
312
|
+
},
|
|
313
|
+
),
|
|
314
|
+
_release_check(
|
|
315
|
+
check_id="release-workflow-present",
|
|
316
|
+
title="Release workflow exists",
|
|
317
|
+
status="pass" if workflow_path.is_file() else "fail",
|
|
318
|
+
evidence=str(workflow_path),
|
|
319
|
+
),
|
|
320
|
+
_release_check(
|
|
321
|
+
check_id="distribution-build-and-verify",
|
|
322
|
+
title="Release workflow builds and verifies distributions before publish",
|
|
323
|
+
status=(
|
|
324
|
+
"pass"
|
|
325
|
+
if (
|
|
326
|
+
" build-distributions:\n" in workflow
|
|
327
|
+
and " verify-distributions:\n" in workflow
|
|
328
|
+
and " needs: build-distributions\n" in workflow
|
|
329
|
+
and "python -m build" in workflow
|
|
330
|
+
and "python -m twine check dist/*" in workflow
|
|
331
|
+
)
|
|
332
|
+
else "fail"
|
|
333
|
+
),
|
|
334
|
+
evidence=str(workflow_path),
|
|
335
|
+
),
|
|
336
|
+
_release_check(
|
|
337
|
+
check_id="manual-dispatch-gates",
|
|
338
|
+
title="Manual workflow dispatch has separate TestPyPI and PyPI inputs",
|
|
339
|
+
status=(
|
|
340
|
+
"pass"
|
|
341
|
+
if "workflow_dispatch:" in workflow
|
|
342
|
+
and "publish_testpypi:" in workflow
|
|
343
|
+
and "publish_pypi:" in workflow
|
|
344
|
+
else "fail"
|
|
345
|
+
),
|
|
346
|
+
evidence=str(workflow_path),
|
|
347
|
+
),
|
|
348
|
+
_release_check(
|
|
349
|
+
check_id="testpypi-gate",
|
|
350
|
+
title="TestPyPI publishing is gated separately",
|
|
351
|
+
status=(
|
|
352
|
+
"pass"
|
|
353
|
+
if (
|
|
354
|
+
_needs_job(testpypi_job, "verify-distributions")
|
|
355
|
+
and "inputs.publish_testpypi" in testpypi_job_text
|
|
356
|
+
and "CODE_MOWER_TESTPYPI_PUBLISH" in testpypi_job_text
|
|
357
|
+
and _job_uses_environment(testpypi_job, "testpypi")
|
|
358
|
+
and _permissions_include(testpypi_job, "id-token", "write")
|
|
359
|
+
and _job_uses_action(testpypi_job, "pypa/gh-action-pypi-publish@")
|
|
360
|
+
and "https://test.pypi.org/legacy/" in testpypi_job_text
|
|
361
|
+
)
|
|
362
|
+
else "fail"
|
|
363
|
+
),
|
|
364
|
+
evidence=str(workflow_path),
|
|
365
|
+
),
|
|
366
|
+
_release_check(
|
|
367
|
+
check_id="pypi-gate",
|
|
368
|
+
title="Production PyPI publishing is gated separately",
|
|
369
|
+
status=(
|
|
370
|
+
"pass"
|
|
371
|
+
if (
|
|
372
|
+
_needs_job(pypi_job, "verify-distributions")
|
|
373
|
+
and "inputs.publish_pypi" in pypi_job_text
|
|
374
|
+
and "CODE_MOWER_PYPI_PUBLISH" in pypi_job_text
|
|
375
|
+
and _job_uses_environment(pypi_job, "pypi")
|
|
376
|
+
and _permissions_include(pypi_job, "id-token", "write")
|
|
377
|
+
and _job_uses_action(pypi_job, "pypa/gh-action-pypi-publish@")
|
|
378
|
+
and "test.pypi.org" not in pypi_job_text
|
|
379
|
+
)
|
|
380
|
+
else "fail"
|
|
381
|
+
),
|
|
382
|
+
evidence=str(workflow_path),
|
|
383
|
+
),
|
|
384
|
+
_release_check(
|
|
385
|
+
check_id="release-tag-docs-current",
|
|
386
|
+
title="Current release tag is present in public install docs",
|
|
387
|
+
status="pass" if release_tag and not missing_release_docs else "fail",
|
|
388
|
+
evidence=release_tag or "missing version",
|
|
389
|
+
detail={
|
|
390
|
+
"docs": version_docs,
|
|
391
|
+
"required_docs": list(REQUIRED_RELEASE_TAG_DOC_PATHS),
|
|
392
|
+
"missing_docs": missing_release_docs,
|
|
393
|
+
},
|
|
394
|
+
),
|
|
395
|
+
_release_check(
|
|
396
|
+
check_id="package-index-rehearsal-docs",
|
|
397
|
+
title="Package-index rehearsal is documented",
|
|
398
|
+
status=(
|
|
399
|
+
"pass"
|
|
400
|
+
if (
|
|
401
|
+
package_index_spec
|
|
402
|
+
and package_index_spec in doc_blob
|
|
403
|
+
and "--pip-index-url https://test.pypi.org/simple/" in doc_blob
|
|
404
|
+
and "--pip-extra-index-url https://pypi.org/simple/" in doc_blob
|
|
405
|
+
and "package-install-rehearsal" in doc_blob
|
|
406
|
+
)
|
|
407
|
+
else "fail"
|
|
408
|
+
),
|
|
409
|
+
evidence=package_index_spec or "missing version",
|
|
410
|
+
detail={"docs": package_index_docs},
|
|
411
|
+
),
|
|
412
|
+
_release_check(
|
|
413
|
+
check_id="ci-package-install-rehearsal",
|
|
414
|
+
title="CI proves the package-installed first-user path",
|
|
415
|
+
status=(
|
|
416
|
+
"pass"
|
|
417
|
+
if (
|
|
418
|
+
"Package-install first-user rehearsal" in ci_workflow
|
|
419
|
+
and "package-install-rehearsal" in ci_workflow
|
|
420
|
+
and '--package-spec "$GITHUB_WORKSPACE"' in ci_workflow
|
|
421
|
+
and '--work-dir "$RUNNER_TEMP/code-mower-package-install"' in ci_workflow
|
|
422
|
+
and "--json" in ci_workflow
|
|
423
|
+
)
|
|
424
|
+
else "fail"
|
|
425
|
+
),
|
|
426
|
+
evidence=str(ci_workflow_path),
|
|
427
|
+
),
|
|
428
|
+
_release_check(
|
|
429
|
+
check_id="trusted-publishing-runbook",
|
|
430
|
+
title="Trusted publishing setup is documented",
|
|
431
|
+
status=(
|
|
432
|
+
"pass"
|
|
433
|
+
if (
|
|
434
|
+
"trusted publishing" in doc_blob.lower()
|
|
435
|
+
and "environment: `testpypi`" in doc_blob
|
|
436
|
+
and "environment: `pypi`" in doc_blob
|
|
437
|
+
and "Workflow Dispatch Matrix" in doc_blob
|
|
438
|
+
)
|
|
439
|
+
else "fail"
|
|
440
|
+
),
|
|
441
|
+
evidence="docs/pypi-release.md",
|
|
442
|
+
),
|
|
443
|
+
_release_check(
|
|
444
|
+
check_id="public-maintainer-docs",
|
|
445
|
+
title="Public maintainer and community files are present",
|
|
446
|
+
status="pass" if not missing_public_hygiene_docs else "fail",
|
|
447
|
+
evidence=", ".join(PUBLIC_HYGIENE_DOC_PATHS),
|
|
448
|
+
detail={"missing_docs": missing_public_hygiene_docs},
|
|
449
|
+
),
|
|
450
|
+
_release_check(
|
|
451
|
+
check_id="public-docs-linked-from-readme",
|
|
452
|
+
title="Public support, security, and conduct docs are linked from README",
|
|
453
|
+
status=(
|
|
454
|
+
"pass"
|
|
455
|
+
if (
|
|
456
|
+
"[Support](SUPPORT.md)" in docs.get("README.md", "")
|
|
457
|
+
and "[Security Policy](SECURITY.md)" in docs.get("README.md", "")
|
|
458
|
+
and "[Code of Conduct](CODE_OF_CONDUCT.md)" in docs.get("README.md", "")
|
|
459
|
+
)
|
|
460
|
+
else "fail"
|
|
461
|
+
),
|
|
462
|
+
evidence="README.md",
|
|
463
|
+
),
|
|
464
|
+
_release_check(
|
|
465
|
+
check_id="public-support-redaction-guidance",
|
|
466
|
+
title="Public support docs warn against sharing sensitive artifacts",
|
|
467
|
+
status="pass" if not missing_redaction_terms else "fail",
|
|
468
|
+
evidence="SUPPORT.md, CODE_OF_CONDUCT.md, SECURITY.md",
|
|
469
|
+
detail={"missing_terms_by_doc": missing_redaction_terms},
|
|
470
|
+
),
|
|
471
|
+
]
|
|
472
|
+
failed = sum(1 for check in checks if check["status"] == "fail")
|
|
473
|
+
warnings = sum(1 for check in checks if check["status"] == "warn")
|
|
474
|
+
passed = sum(1 for check in checks if check["status"] == "pass")
|
|
475
|
+
status = "pass" if failed == 0 else "fail"
|
|
476
|
+
next_actions = [
|
|
477
|
+
{
|
|
478
|
+
"id": "dry-run-release-workflow",
|
|
479
|
+
"title": "Run the release workflow without publishing",
|
|
480
|
+
"command": (
|
|
481
|
+
"gh workflow run release.yml --repo codemower-ai/code-mower "
|
|
482
|
+
"--ref main -f publish_testpypi=false -f publish_pypi=false"
|
|
483
|
+
),
|
|
484
|
+
"url": PACKAGE_INDEX_SETUP_URLS["release_workflow"],
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
"id": "publish-testpypi-candidate",
|
|
488
|
+
"title": "Publish the verified distribution to TestPyPI",
|
|
489
|
+
"command": (
|
|
490
|
+
"gh workflow run release.yml --repo codemower-ai/code-mower "
|
|
491
|
+
"--ref main -f publish_testpypi=true -f publish_pypi=false"
|
|
492
|
+
),
|
|
493
|
+
"url": PACKAGE_INDEX_SETUP_URLS["release_workflow"],
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
"id": "testpypi-install-rehearsal",
|
|
497
|
+
"title": "Install from TestPyPI in a fresh toy repo",
|
|
498
|
+
"command": (
|
|
499
|
+
"code-mower migration package-install-rehearsal "
|
|
500
|
+
f"--package-spec {package_index_spec} "
|
|
501
|
+
"--pip-index-url https://test.pypi.org/simple/ "
|
|
502
|
+
"--pip-extra-index-url https://pypi.org/simple/ "
|
|
503
|
+
"--json"
|
|
504
|
+
),
|
|
505
|
+
"url": PACKAGE_INDEX_SETUP_URLS["testpypi_project"],
|
|
506
|
+
},
|
|
507
|
+
]
|
|
508
|
+
return {
|
|
509
|
+
"mode": "code-mower-release-readiness",
|
|
510
|
+
"status": status,
|
|
511
|
+
"repo_path": str(repo_path),
|
|
512
|
+
"version": version,
|
|
513
|
+
"release_tag": release_tag,
|
|
514
|
+
"alpha_tag": release_tag,
|
|
515
|
+
"package_index_spec": package_index_spec,
|
|
516
|
+
"passed": passed,
|
|
517
|
+
"failed": failed,
|
|
518
|
+
"warnings": warnings,
|
|
519
|
+
"total": len(checks),
|
|
520
|
+
"checks": checks,
|
|
521
|
+
"next_actions": next_actions,
|
|
522
|
+
"setup_urls": PACKAGE_INDEX_SETUP_URLS,
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def render_release_readiness_text(payload: dict[str, Any]) -> str:
|
|
527
|
+
lines = [
|
|
528
|
+
"Code Mower release readiness",
|
|
529
|
+
"",
|
|
530
|
+
f"status: {payload['status']}",
|
|
531
|
+
f"version: {payload.get('version') or 'unknown'}",
|
|
532
|
+
f"release_tag: {payload.get('release_tag') or payload.get('alpha_tag') or 'unknown'}",
|
|
533
|
+
f"checks: {payload['passed']} passed, {payload['failed']} failed, {payload['warnings']} warnings",
|
|
534
|
+
"",
|
|
535
|
+
"Checks:",
|
|
536
|
+
]
|
|
537
|
+
for check in payload["checks"]:
|
|
538
|
+
lines.append(f"- {check['status'].upper()} {check['id']}: {check['title']}")
|
|
539
|
+
lines.append(f" evidence: {check['evidence']}")
|
|
540
|
+
lines.extend(["", "Setup URLs:"])
|
|
541
|
+
for label, url in payload.get("setup_urls", {}).items():
|
|
542
|
+
lines.append(f"- {label}: {url}")
|
|
543
|
+
lines.extend(["", "Next actions:"])
|
|
544
|
+
for action in payload["next_actions"]:
|
|
545
|
+
lines.append(f"- {action['title']}")
|
|
546
|
+
lines.append(f" {action['command']}")
|
|
547
|
+
if action.get("url"):
|
|
548
|
+
lines.append(f" {action['url']}")
|
|
549
|
+
return "\n".join(lines) + "\n"
|