fdai-deployment-cli 0.1.2__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.
Files changed (203) hide show
  1. fdai_deployment_cli/__about__.py +3 -0
  2. fdai_deployment_cli/__init__.py +5 -0
  3. fdai_deployment_cli/__main__.py +5 -0
  4. fdai_deployment_cli/acr_publish.py +585 -0
  5. fdai_deployment_cli/aks_catalog_review.py +137 -0
  6. fdai_deployment_cli/aks_historical_reconciliation.py +732 -0
  7. fdai_deployment_cli/aks_job_environment.py +59 -0
  8. fdai_deployment_cli/aks_job_execution.py +794 -0
  9. fdai_deployment_cli/aks_kubernetes_defaults.py +66 -0
  10. fdai_deployment_cli/aks_preflight.py +422 -0
  11. fdai_deployment_cli/aks_readiness.py +220 -0
  12. fdai_deployment_cli/aks_service_update.py +180 -0
  13. fdai_deployment_cli/aks_workload_jobs.py +281 -0
  14. fdai_deployment_cli/application_state_adoption.py +347 -0
  15. fdai_deployment_cli/azure_actor.py +61 -0
  16. fdai_deployment_cli/azure_naming.py +252 -0
  17. fdai_deployment_cli/azure_retail_prices.py +173 -0
  18. fdai_deployment_cli/bootstrap_approval.py +99 -0
  19. fdai_deployment_cli/bootstrap_channel.py +321 -0
  20. fdai_deployment_cli/bootstrap_executor.py +311 -0
  21. fdai_deployment_cli/bootstrap_plan.py +237 -0
  22. fdai_deployment_cli/bootstrap_reconcile.py +402 -0
  23. fdai_deployment_cli/bootstrap_support.py +138 -0
  24. fdai_deployment_cli/bootstrap_worker.py +448 -0
  25. fdai_deployment_cli/bundle.py +352 -0
  26. fdai_deployment_cli/capacity.py +143 -0
  27. fdai_deployment_cli/catalog_review_profile.py +306 -0
  28. fdai_deployment_cli/cli.py +798 -0
  29. fdai_deployment_cli/cli_help.py +214 -0
  30. fdai_deployment_cli/cli_parser.py +791 -0
  31. fdai_deployment_cli/cli_plan.py +450 -0
  32. fdai_deployment_cli/compiler.py +99 -0
  33. fdai_deployment_cli/console_artifact.py +218 -0
  34. fdai_deployment_cli/console_config.py +214 -0
  35. fdai_deployment_cli/console_update.py +832 -0
  36. fdai_deployment_cli/contracts.py +307 -0
  37. fdai_deployment_cli/control_package.py +140 -0
  38. fdai_deployment_cli/deadline_transport.py +70 -0
  39. fdai_deployment_cli/deployment_cost.py +131 -0
  40. fdai_deployment_cli/deployment_deadline.py +28 -0
  41. fdai_deployment_cli/deployment_kit.py +790 -0
  42. fdai_deployment_cli/deployment_kit_cache.py +191 -0
  43. fdai_deployment_cli/deployment_progress.py +648 -0
  44. fdai_deployment_cli/development_authority_cli.py +56 -0
  45. fdai_deployment_cli/development_authority_profile.py +319 -0
  46. fdai_deployment_cli/doctor.py +136 -0
  47. fdai_deployment_cli/entitlement_preflight.py +68 -0
  48. fdai_deployment_cli/entitlement_transport.py +49 -0
  49. fdai_deployment_cli/entra_bootstrap.py +579 -0
  50. fdai_deployment_cli/entra_control_profile.py +214 -0
  51. fdai_deployment_cli/entra_graph.py +349 -0
  52. fdai_deployment_cli/entra_profiles.py +215 -0
  53. fdai_deployment_cli/entra_source.py +244 -0
  54. fdai_deployment_cli/execution_bundle.py +478 -0
  55. fdai_deployment_cli/execution_copies.py +63 -0
  56. fdai_deployment_cli/foundation_adoption.py +438 -0
  57. fdai_deployment_cli/foundation_adoption_evidence.py +245 -0
  58. fdai_deployment_cli/foundation_adoption_host.py +157 -0
  59. fdai_deployment_cli/foundation_adoption_transport.py +51 -0
  60. fdai_deployment_cli/foundation_failure.py +72 -0
  61. fdai_deployment_cli/foundation_image.py +168 -0
  62. fdai_deployment_cli/foundation_input.py +235 -0
  63. fdai_deployment_cli/foundation_output.py +154 -0
  64. fdai_deployment_cli/foundation_plan.py +381 -0
  65. fdai_deployment_cli/foundation_process.py +64 -0
  66. fdai_deployment_cli/foundation_progress.py +231 -0
  67. fdai_deployment_cli/github_actions.py +171 -0
  68. fdai_deployment_cli/github_apply_artifact.py +259 -0
  69. fdai_deployment_cli/github_artifact_io.py +48 -0
  70. fdai_deployment_cli/github_dispatch.py +248 -0
  71. fdai_deployment_cli/github_plan_artifact.py +181 -0
  72. fdai_deployment_cli/github_provider_artifact.py +170 -0
  73. fdai_deployment_cli/github_transport.py +46 -0
  74. fdai_deployment_cli/github_workflow_values.py +312 -0
  75. fdai_deployment_cli/hub_image_deploy.py +354 -0
  76. fdai_deployment_cli/hub_image_stage.py +162 -0
  77. fdai_deployment_cli/hub_image_trust.py +281 -0
  78. fdai_deployment_cli/hub_ownership.py +161 -0
  79. fdai_deployment_cli/hub_workload_handoff.py +264 -0
  80. fdai_deployment_cli/hub_workload_handoff_inputs.py +227 -0
  81. fdai_deployment_cli/identity_profile.py +227 -0
  82. fdai_deployment_cli/image_targets.py +197 -0
  83. fdai_deployment_cli/installation_scope.py +280 -0
  84. fdai_deployment_cli/license.py +311 -0
  85. fdai_deployment_cli/license_issue.py +239 -0
  86. fdai_deployment_cli/lifecycle_authorization.py +252 -0
  87. fdai_deployment_cli/lifecycle_commands.py +173 -0
  88. fdai_deployment_cli/lifecycle_configuration.py +722 -0
  89. fdai_deployment_cli/lifecycle_configuration_artifact.py +45 -0
  90. fdai_deployment_cli/lifecycle_configuration_cli.py +324 -0
  91. fdai_deployment_cli/lifecycle_configuration_document.py +74 -0
  92. fdai_deployment_cli/lifecycle_configuration_entities.py +146 -0
  93. fdai_deployment_cli/lifecycle_configuration_package.py +492 -0
  94. fdai_deployment_cli/lifecycle_configuration_publication.py +277 -0
  95. fdai_deployment_cli/lifecycle_configuration_publication_cli.py +191 -0
  96. fdai_deployment_cli/lifecycle_configuration_sealing.py +127 -0
  97. fdai_deployment_cli/lifecycle_enrollment.py +45 -0
  98. fdai_deployment_cli/lifecycle_infrastructure_agent.py +174 -0
  99. fdai_deployment_cli/lifecycle_installation_binding.py +65 -0
  100. fdai_deployment_cli/lifecycle_native.py +247 -0
  101. fdai_deployment_cli/lifecycle_native_reconciliation.py +568 -0
  102. fdai_deployment_cli/lifecycle_native_transition.py +339 -0
  103. fdai_deployment_cli/lifecycle_ownership_exclusion.py +78 -0
  104. fdai_deployment_cli/lifecycle_plan.py +800 -0
  105. fdai_deployment_cli/lifecycle_promotion.py +185 -0
  106. fdai_deployment_cli/lifecycle_release.py +308 -0
  107. fdai_deployment_cli/lifecycle_self_upgrade.py +202 -0
  108. fdai_deployment_cli/lifecycle_upgrade_bundle.py +176 -0
  109. fdai_deployment_cli/notification_recipients_input.py +222 -0
  110. fdai_deployment_cli/observer_artifact.py +106 -0
  111. fdai_deployment_cli/oci_archive.py +353 -0
  112. fdai_deployment_cli/offline_kit.py +797 -0
  113. fdai_deployment_cli/offline_prepare.py +135 -0
  114. fdai_deployment_cli/operational_evidence_verifier_input.py +575 -0
  115. fdai_deployment_cli/plan_input.py +101 -0
  116. fdai_deployment_cli/private_output.py +192 -0
  117. fdai_deployment_cli/profile.py +70 -0
  118. fdai_deployment_cli/progress.py +183 -0
  119. fdai_deployment_cli/readiness.py +286 -0
  120. fdai_deployment_cli/run_command_receiver.py +418 -0
  121. fdai_deployment_cli/runtime_build.py +386 -0
  122. fdai_deployment_cli/runtime_profile.py +287 -0
  123. fdai_deployment_cli/runtime_release.py +745 -0
  124. fdai_deployment_cli/runtime_stage.py +82 -0
  125. fdai_deployment_cli/runtime_support_installation.py +215 -0
  126. fdai_deployment_cli/simulation.py +80 -0
  127. fdai_deployment_cli/source_application_inputs.py +397 -0
  128. fdai_deployment_cli/source_azure.py +586 -0
  129. fdai_deployment_cli/source_change_cli.py +107 -0
  130. fdai_deployment_cli/source_change_parser.py +75 -0
  131. fdai_deployment_cli/source_change_plan.py +181 -0
  132. fdai_deployment_cli/source_changed_update.py +196 -0
  133. fdai_deployment_cli/source_deploy.py +170 -0
  134. fdai_deployment_cli/source_foundation.py +174 -0
  135. fdai_deployment_cli/source_host_tools.py +163 -0
  136. fdai_deployment_cli/source_image_stage.py +400 -0
  137. fdai_deployment_cli/source_input.py +173 -0
  138. fdai_deployment_cli/source_projection.py +164 -0
  139. fdai_deployment_cli/source_receiver.py +76 -0
  140. fdai_deployment_cli/source_recovery.py +264 -0
  141. fdai_deployment_cli/source_runtime.py +81 -0
  142. fdai_deployment_cli/source_runtime_support.py +368 -0
  143. fdai_deployment_cli/source_service_update.py +726 -0
  144. fdai_deployment_cli/source_service_update_cli.py +47 -0
  145. fdai_deployment_cli/source_snapshot.py +178 -0
  146. fdai_deployment_cli/source_teardown.py +574 -0
  147. fdai_deployment_cli/source_terraform.py +85 -0
  148. fdai_deployment_cli/source_transport.py +399 -0
  149. fdai_deployment_cli/standalone_aks_inventory.py +558 -0
  150. fdai_deployment_cli/standalone_aks_job_execution.py +769 -0
  151. fdai_deployment_cli/standalone_aks_nodepool_guard.py +141 -0
  152. fdai_deployment_cli/standalone_application.py +977 -0
  153. fdai_deployment_cli/standalone_application_completion.py +151 -0
  154. fdai_deployment_cli/standalone_application_input_recovery.py +107 -0
  155. fdai_deployment_cli/standalone_application_recovery.py +116 -0
  156. fdai_deployment_cli/standalone_approval_input.py +24 -0
  157. fdai_deployment_cli/standalone_catalog_checkpoint.py +122 -0
  158. fdai_deployment_cli/standalone_catalog_review.py +251 -0
  159. fdai_deployment_cli/standalone_checkpoint_failure.py +127 -0
  160. fdai_deployment_cli/standalone_console_publish.py +174 -0
  161. fdai_deployment_cli/standalone_database_principals.py +41 -0
  162. fdai_deployment_cli/standalone_deploy.py +638 -0
  163. fdai_deployment_cli/standalone_development_authority.py +166 -0
  164. fdai_deployment_cli/standalone_foundation_adoption.py +87 -0
  165. fdai_deployment_cli/standalone_foundation_transition.py +767 -0
  166. fdai_deployment_cli/standalone_host.py +4608 -0
  167. fdai_deployment_cli/standalone_host_rebind.py +226 -0
  168. fdai_deployment_cli/standalone_host_residual.py +104 -0
  169. fdai_deployment_cli/standalone_host_state.py +145 -0
  170. fdai_deployment_cli/standalone_host_values.py +563 -0
  171. fdai_deployment_cli/standalone_kubernetes_tools.py +100 -0
  172. fdai_deployment_cli/standalone_license_installation.py +177 -0
  173. fdai_deployment_cli/standalone_management_egress.py +83 -0
  174. fdai_deployment_cli/standalone_migration_evidence.py +38 -0
  175. fdai_deployment_cli/standalone_operational_evidence.py +356 -0
  176. fdai_deployment_cli/standalone_planned_outputs.py +147 -0
  177. fdai_deployment_cli/standalone_private_access_probe.py +100 -0
  178. fdai_deployment_cli/standalone_product_profile.py +110 -0
  179. fdai_deployment_cli/standalone_remote_prepare.py +442 -0
  180. fdai_deployment_cli/standalone_residual_apply.py +540 -0
  181. fdai_deployment_cli/standalone_residual_successor.py +431 -0
  182. fdai_deployment_cli/standalone_review.py +266 -0
  183. fdai_deployment_cli/standalone_service_update_finalization.py +116 -0
  184. fdai_deployment_cli/standalone_service_update_recovery.py +277 -0
  185. fdai_deployment_cli/standalone_stage_targets.py +325 -0
  186. fdai_deployment_cli/standalone_status.py +67 -0
  187. fdai_deployment_cli/standalone_terraform_environment.py +237 -0
  188. fdai_deployment_cli/standalone_transfer_cleanup.py +237 -0
  189. fdai_deployment_cli/standalone_trial_activation.py +193 -0
  190. fdai_deployment_cli/state.py +434 -0
  191. fdai_deployment_cli/state_handoff.py +253 -0
  192. fdai_deployment_cli/status_projection.py +97 -0
  193. fdai_deployment_cli/support_install.py +257 -0
  194. fdai_deployment_cli/target.py +46 -0
  195. fdai_deployment_cli/trust/capabilities.json +62 -0
  196. fdai_deployment_cli/trust/deployment-bundle-root.pub +3 -0
  197. fdai_deployment_cli/trust/deployment-release-root.pub +3 -0
  198. fdai_deployment_cli/trust/upstream-signing-key.pub +3 -0
  199. fdai_deployment_cli/trust_roots.py +50 -0
  200. fdai_deployment_cli-0.1.2.dist-info/METADATA +112 -0
  201. fdai_deployment_cli-0.1.2.dist-info/RECORD +203 -0
  202. fdai_deployment_cli-0.1.2.dist-info/WHEEL +4 -0
  203. fdai_deployment_cli-0.1.2.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,231 @@
