@topy-ai/maggie 0.7.48 → 0.7.49
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.
- package/README-zh-TW.md +2 -2
- package/README.md +31 -4
- package/bin/maggie.js +17 -4
- package/bundled-contracts/google-integrations/capability-report-v2.schema.json +2 -1
- package/bundled-contracts/maggie-content/email-localization-v1.schema.json +89 -0
- package/bundled-contracts/maggie-content/lifecycle-email-v1.schema.json +76 -0
- package/bundled-contracts/maggie-deployment/readiness-v1.schema.json +6 -1
- package/bundled-contracts/maggie-ops/marketing-run-v1.schema.json +89 -0
- package/bundled-contracts/maggie-ops/notification-catalogue-example-v1.json +941 -0
- package/bundled-contracts/maggie-ops/notification-catalogue-v1.schema.json +273 -0
- package/bundled-contracts/maggie-service-booking/fact-provenance-v1.schema.json +128 -0
- package/bundled-contracts/maggie-service-booking/import-stages-v1.schema.json +191 -0
- package/bundled-references/google-integrations-runbook.md +54 -0
- package/bundled-skills/maggie-content-localization/SKILL.md +17 -1
- package/bundled-skills/maggie-content-localization/references/email-and-lifecycle.md +83 -0
- package/bundled-skills/maggie-deployment/SKILL.md +89 -7
- package/bundled-skills/maggie-deployment/references/vps.md +38 -6
- package/bundled-skills/maggie-design/SKILL.md +55 -0
- package/bundled-skills/maggie-feedback/SKILL.md +29 -0
- package/bundled-skills/maggie-ops/SKILL.md +37 -0
- package/bundled-skills/maggie-ops/references/notifications-operations.md +154 -0
- package/bundled-skills/maggie-service-booking/SKILL.md +59 -0
- package/bundled-skills/maggie-social-share/SKILL.md +19 -2
- package/bundled-skills/maggie-social-share/references/marketing-run.md +57 -0
- package/bundled-skills/maggie-social-share/references/queue-and-connections.md +51 -0
- package/bundled-tools/clis/maggie_deployment.py +58 -17
- package/bundled-tools/clis/maggie_deployment_auth_canary.py +216 -0
- package/bundled-tools/clis/maggie_deployment_collect.py +225 -0
- package/bundled-tools/clis/maggie_deployment_readiness.py +53 -3
- package/bundled-tools/clis/maggie_design.py +8 -0
- package/bundled-tools/clis/maggie_design_verify.py +359 -0
- package/bundled-tools/clis/maggie_docs.py +82 -4
- package/bundled-tools/clis/maggie_email_localization.py +382 -0
- package/bundled-tools/clis/maggie_feedback.py +9 -2
- package/bundled-tools/clis/maggie_icon_glyph_check.py +382 -0
- package/bundled-tools/clis/maggie_icon_release_gate.py +28 -1
- package/bundled-tools/clis/maggie_localization.py +5 -0
- package/bundled-tools/clis/maggie_marketing_run.py +272 -0
- package/bundled-tools/clis/maggie_notifications.py +528 -0
- package/bundled-tools/clis/maggie_ops.py +87 -0
- package/bundled-tools/clis/maggie_schedule.py +12 -2
- package/bundled-tools/clis/maggie_service_import.py +520 -0
- package/bundled-tools/clis/maggie_social_queue.py +104 -0
- package/bundled-tools/clis/maggie_social_share.py +6 -2
- package/bundled-tools/runtime/google_ads.py +306 -0
- package/bundled-tools/runtime/google_capabilities.py +18 -2
- package/bundled-tools/runtime/google_remediation.py +152 -0
- package/package.json +1 -1
- package/references/google-integrations-runbook.md +54 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Collect standard host release evidence and emit one reproducible readiness report.
|
|
3
|
+
|
|
4
|
+
`maggie deployment readiness` validates evidence files but does not produce
|
|
5
|
+
them, so each host used to hand-roll browser, SSH, systemd, and retention
|
|
6
|
+
scripts. This collector runs the standard adapters declared in one reviewed
|
|
7
|
+
config (edge/rendered canary, host health, rollback consistency, remote
|
|
8
|
+
release identity, optional authenticated API canary), writes each adapter's
|
|
9
|
+
evidence, and summarizes everything with the existing
|
|
10
|
+
`maggie-deployment-readiness.v1` report. It never deploys, never uses a
|
|
11
|
+
shell, and never records credentials or response bodies.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import hashlib
|
|
18
|
+
import importlib.util
|
|
19
|
+
import json
|
|
20
|
+
import re
|
|
21
|
+
import subprocess
|
|
22
|
+
import sys
|
|
23
|
+
import urllib.error
|
|
24
|
+
import urllib.request
|
|
25
|
+
from datetime import datetime, timezone
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Any
|
|
28
|
+
from urllib.parse import urlparse
|
|
29
|
+
|
|
30
|
+
CONFIG_SCHEMA = "maggie-evidence-collector.v1"
|
|
31
|
+
HERE = Path(__file__).resolve().parent
|
|
32
|
+
STANDARD_EVIDENCE = {
|
|
33
|
+
"unitRegression": ".maggie/verification/unit-regression.json",
|
|
34
|
+
"packageSmoke": ".maggie/verification/package-smoke.json",
|
|
35
|
+
"browserEvidence": ".maggie/verification/browser-evidence.json",
|
|
36
|
+
"deploymentPreflight": ".maggie/release-preflight.json",
|
|
37
|
+
}
|
|
38
|
+
SECRET_ARG = re.compile(r"(?:password|passwd|token|secret|apikey|api_key)=|Bearer\s|sk[-_](?:live|test)[-_]|eyJ[A-Za-z0-9_-]{10,}\.", re.IGNORECASE)
|
|
39
|
+
RELEASE_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def load_module(name: str):
|
|
43
|
+
spec = importlib.util.spec_from_file_location(name, HERE / f"{name}.py")
|
|
44
|
+
assert spec and spec.loader
|
|
45
|
+
module = importlib.util.module_from_spec(spec)
|
|
46
|
+
spec.loader.exec_module(module)
|
|
47
|
+
return module
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def now() -> str:
|
|
51
|
+
return datetime.now(timezone.utc).isoformat()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def resolve(project: Path, value: str | Path) -> Path:
|
|
55
|
+
path = Path(value).expanduser()
|
|
56
|
+
return path if path.is_absolute() else project / path
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def write(path: Path, payload: dict) -> None:
|
|
60
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
61
|
+
path.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def check_argv(argv: object, label: str) -> list[str]:
|
|
65
|
+
if not isinstance(argv, list) or not argv or not all(isinstance(item, str) and item for item in argv):
|
|
66
|
+
raise ValueError(f"{label} command must be a non-empty argv list (no shell string)")
|
|
67
|
+
if any(SECRET_ARG.search(item) for item in argv):
|
|
68
|
+
raise ValueError(f"{label} command contains a literal credential; use host configuration or SSH keys")
|
|
69
|
+
return argv
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def http_status(url: str, timeout: float) -> int | None:
|
|
73
|
+
request = urllib.request.Request(url, headers={"User-Agent": "maggie-deployment-collect/1.0"}, method="GET")
|
|
74
|
+
try:
|
|
75
|
+
with urllib.request.urlopen(request, timeout=timeout) as response:
|
|
76
|
+
return response.status
|
|
77
|
+
except urllib.error.HTTPError as error:
|
|
78
|
+
return error.code
|
|
79
|
+
except (urllib.error.URLError, OSError):
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def collect_health(config: dict, timeout: float) -> dict:
|
|
84
|
+
urls = config.get("urls")
|
|
85
|
+
if not isinstance(urls, list) or not urls:
|
|
86
|
+
raise ValueError("health adapter needs a non-empty urls list")
|
|
87
|
+
expect = config.get("expectStatus", [200])
|
|
88
|
+
checks = []
|
|
89
|
+
for url in urls:
|
|
90
|
+
parsed = urlparse(str(url))
|
|
91
|
+
if parsed.scheme not in {"http", "https"} or parsed.query:
|
|
92
|
+
raise ValueError("health URLs must be http(s) without query strings")
|
|
93
|
+
status = http_status(str(url), timeout)
|
|
94
|
+
checks.append({"url": f"{parsed.scheme}://{parsed.netloc}{parsed.path or '/'}", "status": status, "passed": status in expect})
|
|
95
|
+
return {"schemaVersion": "maggie-deployment-health.v1", "generatedAt": now(), "checks": checks, "passed": all(item["passed"] for item in checks)}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def collect_release_identity(config: dict, release: str, timeout: float) -> tuple[dict, dict]:
|
|
99
|
+
run: dict[str, Any] = {}
|
|
100
|
+
observed = ""
|
|
101
|
+
if config.get("command"):
|
|
102
|
+
argv = check_argv(config["command"], "releaseIdentity")
|
|
103
|
+
completed = subprocess.run(argv, capture_output=True, text=True, timeout=timeout, check=False)
|
|
104
|
+
run = {"argv": argv, "exitCode": completed.returncode}
|
|
105
|
+
raw = completed.stdout.strip().splitlines()[-1].strip() if completed.returncode == 0 and completed.stdout.strip() else ""
|
|
106
|
+
# A `readlink -f <current>` path resolves to .../releases/<release-id>.
|
|
107
|
+
observed = raw.rstrip("/").rsplit("/", 1)[-1] if raw else ""
|
|
108
|
+
elif config.get("url"):
|
|
109
|
+
url = str(config["url"])
|
|
110
|
+
field = str(config.get("field") or "release")
|
|
111
|
+
request = urllib.request.Request(url, headers={"User-Agent": "maggie-deployment-collect/1.0", "Accept": "application/json"})
|
|
112
|
+
try:
|
|
113
|
+
with urllib.request.urlopen(request, timeout=timeout) as response:
|
|
114
|
+
payload = json.loads(response.read(200_000).decode("utf-8"))
|
|
115
|
+
observed = str(payload.get(field, "")) if isinstance(payload, dict) else ""
|
|
116
|
+
run = {"url": url, "field": field}
|
|
117
|
+
except (urllib.error.URLError, OSError, ValueError) as error:
|
|
118
|
+
run = {"url": url, "field": field, "error": type(error).__name__}
|
|
119
|
+
else:
|
|
120
|
+
raise ValueError("releaseIdentity adapter needs command (argv) or url")
|
|
121
|
+
if observed and not RELEASE_ID.fullmatch(observed):
|
|
122
|
+
observed = "<unrecognized>"
|
|
123
|
+
evidence = {"schemaVersion": "maggie-release-identity.v1", "generatedAt": now(), "expected": release, "observed": observed, "passed": bool(observed) and observed == release}
|
|
124
|
+
return evidence, run
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def collect_rollback(project: Path, config: dict) -> dict:
|
|
128
|
+
deployment = load_module("maggie_deployment")
|
|
129
|
+
evidence_path = resolve(project, config.get("evidence", "docs/deployment-rollback-smoke.json"))
|
|
130
|
+
keep = None
|
|
131
|
+
if config.get("plan"):
|
|
132
|
+
plan = json.loads(resolve(project, config["plan"]).read_text(encoding="utf-8"))
|
|
133
|
+
keep = (plan.get("retention") or {}).get("keep")
|
|
134
|
+
try:
|
|
135
|
+
result = deployment.validate_rollback_evidence(json.loads(evidence_path.read_text(encoding="utf-8")), keep)
|
|
136
|
+
except (OSError, json.JSONDecodeError) as error:
|
|
137
|
+
result = {"passed": False, "errors": [f"rollback evidence unavailable: {type(error).__name__}"]}
|
|
138
|
+
return {"schemaVersion": "maggie-deployment-rollback-check.v1", "generatedAt": now(), "retentionKeep": keep, "rollbackAvailable": None if keep is None else keep >= 2, "passed": result["passed"], "errors": result["errors"]}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def collect_canary(project: Path, config: dict, destination: Path, timeout: float) -> dict:
|
|
142
|
+
argv = [sys.executable, str(HERE / "maggie_deployment_canary.py"), "--project", str(project), "--output", str(destination)]
|
|
143
|
+
for asset in config.get("assets", []):
|
|
144
|
+
argv += ["--asset", str(asset)]
|
|
145
|
+
if config.get("renderReport"):
|
|
146
|
+
argv += ["--render-report", str(resolve(project, config["renderReport"]))]
|
|
147
|
+
completed = subprocess.run(argv, capture_output=True, text=True, timeout=timeout, check=False)
|
|
148
|
+
return {"adapter": "maggie_deployment_canary.py", "exitCode": completed.returncode}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def collect(project: Path, config: dict, evidence_dir: Path, timeout: float = 60.0) -> dict:
|
|
152
|
+
if config.get("schemaVersion") != CONFIG_SCHEMA:
|
|
153
|
+
raise ValueError(f"collector config schemaVersion must be {CONFIG_SCHEMA}")
|
|
154
|
+
release = str(config.get("release") or "")
|
|
155
|
+
if not RELEASE_ID.fullmatch(release):
|
|
156
|
+
raise ValueError("collector config needs the expected release id")
|
|
157
|
+
adapters = config.get("adapters") or {}
|
|
158
|
+
unknown = set(adapters) - {"canary", "health", "rollback", "releaseIdentity", "authCanary"}
|
|
159
|
+
if unknown:
|
|
160
|
+
raise ValueError("unsupported adapters: " + ", ".join(sorted(unknown)))
|
|
161
|
+
readiness = load_module("maggie_deployment_readiness")
|
|
162
|
+
paths = {name: resolve(project, (config.get("evidence") or {}).get(name, default)) for name, default in STANDARD_EVIDENCE.items()}
|
|
163
|
+
runs: dict[str, Any] = {}
|
|
164
|
+
canary_path = evidence_dir / "rendered-canary.json"
|
|
165
|
+
if "canary" in adapters:
|
|
166
|
+
runs["canary"] = collect_canary(project, adapters["canary"], canary_path, timeout)
|
|
167
|
+
paths["renderedCanary"] = canary_path
|
|
168
|
+
else:
|
|
169
|
+
paths["renderedCanary"] = resolve(project, (config.get("evidence") or {}).get("renderedCanary", ".maggie/deployment-canary.json"))
|
|
170
|
+
if "health" in adapters:
|
|
171
|
+
write(evidence_dir / "host-health.json", collect_health(adapters["health"], timeout))
|
|
172
|
+
paths["hostHealth"] = evidence_dir / "host-health.json"
|
|
173
|
+
runs["health"] = {"urls": len(adapters["health"].get("urls", []))}
|
|
174
|
+
if "rollback" in adapters:
|
|
175
|
+
write(evidence_dir / "rollback-check.json", collect_rollback(project, adapters["rollback"]))
|
|
176
|
+
paths["rollbackEvidence"] = evidence_dir / "rollback-check.json"
|
|
177
|
+
runs["rollback"] = {"validator": "validate_rollback_evidence"}
|
|
178
|
+
if "releaseIdentity" in adapters:
|
|
179
|
+
evidence, run = collect_release_identity(adapters["releaseIdentity"], release, timeout)
|
|
180
|
+
write(evidence_dir / "release-identity.json", evidence)
|
|
181
|
+
paths["releaseIdentity"] = evidence_dir / "release-identity.json"
|
|
182
|
+
runs["releaseIdentity"] = run
|
|
183
|
+
if "authCanary" in adapters:
|
|
184
|
+
auth = load_module("maggie_deployment_auth_canary")
|
|
185
|
+
spec = adapters["authCanary"]
|
|
186
|
+
plan = json.loads(resolve(project, spec["plan"]).read_text(encoding="utf-8"))
|
|
187
|
+
write(evidence_dir / "auth-canary.json", auth.auth_canary(plan, bool(spec.get("allowMutations")), timeout))
|
|
188
|
+
paths["authenticatedCanary"] = evidence_dir / "auth-canary.json"
|
|
189
|
+
runs["authCanary"] = {"plan": str(spec["plan"]), "allowMutations": bool(spec.get("allowMutations"))}
|
|
190
|
+
report = readiness.readiness_report(project, paths)
|
|
191
|
+
canonical = json.dumps(config, sort_keys=True, ensure_ascii=False).encode("utf-8")
|
|
192
|
+
report["collector"] = {
|
|
193
|
+
"schemaVersion": CONFIG_SCHEMA,
|
|
194
|
+
"configSha256": hashlib.sha256(canonical).hexdigest(),
|
|
195
|
+
"release": release,
|
|
196
|
+
"evidenceDir": readiness.display_path(evidence_dir, project),
|
|
197
|
+
"adapters": runs,
|
|
198
|
+
"mutation": "not executed",
|
|
199
|
+
}
|
|
200
|
+
return report
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def main() -> int:
|
|
204
|
+
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
205
|
+
parser.add_argument("--project", type=Path, default=Path.cwd())
|
|
206
|
+
parser.add_argument("--config", type=Path, default=Path(".maggie/deployment/collector.json"), help=f"{CONFIG_SCHEMA} config")
|
|
207
|
+
parser.add_argument("--evidence-dir", type=Path, default=Path(".maggie/deployment/collected"))
|
|
208
|
+
parser.add_argument("--timeout", type=float, default=60.0)
|
|
209
|
+
parser.add_argument("--output", type=Path, help="readiness report path (default: <evidence-dir>/readiness.json)")
|
|
210
|
+
args = parser.parse_args()
|
|
211
|
+
project = args.project.resolve()
|
|
212
|
+
evidence_dir = resolve(project, args.evidence_dir)
|
|
213
|
+
try:
|
|
214
|
+
config = json.loads(resolve(project, args.config).read_text(encoding="utf-8"))
|
|
215
|
+
report = collect(project, config, evidence_dir, args.timeout)
|
|
216
|
+
except (OSError, ValueError, KeyError, subprocess.SubprocessError) as error:
|
|
217
|
+
print(f"BLOCKED: maggie deployment collect: {error}", file=sys.stderr)
|
|
218
|
+
return 2
|
|
219
|
+
write(resolve(project, args.output) if args.output else evidence_dir / "readiness.json", report)
|
|
220
|
+
print(json.dumps(report, indent=2, ensure_ascii=False))
|
|
221
|
+
return 0 if report["status"] == "passed" else 1
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
if __name__ == "__main__":
|
|
225
|
+
raise SystemExit(main())
|
|
@@ -92,6 +92,39 @@ def validate_runtime_parity(payload: dict[str, Any]) -> tuple[bool, str]:
|
|
|
92
92
|
return True, "worker/browser runtime parity passed"
|
|
93
93
|
|
|
94
94
|
|
|
95
|
+
def validate_host_health(payload: dict[str, Any]) -> tuple[bool, str]:
|
|
96
|
+
if payload.get("schemaVersion") != "maggie-deployment-health.v1":
|
|
97
|
+
return False, "host health evidence schemaVersion is unsupported"
|
|
98
|
+
checks = payload.get("checks")
|
|
99
|
+
if payload.get("passed") is not True or not isinstance(checks, list) or not checks:
|
|
100
|
+
return False, "host health evidence must contain passing checks"
|
|
101
|
+
return True, "host health checks passed"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def validate_release_identity(payload: dict[str, Any]) -> tuple[bool, str]:
|
|
105
|
+
if payload.get("schemaVersion") != "maggie-release-identity.v1":
|
|
106
|
+
return False, "release identity evidence schemaVersion is unsupported"
|
|
107
|
+
if payload.get("passed") is not True or not payload.get("expected") or payload.get("observed") != payload.get("expected"):
|
|
108
|
+
return False, "remote release identity does not match the expected release"
|
|
109
|
+
return True, "remote release identity matches"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def validate_rollback_check(payload: dict[str, Any]) -> tuple[bool, str]:
|
|
113
|
+
if payload.get("schemaVersion") != "maggie-deployment-rollback-check.v1":
|
|
114
|
+
return False, "rollback check schemaVersion is unsupported"
|
|
115
|
+
if payload.get("passed") is not True:
|
|
116
|
+
return False, "rollback evidence is inconsistent with the retention policy or not passed"
|
|
117
|
+
return True, "rollback evidence is consistent with the retention policy"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def validate_auth_canary(payload: dict[str, Any]) -> tuple[bool, str]:
|
|
121
|
+
if payload.get("schemaVersion") != "maggie-deployment-auth-canary.v1":
|
|
122
|
+
return False, "authenticated canary schemaVersion is unsupported"
|
|
123
|
+
if payload.get("status") != "passed" or payload.get("passed") is not True:
|
|
124
|
+
return False, "authenticated API canary did not pass"
|
|
125
|
+
return True, "authenticated API canary passed"
|
|
126
|
+
|
|
127
|
+
|
|
95
128
|
VALIDATORS: dict[str, Callable[[dict[str, Any]], tuple[bool, str]]] = {
|
|
96
129
|
"unitRegression": validate_unit,
|
|
97
130
|
"packageSmoke": validate_package,
|
|
@@ -99,7 +132,12 @@ VALIDATORS: dict[str, Callable[[dict[str, Any]], tuple[bool, str]]] = {
|
|
|
99
132
|
"renderedCanary": validate_canary,
|
|
100
133
|
"deploymentPreflight": validate_preflight,
|
|
101
134
|
"runtimeParity": validate_runtime_parity,
|
|
135
|
+
"hostHealth": validate_host_health,
|
|
136
|
+
"releaseIdentity": validate_release_identity,
|
|
137
|
+
"rollbackEvidence": validate_rollback_check,
|
|
138
|
+
"authenticatedCanary": validate_auth_canary,
|
|
102
139
|
}
|
|
140
|
+
OPTIONAL_CHECKS = ("runtimeParity", "hostHealth", "releaseIdentity", "rollbackEvidence", "authenticatedCanary")
|
|
103
141
|
|
|
104
142
|
|
|
105
143
|
def inspect_check(name: str, path: Path, project: Path) -> dict[str, Any]:
|
|
@@ -120,7 +158,7 @@ def inspect_check(name: str, path: Path, project: Path) -> dict[str, Any]:
|
|
|
120
158
|
return result
|
|
121
159
|
|
|
122
160
|
|
|
123
|
-
def
|
|
161
|
+
def readiness_report(project: Path, paths: dict[str, Path]) -> dict[str, Any]:
|
|
124
162
|
checks = {name: inspect_check(name, path, project) for name, path in paths.items()}
|
|
125
163
|
states = {item["state"] for item in checks.values()}
|
|
126
164
|
status = "failed" if "failed" in states else "inconclusive" if "inconclusive" in states else "passed"
|
|
@@ -132,6 +170,12 @@ def readiness(project: Path, paths: dict[str, Path], output: Path | None = None)
|
|
|
132
170
|
"checks": checks,
|
|
133
171
|
"nextAction": "resolve failed evidence before release" if status == "failed" else "collect the missing host adapter evidence" if status == "inconclusive" else "all declared readiness evidence passed",
|
|
134
172
|
}
|
|
173
|
+
return report
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def readiness(project: Path, paths: dict[str, Path], output: Path | None = None) -> int:
|
|
177
|
+
report = readiness_report(project, paths)
|
|
178
|
+
status = report["status"]
|
|
135
179
|
if output:
|
|
136
180
|
destination = output if output.is_absolute() else project / output
|
|
137
181
|
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -150,6 +194,10 @@ def main() -> int:
|
|
|
150
194
|
parser.add_argument("--rendered-canary", type=Path, default=Path(".maggie/deployment-canary.json"))
|
|
151
195
|
parser.add_argument("--deployment-preflight", type=Path, default=Path(".maggie/release-preflight.json"))
|
|
152
196
|
parser.add_argument("--runtime-parity", type=Path, help="validated worker/browser parity report from maggie deployment parity")
|
|
197
|
+
parser.add_argument("--host-health", type=Path, help="maggie-deployment-health.v1 evidence (e.g. from maggie deployment collect)")
|
|
198
|
+
parser.add_argument("--release-identity", type=Path, help="maggie-release-identity.v1 evidence")
|
|
199
|
+
parser.add_argument("--rollback-check", type=Path, help="maggie-deployment-rollback-check.v1 evidence")
|
|
200
|
+
parser.add_argument("--auth-canary", type=Path, help="maggie-deployment-auth-canary.v1 evidence")
|
|
153
201
|
parser.add_argument("--output", type=Path)
|
|
154
202
|
args = parser.parse_args()
|
|
155
203
|
project = args.project.resolve()
|
|
@@ -159,8 +207,10 @@ def main() -> int:
|
|
|
159
207
|
name: (getattr(args, option).resolve() if getattr(args, option).is_absolute() else project / getattr(args, option))
|
|
160
208
|
for name, option in CHECKS
|
|
161
209
|
}
|
|
162
|
-
|
|
163
|
-
|
|
210
|
+
for name, option in (("runtimeParity", "runtime_parity"), ("hostHealth", "host_health"), ("releaseIdentity", "release_identity"), ("rollbackEvidence", "rollback_check"), ("authenticatedCanary", "auth_canary")):
|
|
211
|
+
value = getattr(args, option)
|
|
212
|
+
if value:
|
|
213
|
+
paths[name] = value.resolve() if value.is_absolute() else project / value
|
|
164
214
|
try:
|
|
165
215
|
return readiness(project, paths, args.output)
|
|
166
216
|
except (OSError, ValueError) as error:
|
|
@@ -857,12 +857,20 @@ commands:
|
|
|
857
857
|
pressable-check flag pressables/buttons nested in another pressable (JSX/TSX)
|
|
858
858
|
layer-check flag unlayered CSS that overrides Tailwind v4 utilities
|
|
859
859
|
palette generate or validate a semantic palette role contract
|
|
860
|
+
verify run automatable design-job steps and record evidence
|
|
861
|
+
icon-glyph-check render icons and prove font load and glyph paint
|
|
860
862
|
reference-ui, init, validate-ui, app-init, app-validate, rebrand, review
|
|
861
863
|
""")
|
|
862
864
|
return 0
|
|
863
865
|
if len(sys.argv) > 1 and sys.argv[1] in {"pressable-check", "layer-check"}:
|
|
864
866
|
from maggie_design_lint import main as lint_main
|
|
865
867
|
return lint_main(sys.argv[1:])
|
|
868
|
+
if len(sys.argv) > 1 and sys.argv[1] == "verify":
|
|
869
|
+
from maggie_design_verify import main as verify_main
|
|
870
|
+
return verify_main(sys.argv[2:])
|
|
871
|
+
if len(sys.argv) > 1 and sys.argv[1] == "icon-glyph-check":
|
|
872
|
+
from maggie_icon_glyph_check import main as glyph_main
|
|
873
|
+
return glyph_main(sys.argv[2:])
|
|
866
874
|
if len(sys.argv) > 1 and sys.argv[1] == "palette":
|
|
867
875
|
from maggie_palette import main as palette_main
|
|
868
876
|
return palette_main(sys.argv[2:])
|