Coverage for cli / commands / release_cmd.py: 100.00%

109 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-09-14 22:07 +0000

1"""Release lifecycle commands. 

2 

3``gco release validate`` wraps the live release validation harness 

4(``scripts/live_release_validation``) so an operator runs one command 

5instead of exporting six environment variables and assembling a module 

6invocation by hand. The wrapper derives everything derivable — commit SHA, 

7branch, run id, report directory — and reserves flags for the things a 

8human must consciously assert: 

9 

10* which account the run may touch (``--expected-account``); and 

11* that they understand it deploys and destroys paid infrastructure 

12 (``--i-understand-this-deploys-and-destroys-infrastructure``, plus 

13 ``--confirm-kms-key-deletion`` whenever the deploy action is selected). 

14 

15There are deliberately NO interactive prompts: presence of the flags is the 

16consent, which makes the command automatable while keeping accidental 

17invocation implausible. The harness itself re-verifies every identity claim 

18(account, SHA, branch, clean worktree) before acting, so this wrapper adds 

19convenience on top of those guarantees rather than replacing them. 

20 

21``--emulator-endpoint`` runs the identical harness against a local AWS 

22emulator (Floci) for CI rehearsal; the harness proves the endpoint is an 

23emulator before touching anything (see 

24``scripts/live_release_validation/emulator.py`` and docs/FLOCI_TESTING.md). 

25""" 

26 

27from __future__ import annotations 

28 

29import os 

30import re 

31import subprocess 

32import sys 

33from datetime import UTC, datetime 

34from pathlib import Path 

35from typing import NoReturn 

36 

37import click 

38 

39from .._image_reference import immutable_sha256_digest 

40 

41_ACCOUNT_RE = re.compile(r"\d{12}") 

42 

43#: The consent flag's exact name, referenced from error messages and docs. 

44CONSENT_FLAG = "--i-understand-this-deploys-and-destroys-infrastructure" 

45 

46 

47def _fail(message: str) -> NoReturn: 

48 raise click.ClickException(message) 

49 

50 

51def _run_git(repo_root: Path | None, *args: str) -> str: 

52 result = subprocess.run( 

53 ["git", *args], 

54 cwd=repo_root, 

55 capture_output=True, 

56 text=True, 

57 check=False, 

58 ) 

59 if result.returncode != 0: 

60 _fail(f"git {' '.join(args)} failed: {result.stderr.strip()}") 

61 return result.stdout.strip() 

62 

63 

64def _repo_root() -> Path: 

65 root = Path(_run_git(None, "rev-parse", "--show-toplevel")) 

66 if not (root / "cdk.json").is_file() or not (root / "scripts").is_dir(): 

67 _fail( 

68 f"{root} is not a GCO checkout (cdk.json or scripts/ missing); " 

69 "run from inside the repository" 

70 ) 

71 return root 

72 

73 

74@click.group() 

75def release() -> None: 

76 """Release validation lifecycle.""" 

77 

78 

79@release.command("validate") 

80@click.option( 

81 "--expected-account", 

82 required=True, 

83 metavar="ACCOUNT_ID", 

84 help="Exact 12-digit AWS account id this run is allowed to touch.", 

85) 

86@click.option( 

87 CONSENT_FLAG, 

88 "authorized", 

89 is_flag=True, 

90 default=False, 

91 help=( 

92 "Required consent: the run deploys real, paid infrastructure into the " 

93 "expected account and destroys it afterwards. No prompt will ask again." 

94 ), 

95) 

96@click.option( 

97 "--confirm-kms-key-deletion", 

98 is_flag=True, 

99 default=False, 

100 help=( 

101 "Authorize scheduling this run's retained EKS KMS keys for their 7-day " 

102 "deletion window during cleanup. Required whenever the deploy action runs." 

103 ), 

104) 

105@click.option( 

106 "--actions", 

107 default="all", 

108 show_default=True, 

109 metavar="NAME[,NAME...]", 

110 help="Harness actions to run; dependencies are added automatically.", 

111) 

112@click.option("--inference-region", default=None, help="Region for the inference matrix.") 

113@click.option("--inference-vllm-image", default=None, help="Immutable vLLM @sha256 image.") 

114@click.option("--inference-vllm-model-id", default=None, help="Exact vLLM model identifier.") 

115@click.option( 

116 "--inference-vllm-model-revision", 

117 default=None, 

118 help="Full immutable 40-hex vLLM model commit.", 

119) 

120@click.option("--inference-tgi-image", default=None, help="Immutable TGI @sha256 image.") 

121@click.option("--inference-tgi-model-id", default=None, help="Exact TGI model identifier.") 

122@click.option( 

123 "--inference-tgi-model-revision", 

124 default=None, 

125 help="Full immutable 40-hex TGI model commit.", 

126) 

