core-runtime-engine 11.5.1__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.
- core_runtime/__init__.py +10 -0
- core_runtime/__version__.py +17 -0
- core_runtime/cli/__init__.py +7 -0
- core_runtime/cli/__main__.py +22 -0
- core_runtime/cli/bump_version.py +176 -0
- core_runtime/cli/contract_preflight.py +69 -0
- core_runtime/cli/create_domain.py +66 -0
- core_runtime/cli/doctor.py +44 -0
- core_runtime/cli/inventory.py +85 -0
- core_runtime/cli/lint.py +8 -0
- core_runtime/cli/main.py +579 -0
- core_runtime/cli/release_check.py +65 -0
- core_runtime/cli/repair_artifact_paths.py +48 -0
- core_runtime/cli/sync_template.py +67 -0
- core_runtime/cli/validate.py +73 -0
- core_runtime/core/__init__.py +34 -0
- core_runtime/core/audit_event.py +760 -0
- core_runtime/core/audit_trail_index.py +100 -0
- core_runtime/core/canonicalization.py +55 -0
- core_runtime/core/contract_evaluator.py +945 -0
- core_runtime/core/contract_executability.py +307 -0
- core_runtime/core/contract_loader.py +57 -0
- core_runtime/core/contract_probes.py +630 -0
- core_runtime/core/contract_program.py +131 -0
- core_runtime/core/contract_program_registry.py +104 -0
- core_runtime/core/contract_program_v2.py +126 -0
- core_runtime/core/dsk_v3.py +142 -0
- core_runtime/core/explainability.py +2115 -0
- core_runtime/core/numeric_normalization.py +120 -0
- core_runtime/core/rule_anchor.py +1388 -0
- core_runtime/core/schema_fingerprint.py +69 -0
- core_runtime/core/sensor_evidence.py +548 -0
- core_runtime/data/contracts/CoreAnchor.sol +109 -0
- core_runtime/data/contracts/CoreRuleAnchor.abi.json +111 -0
- core_runtime/data/contracts/CoreRuleAnchor.bin +1 -0
- core_runtime/data/contracts/CoreRuleAnchor.build.json +20 -0
- core_runtime/data/contracts/CoreRuleAnchor.runtime.bin +1 -0
- core_runtime/data/contracts/CoreRuleAnchor.sol +74 -0
- core_runtime/data/package_data_manifest.v1.json +173 -0
- core_runtime/data/schemas/core/causal_trace.v1.json +141 -0
- core_runtime/data/schemas/core/context_gate.v1.json +38 -0
- core_runtime/data/schemas/core/context_threshold.v1.json +46 -0
- core_runtime/data/schemas/core/contract_program.v1.json +186 -0
- core_runtime/data/schemas/core/contract_program.v2.json +187 -0
- core_runtime/data/schemas/core/control_decision.v1.json +114 -0
- core_runtime/data/schemas/core/dsk.v3.json +105 -0
- core_runtime/data/schemas/core/effect_result.v1.json +39 -0
- core_runtime/data/schemas/core/entropy_signal.v1.json +108 -0
- core_runtime/data/schemas/core/execution_receipt.v1.json +93 -0
- core_runtime/data/schemas/core/frozen_release_manifest.v1.json +87 -0
- core_runtime/data/schemas/core/frozen_release_manifest.v2.json +72 -0
- core_runtime/data/schemas/core/frozen_release_manifest.v3.json +38 -0
- core_runtime/data/schemas/core/frozen_release_manifest.v4.json +72 -0
- core_runtime/data/schemas/core/frozen_release_manifest.v5.json +38 -0
- core_runtime/data/schemas/core/frozen_release_manifest.v6.json +116 -0
- core_runtime/data/schemas/core/frozen_release_manifest.v7.json +37 -0
- core_runtime/data/schemas/core/frozen_release_manifest.v8.json +114 -0
- core_runtime/data/schemas/core/frozen_rule_set.v1.json +282 -0
- core_runtime/data/schemas/core/memory_artifact.v1.json +120 -0
- core_runtime/data/schemas/core/memory_generation_result.v1.json +37 -0
- core_runtime/data/schemas/core/operational_learning_event.v1.json +63 -0
- core_runtime/data/schemas/core/pattern_candidate.v1.json +114 -0
- core_runtime/data/schemas/core/physical_safety_assurance_case.v1.json +676 -0
- core_runtime/data/schemas/core/policy_lifecycle.v1.json +99 -0
- core_runtime/data/schemas/core/retention_manifest.v1.json +53 -0
- core_runtime/data/schemas/core/reversibility_policy.v1.json +107 -0
- core_runtime/data/schemas/core/rule_anchor_batch.v1.json +93 -0
- core_runtime/data/schemas/core/rule_anchor_chain_evidence.v1.json +56 -0
- core_runtime/data/schemas/core/rule_approval.v1.json +49 -0
- core_runtime/data/schemas/core/rule_approval_request.v1.json +42 -0
- core_runtime/data/schemas/core/state_transition.v1.json +115 -0
- core_runtime/data/schemas/core/task_closeout.v1.json +47 -0
- core_runtime/data/schemas/core/template_promotion_candidate.v1.json +83 -0
- core_runtime/data/schemas/core/unsigned_rule_anchor_deployment.v1.json +106 -0
- core_runtime/data/schemas/core/unsigned_rule_anchor_transaction.v1.json +116 -0
- core_runtime/tooling/__init__.py +48 -0
- core_runtime/tooling/bump_version.py +900 -0
- core_runtime/tooling/contract_preflight.py +293 -0
- core_runtime/tooling/create_domain.py +254 -0
- core_runtime/tooling/diagnostics.py +129 -0
- core_runtime/tooling/doctor.py +482 -0
- core_runtime/tooling/file_inventory.py +182 -0
- core_runtime/tooling/json_checks.py +100 -0
- core_runtime/tooling/release_check.py +1017 -0
- core_runtime/tooling/repair_artifact_paths.py +458 -0
- core_runtime/tooling/report_writer.py +176 -0
- core_runtime/tooling/repository_inventory.py +399 -0
- core_runtime/tooling/safety_checks.py +172 -0
- core_runtime/tooling/sync_template.py +303 -0
- core_runtime/tooling/validation.py +507 -0
- core_runtime/tooling/version_inventory.py +256 -0
- core_runtime_engine-11.5.1.dist-info/METADATA +35 -0
- core_runtime_engine-11.5.1.dist-info/RECORD +96 -0
- core_runtime_engine-11.5.1.dist-info/WHEEL +5 -0
- core_runtime_engine-11.5.1.dist-info/entry_points.txt +2 -0
- core_runtime_engine-11.5.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1017 @@
|
|
|
1
|
+
"""Release-check wrapper around the authoritative release verification script."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import py_compile
|
|
7
|
+
import re
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
import time
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Optional
|
|
14
|
+
|
|
15
|
+
from core_runtime.tooling.diagnostics import DiagnosticCollection
|
|
16
|
+
from core_runtime.tooling.version_inventory import VersionInventory
|
|
17
|
+
|
|
18
|
+
DEFAULT_RELEASE_CHECK_TIMEOUT_SECONDS = 120
|
|
19
|
+
OUTPUT_PREVIEW_LIMIT = 2048
|
|
20
|
+
|
|
21
|
+
RELEASE_CHECK_PROFILES: dict[str, dict[str, tuple[str, ...] | str]] = {
|
|
22
|
+
"fast": {
|
|
23
|
+
"description": "Tooling, metadata, and tooling-test baseline.",
|
|
24
|
+
"groups": ("tooling", "release-metadata", "tests-tooling"),
|
|
25
|
+
"notes": ("Fast developer signal with bounded deterministic checks.",),
|
|
26
|
+
},
|
|
27
|
+
"local": {
|
|
28
|
+
"description": "Fast profile plus bounded developer test slices.",
|
|
29
|
+
"groups": ("tooling", "release-metadata", "tests-tooling", "tests-replay", "tests-integration"),
|
|
30
|
+
"notes": ("Useful for local iteration without the longest contracts/core slices.",),
|
|
31
|
+
},
|
|
32
|
+
"full": {
|
|
33
|
+
"description": "Full deterministic release surface.",
|
|
34
|
+
"groups": (
|
|
35
|
+
"tooling",
|
|
36
|
+
"release-metadata",
|
|
37
|
+
"replay",
|
|
38
|
+
"tests-tooling",
|
|
39
|
+
"tests-replay",
|
|
40
|
+
"tests-integration",
|
|
41
|
+
"tests-contracts",
|
|
42
|
+
"tests-core",
|
|
43
|
+
),
|
|
44
|
+
"notes": ("Matches the current bounded full release gate.",),
|
|
45
|
+
},
|
|
46
|
+
"release-candidate": {
|
|
47
|
+
"description": "Full release surface with future tag and package metadata expectations.",
|
|
48
|
+
"groups": (
|
|
49
|
+
"tooling",
|
|
50
|
+
"release-metadata",
|
|
51
|
+
"replay",
|
|
52
|
+
"tests-tooling",
|
|
53
|
+
"tests-replay",
|
|
54
|
+
"tests-integration",
|
|
55
|
+
"tests-contracts",
|
|
56
|
+
"tests-core",
|
|
57
|
+
),
|
|
58
|
+
"notes": (
|
|
59
|
+
"Future package metadata and tag expectation checks remain advisory-only.",
|
|
60
|
+
),
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
_STRICT_VERSION_RE = re.compile(r"^\d+\.\d+\.\d+$")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def validate_release_target(target: str) -> Optional[str]:
|
|
68
|
+
"""Return None when *target* is a supported release target."""
|
|
69
|
+
if target.startswith("v"):
|
|
70
|
+
target = target[1:]
|
|
71
|
+
if not _STRICT_VERSION_RE.match(target):
|
|
72
|
+
return "Invalid release target: '{0}'. Expected MAJOR.MINOR.PATCH or vMAJOR.MINOR.PATCH.".format(
|
|
73
|
+
target
|
|
74
|
+
)
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def normalize_release_target(target: str) -> str:
|
|
79
|
+
"""Normalize a target to the canonical release-check form."""
|
|
80
|
+
return target[1:] if target.startswith("v") else target
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def release_target_argument(target: str) -> str:
|
|
84
|
+
"""Format the normalized target for scripts/verify_release.py."""
|
|
85
|
+
return "v{0}".format(normalize_release_target(target))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _coerce_text(value: Any) -> str:
|
|
89
|
+
if value is None:
|
|
90
|
+
return ""
|
|
91
|
+
if isinstance(value, bytes):
|
|
92
|
+
return value.decode("utf-8", errors="replace")
|
|
93
|
+
return str(value)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _truncate_text(text: str, limit: int = OUTPUT_PREVIEW_LIMIT) -> tuple[str, bool]:
|
|
97
|
+
if len(text) <= limit:
|
|
98
|
+
return text, False
|
|
99
|
+
return text[:limit] + "…[truncated]", True
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass
|
|
103
|
+
class SubprocessCapture:
|
|
104
|
+
"""Captured subprocess execution details."""
|
|
105
|
+
|
|
106
|
+
command: list[str]
|
|
107
|
+
cwd: str
|
|
108
|
+
expect_json: bool
|
|
109
|
+
returncode: Optional[int]
|
|
110
|
+
stdout: str
|
|
111
|
+
stderr: str
|
|
112
|
+
timed_out: bool = False
|
|
113
|
+
timeout_seconds: Optional[int] = None
|
|
114
|
+
elapsed_seconds: Optional[float] = None
|
|
115
|
+
json_detected: bool = False
|
|
116
|
+
json_payload: dict[str, Any] | None = None
|
|
117
|
+
report_path: Optional[str] = None
|
|
118
|
+
target_argument: Optional[str] = None
|
|
119
|
+
help_contains_target: Optional[bool] = None
|
|
120
|
+
stdout_preview: str = ""
|
|
121
|
+
stderr_preview: str = ""
|
|
122
|
+
stdout_truncated: bool = False
|
|
123
|
+
stderr_truncated: bool = False
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def status(self) -> str:
|
|
127
|
+
if self.timed_out:
|
|
128
|
+
return "blocked"
|
|
129
|
+
if self.returncode is None:
|
|
130
|
+
return "internal_error"
|
|
131
|
+
if self.returncode == 0:
|
|
132
|
+
if self.expect_json and not self.json_detected:
|
|
133
|
+
return "internal_error"
|
|
134
|
+
return "pass"
|
|
135
|
+
if self.returncode == 1:
|
|
136
|
+
return "error"
|
|
137
|
+
if self.returncode == 2:
|
|
138
|
+
return "blocked"
|
|
139
|
+
return "internal_error"
|
|
140
|
+
|
|
141
|
+
def to_dict(self) -> dict[str, Any]:
|
|
142
|
+
result: dict[str, Any] = {
|
|
143
|
+
"command": self.command,
|
|
144
|
+
"cwd": self.cwd,
|
|
145
|
+
"expect_json": self.expect_json,
|
|
146
|
+
"exit_code": self.returncode,
|
|
147
|
+
"json_detected": self.json_detected,
|
|
148
|
+
"status": self.status,
|
|
149
|
+
"report_path": self.report_path,
|
|
150
|
+
"stdout_preview": self.stdout_preview,
|
|
151
|
+
"stderr_preview": self.stderr_preview,
|
|
152
|
+
"stdout_truncated": self.stdout_truncated,
|
|
153
|
+
"stderr_truncated": self.stderr_truncated,
|
|
154
|
+
"timed_out": self.timed_out,
|
|
155
|
+
"timeout_seconds": self.timeout_seconds,
|
|
156
|
+
}
|
|
157
|
+
if self.elapsed_seconds is not None:
|
|
158
|
+
result["elapsed_seconds"] = self.elapsed_seconds
|
|
159
|
+
if self.target_argument is not None:
|
|
160
|
+
result["target_argument"] = self.target_argument
|
|
161
|
+
if self.help_contains_target is not None:
|
|
162
|
+
result["help_contains_target"] = self.help_contains_target
|
|
163
|
+
if self.json_payload is not None:
|
|
164
|
+
result["json_payload"] = self.json_payload
|
|
165
|
+
return result
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@dataclass
|
|
169
|
+
class ReleaseCheckReport:
|
|
170
|
+
"""Normalized release-check wrapper report."""
|
|
171
|
+
|
|
172
|
+
mode: str
|
|
173
|
+
target: str
|
|
174
|
+
target_argument: str
|
|
175
|
+
timeout_seconds: int
|
|
176
|
+
profile: Optional[str] = None
|
|
177
|
+
tool: str = "core-runtime release-check"
|
|
178
|
+
status: str = "internal_error"
|
|
179
|
+
mutation_performed: bool = False
|
|
180
|
+
summary: dict[str, Any] = field(default_factory=dict)
|
|
181
|
+
diagnostics: DiagnosticCollection = field(default_factory=DiagnosticCollection)
|
|
182
|
+
profile_definition: dict[str, Any] | None = None
|
|
183
|
+
profile_runs: list[SubprocessCapture] = field(default_factory=list)
|
|
184
|
+
tooling_lint: SubprocessCapture | None = None
|
|
185
|
+
release_gate_help: SubprocessCapture | None = None
|
|
186
|
+
release_gate: SubprocessCapture | None = None
|
|
187
|
+
preflight_checks: dict[str, Any] = field(default_factory=dict)
|
|
188
|
+
debug: dict[str, Any] | None = None
|
|
189
|
+
|
|
190
|
+
def _summary(self) -> dict[str, Any]:
|
|
191
|
+
summary = dict(self.summary)
|
|
192
|
+
summary.setdefault("tooling_lint_status", self.tooling_lint.status if self.tooling_lint else "skipped")
|
|
193
|
+
summary.setdefault("release_gate_status", self.release_gate.status if self.release_gate else "skipped")
|
|
194
|
+
summary.setdefault("preflight_status", self.preflight_checks.get("status", "skipped"))
|
|
195
|
+
counts = self.diagnostics.count_by_severity()
|
|
196
|
+
summary.setdefault("info", counts["info"])
|
|
197
|
+
summary.setdefault("warning", counts["warning"])
|
|
198
|
+
summary.setdefault("error", counts["error"])
|
|
199
|
+
summary.setdefault("blocked", counts["blocked"])
|
|
200
|
+
return summary
|
|
201
|
+
|
|
202
|
+
def to_dict(self) -> dict[str, Any]:
|
|
203
|
+
result = {
|
|
204
|
+
"tool": self.tool,
|
|
205
|
+
"mode": self.mode,
|
|
206
|
+
"target": self.target,
|
|
207
|
+
"target_argument": self.target_argument,
|
|
208
|
+
"profile": self.profile,
|
|
209
|
+
"status": self.status,
|
|
210
|
+
"mutation_performed": self.mutation_performed,
|
|
211
|
+
"summary": self._summary(),
|
|
212
|
+
"diagnostics": [d.to_dict() for d in self.diagnostics.diagnostics],
|
|
213
|
+
"preflight_checks": self.preflight_checks,
|
|
214
|
+
"timeout_seconds": self.timeout_seconds,
|
|
215
|
+
}
|
|
216
|
+
if self.profile_definition is not None:
|
|
217
|
+
result["profile_definition"] = self.profile_definition
|
|
218
|
+
if self.profile_runs:
|
|
219
|
+
result["profile_runs"] = [capture.to_dict() for capture in self.profile_runs]
|
|
220
|
+
if self.tooling_lint is not None:
|
|
221
|
+
result["tooling_lint"] = self.tooling_lint.to_dict()
|
|
222
|
+
if self.release_gate_help is not None:
|
|
223
|
+
result["release_gate_help"] = self.release_gate_help.to_dict()
|
|
224
|
+
if self.release_gate is not None:
|
|
225
|
+
result["release_gate"] = self.release_gate.to_dict()
|
|
226
|
+
if self.debug is not None:
|
|
227
|
+
result["debug"] = self.debug
|
|
228
|
+
return result
|
|
229
|
+
|
|
230
|
+
def to_markdown(self) -> str:
|
|
231
|
+
counts = self.diagnostics.count_by_severity()
|
|
232
|
+
summary = self._summary()
|
|
233
|
+
lines: list[str] = []
|
|
234
|
+
lines.append("# CORE release-check report")
|
|
235
|
+
lines.append("")
|
|
236
|
+
lines.append("## Summary")
|
|
237
|
+
lines.append("")
|
|
238
|
+
lines.append("| Metric | Value |")
|
|
239
|
+
lines.append("|--------|-------|")
|
|
240
|
+
lines.append("| Tool | core-runtime release-check |")
|
|
241
|
+
lines.append("| Mode | {0} |".format(self.mode))
|
|
242
|
+
lines.append("| Profile | {0} |".format(self.profile or "none"))
|
|
243
|
+
lines.append("| Target | {0} |".format(self.target))
|
|
244
|
+
lines.append("| Target Argument | {0} |".format(self.target_argument))
|
|
245
|
+
lines.append("| Status | {0} |".format(self.status.upper()))
|
|
246
|
+
lines.append("| Mutation Performed | No |")
|
|
247
|
+
lines.append("| Tooling Lint | {0} |".format(summary.get("tooling_lint_status", "skipped")))
|
|
248
|
+
lines.append("| Release Gate | {0} |".format(summary.get("release_gate_status", "skipped")))
|
|
249
|
+
lines.append("| Preflight | {0} |".format(summary.get("preflight_status", "skipped")))
|
|
250
|
+
lines.append("| Info | {0} |".format(counts["info"]))
|
|
251
|
+
lines.append("| Warning | {0} |".format(counts["warning"]))
|
|
252
|
+
lines.append("| Error | {0} |".format(counts["error"]))
|
|
253
|
+
lines.append("| Blocked | {0} |".format(counts["blocked"]))
|
|
254
|
+
lines.append("")
|
|
255
|
+
|
|
256
|
+
lines.append("## Target")
|
|
257
|
+
lines.append("")
|
|
258
|
+
lines.append("- Normalized target: `{0}`".format(self.target))
|
|
259
|
+
lines.append("- Release gate argument: `{0}`".format(self.target_argument))
|
|
260
|
+
lines.append("")
|
|
261
|
+
|
|
262
|
+
lines.append("## Tooling Lint Precheck")
|
|
263
|
+
lines.append("")
|
|
264
|
+
if self.tooling_lint is None:
|
|
265
|
+
lines.append("- Skipped by request.")
|
|
266
|
+
else:
|
|
267
|
+
lines.append("- Command: `{0}`".format(" ".join(self.tooling_lint.command)))
|
|
268
|
+
lines.append("- cwd: `{0}`".format(self.tooling_lint.cwd))
|
|
269
|
+
lines.append("- Exit code: `{0}`".format(self.tooling_lint.returncode))
|
|
270
|
+
lines.append("- JSON detected: `{0}`".format("yes" if self.tooling_lint.json_detected else "no"))
|
|
271
|
+
lines.append("- Status: `{0}`".format(self.tooling_lint.status))
|
|
272
|
+
lines.append("")
|
|
273
|
+
|
|
274
|
+
lines.append("## Preflight Checks")
|
|
275
|
+
lines.append("")
|
|
276
|
+
if self.preflight_checks:
|
|
277
|
+
for key, value in self.preflight_checks.items():
|
|
278
|
+
lines.append("- `{0}`: {1}".format(key, value))
|
|
279
|
+
else:
|
|
280
|
+
lines.append("- None")
|
|
281
|
+
lines.append("")
|
|
282
|
+
|
|
283
|
+
lines.append("## Profile")
|
|
284
|
+
lines.append("")
|
|
285
|
+
if self.profile_definition is None:
|
|
286
|
+
lines.append("- None")
|
|
287
|
+
else:
|
|
288
|
+
lines.append("- Name: `{0}`".format(self.profile or ""))
|
|
289
|
+
lines.append("- Description: {0}".format(self.profile_definition.get("description", "")))
|
|
290
|
+
groups = self.profile_definition.get("groups", ())
|
|
291
|
+
if groups:
|
|
292
|
+
lines.append("- Groups: {0}".format(", ".join(f"`{group}`" for group in groups)))
|
|
293
|
+
notes = self.profile_definition.get("notes", ())
|
|
294
|
+
for note in notes:
|
|
295
|
+
lines.append("- Note: {0}".format(note))
|
|
296
|
+
if self.profile_runs:
|
|
297
|
+
lines.append("- Runs:")
|
|
298
|
+
for capture in self.profile_runs:
|
|
299
|
+
lines.append(
|
|
300
|
+
" - `{0}` -> `{1}` ({2})".format(
|
|
301
|
+
" ".join(capture.command),
|
|
302
|
+
capture.status,
|
|
303
|
+
capture.elapsed_seconds if capture.elapsed_seconds is not None else "n/a",
|
|
304
|
+
)
|
|
305
|
+
)
|
|
306
|
+
lines.append("")
|
|
307
|
+
|
|
308
|
+
lines.append("## Release Gate")
|
|
309
|
+
lines.append("")
|
|
310
|
+
if self.release_gate is None:
|
|
311
|
+
lines.append("- Not run.")
|
|
312
|
+
else:
|
|
313
|
+
lines.append("- Script: `scripts/verify_release.py`")
|
|
314
|
+
lines.append("- Command: `{0}`".format(" ".join(self.release_gate.command)))
|
|
315
|
+
lines.append("- cwd: `{0}`".format(self.release_gate.cwd))
|
|
316
|
+
lines.append("- Exit code: `{0}`".format(self.release_gate.returncode))
|
|
317
|
+
lines.append("- JSON detected: `{0}`".format("yes" if self.release_gate.json_detected else "no"))
|
|
318
|
+
lines.append("- Timed out: `{0}`".format("yes" if self.release_gate.timed_out else "no"))
|
|
319
|
+
lines.append("- Status: `{0}`".format(self.release_gate.status))
|
|
320
|
+
lines.append("- Stdout preview: `{0}`".format(self.release_gate.stdout_preview or ""))
|
|
321
|
+
lines.append("- Stderr preview: `{0}`".format(self.release_gate.stderr_preview or ""))
|
|
322
|
+
lines.append("")
|
|
323
|
+
|
|
324
|
+
lines.append("## Diagnostics")
|
|
325
|
+
lines.append("")
|
|
326
|
+
if self.diagnostics.diagnostics:
|
|
327
|
+
for diagnostic in self.diagnostics.diagnostics:
|
|
328
|
+
lines.append("- `{0}` {1}: {2}".format(diagnostic.severity.value, diagnostic.code, diagnostic.message))
|
|
329
|
+
else:
|
|
330
|
+
lines.append("- None")
|
|
331
|
+
lines.append("")
|
|
332
|
+
|
|
333
|
+
lines.append("## Final Status")
|
|
334
|
+
lines.append("")
|
|
335
|
+
if self.status == "pass":
|
|
336
|
+
lines.append("✅ **PASS** - Release-check completed successfully.")
|
|
337
|
+
elif self.status == "warning":
|
|
338
|
+
lines.append("⚠️ **WARNING** - Release-check completed with warnings.")
|
|
339
|
+
elif self.status == "error":
|
|
340
|
+
lines.append("❌ **ERROR** - Release-check failed.")
|
|
341
|
+
elif self.status == "blocked":
|
|
342
|
+
lines.append("🚫 **BLOCKED** - Release-check could not safely run.")
|
|
343
|
+
else:
|
|
344
|
+
lines.append("⚠️ **INTERNAL ERROR** - Release-check tooling failure.")
|
|
345
|
+
lines.append("")
|
|
346
|
+
return "\n".join(lines)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
class ReleaseCheckRunner:
|
|
350
|
+
"""Run tooling lint precheck and the authoritative release gate."""
|
|
351
|
+
|
|
352
|
+
def __init__(self, repo_root: Path):
|
|
353
|
+
self.repo_root = repo_root
|
|
354
|
+
self.version_inventory = VersionInventory(repo_root)
|
|
355
|
+
|
|
356
|
+
def resolve_target(self, target: Optional[str]) -> tuple[str, str]:
|
|
357
|
+
"""Return (normalized_target, gate_argument)."""
|
|
358
|
+
if target is None:
|
|
359
|
+
canonical = self.version_inventory.get_canonical_version()
|
|
360
|
+
if canonical is None:
|
|
361
|
+
raise ValueError("canonical version missing")
|
|
362
|
+
target = canonical
|
|
363
|
+
|
|
364
|
+
normalized = normalize_release_target(target)
|
|
365
|
+
error = validate_release_target(normalized)
|
|
366
|
+
if error is not None:
|
|
367
|
+
raise ValueError(error)
|
|
368
|
+
|
|
369
|
+
return normalized, release_target_argument(normalized)
|
|
370
|
+
|
|
371
|
+
def run(
|
|
372
|
+
self,
|
|
373
|
+
target: Optional[str] = None,
|
|
374
|
+
skip_tooling_lint: bool = False,
|
|
375
|
+
timeout_seconds: int = DEFAULT_RELEASE_CHECK_TIMEOUT_SECONDS,
|
|
376
|
+
preflight_only: bool = False,
|
|
377
|
+
debug: bool = False,
|
|
378
|
+
group: Optional[str] = None,
|
|
379
|
+
profile: Optional[str] = None,
|
|
380
|
+
list_checks: bool = False,
|
|
381
|
+
plan: bool = False,
|
|
382
|
+
timing_json: Optional[str] = None,
|
|
383
|
+
) -> ReleaseCheckReport:
|
|
384
|
+
"""Execute the release-check workflow."""
|
|
385
|
+
diagnostics = DiagnosticCollection()
|
|
386
|
+
if list_checks and plan:
|
|
387
|
+
diagnostics.add_blocked(
|
|
388
|
+
code="core.release_check.invalid_mode",
|
|
389
|
+
message="List-checks and plan modes cannot be combined.",
|
|
390
|
+
path="core_runtime.cli release-check",
|
|
391
|
+
)
|
|
392
|
+
report = ReleaseCheckReport(
|
|
393
|
+
mode="blocked",
|
|
394
|
+
target="",
|
|
395
|
+
target_argument="",
|
|
396
|
+
timeout_seconds=timeout_seconds,
|
|
397
|
+
diagnostics=diagnostics,
|
|
398
|
+
status="blocked",
|
|
399
|
+
)
|
|
400
|
+
return report
|
|
401
|
+
|
|
402
|
+
if profile and group:
|
|
403
|
+
diagnostics.add_blocked(
|
|
404
|
+
code="core.release_check.invalid_mode",
|
|
405
|
+
message="Profile and group modes cannot be combined.",
|
|
406
|
+
path="core_runtime.cli release-check",
|
|
407
|
+
)
|
|
408
|
+
report = ReleaseCheckReport(
|
|
409
|
+
mode="blocked",
|
|
410
|
+
target="",
|
|
411
|
+
target_argument="",
|
|
412
|
+
timeout_seconds=timeout_seconds,
|
|
413
|
+
diagnostics=diagnostics,
|
|
414
|
+
status="blocked",
|
|
415
|
+
)
|
|
416
|
+
return report
|
|
417
|
+
|
|
418
|
+
if profile and preflight_only:
|
|
419
|
+
diagnostics.add_blocked(
|
|
420
|
+
code="core.release_check.invalid_mode",
|
|
421
|
+
message="Profile and preflight-only modes cannot be combined.",
|
|
422
|
+
path="core_runtime.cli release-check",
|
|
423
|
+
)
|
|
424
|
+
report = ReleaseCheckReport(
|
|
425
|
+
mode="blocked",
|
|
426
|
+
target="",
|
|
427
|
+
target_argument="",
|
|
428
|
+
timeout_seconds=timeout_seconds,
|
|
429
|
+
diagnostics=diagnostics,
|
|
430
|
+
status="blocked",
|
|
431
|
+
)
|
|
432
|
+
return report
|
|
433
|
+
|
|
434
|
+
if profile and profile not in RELEASE_CHECK_PROFILES:
|
|
435
|
+
diagnostics.add_blocked(
|
|
436
|
+
code="core.release_check.profile_unknown",
|
|
437
|
+
message="Unknown release-check profile.",
|
|
438
|
+
path="core_runtime.cli release-check",
|
|
439
|
+
details={"profile": profile, "allowed_profiles": sorted(RELEASE_CHECK_PROFILES)},
|
|
440
|
+
)
|
|
441
|
+
report = ReleaseCheckReport(
|
|
442
|
+
mode="blocked",
|
|
443
|
+
target="",
|
|
444
|
+
target_argument="",
|
|
445
|
+
timeout_seconds=timeout_seconds,
|
|
446
|
+
diagnostics=diagnostics,
|
|
447
|
+
status="blocked",
|
|
448
|
+
)
|
|
449
|
+
return report
|
|
450
|
+
|
|
451
|
+
if list_checks:
|
|
452
|
+
mode = "list-checks"
|
|
453
|
+
elif plan:
|
|
454
|
+
mode = "plan"
|
|
455
|
+
elif profile:
|
|
456
|
+
mode = f"profile:{profile}"
|
|
457
|
+
elif group:
|
|
458
|
+
mode = f"group:{group}"
|
|
459
|
+
elif preflight_only:
|
|
460
|
+
mode = "preflight-only"
|
|
461
|
+
else:
|
|
462
|
+
mode = "full"
|
|
463
|
+
report = ReleaseCheckReport(
|
|
464
|
+
mode=mode,
|
|
465
|
+
target="",
|
|
466
|
+
target_argument="",
|
|
467
|
+
timeout_seconds=timeout_seconds,
|
|
468
|
+
profile=profile,
|
|
469
|
+
diagnostics=diagnostics,
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
try:
|
|
473
|
+
normalized_target, gate_target_argument = self.resolve_target(target)
|
|
474
|
+
except ValueError as exc:
|
|
475
|
+
diagnostics.add_blocked(
|
|
476
|
+
code="core.release_check.invalid_target",
|
|
477
|
+
message=str(exc),
|
|
478
|
+
path="target",
|
|
479
|
+
)
|
|
480
|
+
report.status = "blocked"
|
|
481
|
+
report.preflight_checks["status"] = "blocked"
|
|
482
|
+
report.target = normalize_release_target(target) if target else (self.version_inventory.get_canonical_version() or "")
|
|
483
|
+
return report
|
|
484
|
+
|
|
485
|
+
report.target = normalized_target
|
|
486
|
+
report.target_argument = gate_target_argument
|
|
487
|
+
report.preflight_checks["canonical_version"] = self.version_inventory.get_canonical_version()
|
|
488
|
+
report.preflight_checks["target_argument"] = gate_target_argument
|
|
489
|
+
report.preflight_checks["target_valid"] = True
|
|
490
|
+
if profile:
|
|
491
|
+
profile_definition = self._profile_definition(profile)
|
|
492
|
+
report.profile_definition = profile_definition
|
|
493
|
+
report.preflight_checks["profile"] = profile
|
|
494
|
+
report.preflight_checks["profile_groups"] = list(profile_definition["groups"])
|
|
495
|
+
report.preflight_checks["profile_description"] = profile_definition["description"]
|
|
496
|
+
|
|
497
|
+
if not skip_tooling_lint:
|
|
498
|
+
lint_capture = self._run_tooling_lint(timeout_seconds)
|
|
499
|
+
report.tooling_lint = lint_capture
|
|
500
|
+
report.preflight_checks["tooling_lint"] = lint_capture.status
|
|
501
|
+
if lint_capture.status == "blocked":
|
|
502
|
+
diagnostics.add_blocked(
|
|
503
|
+
code="core.release_check.tooling_lint_blocked",
|
|
504
|
+
message="Tooling lint precheck was blocked; release gate not run.",
|
|
505
|
+
path="core_runtime.cli lint",
|
|
506
|
+
)
|
|
507
|
+
report.preflight_checks["status"] = "blocked"
|
|
508
|
+
report.status = "blocked"
|
|
509
|
+
return report
|
|
510
|
+
if lint_capture.status == "error":
|
|
511
|
+
diagnostics.add_error(
|
|
512
|
+
code="core.release_check.tooling_lint_failed",
|
|
513
|
+
message="Tooling lint precheck failed; release gate not run.",
|
|
514
|
+
path="core_runtime.cli lint",
|
|
515
|
+
details="exit_code={0}".format(lint_capture.returncode),
|
|
516
|
+
)
|
|
517
|
+
report.preflight_checks["status"] = "error"
|
|
518
|
+
report.status = "error"
|
|
519
|
+
return report
|
|
520
|
+
if lint_capture.status == "internal_error":
|
|
521
|
+
diagnostics.add_error(
|
|
522
|
+
code="core.release_check.tooling_lint_internal_error",
|
|
523
|
+
message="Tooling lint precheck had an internal error; release gate not run.",
|
|
524
|
+
path="core_runtime.cli lint",
|
|
525
|
+
details="exit_code={0}".format(lint_capture.returncode),
|
|
526
|
+
)
|
|
527
|
+
report.preflight_checks["status"] = "internal_error"
|
|
528
|
+
report.status = "internal_error"
|
|
529
|
+
return report
|
|
530
|
+
lint_summary = lint_capture.json_payload.get("summary", {}) if lint_capture.json_payload else {}
|
|
531
|
+
warnings = int(lint_summary.get("warning", 0) or 0)
|
|
532
|
+
if warnings:
|
|
533
|
+
diagnostics.add_warning(
|
|
534
|
+
code="core.release_check.tooling_lint_warning",
|
|
535
|
+
message="Tooling lint completed with warnings.",
|
|
536
|
+
path="core_runtime.cli lint",
|
|
537
|
+
details="warning_count={0}".format(warnings),
|
|
538
|
+
)
|
|
539
|
+
report.preflight_checks["tooling_lint_warnings"] = warnings
|
|
540
|
+
else:
|
|
541
|
+
report.preflight_checks["tooling_lint_warnings"] = 0
|
|
542
|
+
else:
|
|
543
|
+
report.preflight_checks["tooling_lint"] = "skipped"
|
|
544
|
+
report.preflight_checks["tooling_lint_warnings"] = None
|
|
545
|
+
|
|
546
|
+
script_path = self.repo_root / "scripts" / "verify_release.py"
|
|
547
|
+
report.preflight_checks["script_path"] = str(script_path)
|
|
548
|
+
script_preflight = self._check_release_script(
|
|
549
|
+
script_path,
|
|
550
|
+
gate_target_argument,
|
|
551
|
+
timeout_seconds,
|
|
552
|
+
require_help=preflight_only,
|
|
553
|
+
debug=debug,
|
|
554
|
+
)
|
|
555
|
+
report.release_gate_help = script_preflight["help_capture"]
|
|
556
|
+
report.preflight_checks["script_exists"] = script_preflight["script_exists"]
|
|
557
|
+
report.preflight_checks["script_compiles"] = script_preflight["script_compiles"]
|
|
558
|
+
report.preflight_checks["help_status"] = script_preflight["help_status"]
|
|
559
|
+
report.preflight_checks["help_contains_target"] = script_preflight["help_contains_target"]
|
|
560
|
+
|
|
561
|
+
if script_preflight["status"] != "pass":
|
|
562
|
+
for diagnostic in script_preflight["diagnostics"]:
|
|
563
|
+
diagnostics.add(diagnostic)
|
|
564
|
+
report.preflight_checks["status"] = script_preflight["status"]
|
|
565
|
+
report.status = script_preflight["status"]
|
|
566
|
+
self._finalize_status(report)
|
|
567
|
+
return report
|
|
568
|
+
|
|
569
|
+
if preflight_only:
|
|
570
|
+
report.preflight_checks["status"] = "pass"
|
|
571
|
+
self._finalize_status(report)
|
|
572
|
+
return report
|
|
573
|
+
|
|
574
|
+
if list_checks or plan:
|
|
575
|
+
gate_capture = self._run_release_gate(
|
|
576
|
+
script_path,
|
|
577
|
+
gate_target_argument,
|
|
578
|
+
timeout_seconds,
|
|
579
|
+
group=group,
|
|
580
|
+
list_checks=list_checks,
|
|
581
|
+
plan=plan,
|
|
582
|
+
timing_json=timing_json,
|
|
583
|
+
)
|
|
584
|
+
report.release_gate = gate_capture
|
|
585
|
+
report.preflight_checks["status"] = "pass"
|
|
586
|
+
report.preflight_checks["release_gate"] = gate_capture.status
|
|
587
|
+
self._finalize_status(report)
|
|
588
|
+
return report
|
|
589
|
+
|
|
590
|
+
if profile:
|
|
591
|
+
profile_groups = list(report.profile_definition.get("groups", ())) if report.profile_definition else []
|
|
592
|
+
capture = None
|
|
593
|
+
for selected_group in profile_groups:
|
|
594
|
+
capture = self._run_release_gate(
|
|
595
|
+
script_path,
|
|
596
|
+
gate_target_argument,
|
|
597
|
+
timeout_seconds,
|
|
598
|
+
group=selected_group,
|
|
599
|
+
list_checks=list_checks,
|
|
600
|
+
plan=plan,
|
|
601
|
+
timing_json=timing_json,
|
|
602
|
+
)
|
|
603
|
+
report.profile_runs.append(capture)
|
|
604
|
+
report.release_gate = capture
|
|
605
|
+
if capture.timed_out:
|
|
606
|
+
diagnostics.add_blocked(
|
|
607
|
+
code="core.release_check.profile_group_timeout",
|
|
608
|
+
message="Release profile subgroup did not complete within the configured timeout.",
|
|
609
|
+
path="scripts/verify_release.py",
|
|
610
|
+
details={"profile": profile, "group": selected_group, "timeout_seconds": timeout_seconds},
|
|
611
|
+
)
|
|
612
|
+
break
|
|
613
|
+
if capture.status == "blocked":
|
|
614
|
+
diagnostics.add_blocked(
|
|
615
|
+
code="core.release_check.profile_group_blocked",
|
|
616
|
+
message="Release profile subgroup returned a blocked result.",
|
|
617
|
+
path="scripts/verify_release.py",
|
|
618
|
+
details={"profile": profile, "group": selected_group, "exit_code": capture.returncode},
|
|
619
|
+
)
|
|
620
|
+
break
|
|
621
|
+
if capture.status == "error":
|
|
622
|
+
diagnostics.add_error(
|
|
623
|
+
code="core.release_check.profile_group_failed",
|
|
624
|
+
message="Release profile subgroup returned a failing result.",
|
|
625
|
+
path="scripts/verify_release.py",
|
|
626
|
+
details={"profile": profile, "group": selected_group, "exit_code": capture.returncode},
|
|
627
|
+
)
|
|
628
|
+
break
|
|
629
|
+
if capture.status == "internal_error":
|
|
630
|
+
diagnostics.add_error(
|
|
631
|
+
code="core.release_check.profile_group_internal_error",
|
|
632
|
+
message="Release profile subgroup had an internal error.",
|
|
633
|
+
path="scripts/verify_release.py",
|
|
634
|
+
details={"profile": profile, "group": selected_group, "exit_code": capture.returncode},
|
|
635
|
+
)
|
|
636
|
+
break
|
|
637
|
+
if capture is None:
|
|
638
|
+
diagnostics.add_blocked(
|
|
639
|
+
code="core.release_check.profile_empty",
|
|
640
|
+
message="Release profile did not resolve to any groups.",
|
|
641
|
+
path="core_runtime.cli release-check",
|
|
642
|
+
details={"profile": profile},
|
|
643
|
+
)
|
|
644
|
+
report.preflight_checks["status"] = "pass"
|
|
645
|
+
else:
|
|
646
|
+
gate_capture = self._run_release_gate(
|
|
647
|
+
script_path,
|
|
648
|
+
gate_target_argument,
|
|
649
|
+
timeout_seconds,
|
|
650
|
+
group=group,
|
|
651
|
+
list_checks=list_checks,
|
|
652
|
+
plan=plan,
|
|
653
|
+
timing_json=timing_json,
|
|
654
|
+
)
|
|
655
|
+
report.release_gate = gate_capture
|
|
656
|
+
report.preflight_checks["status"] = "pass"
|
|
657
|
+
report.preflight_checks["release_gate"] = gate_capture.status
|
|
658
|
+
if gate_capture.timed_out:
|
|
659
|
+
diagnostics.add_blocked(
|
|
660
|
+
code="core.release_check.timeout",
|
|
661
|
+
message="Release gate did not complete within the configured timeout.",
|
|
662
|
+
path="scripts/verify_release.py",
|
|
663
|
+
details={
|
|
664
|
+
"timeout_seconds": timeout_seconds,
|
|
665
|
+
"script": "scripts/verify_release.py",
|
|
666
|
+
},
|
|
667
|
+
)
|
|
668
|
+
elif gate_capture.status == "blocked":
|
|
669
|
+
diagnostics.add_blocked(
|
|
670
|
+
code="core.release_check.release_gate_blocked",
|
|
671
|
+
message="Release gate returned a blocked result.",
|
|
672
|
+
path="scripts/verify_release.py",
|
|
673
|
+
details="exit_code={0}".format(gate_capture.returncode),
|
|
674
|
+
)
|
|
675
|
+
elif gate_capture.status == "error":
|
|
676
|
+
diagnostics.add_error(
|
|
677
|
+
code="core.release_check.release_gate_failed",
|
|
678
|
+
message="Release gate returned a failing result.",
|
|
679
|
+
path="scripts/verify_release.py",
|
|
680
|
+
details="exit_code={0}".format(gate_capture.returncode),
|
|
681
|
+
)
|
|
682
|
+
elif gate_capture.status == "internal_error":
|
|
683
|
+
diagnostics.add_error(
|
|
684
|
+
code="core.release_check.release_gate_internal_error",
|
|
685
|
+
message="Release gate subprocess had an internal error.",
|
|
686
|
+
path="scripts/verify_release.py",
|
|
687
|
+
details="exit_code={0}".format(gate_capture.returncode),
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
if debug:
|
|
691
|
+
report.debug = {
|
|
692
|
+
"tooling_lint_command": report.tooling_lint.command if report.tooling_lint else None,
|
|
693
|
+
"release_gate_help_command": report.release_gate_help.command if report.release_gate_help else None,
|
|
694
|
+
"release_gate_command": report.release_gate.command if report.release_gate else None,
|
|
695
|
+
"release_gate_cwd": report.release_gate.cwd if report.release_gate else None,
|
|
696
|
+
"release_gate_target_argument": gate_target_argument,
|
|
697
|
+
"timeout_seconds": timeout_seconds,
|
|
698
|
+
"elapsed_seconds": report.release_gate.elapsed_seconds if report.release_gate else None,
|
|
699
|
+
"stdout_preview": report.release_gate.stdout_preview if report.release_gate else "",
|
|
700
|
+
"stderr_preview": report.release_gate.stderr_preview if report.release_gate else "",
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
self._finalize_status(report)
|
|
704
|
+
return report
|
|
705
|
+
|
|
706
|
+
def _finalize_status(self, report: ReleaseCheckReport) -> None:
|
|
707
|
+
counts = report.diagnostics.count_by_severity()
|
|
708
|
+
report.summary.update(
|
|
709
|
+
{
|
|
710
|
+
"info": counts["info"],
|
|
711
|
+
"warning": counts["warning"],
|
|
712
|
+
"error": counts["error"],
|
|
713
|
+
"blocked": counts["blocked"],
|
|
714
|
+
"tooling_lint_status": report.tooling_lint.status if report.tooling_lint else report.summary.get("tooling_lint_status", "skipped"),
|
|
715
|
+
"release_gate_status": report.release_gate.status if report.release_gate else report.summary.get("release_gate_status", "skipped"),
|
|
716
|
+
"preflight_status": report.preflight_checks.get("status", "skipped"),
|
|
717
|
+
"release_gate_target_argument": report.target_argument,
|
|
718
|
+
}
|
|
719
|
+
)
|
|
720
|
+
if report.diagnostics.has_blocked():
|
|
721
|
+
report.status = "blocked"
|
|
722
|
+
elif report.diagnostics.has_errors():
|
|
723
|
+
report.status = "error"
|
|
724
|
+
elif counts["warning"] > 0:
|
|
725
|
+
report.status = "warning"
|
|
726
|
+
else:
|
|
727
|
+
report.status = "pass"
|
|
728
|
+
|
|
729
|
+
def _profile_definition(self, profile: str) -> dict[str, Any]:
|
|
730
|
+
definition = RELEASE_CHECK_PROFILES[profile]
|
|
731
|
+
return {
|
|
732
|
+
"name": profile,
|
|
733
|
+
"description": definition["description"],
|
|
734
|
+
"groups": tuple(definition["groups"]),
|
|
735
|
+
"notes": tuple(definition["notes"]),
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
def _check_release_script(
|
|
739
|
+
self,
|
|
740
|
+
script_path: Path,
|
|
741
|
+
target_argument: str,
|
|
742
|
+
timeout_seconds: int,
|
|
743
|
+
require_help: bool,
|
|
744
|
+
debug: bool,
|
|
745
|
+
) -> dict[str, Any]:
|
|
746
|
+
diagnostics = []
|
|
747
|
+
help_capture: SubprocessCapture | None = None
|
|
748
|
+
|
|
749
|
+
if not script_path.exists():
|
|
750
|
+
diagnostics.append(
|
|
751
|
+
self._blocked_diagnostic(
|
|
752
|
+
code="core.release_check.release_script_missing",
|
|
753
|
+
message="Release gate script is missing.",
|
|
754
|
+
path=str(script_path.relative_to(self.repo_root)) if script_path.is_relative_to(self.repo_root) else str(script_path),
|
|
755
|
+
)
|
|
756
|
+
)
|
|
757
|
+
return {
|
|
758
|
+
"status": "blocked",
|
|
759
|
+
"script_exists": False,
|
|
760
|
+
"script_compiles": False,
|
|
761
|
+
"help_status": "skipped",
|
|
762
|
+
"help_contains_target": False,
|
|
763
|
+
"help_capture": None,
|
|
764
|
+
"diagnostics": diagnostics,
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
try:
|
|
768
|
+
py_compile.compile(str(script_path), doraise=True)
|
|
769
|
+
except py_compile.PyCompileError as exc:
|
|
770
|
+
diagnostics.append(
|
|
771
|
+
self._blocked_diagnostic(
|
|
772
|
+
code="core.release_check.release_script_compile_error",
|
|
773
|
+
message="Release gate script failed to compile.",
|
|
774
|
+
path="scripts/verify_release.py",
|
|
775
|
+
details=str(exc.msg),
|
|
776
|
+
)
|
|
777
|
+
)
|
|
778
|
+
return {
|
|
779
|
+
"status": "blocked",
|
|
780
|
+
"script_exists": True,
|
|
781
|
+
"script_compiles": False,
|
|
782
|
+
"help_status": "skipped",
|
|
783
|
+
"help_contains_target": False,
|
|
784
|
+
"help_capture": None,
|
|
785
|
+
"diagnostics": diagnostics,
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
if require_help:
|
|
789
|
+
help_cmd = [sys.executable, str(script_path), "--help"]
|
|
790
|
+
help_capture = self._run_command(help_cmd, timeout_seconds=timeout_seconds, expect_json=False)
|
|
791
|
+
help_text = "{0}\n{1}".format(help_capture.stdout, help_capture.stderr)
|
|
792
|
+
help_contains_target = "--target" in help_text
|
|
793
|
+
help_capture.help_contains_target = help_contains_target
|
|
794
|
+
if help_capture.status != "pass":
|
|
795
|
+
diagnostics.append(
|
|
796
|
+
self._blocked_diagnostic(
|
|
797
|
+
code="core.release_check.help_check_failed",
|
|
798
|
+
message="Release gate help did not complete successfully.",
|
|
799
|
+
path="scripts/verify_release.py",
|
|
800
|
+
details={
|
|
801
|
+
"timeout_seconds": timeout_seconds,
|
|
802
|
+
"script": "scripts/verify_release.py",
|
|
803
|
+
},
|
|
804
|
+
)
|
|
805
|
+
)
|
|
806
|
+
return {
|
|
807
|
+
"status": "blocked",
|
|
808
|
+
"script_exists": True,
|
|
809
|
+
"script_compiles": True,
|
|
810
|
+
"help_status": help_capture.status,
|
|
811
|
+
"help_contains_target": help_contains_target,
|
|
812
|
+
"help_capture": help_capture,
|
|
813
|
+
"diagnostics": diagnostics,
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
if not help_contains_target:
|
|
817
|
+
diagnostics.append(
|
|
818
|
+
self._blocked_diagnostic(
|
|
819
|
+
code="core.release_check.target_mapping_unverified",
|
|
820
|
+
message="Release gate help output does not advertise the expected target argument.",
|
|
821
|
+
path="scripts/verify_release.py",
|
|
822
|
+
details={
|
|
823
|
+
"expected": "vMAJOR.MINOR.PATCH",
|
|
824
|
+
"observed": "help output missing --target",
|
|
825
|
+
},
|
|
826
|
+
)
|
|
827
|
+
)
|
|
828
|
+
return {
|
|
829
|
+
"status": "blocked",
|
|
830
|
+
"script_exists": True,
|
|
831
|
+
"script_compiles": True,
|
|
832
|
+
"help_status": help_capture.status,
|
|
833
|
+
"help_contains_target": help_contains_target,
|
|
834
|
+
"help_capture": help_capture,
|
|
835
|
+
"diagnostics": diagnostics,
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
if debug:
|
|
839
|
+
help_capture.stdout_preview, help_capture.stdout_truncated = _truncate_text(help_capture.stdout)
|
|
840
|
+
help_capture.stderr_preview, help_capture.stderr_truncated = _truncate_text(help_capture.stderr)
|
|
841
|
+
|
|
842
|
+
return {
|
|
843
|
+
"status": "pass",
|
|
844
|
+
"script_exists": True,
|
|
845
|
+
"script_compiles": True,
|
|
846
|
+
"help_status": help_capture.status,
|
|
847
|
+
"help_contains_target": help_contains_target,
|
|
848
|
+
"help_capture": help_capture,
|
|
849
|
+
"diagnostics": diagnostics,
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
return {
|
|
853
|
+
"status": "pass",
|
|
854
|
+
"script_exists": True,
|
|
855
|
+
"script_compiles": True,
|
|
856
|
+
"help_status": "skipped",
|
|
857
|
+
"help_contains_target": None,
|
|
858
|
+
"help_capture": None,
|
|
859
|
+
"diagnostics": diagnostics,
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
def _run_tooling_lint(self, timeout_seconds: int) -> SubprocessCapture:
|
|
863
|
+
cmd = [
|
|
864
|
+
sys.executable,
|
|
865
|
+
"-m",
|
|
866
|
+
"core_runtime.cli",
|
|
867
|
+
"lint",
|
|
868
|
+
"--scope",
|
|
869
|
+
"tooling",
|
|
870
|
+
"--format",
|
|
871
|
+
"json",
|
|
872
|
+
]
|
|
873
|
+
return self._run_command(cmd, timeout_seconds=timeout_seconds, expect_json=True)
|
|
874
|
+
|
|
875
|
+
def _run_release_gate(
|
|
876
|
+
self,
|
|
877
|
+
script_path: Path,
|
|
878
|
+
target_argument: str,
|
|
879
|
+
timeout_seconds: int,
|
|
880
|
+
group: Optional[str] = None,
|
|
881
|
+
list_checks: bool = False,
|
|
882
|
+
plan: bool = False,
|
|
883
|
+
timing_json: Optional[str] = None,
|
|
884
|
+
) -> SubprocessCapture:
|
|
885
|
+
cmd = [
|
|
886
|
+
sys.executable,
|
|
887
|
+
str(script_path),
|
|
888
|
+
"--target",
|
|
889
|
+
target_argument,
|
|
890
|
+
]
|
|
891
|
+
if group:
|
|
892
|
+
cmd.extend(["--group", group])
|
|
893
|
+
if list_checks:
|
|
894
|
+
cmd.append("--list-checks")
|
|
895
|
+
if plan:
|
|
896
|
+
cmd.append("--plan")
|
|
897
|
+
if timing_json:
|
|
898
|
+
cmd.extend(["--timing-json", timing_json])
|
|
899
|
+
capture = self._run_command(cmd, timeout_seconds=timeout_seconds, expect_json=True, target_argument=target_argument)
|
|
900
|
+
if capture.json_payload is not None and isinstance(capture.json_payload, dict):
|
|
901
|
+
report_path = capture.json_payload.get("report_path")
|
|
902
|
+
if isinstance(report_path, str):
|
|
903
|
+
capture.report_path = report_path
|
|
904
|
+
return capture
|
|
905
|
+
|
|
906
|
+
def _run_command(
|
|
907
|
+
self,
|
|
908
|
+
command: list[str],
|
|
909
|
+
timeout_seconds: int,
|
|
910
|
+
expect_json: bool,
|
|
911
|
+
target_argument: Optional[str] = None,
|
|
912
|
+
) -> SubprocessCapture:
|
|
913
|
+
cwd = str(self.repo_root)
|
|
914
|
+
start = time.monotonic()
|
|
915
|
+
try:
|
|
916
|
+
completed = subprocess.run(
|
|
917
|
+
command,
|
|
918
|
+
cwd=self.repo_root,
|
|
919
|
+
input="",
|
|
920
|
+
text=True,
|
|
921
|
+
capture_output=True,
|
|
922
|
+
timeout=timeout_seconds,
|
|
923
|
+
check=False,
|
|
924
|
+
)
|
|
925
|
+
elapsed = time.monotonic() - start
|
|
926
|
+
stdout = _coerce_text(completed.stdout)
|
|
927
|
+
stderr = _coerce_text(completed.stderr)
|
|
928
|
+
json_payload: dict[str, Any] | None = None
|
|
929
|
+
json_detected = False
|
|
930
|
+
if expect_json and stdout.strip():
|
|
931
|
+
try:
|
|
932
|
+
json_payload = json.loads(stdout)
|
|
933
|
+
json_detected = True
|
|
934
|
+
except json.JSONDecodeError:
|
|
935
|
+
json_payload = None
|
|
936
|
+
|
|
937
|
+
stdout_preview, stdout_truncated = _truncate_text(stdout)
|
|
938
|
+
stderr_preview, stderr_truncated = _truncate_text(stderr)
|
|
939
|
+
return SubprocessCapture(
|
|
940
|
+
command=command,
|
|
941
|
+
cwd=cwd,
|
|
942
|
+
expect_json=expect_json,
|
|
943
|
+
returncode=completed.returncode,
|
|
944
|
+
stdout=stdout,
|
|
945
|
+
stderr=stderr,
|
|
946
|
+
timed_out=False,
|
|
947
|
+
timeout_seconds=timeout_seconds,
|
|
948
|
+
elapsed_seconds=elapsed,
|
|
949
|
+
json_detected=json_detected,
|
|
950
|
+
json_payload=json_payload,
|
|
951
|
+
report_path=None,
|
|
952
|
+
target_argument=target_argument,
|
|
953
|
+
stdout_preview=stdout_preview,
|
|
954
|
+
stderr_preview=stderr_preview,
|
|
955
|
+
stdout_truncated=stdout_truncated,
|
|
956
|
+
stderr_truncated=stderr_truncated,
|
|
957
|
+
)
|
|
958
|
+
except subprocess.TimeoutExpired as exc:
|
|
959
|
+
elapsed = time.monotonic() - start
|
|
960
|
+
stdout = _coerce_text(exc.output)
|
|
961
|
+
stderr = _coerce_text(exc.stderr)
|
|
962
|
+
stdout_preview, stdout_truncated = _truncate_text(stdout)
|
|
963
|
+
stderr_preview, stderr_truncated = _truncate_text(stderr)
|
|
964
|
+
return SubprocessCapture(
|
|
965
|
+
command=command,
|
|
966
|
+
cwd=cwd,
|
|
967
|
+
expect_json=expect_json,
|
|
968
|
+
returncode=None,
|
|
969
|
+
stdout=stdout,
|
|
970
|
+
stderr=stderr,
|
|
971
|
+
timed_out=True,
|
|
972
|
+
timeout_seconds=timeout_seconds,
|
|
973
|
+
elapsed_seconds=elapsed,
|
|
974
|
+
json_detected=False,
|
|
975
|
+
json_payload=None,
|
|
976
|
+
report_path=None,
|
|
977
|
+
target_argument=target_argument,
|
|
978
|
+
stdout_preview=stdout_preview,
|
|
979
|
+
stderr_preview=stderr_preview,
|
|
980
|
+
stdout_truncated=stdout_truncated,
|
|
981
|
+
stderr_truncated=stderr_truncated,
|
|
982
|
+
)
|
|
983
|
+
except (FileNotFoundError, OSError) as exc:
|
|
984
|
+
elapsed = time.monotonic() - start
|
|
985
|
+
stderr = str(exc)
|
|
986
|
+
stdout_preview, stdout_truncated = _truncate_text("")
|
|
987
|
+
stderr_preview, stderr_truncated = _truncate_text(stderr)
|
|
988
|
+
return SubprocessCapture(
|
|
989
|
+
command=command,
|
|
990
|
+
cwd=cwd,
|
|
991
|
+
expect_json=expect_json,
|
|
992
|
+
returncode=None,
|
|
993
|
+
stdout="",
|
|
994
|
+
stderr=stderr,
|
|
995
|
+
timed_out=False,
|
|
996
|
+
timeout_seconds=timeout_seconds,
|
|
997
|
+
elapsed_seconds=elapsed,
|
|
998
|
+
json_detected=False,
|
|
999
|
+
json_payload=None,
|
|
1000
|
+
report_path=None,
|
|
1001
|
+
target_argument=target_argument,
|
|
1002
|
+
stdout_preview=stdout_preview,
|
|
1003
|
+
stderr_preview=stderr_preview,
|
|
1004
|
+
stdout_truncated=stdout_truncated,
|
|
1005
|
+
stderr_truncated=stderr_truncated,
|
|
1006
|
+
)
|
|
1007
|
+
|
|
1008
|
+
def _blocked_diagnostic(
|
|
1009
|
+
self,
|
|
1010
|
+
code: str,
|
|
1011
|
+
message: str,
|
|
1012
|
+
path: str,
|
|
1013
|
+
details: Any = None,
|
|
1014
|
+
):
|
|
1015
|
+
diag = DiagnosticCollection()
|
|
1016
|
+
diag.add_blocked(code=code, message=message, path=path, details=details)
|
|
1017
|
+
return diag.diagnostics[-1]
|