1
+ """Adapt known Genesis presentation records without deriving execution authority."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from collections.abc import Callable
7
+ from dataclasses import dataclass
8
+
9
+ CHECKPOINT_LABELS = (
10
+ "Toolchain prerequisites",
11
+ "Azure target verification",
12
+ "Source and required CI",
13
+ "Azure resource providers",
14
+ "Tenant policy route",
15
+ "Deployment route selection",
16
+ "Runner image exact plan",
17
+ "Runner image exact apply",
18
+ "Foundation exact plan",
19
+ "Foundation exact apply",
20
+ "Runner enrollment and attestation",
21
+ "Foundation private state handoff",
22
+ "Protected application exact plan",
23
+ "Protected application exact apply",
24
+ "Post-deployment verification",
25
+ )
26
+ _TOTAL = len(CHECKPOINT_LABELS)
27
+ _MAX_LINE = 512
28
+ _CHUNK_SIZE = 4096
29
+ _LEGACY_RECORD = re.compile(
30
+ r"\[([#.]{24})\] +([0-9]{1,3})% done ([0-9]{1,2})/15 \| "
31
+ r"stage ([0-9]{1,2})/15 ([^\r\n]{1,80}) - "
32
+ r"(RUNNING|WAITING|BLOCKED|FAILED|COMPLETE) \| "
33
+ r"skipped ([0-9]{1,2}) \| remaining ([0-9]{1,2})"
34
+ )
35
+ _INSTALLER_RECORD = re.compile(
36
+ r"(==>|WAIT| OK |HOLD|FAIL) \[([0-9]{2})/15\] ([^|\r\n]{1,80}) \| "
37
+ r"(RUNNING|WAITING|BLOCKED|FAILED|COMPLETE) \| "
38
+ r"([0-9]{1,2}) complete \| ([0-9]{1,2}) skipped \| ([0-9]{1,2}) remaining"
39
+ )
40
+ _STATE_PREFIXES = {
41
+ "RUNNING": "==>",
42
+ "WAITING": "WAIT",
43
+ "COMPLETE": " OK ",
44
+ "BLOCKED": "HOLD",
45
+ "FAILED": "FAIL",
46
+ }
47
+ _HEARTBEAT = re.compile(r" {4}\.\. still running \| elapsed (?:[0-9]{2}:){1,2}[0-9]{2}")
48
+
49
+
50
+ @dataclass(frozen=True)
51
+ class FoundationCheckpoint:
52
+ """A checked display record, never a status receipt or approval."""
53
+
54
+ number: int
55
+ label: str
56
+ state: str
57
+ completed: int
58
+ skipped: int
59
+ total: int = _TOTAL
60
+
61
+
62
+ def _prefix(completed: int) -> str:
63
+ filled = 24 * completed // _TOTAL
64
+ return (
65
+ f"[{'#' * filled}{'.' * (24 - filled)}] {completed * 100 // _TOTAL:3d}% "
66
+ f"done {completed}/{_TOTAL} | stage "
67
+ )
68
+
69
+
70
+ _PREFIXES = (
71
+ *(_prefix(count) for count in range(_TOTAL + 1)),
72
+ "==> ",
73
+ "WAIT ",
74
+ " OK ",
75
+ "HOLD ",
76
+ "FAIL ",
77
+ " .. still running | elapsed ",
78
+ )
79
+
80
+
81
+ def parse_checkpoint(line: str) -> FoundationCheckpoint | None:
82
+ """Accept only current or legacy producer labels, arithmetic, and field layout."""
83
+
84
+ if (match := _INSTALLER_RECORD.fullmatch(line)) is not None:
85
+ marker, number, label, state, count, skipped, remaining = match.groups()
86
+ completed, index, omitted = int(count), int(number), int(skipped)
87
+ if (
88
+ _STATE_PREFIXES[state] != marker
89
+ or not 0 <= omitted <= completed <= _TOTAL
90
+ or not 1 <= index <= _TOTAL
91
+ or label != CHECKPOINT_LABELS[index - 1]
92
+ or int(remaining) != _TOTAL - completed
93
+ ):
94
+ return None
95
+ expected = (
96
+ f"{marker} [{index:02d}/{_TOTAL}] {label} | {state} | "
97
+ f"{completed} complete | {omitted} skipped | {_TOTAL - completed} remaining"
98
+ )
99
+ if line != expected:
100
+ return None
101
+ return FoundationCheckpoint(index, label, state, completed, omitted)
102
+ match = _LEGACY_RECORD.fullmatch(line)
103
+ if match is None:
104
+ return None
105
+ _bar, _percent, count, number, label, state, skipped, remaining = match.groups()
106
+ completed, index, omitted = int(count), int(number), int(skipped)
107
+ if (
108
+ not 0 <= omitted <= completed <= _TOTAL
109
+ or not 1 <= index <= _TOTAL
110
+ or label != CHECKPOINT_LABELS[index - 1]
111
+ or int(remaining) != _TOTAL - completed
112
+ ):
113
+ return None
114
+ expected = (
115
+ f"{_prefix(completed)}{index}/{_TOTAL} {label} - {state} | "
116
+ f"skipped {omitted} | remaining {_TOTAL - completed}"
117
+ )
118
+ if line != expected:
119
+ return None
120
+ return FoundationCheckpoint(index, label, state, completed, omitted)
121
+
122
+
123
+ def _intro(mode: str) -> str:
124
+ return (
125
+ f"FDAI Azure Genesis\nMode: {mode}; prompts: none\nProcedure:\n"
126
+ + "".join(f" {index}. {label}\n" for index, label in enumerate(CHECKPOINT_LABELS, 1))
127
+ + "Routes: public-dev -> preview and exact-plan wait; "
128
+ "private-runner -> image, Foundation, enrollment, and state checkpoints\n"
129
+ "Safety: no route fallback, unsealed apply, or readiness claim\n"
130
+ )
131
+
132
+
133
+ _INTROS = tuple(
134
+ (mode, _intro(mode)) for mode in ("mutation-enabled preflight", "read-only inspection")
135
+ )
136
+
137
+
138
+ class FoundationProgressStream:
139
+ """Compact known renderer output; pass all other text through without waiting for EOF.
140
+
141
+ The compatibility adapter activates only after the complete fixed Genesis introduction.
142
+ Unknown versions and malformed records remain native diagnostics. No record advances the
143
+ coordinator state machine, writes status, or grants approval.
144
+ """
145
+
146
+ def __init__(
147
+ self,
148
+ *,
149
+ output: Callable[[str], None],
150
+ checkpoint: Callable[[FoundationCheckpoint], None],
151
+ heartbeat: Callable[[], None],
152
+ mode: Callable[[str], None],
153
+ ) -> None:
154
+ self._output = output
155
+ self._checkpoint = checkpoint
156
+ self._heartbeat = heartbeat
157
+ self._mode = mode
158
+ self._intro_buffer = ""
159
+ self._pending = ""
160
+ self._started = False
161
+ self._native_only = False
162
+ self._native_line = False
163
+
164
+ def feed(self, text: str) -> None:
165
+ """Consume bounded pieces, promptly forwarding an unknown unterminated diagnostic."""
166
+ for offset in range(0, len(text), _CHUNK_SIZE):
167
+ self._feed(text[offset : offset + _CHUNK_SIZE])
168
+
169
+ def _feed(self, text: str) -> None:
170
+ if self._native_only:
171
+ self._output(text)
172
+ return
173
+ if not self._started:
174
+ self._intro_buffer += text
175
+ for mode, introduction in _INTROS:
176
+ if self._intro_buffer.startswith(introduction):
177
+ text = self._intro_buffer[len(introduction) :]
178
+ self._intro_buffer = ""
179
+ self._started = True
180
+ self._mode(mode)
181
+ break
182
+ else:
183
+ if any(introduction.startswith(self._intro_buffer) for _, introduction in _INTROS):
184
+ return
185
+ self._native_only = True
186
+ self._output(self._intro_buffer)
187
+ self._intro_buffer = ""
188
+ return
189
+ self._body(text)
190
+
191
+ def _body(self, text: str) -> None:
192
+ while text:
193
+ part, newline, text = text.partition("\n")
194
+ if self._native_line:
195
+ self._output(part + newline)
196
+ self._native_line = not bool(newline)
197
+ continue
198
+ self._pending += part
199
+ if newline:
200
+ line, self._pending = self._pending, ""
201
+ candidate = line.lstrip(".")
202
+ if (not candidate and line) or _HEARTBEAT.fullmatch(line):
203
+ self._heartbeat()
204
+ elif (record := parse_checkpoint(candidate)) is not None:
205
+ self._checkpoint(record)
206
+ else:
207
+ self._output(line + newline)
208
+ continue
209
+ candidate = self._pending.lstrip(".")
210
+ possible = not candidate or any(
211
+ prefix.startswith(candidate) or candidate.startswith(prefix) for prefix in _PREFIXES
212
+ )
213
+ if possible and len(self._pending) <= _MAX_LINE:
214
+ if not candidate and self._pending:
215
+ self._heartbeat()
216
+ return
217
+ self._output(self._pending)
218
+ self._pending = ""
219
+ self._native_line = True
220
+
221
+ def finish(self) -> None:
222
+ """Preserve incomplete records; heartbeat dots alone never imply completion."""
223
+ if self._intro_buffer:
224
+ self._output(self._intro_buffer)
225
+ self._intro_buffer = ""
226
+ if self._pending:
227
+ if self._pending.strip("."):
228
+ self._output(self._pending)
229
+ else:
230
+ self._heartbeat()
231
+ self._pending = ""
@@ -0,0 +1,171 @@
1
+ """Bounded GitHub Actions transport for protected FDAI deployment workflows."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import UTC, datetime
6
+
7
+ from fdai_deployment_cli.github_transport import _artifact_runner, run_github_cli
8
+ from fdai_deployment_cli.github_workflow_values import (
9
+ _COMMIT,
10
+ _DIGEST,
11
+ _REQUEST_ID,
12
+ CommandResult,
13
+ CommandRunner,
14
+ DeploymentSelection,
15
+ WorkflowDispatch,
16
+ _request_binding_from_id,
17
+ _validate_repository,
18
+ deployment_context_digest,
19
+ enforce_plan_not_expired,
20
+ parse_plan_expiry,
21
+ request_binding_prefix,
22
+ request_id,
23
+ )
24
+ from fdai_deployment_cli.github_dispatch import dispatch_apply, dispatch_plan
25
+ from fdai_deployment_cli.github_plan_artifact import _download_plan_metadata
26
+ from fdai_deployment_cli.github_apply_artifact import _download_apply_receipt
27
+ from fdai_deployment_cli.github_artifact_io import (
28
+ _json_array,
29
+ )
30
+
31
+
32
+ # Artifact downloads are larger than metadata queries; use a longer timeout.
33
+
34
+
35
+ def workflow_status(
36
+ *,
37
+ repository: str,
38
+ request_id_value: str,
39
+ expected_commit: str,
40
+ expected_context_digest: str,
41
+ target_binding: str,
42
+ expected_region: str,
43
+ resume_verification: bool = False,
44
+ expected_plan_id: str | None = None,
45
+ expected_plan_digest: str | None = None,
46
+ run: CommandRunner | None = None,
47
+ ) -> dict[str, object]:
48
+ """Read one uniquely matched workflow run without polling."""
49
+
50
+ _validate_repository(repository)
51
+ if _REQUEST_ID.fullmatch(request_id_value) is None:
52
+ raise ValueError("request_id is invalid")
53
+ if _COMMIT.fullmatch(expected_commit) is None:
54
+ raise ValueError("expected_commit MUST be a lowercase git SHA")
55
+ if _DIGEST.fullmatch(expected_context_digest) is None:
56
+ raise ValueError("expected_context_digest MUST be a lowercase SHA-256")
57
+ request_mode = (
58
+ "plan"
59
+ if request_id_value.startswith("plan-")
60
+ else ("resume" if resume_verification else "apply")
61
+ )
62
+ expected_binding = request_binding_prefix(
63
+ target_binding=target_binding,
64
+ context_digest=expected_context_digest,
65
+ mode=request_mode,
66
+ region=expected_region,
67
+ )
68
+ if _request_binding_from_id(request_id_value) != expected_binding:
69
+ raise ValueError("request_id does not match the approved deployment context")
70
+ runner = run or run_github_cli
71
+ result = runner(
72
+ (
73
+ "run",
74
+ "list",
75
+ "--repo",
76
+ repository,
77
+ "--workflow",
78
+ "deploy-dev.yml",
79
+ "--event",
80
+ "workflow_dispatch",
81
+ "--limit",
82
+ "50",
83
+ "--json",
84
+ "databaseId,displayTitle,status,conclusion,url,headSha",
85
+ )
86
+ )
87
+ if result.returncode != 0:
88
+ raise ValueError("github_workflow_status_unavailable")
89
+ payload = _json_array(result.stdout, "workflow runs")
90
+ expected_title = f"deploy-{request_id_value}"
91
+ matches = [item for item in payload if item.get("displayTitle") == expected_title]
92
+ if not matches:
93
+ raise ValueError("github_workflow_run_not_found")
94
+ if len(matches) != 1:
95
+ raise ValueError("github_workflow_run_ambiguous")
96
+ selected = matches[0]
97
+ database_id = selected.get("databaseId")
98
+ status = selected.get("status")
99
+ conclusion = selected.get("conclusion")
100
+ head_sha = selected.get("headSha")
101
+ if (
102
+ not isinstance(database_id, int)
103
+ or not isinstance(status, str)
104
+ or (conclusion is not None and not isinstance(conclusion, str))
105
+ or not isinstance(head_sha, str)
106
+ or _COMMIT.fullmatch(head_sha) is None
107
+ ):
108
+ raise ValueError("github_workflow_run_invalid")
109
+ projected: dict[str, object] = {
110
+ "schema_version": "fdai.workflow-status.v1",
111
+ "request_id": request_id_value,
112
+ "workflow_run_id": database_id,
113
+ "status": status,
114
+ "conclusion": conclusion,
115
+ "dispatch_ref_sha": head_sha,
116
+ "requested_commit": expected_commit,
117
+ "url": selected.get("url") if isinstance(selected.get("url"), str) else None,
118
+ "mutation_performed": False,
119
+ }
120
+ if request_id_value.startswith("plan-") and status == "completed" and conclusion == "success":
121
+ plan_meta = _download_plan_metadata(
122
+ repository=repository,
123
+ workflow_run_id=database_id,
124
+ request_id_value=request_id_value,
125
+ expected_commit=expected_commit,
126
+ expected_context_digest=expected_context_digest,
127
+ run=_artifact_runner(runner),
128
+ )
129
+ # Expose expired as read-only status without blocking the status query.
130
+ expires_at = plan_meta.get("expires_at")
131
+ if isinstance(expires_at, str):
132
+ try:
133
+ deadline = parse_plan_expiry(expires_at)
134
+ plan_meta["expired"] = datetime.now(UTC) >= deadline
135
+ except ValueError:
136
+ plan_meta["expired"] = True # Fail-closed on unparseable expiry.
137
+ projected["plan"] = plan_meta
138
+ elif (
139
+ status == "completed"
140
+ and conclusion == "success"
141
+ and (expected_plan_id is not None or expected_plan_digest is not None)
142
+ ):
143
+ if expected_plan_id is None or expected_plan_digest is None:
144
+ raise ValueError("apply status requires plan id and digest together")
145
+ projected["apply_receipt"] = _download_apply_receipt(
146
+ repository=repository,
147
+ workflow_run_id=database_id,
148
+ request_id_value=request_id_value,
149
+ expected_commit=expected_commit,
150
+ expected_context_digest=expected_context_digest,
151
+ expected_plan_id=expected_plan_id,
152
+ expected_plan_digest=expected_plan_digest,
153
+ run=_artifact_runner(runner),
154
+ )
155
+ return projected
156
+
157
+
158
+ __all__ = [
159
+ "CommandResult",
160
+ "DeploymentSelection",
161
+ "WorkflowDispatch",
162
+ "deployment_context_digest",
163
+ "dispatch_apply",
164
+ "dispatch_plan",
165
+ "enforce_plan_not_expired",
166
+ "parse_plan_expiry",
167
+ "request_binding_prefix",
168
+ "request_id",
169
+ "run_github_cli",
170
+ "workflow_status",
171
+ ]
@@ -0,0 +1,259 @@
1
+ """Protected apply receipt download and observation validation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import tempfile
8
+ from collections.abc import Mapping
9
+ from pathlib import Path
10
+
11
+ from fdai_deployment_cli.contracts import canonical_digest
12
+ from fdai_deployment_cli.github_artifact_io import (
13
+ _json_object,
14
+ _private_artifact_bytes,
15
+ _private_artifact_json,
16
+ )
17
+ from fdai_deployment_cli.github_provider_artifact import (
18
+ _validate_provider_schema_core_baseline,
19
+ _validate_provider_schema_evidence,
20
+ )
21
+ from fdai_deployment_cli.github_workflow_values import (
22
+ _COMMIT,
23
+ _DIGEST,
24
+ _OCI_DIGEST,
25
+ _PLAN_ID,
26
+ CommandRunner,
27
+ )
28
+
29
+
30
+ def _download_apply_receipt(
31
+ *,
32
+ repository: str,
33
+ workflow_run_id: int,
34
+ request_id_value: str,
35
+ expected_commit: str,
36
+ expected_context_digest: str,
37
+ expected_plan_id: str,
38
+ expected_plan_digest: str,
39
+ run: CommandRunner,
40
+ ) -> dict[str, object]:
41
+ """Download and validate one application receipt and its profile observation."""
42
+
43
+ if (
44
+ _PLAN_ID.fullmatch(expected_plan_id) is None
45
+ or _DIGEST.fullmatch(expected_plan_digest) is None
46
+ ):
47
+ raise ValueError("expected apply plan identity is invalid")
48
+ with tempfile.TemporaryDirectory(prefix="fdai-apply-status-") as raw_directory:
49
+ directory = Path(raw_directory)
50
+ result = run(
51
+ (
52
+ "run",
53
+ "download",
54
+ str(workflow_run_id),
55
+ "--repo",
56
+ repository,
57
+ "--name",
58
+ f"deployment-apply-receipt-{expected_plan_id}",
59
+ "--dir",
60
+ str(directory),
61
+ )
62
+ )
63
+ if result.returncode != 0:
64
+ raise ValueError("github_apply_receipt_unavailable")
65
+ receipt = _private_artifact_json(directory / "apply-receipt.json", "apply receipt")
66
+ provider_schema = request_id_value.startswith("apply-provider-")
67
+ cost_governance = request_id_value.startswith("apply-cost-")
68
+ if provider_schema:
69
+ provider_evidence_bytes = _private_artifact_bytes(
70
+ directory / "provider-schema-deployment-evidence.json",
71
+ "provider-schema deployment evidence",
72
+ )
73
+ provider_evidence = dict(
74
+ _json_object(
75
+ provider_evidence_bytes.decode("utf-8"),
76
+ "provider-schema deployment evidence",
77
+ )
78
+ )
79
+ provider_baseline_bytes = _private_artifact_bytes(
80
+ directory / "provider-schema-core-baseline.json",
81
+ "provider-schema Core baseline",
82
+ )
83
+ provider_baseline = dict(
84
+ _json_object(
85
+ provider_baseline_bytes.decode("utf-8"),
86
+ "provider-schema Core baseline",
87
+ )
88
+ )
89
+ observation_bytes = b""
90
+ observation: dict[str, object] = {}
91
+ else:
92
+ observation_path = directory / (
93
+ "cost-governance-job-image-readback.json"
94
+ if cost_governance
95
+ else "initial-inventory-receipt.json"
96
+ )
97
+ observation_label = (
98
+ "Cost Governance Job image readback"
99
+ if cost_governance
100
+ else "initial inventory execution receipt"
101
+ )
102
+ observation_bytes = _private_artifact_bytes(observation_path, observation_label)
103
+ observation = dict(_json_object(observation_bytes.decode("utf-8"), observation_label))
104
+ expected = {
105
+ "schema_version": "fdai.deployment-apply-receipt.v1",
106
+ "plan_id": expected_plan_id,
107
+ "plan_digest": expected_plan_digest,
108
+ "request_id": request_id_value,
109
+ "context_digest": expected_context_digest,
110
+ "source_commit": expected_commit,
111
+ "status": "applied",
112
+ "terraform_zero_change_verified": True,
113
+ "subscription_ready": False,
114
+ }
115
+ if provider_schema:
116
+ expected.update(
117
+ {
118
+ "terraform_zero_change_verified": True,
119
+ "provider_schema_runtime_image_profile": (
120
+ "cost-governance"
121
+ if request_id_value.startswith("apply-provider-cost-")
122
+ else "core-control-plane"
123
+ ),
124
+ }
125
+ )
126
+ elif cost_governance:
127
+ expected["cost_governance_job_images_verified"] = True
128
+ else:
129
+ expected.update(
130
+ {
131
+ "migration_stage_verified": True,
132
+ "runtime_health_verified": True,
133
+ "canary_verified": True,
134
+ }
135
+ )
136
+ receipt_digest = receipt.get("receipt_digest")
137
+ receipt_body = {key: item for key, item in receipt.items() if key != "receipt_digest"}
138
+ if any(receipt.get(key) != item for key, item in expected.items()):
139
+ raise ValueError("github_apply_receipt_context_mismatch")
140
+ if (
141
+ not isinstance(receipt_digest, str)
142
+ or _DIGEST.fullmatch(receipt_digest) is None
143
+ or canonical_digest(receipt_body) != receipt_digest
144
+ ):
145
+ raise ValueError("github_apply_receipt_digest_invalid")
146
+ if provider_schema:
147
+ runtime_revision = receipt.get("provider_schema_runtime_image_revision")
148
+ runtime_digest = receipt.get("provider_schema_runtime_image_digest")
149
+ if (
150
+ not isinstance(runtime_revision, str)
151
+ or _COMMIT.fullmatch(runtime_revision) is None
152
+ or not isinstance(runtime_digest, str)
153
+ or _OCI_DIGEST.fullmatch(runtime_digest) is None
154
+ or receipt.get("provider_schema_core_baseline_digest")
155
+ != hashlib.sha256(provider_baseline_bytes).hexdigest()
156
+ or receipt.get("provider_schema_deployment_evidence_digest")
157
+ != hashlib.sha256(provider_evidence_bytes).hexdigest()
158
+ ):
159
+ raise ValueError("github_provider_schema_receipt_invalid")
160
+ _validate_provider_schema_core_baseline(
161
+ provider_baseline,
162
+ expected_source_revision=runtime_revision,
163
+ expected_image_digest=runtime_digest,
164
+ )
165
+ provider_summary = _validate_provider_schema_evidence(
166
+ provider_evidence,
167
+ expected_application_source=expected_commit,
168
+ expected_runtime_revision=runtime_revision,
169
+ expected_plan_id=expected_plan_id,
170
+ )
171
+ return {
172
+ **expected,
173
+ "provider_schema_runtime_image_revision": runtime_revision,
174
+ "provider_schema_runtime_image_digest": runtime_digest,
175
+ "provider_schema_core_baseline_digest": hashlib.sha256(
176
+ provider_baseline_bytes
177
+ ).hexdigest(),
178
+ "provider_schema_deployment_evidence_digest": hashlib.sha256(
179
+ provider_evidence_bytes
180
+ ).hexdigest(),
181
+ "provider_schema": provider_summary,
182
+ }
183
+ observation_digest = hashlib.sha256(observation_bytes).hexdigest()
184
+ if cost_governance:
185
+ image_digest = receipt.get("cost_governance_image_digest")
186
+ if (
187
+ receipt.get("cost_governance_job_image_readback_digest") != observation_digest
188
+ or not isinstance(image_digest, str)
189
+ or _OCI_DIGEST.fullmatch(image_digest) is None
190
+ ):
191
+ raise ValueError("github_cost_governance_readback_invalid")
192
+ _validate_cost_governance_readback(observation, expected_digest=image_digest)
193
+ return {
194
+ **expected,
195
+ "cost_governance_image_digest": image_digest,
196
+ "cost_governance_job_image_readback_digest": observation_digest,
197
+ }
198
+ inventory_receipt_digest = observation.get("receipt_digest")
199
+ inventory_body = {key: item for key, item in observation.items() if key != "receipt_digest"}
200
+ if (
201
+ receipt.get("initial_inventory_execution_receipt_digest") != observation_digest
202
+ or observation.get("schema_version")
203
+ != "fdai.genesis-initial-inventory-execution-receipt.v1"
204
+ or observation.get("source_commit") != expected_commit
205
+ or observation.get("status") != "succeeded"
206
+ or observation.get("active_generation_verified") is not False
207
+ or observation.get("subscription_ready") is not False
208
+ or not isinstance(inventory_receipt_digest, str)
209
+ or _DIGEST.fullmatch(inventory_receipt_digest) is None
210
+ or hashlib.sha256(
211
+ json.dumps(inventory_body, sort_keys=True, separators=(",", ":")).encode()
212
+ ).hexdigest()
213
+ != inventory_receipt_digest
214
+ ):
215
+ raise ValueError("github_initial_inventory_receipt_invalid")
216
+ return {
217
+ "plan_id": expected_plan_id,
218
+ "plan_digest": expected_plan_digest,
219
+ "request_id": request_id_value,
220
+ "context_digest": expected_context_digest,
221
+ "source_commit": expected_commit,
222
+ "status": "applied",
223
+ "terraform_zero_change_verified": True,
224
+ "migration_stage_verified": True,
225
+ "runtime_health_verified": True,
226
+ "canary_verified": True,
227
+ "initial_inventory_execution_receipt_digest": observation_digest,
228
+ "subscription_ready": False,
229
+ }
230
+
231
+
232
+ def _validate_cost_governance_readback(
233
+ value: Mapping[str, object],
234
+ *,
235
+ expected_digest: str,
236
+ ) -> None:
237
+ """Reject incomplete or mismatched Cost Governance Job observations."""
238
+ if (
239
+ set(value) != {"schema_version", "image_digest", "jobs"}
240
+ or value.get("schema_version") != "fdai.cost-governance-job-image-readback.v1"
241
+ or value.get("image_digest") != expected_digest
242
+ ):
243
+ raise ValueError("github_cost_governance_readback_invalid")
244
+ jobs = value.get("jobs")
245
+ expected_containers = {
246
+ "analyzer": "cost-governance-analyzer",
247
+ "collector": "cost-governance-collector",
248
+ }
249
+ if not isinstance(jobs, dict) or set(jobs) != set(expected_containers):
250
+ raise ValueError("github_cost_governance_readback_invalid")
251
+ for role, container in expected_containers.items():
252
+ job = jobs[role]
253
+ if (
254
+ not isinstance(job, dict)
255
+ or set(job) != {"container", "image_digest"}
256
+ or job.get("container") != container
257
+ or job.get("image_digest") != expected_digest
258
+ ):
259
+ raise ValueError("github_cost_governance_readback_invalid")