127@click.option("--inference-gpu-count", type=click.IntRange(min=0), default=0, show_default=True) 

128@click.option( 

129 "--optional-schedulers", 

130 "optional_schedulers", 

131 default=None, 

132 metavar="NAME[,NAME...]", 

133 help=( 

134 "Force-enable off-by-default schedulers (yunikorn, slurm, or all) for " 

135 "this run's deploy so the schedulers action proves them too." 

136 ), 

137) 

138@click.option( 

139 "--profile", 

140 type=click.Choice(["configured", "single-region", "multi-region"]), 

141 default="configured", 

142 show_default=True, 

143 help="Topology profile to validate against cdk.json (never rewritten).", 

144) 

145@click.option("--run-id", default=None, help="Stable run id (default: UTC timestamp + SHA).") 

146@click.option( 

147 "--report-dir", 

148 default=None, 

149 type=click.Path(path_type=Path), 

150 help="Report directory (default: ~/gco-live-release-validation-reports/<run-id>).", 

151) 

152@click.option( 

153 "--resume", 

154 is_flag=True, 

155 default=False, 

156 help="Resume an interrupted run; requires the original --run-id and --report-dir.", 

157) 

158@click.option( 

159 "--protected-stack", 

160 multiple=True, 

161 metavar="NAME", 

162 help="Additional non-project CloudFormation stack to preserve exactly (repeatable).", 

163) 

164@click.option( 

165 "--emulator-endpoint", 

166 default=None, 

167 metavar="URL", 

168 help=( 

169 "Run the identical harness against a local AWS emulator (Floci) instead of " 

170 "real AWS. The harness verifies the endpoint is an emulator before acting." 

171 ), 

172) 

173def release_validate( 

174 expected_account: str, 

175 authorized: bool, 

176 confirm_kms_key_deletion: bool, 

177 actions: str, 

178 inference_region: str | None, 

179 inference_vllm_image: str | None, 

180 inference_vllm_model_id: str | None, 

181 inference_vllm_model_revision: str | None, 

182 inference_tgi_image: str | None, 

183 inference_tgi_model_id: str | None, 

184 inference_tgi_model_revision: str | None, 

185 inference_gpu_count: int, 

186 optional_schedulers: str | None, 

187 profile: str, 

188 run_id: str | None, 

189 report_dir: Path | None, 

190 resume: bool, 

191 protected_stack: tuple[str, ...], 

192 emulator_endpoint: str | None, 

193) -> None: 

194 """Run live release validation end to end without prompts. 

195 

196 Derives the expected commit SHA and branch from the current checkout, 

197 generates a run id and a private report directory outside the worktree, 

198 and executes ``python -m scripts.live_release_validation``. Exits with 

199 the harness's exit code; reports land in the report directory. 

200 """ 

201 if not _ACCOUNT_RE.fullmatch(expected_account): 

202 _fail("--expected-account must be an exact 12-digit AWS account id") 

203 if not authorized: 

204 _fail( 

205 "Refusing to run without explicit consent. Add " 

206 f"{CONSENT_FLAG} to acknowledge that this deploys and destroys real " 

207 "infrastructure in account " + expected_account + "." 

208 ) 

209 selected = {name.strip() for name in actions.split(",") if name.strip()} 

210 if not selected: 

211 _fail("--actions must name at least one action") 

212 inference_selected = bool(selected & {"all", "inference"}) 

213 # Every action other than preflight/baseline transitively depends on 

214 # deploy, and the harness expands dependencies automatically — so any 

215 # such selection deploys real infrastructure and creates retained EKS 

216 # KMS keys, not just a literal `deploy`/`all`. 

217 deploy_selected = bool(selected & {"all", "deploy"}) or bool( 

218 selected - {"preflight", "baseline"} 

219 ) 

220 if deploy_selected and not confirm_kms_key_deletion: 

221 _fail( 

222 "The selected actions imply the deploy action, which creates retained " 

223 "EKS KMS keys; add --confirm-kms-key-deletion to authorize scheduling " 

224 "exactly this run's keys for deletion during cleanup." 

225 ) 

226 if resume and (run_id is None or report_dir is None): 

227 _fail( 

228 "--resume replays an exact checkpoint identity: pass the original " 

229 "--run-id and --report-dir from the interrupted run." 

230 ) 

231 if inference_selected: 

232 required_inference = { 

233 "--inference-region": inference_region, 

234 "--inference-vllm-image": inference_vllm_image, 

235 "--inference-vllm-model-id": inference_vllm_model_id, 

236 "--inference-vllm-model-revision": inference_vllm_model_revision, 

237 "--inference-tgi-image": inference_tgi_image, 

238 "--inference-tgi-model-id": inference_tgi_model_id, 

239 "--inference-tgi-model-revision": inference_tgi_model_revision, 

240 } 

