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.
- fdai_deployment_cli/__about__.py +3 -0
- fdai_deployment_cli/__init__.py +5 -0
- fdai_deployment_cli/__main__.py +5 -0
- fdai_deployment_cli/acr_publish.py +585 -0
- fdai_deployment_cli/aks_catalog_review.py +137 -0
- fdai_deployment_cli/aks_historical_reconciliation.py +732 -0
- fdai_deployment_cli/aks_job_environment.py +59 -0
- fdai_deployment_cli/aks_job_execution.py +794 -0
- fdai_deployment_cli/aks_kubernetes_defaults.py +66 -0
- fdai_deployment_cli/aks_preflight.py +422 -0
- fdai_deployment_cli/aks_readiness.py +220 -0
- fdai_deployment_cli/aks_service_update.py +180 -0
- fdai_deployment_cli/aks_workload_jobs.py +281 -0
- fdai_deployment_cli/application_state_adoption.py +347 -0
- fdai_deployment_cli/azure_actor.py +61 -0
- fdai_deployment_cli/azure_naming.py +252 -0
- fdai_deployment_cli/azure_retail_prices.py +173 -0
- fdai_deployment_cli/bootstrap_approval.py +99 -0
- fdai_deployment_cli/bootstrap_channel.py +321 -0
- fdai_deployment_cli/bootstrap_executor.py +311 -0
- fdai_deployment_cli/bootstrap_plan.py +237 -0
- fdai_deployment_cli/bootstrap_reconcile.py +402 -0
- fdai_deployment_cli/bootstrap_support.py +138 -0
- fdai_deployment_cli/bootstrap_worker.py +448 -0
- fdai_deployment_cli/bundle.py +352 -0
- fdai_deployment_cli/capacity.py +143 -0
- fdai_deployment_cli/catalog_review_profile.py +306 -0
- fdai_deployment_cli/cli.py +798 -0
- fdai_deployment_cli/cli_help.py +214 -0
- fdai_deployment_cli/cli_parser.py +791 -0
- fdai_deployment_cli/cli_plan.py +450 -0
- fdai_deployment_cli/compiler.py +99 -0
- fdai_deployment_cli/console_artifact.py +218 -0
- fdai_deployment_cli/console_config.py +214 -0
- fdai_deployment_cli/console_update.py +832 -0
- fdai_deployment_cli/contracts.py +307 -0
- fdai_deployment_cli/control_package.py +140 -0
- fdai_deployment_cli/deadline_transport.py +70 -0
- fdai_deployment_cli/deployment_cost.py +131 -0
- fdai_deployment_cli/deployment_deadline.py +28 -0
- fdai_deployment_cli/deployment_kit.py +790 -0
- fdai_deployment_cli/deployment_kit_cache.py +191 -0
- fdai_deployment_cli/deployment_progress.py +648 -0
- fdai_deployment_cli/development_authority_cli.py +56 -0
- fdai_deployment_cli/development_authority_profile.py +319 -0
- fdai_deployment_cli/doctor.py +136 -0
- fdai_deployment_cli/entitlement_preflight.py +68 -0
- fdai_deployment_cli/entitlement_transport.py +49 -0
- fdai_deployment_cli/entra_bootstrap.py +579 -0
- fdai_deployment_cli/entra_control_profile.py +214 -0
- fdai_deployment_cli/entra_graph.py +349 -0
- fdai_deployment_cli/entra_profiles.py +215 -0
- fdai_deployment_cli/entra_source.py +244 -0
- fdai_deployment_cli/execution_bundle.py +478 -0
- fdai_deployment_cli/execution_copies.py +63 -0
- fdai_deployment_cli/foundation_adoption.py +438 -0
- fdai_deployment_cli/foundation_adoption_evidence.py +245 -0
- fdai_deployment_cli/foundation_adoption_host.py +157 -0
- fdai_deployment_cli/foundation_adoption_transport.py +51 -0
- fdai_deployment_cli/foundation_failure.py +72 -0
- fdai_deployment_cli/foundation_image.py +168 -0
- fdai_deployment_cli/foundation_input.py +235 -0
- fdai_deployment_cli/foundation_output.py +154 -0
- fdai_deployment_cli/foundation_plan.py +381 -0
- fdai_deployment_cli/foundation_process.py +64 -0
- fdai_deployment_cli/foundation_progress.py +231 -0
- fdai_deployment_cli/github_actions.py +171 -0
- fdai_deployment_cli/github_apply_artifact.py +259 -0
- fdai_deployment_cli/github_artifact_io.py +48 -0
- fdai_deployment_cli/github_dispatch.py +248 -0
- fdai_deployment_cli/github_plan_artifact.py +181 -0
- fdai_deployment_cli/github_provider_artifact.py +170 -0
- fdai_deployment_cli/github_transport.py +46 -0
- fdai_deployment_cli/github_workflow_values.py +312 -0
- fdai_deployment_cli/hub_image_deploy.py +354 -0
- fdai_deployment_cli/hub_image_stage.py +162 -0
- fdai_deployment_cli/hub_image_trust.py +281 -0
- fdai_deployment_cli/hub_ownership.py +161 -0
- fdai_deployment_cli/hub_workload_handoff.py +264 -0
- fdai_deployment_cli/hub_workload_handoff_inputs.py +227 -0
- fdai_deployment_cli/identity_profile.py +227 -0
- fdai_deployment_cli/image_targets.py +197 -0
- fdai_deployment_cli/installation_scope.py +280 -0
- fdai_deployment_cli/license.py +311 -0
- fdai_deployment_cli/license_issue.py +239 -0
- fdai_deployment_cli/lifecycle_authorization.py +252 -0
- fdai_deployment_cli/lifecycle_commands.py +173 -0
- fdai_deployment_cli/lifecycle_configuration.py +722 -0
- fdai_deployment_cli/lifecycle_configuration_artifact.py +45 -0
- fdai_deployment_cli/lifecycle_configuration_cli.py +324 -0
- fdai_deployment_cli/lifecycle_configuration_document.py +74 -0
- fdai_deployment_cli/lifecycle_configuration_entities.py +146 -0
- fdai_deployment_cli/lifecycle_configuration_package.py +492 -0
- fdai_deployment_cli/lifecycle_configuration_publication.py +277 -0
- fdai_deployment_cli/lifecycle_configuration_publication_cli.py +191 -0
- fdai_deployment_cli/lifecycle_configuration_sealing.py +127 -0
- fdai_deployment_cli/lifecycle_enrollment.py +45 -0
- fdai_deployment_cli/lifecycle_infrastructure_agent.py +174 -0
- fdai_deployment_cli/lifecycle_installation_binding.py +65 -0
- fdai_deployment_cli/lifecycle_native.py +247 -0
- fdai_deployment_cli/lifecycle_native_reconciliation.py +568 -0
- fdai_deployment_cli/lifecycle_native_transition.py +339 -0
- fdai_deployment_cli/lifecycle_ownership_exclusion.py +78 -0
- fdai_deployment_cli/lifecycle_plan.py +800 -0
- fdai_deployment_cli/lifecycle_promotion.py +185 -0
- fdai_deployment_cli/lifecycle_release.py +308 -0
- fdai_deployment_cli/lifecycle_self_upgrade.py +202 -0
- fdai_deployment_cli/lifecycle_upgrade_bundle.py +176 -0
- fdai_deployment_cli/notification_recipients_input.py +222 -0
- fdai_deployment_cli/observer_artifact.py +106 -0
- fdai_deployment_cli/oci_archive.py +353 -0
- fdai_deployment_cli/offline_kit.py +797 -0
- fdai_deployment_cli/offline_prepare.py +135 -0
- fdai_deployment_cli/operational_evidence_verifier_input.py +575 -0
- fdai_deployment_cli/plan_input.py +101 -0
- fdai_deployment_cli/private_output.py +192 -0
- fdai_deployment_cli/profile.py +70 -0
- fdai_deployment_cli/progress.py +183 -0
- fdai_deployment_cli/readiness.py +286 -0
- fdai_deployment_cli/run_command_receiver.py +418 -0
- fdai_deployment_cli/runtime_build.py +386 -0
- fdai_deployment_cli/runtime_profile.py +287 -0
- fdai_deployment_cli/runtime_release.py +745 -0
- fdai_deployment_cli/runtime_stage.py +82 -0
- fdai_deployment_cli/runtime_support_installation.py +215 -0
- fdai_deployment_cli/simulation.py +80 -0
- fdai_deployment_cli/source_application_inputs.py +397 -0
- fdai_deployment_cli/source_azure.py +586 -0
- fdai_deployment_cli/source_change_cli.py +107 -0
- fdai_deployment_cli/source_change_parser.py +75 -0
- fdai_deployment_cli/source_change_plan.py +181 -0
- fdai_deployment_cli/source_changed_update.py +196 -0
- fdai_deployment_cli/source_deploy.py +170 -0
- fdai_deployment_cli/source_foundation.py +174 -0
- fdai_deployment_cli/source_host_tools.py +163 -0
- fdai_deployment_cli/source_image_stage.py +400 -0
- fdai_deployment_cli/source_input.py +173 -0
- fdai_deployment_cli/source_projection.py +164 -0
- fdai_deployment_cli/source_receiver.py +76 -0
- fdai_deployment_cli/source_recovery.py +264 -0
- fdai_deployment_cli/source_runtime.py +81 -0
- fdai_deployment_cli/source_runtime_support.py +368 -0
- fdai_deployment_cli/source_service_update.py +726 -0
- fdai_deployment_cli/source_service_update_cli.py +47 -0
- fdai_deployment_cli/source_snapshot.py +178 -0
- fdai_deployment_cli/source_teardown.py +574 -0
- fdai_deployment_cli/source_terraform.py +85 -0
- fdai_deployment_cli/source_transport.py +399 -0
- fdai_deployment_cli/standalone_aks_inventory.py +558 -0
- fdai_deployment_cli/standalone_aks_job_execution.py +769 -0
- fdai_deployment_cli/standalone_aks_nodepool_guard.py +141 -0
- fdai_deployment_cli/standalone_application.py +977 -0
- fdai_deployment_cli/standalone_application_completion.py +151 -0
- fdai_deployment_cli/standalone_application_input_recovery.py +107 -0
- fdai_deployment_cli/standalone_application_recovery.py +116 -0
- fdai_deployment_cli/standalone_approval_input.py +24 -0
- fdai_deployment_cli/standalone_catalog_checkpoint.py +122 -0
- fdai_deployment_cli/standalone_catalog_review.py +251 -0
- fdai_deployment_cli/standalone_checkpoint_failure.py +127 -0
- fdai_deployment_cli/standalone_console_publish.py +174 -0
- fdai_deployment_cli/standalone_database_principals.py +41 -0
- fdai_deployment_cli/standalone_deploy.py +638 -0
- fdai_deployment_cli/standalone_development_authority.py +166 -0
- fdai_deployment_cli/standalone_foundation_adoption.py +87 -0
- fdai_deployment_cli/standalone_foundation_transition.py +767 -0
- fdai_deployment_cli/standalone_host.py +4608 -0
- fdai_deployment_cli/standalone_host_rebind.py +226 -0
- fdai_deployment_cli/standalone_host_residual.py +104 -0
- fdai_deployment_cli/standalone_host_state.py +145 -0
- fdai_deployment_cli/standalone_host_values.py +563 -0
- fdai_deployment_cli/standalone_kubernetes_tools.py +100 -0
- fdai_deployment_cli/standalone_license_installation.py +177 -0
- fdai_deployment_cli/standalone_management_egress.py +83 -0
- fdai_deployment_cli/standalone_migration_evidence.py +38 -0
- fdai_deployment_cli/standalone_operational_evidence.py +356 -0
- fdai_deployment_cli/standalone_planned_outputs.py +147 -0
- fdai_deployment_cli/standalone_private_access_probe.py +100 -0
- fdai_deployment_cli/standalone_product_profile.py +110 -0
- fdai_deployment_cli/standalone_remote_prepare.py +442 -0
- fdai_deployment_cli/standalone_residual_apply.py +540 -0
- fdai_deployment_cli/standalone_residual_successor.py +431 -0
- fdai_deployment_cli/standalone_review.py +266 -0
- fdai_deployment_cli/standalone_service_update_finalization.py +116 -0
- fdai_deployment_cli/standalone_service_update_recovery.py +277 -0
- fdai_deployment_cli/standalone_stage_targets.py +325 -0
- fdai_deployment_cli/standalone_status.py +67 -0
- fdai_deployment_cli/standalone_terraform_environment.py +237 -0
- fdai_deployment_cli/standalone_transfer_cleanup.py +237 -0
- fdai_deployment_cli/standalone_trial_activation.py +193 -0
- fdai_deployment_cli/state.py +434 -0
- fdai_deployment_cli/state_handoff.py +253 -0
- fdai_deployment_cli/status_projection.py +97 -0
- fdai_deployment_cli/support_install.py +257 -0
- fdai_deployment_cli/target.py +46 -0
- fdai_deployment_cli/trust/capabilities.json +62 -0
- fdai_deployment_cli/trust/deployment-bundle-root.pub +3 -0
- fdai_deployment_cli/trust/deployment-release-root.pub +3 -0
- fdai_deployment_cli/trust/upstream-signing-key.pub +3 -0
- fdai_deployment_cli/trust_roots.py +50 -0
- fdai_deployment_cli-0.1.2.dist-info/METADATA +112 -0
- fdai_deployment_cli-0.1.2.dist-info/RECORD +203 -0
- fdai_deployment_cli-0.1.2.dist-info/WHEEL +4 -0
- 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")
|