241 missing = [name for name, value in required_inference.items() if not value] 

242 if missing: 

243 _fail("The inference action requires " + ", ".join(missing) + ".") 

244 image_digests: list[str] = [] 

245 for option, image in ( 

246 ("--inference-vllm-image", inference_vllm_image), 

247 ("--inference-tgi-image", inference_tgi_image), 

248 ): 

249 digest = immutable_sha256_digest(image) 

250 if digest is None: 

251 _fail(f"{option} must be an immutable lowercase @sha256: reference") 

252 image_digests.append(digest) 

253 if len(set(image_digests)) != 2: 

254 _fail("vLLM and TGI inference images must have distinct immutable digests") 

255 for option, revision in ( 

256 ("--inference-vllm-model-revision", inference_vllm_model_revision), 

257 ("--inference-tgi-model-revision", inference_tgi_model_revision), 

258 ): 

259 if revision is None or not re.fullmatch(r"[0-9a-f]{40}", revision): 

260 _fail(f"{option} must be a full lowercase 40-hex commit") 

261 

262 repo_root = _repo_root() 

263 expected_sha = _run_git(repo_root, "rev-parse", "HEAD") 

264 expected_branch = _run_git(repo_root, "symbolic-ref", "--short", "HEAD") 

265 resolved_run_id = run_id or ( 

266 datetime.now(UTC).strftime("%Y%m%dT%H%M%SZ") + "-" + expected_sha[:12] 

267 ) 

268 resolved_report_dir = report_dir or ( 

269 Path.home() / "gco-live-release-validation-reports" / resolved_run_id 

270 ) 

271 

272 env = dict(os.environ) 

273 if emulator_endpoint: 

274 normalized = emulator_endpoint.rstrip("/") 

275 # The harness verifies these before acting; setting both here keeps a 

276 # single flag sufficient and makes a split-endpoint run impossible. 

277 env["GCO_LIVE_VALIDATION_EMULATOR"] = normalized 

278 env["AWS_ENDPOINT_URL"] = normalized 

279 

280 command = [ 

281 sys.executable, 

282 "-m", 

283 "scripts.live_release_validation", 

284 "--repo-root", 

285 str(repo_root), 

286 "--expected-account", 

287 expected_account, 

288 "--expected-sha", 

289 expected_sha, 

290 "--expected-branch", 

291 expected_branch, 

292 "--profile", 

293 profile, 

294 "--actions", 

295 ",".join(sorted(selected)), 

296 "--run-id", 

297 resolved_run_id, 

298 "--report-dir", 

299 str(resolved_report_dir), 

300 "--checkpoint", 

301 str(resolved_report_dir / "checkpoint.json"), 

302 ] 

303 if inference_selected: 

304 command.extend( 

305 [ 

306 "--inference-region", 

307 str(inference_region), 

308 "--inference-vllm-image", 

309 str(inference_vllm_image), 

310 "--inference-vllm-model-id", 

311 str(inference_vllm_model_id), 

312 "--inference-vllm-model-revision", 

313 str(inference_vllm_model_revision), 

314 "--inference-tgi-image", 

315 str(inference_tgi_image), 

316 "--inference-tgi-model-id", 

317 str(inference_tgi_model_id), 

318 "--inference-tgi-model-revision", 

319 str(inference_tgi_model_revision), 

320 "--inference-gpu-count", 

321 str(inference_gpu_count), 

322 "--confirm-inference-deployment", 

323 ] 

324 ) 

325 if confirm_kms_key_deletion: 

326 command.append("--confirm-kms-key-deletion") 

327 if optional_schedulers: 

328 command.extend(["--optional-schedulers", optional_schedulers]) 

329 if resume: 

330 command.append("--resume") 

331 for name in protected_stack: 

332 command.extend(["--protected-stack", name]) 

333 

334 click.echo(f"run-id: {resolved_run_id}") 

335 click.echo(f"sha: {expected_sha}") 

336 click.echo(f"branch: {expected_branch}") 

337 click.echo(f"account: {expected_account}") 

338 click.echo(f"actions: {','.join(sorted(selected))}") 

339 if optional_schedulers: 

340 click.echo(f"schedulers: {optional_schedulers} (force-enabled for this run)") 

341 click.echo(f"report-dir: {resolved_report_dir}") 

342 if emulator_endpoint: 

343 click.echo(f"emulator: {emulator_endpoint} (verified by the harness before use)") 

344 

345 # Stream harness output directly; operators watch progress live and the 

346 # harness owns its own reporting/cleanup guarantees. 

347 result = subprocess.run(command, cwd=repo_root, env=env, check=False) 

348 sys.exit(result.returncode)