worktree-controller 1.30.4__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.
- worktree_controller/__init__.py +3 -0
- worktree_controller/activation_layout.py +483 -0
- worktree_controller/branch_retirement.py +1865 -0
- worktree_controller/canonical_refresh.py +150 -0
- worktree_controller/canonical_transition.py +324 -0
- worktree_controller/cleanup_attempts.py +442 -0
- worktree_controller/cleanup_evidence.py +75 -0
- worktree_controller/cleanup_recovery.py +912 -0
- worktree_controller/cli.py +1907 -0
- worktree_controller/controller.py +1974 -0
- worktree_controller/convergence.py +893 -0
- worktree_controller/delivered_closeout.py +928 -0
- worktree_controller/disposable.py +677 -0
- worktree_controller/environment.py +1038 -0
- worktree_controller/frozen_recovery.py +291 -0
- worktree_controller/git_backend.py +682 -0
- worktree_controller/governance.py +365 -0
- worktree_controller/hardlinks.py +294 -0
- worktree_controller/integration_cleanup.py +92 -0
- worktree_controller/integration_conflict.py +117 -0
- worktree_controller/integration_retry.py +526 -0
- worktree_controller/keyed_document_scope.py +162 -0
- worktree_controller/lifecycle.py +8992 -0
- worktree_controller/lifecycle_history.py +358 -0
- worktree_controller/lifecycle_observation.py +234 -0
- worktree_controller/metadata_codec.py +1694 -0
- worktree_controller/metadata_relocation.py +131 -0
- worktree_controller/models.py +546 -0
- worktree_controller/mount_diagnostics.py +324 -0
- worktree_controller/multiple_cleanup_recovery.py +730 -0
- worktree_controller/namespace_migration.py +1412 -0
- worktree_controller/native_session_recovery.py +171 -0
- worktree_controller/owner_actions.py +34 -0
- worktree_controller/policy.py +333 -0
- worktree_controller/preservation.py +983 -0
- worktree_controller/preserved_cleanup.py +849 -0
- worktree_controller/publication_execution.py +361 -0
- worktree_controller/reconciler.py +430 -0
- worktree_controller/recovery_guidance.py +205 -0
- worktree_controller/registration_backfill.py +166 -0
- worktree_controller/registration_prune.py +391 -0
- worktree_controller/rendering.py +179 -0
- worktree_controller/resource_admission.py +1736 -0
- worktree_controller/retirement_adapters.py +130 -0
- worktree_controller/retirement_execution.py +234 -0
- worktree_controller/semantics.py +459 -0
- worktree_controller/session.py +505 -0
- worktree_controller/session_admission.py +265 -0
- worktree_controller/source_cleanup.py +515 -0
- worktree_controller/source_preservation.py +692 -0
- worktree_controller/source_recovery.py +592 -0
- worktree_controller/store.py +1228 -0
- worktree_controller/tool_cli_contract.json +1791 -0
- worktree_controller/transaction_scope.py +876 -0
- worktree_controller/writer_lease.py +662 -0
- worktree_controller/writer_session.py +887 -0
- worktree_controller-1.30.4.dist-info/METADATA +3149 -0
- worktree_controller-1.30.4.dist-info/RECORD +61 -0
- worktree_controller-1.30.4.dist-info/WHEEL +4 -0
- worktree_controller-1.30.4.dist-info/entry_points.txt +5 -0
- worktree_controller-1.30.4.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Compatibility adapters between consumer wire formats and the neutral schema.
|
|
2
|
+
|
|
3
|
+
Core governance speaks `retirement_execution` only. These adapters translate the
|
|
4
|
+
released 1.5.0/1.6.0 consumer vocabulary so existing integrations keep working
|
|
5
|
+
while nothing in the governance path depends on one consumer product.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
from collections.abc import Mapping
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from . import retirement_execution as execution
|
|
16
|
+
from .models import ControllerError
|
|
17
|
+
|
|
18
|
+
GIT_FINALIZER_CONTRACT = "git-finalizer-summary/v1"
|
|
19
|
+
|
|
20
|
+
_SUMMARY_OUTCOMES = {
|
|
21
|
+
"LOCAL_BRANCH_RETIRED_VERIFIED": (
|
|
22
|
+
execution.KIND_CAS_DELETE,
|
|
23
|
+
execution.REF_REMOVED_AT_EXPECTED_OID,
|
|
24
|
+
),
|
|
25
|
+
"REMOTE_BRANCH_RETIRED_VERIFIED": (
|
|
26
|
+
execution.KIND_CAS_DELETE,
|
|
27
|
+
execution.REF_REMOVED_AT_EXPECTED_OID,
|
|
28
|
+
),
|
|
29
|
+
"ALREADY_ABSENT_VERIFIED": (execution.KIND_ATTEST_ABSENT, execution.REF_ALREADY_ABSENT),
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _ref(operation: str, result: Mapping[str, Any]) -> str | None:
|
|
34
|
+
branch = result.get("branch")
|
|
35
|
+
if not isinstance(branch, str) or not branch:
|
|
36
|
+
return None
|
|
37
|
+
if operation == "local":
|
|
38
|
+
return f"refs/heads/{branch}"
|
|
39
|
+
remote = result.get("remote")
|
|
40
|
+
return f"refs/remotes/{remote}/{branch}" if isinstance(remote, str) and remote else None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def load_summary(path: Path) -> tuple[bytes, dict[str, Any]]:
|
|
44
|
+
try:
|
|
45
|
+
raw = path.read_bytes()
|
|
46
|
+
except OSError as exc:
|
|
47
|
+
raise ControllerError("INVALID_FINALIZER_SUMMARY", f"cannot read summary: {exc}") from exc
|
|
48
|
+
import json
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
summary = json.loads(raw)
|
|
52
|
+
except (UnicodeError, json.JSONDecodeError) as exc:
|
|
53
|
+
raise ControllerError("INVALID_FINALIZER_SUMMARY", f"cannot read summary: {exc}") from exc
|
|
54
|
+
if not isinstance(summary, dict):
|
|
55
|
+
raise ControllerError("INVALID_FINALIZER_SUMMARY", "summary must be a JSON object")
|
|
56
|
+
return raw, summary
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def from_summary(summary: Mapping[str, Any]) -> dict[str, Any]:
|
|
60
|
+
"""Translate one released consumer summary into a neutral execution result.
|
|
61
|
+
|
|
62
|
+
Unrecognised shapes fail with the released error code so 1.5.0/1.6.0 callers
|
|
63
|
+
keep their existing failure contract.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
status = summary.get("status")
|
|
67
|
+
mode = summary.get("mode")
|
|
68
|
+
result = summary.get("mode_result")
|
|
69
|
+
if status != "success" or not isinstance(mode, str) or not isinstance(result, Mapping):
|
|
70
|
+
raise ControllerError(
|
|
71
|
+
"FINALIZER_RESULT_MISMATCH", "Finalizer summary does not prove this plan"
|
|
72
|
+
)
|
|
73
|
+
operation = {"retire_local_branch": "local", "retire_remote_branch": "remote"}.get(mode)
|
|
74
|
+
if operation is None:
|
|
75
|
+
raise ControllerError(
|
|
76
|
+
"FINALIZER_RESULT_MISMATCH", "Finalizer summary does not prove this plan"
|
|
77
|
+
)
|
|
78
|
+
reported = result.get("result")
|
|
79
|
+
if reported not in _SUMMARY_OUTCOMES:
|
|
80
|
+
raise ControllerError(
|
|
81
|
+
"FINALIZER_RESULT_MISMATCH", "Finalizer summary does not prove this plan"
|
|
82
|
+
)
|
|
83
|
+
kind, outcome = _SUMMARY_OUTCOMES[reported]
|
|
84
|
+
executor_version = summary.get("finalizer_version")
|
|
85
|
+
return {
|
|
86
|
+
"schema_version": execution.RESULT_SCHEMA_VERSION,
|
|
87
|
+
"contract": execution.RESULT_CONTRACT,
|
|
88
|
+
"plan_id": result.get("retirement_plan_id"),
|
|
89
|
+
"operation": operation,
|
|
90
|
+
"kind": kind,
|
|
91
|
+
"outcome": outcome,
|
|
92
|
+
"ref": _ref(operation, result),
|
|
93
|
+
"expected_oid": result.get("expected_oid"),
|
|
94
|
+
"integrated_ref": result.get("integrated_ref"),
|
|
95
|
+
"expected_integrated_oid": result.get("integrated_into_oid"),
|
|
96
|
+
"performed_at": summary.get("finished_at") or summary.get("started_at"),
|
|
97
|
+
"executor": {
|
|
98
|
+
"name": GIT_FINALIZER_CONTRACT,
|
|
99
|
+
"version": executor_version if isinstance(executor_version, str) else "unknown",
|
|
100
|
+
},
|
|
101
|
+
"evidence": {"remote": result.get("remote")},
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def arguments(plan: Mapping[str, Any], operation: str, *, repository_root: Path) -> list[str]:
|
|
106
|
+
"""The released consumer invocation for one authorized retirement operation."""
|
|
107
|
+
|
|
108
|
+
expected_flag = "--expected-local-oid" if operation == "local" else "--expected-remote-oid"
|
|
109
|
+
return [
|
|
110
|
+
"git-finalize",
|
|
111
|
+
f"--retire-{operation}-branch",
|
|
112
|
+
str(plan["branch"]),
|
|
113
|
+
"--remote",
|
|
114
|
+
str(plan["remote"]),
|
|
115
|
+
"--integrated-into",
|
|
116
|
+
str(plan["integrated_into"]),
|
|
117
|
+
expected_flag,
|
|
118
|
+
str(plan["expected_oid"]),
|
|
119
|
+
"--expected-integrated-oid",
|
|
120
|
+
str(plan["expected_integrated_oid"]),
|
|
121
|
+
"--retirement-plan-id",
|
|
122
|
+
str(plan["plan_id"]),
|
|
123
|
+
"--repo",
|
|
124
|
+
str(repository_root),
|
|
125
|
+
"--summary",
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def summary_sha256(raw: bytes) -> str:
|
|
130
|
+
return hashlib.sha256(raw).hexdigest()
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""Provider-neutral vocabulary for authorized retirement execution.
|
|
2
|
+
|
|
3
|
+
The Controller owns the decision; a consumer owns the ref mutation. These are the
|
|
4
|
+
two documents that cross that boundary: an *authorization* the Controller issues
|
|
5
|
+
and a *result* the consumer reports back. Neither one names a consumer product,
|
|
6
|
+
and neither one is trusted for what it claims about Git state — the Controller
|
|
7
|
+
re-observes that itself.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from collections.abc import Mapping
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from .models import ControllerError
|
|
16
|
+
|
|
17
|
+
AUTHORIZATION_SCHEMA_VERSION = 1
|
|
18
|
+
AUTHORIZATION_CONTRACT = "branch-retirement-execution/v1"
|
|
19
|
+
|
|
20
|
+
RESULT_SCHEMA_VERSION = 1
|
|
21
|
+
RESULT_CONTRACT = "retirement-execution-result/v1"
|
|
22
|
+
|
|
23
|
+
#: The plan publishes this recipe so a consumer knows what it may be granted to
|
|
24
|
+
#: compare-and-delete without importing any Controller helper.
|
|
25
|
+
RECIPE_SCHEMA_VERSION = 1
|
|
26
|
+
RECIPE_CONTRACT = "retirement-execution-recipe/v1"
|
|
27
|
+
|
|
28
|
+
KIND_CAS_DELETE = "CAS_DELETE"
|
|
29
|
+
KIND_ATTEST_ABSENT = "ATTEST_ABSENT"
|
|
30
|
+
EXECUTION_KINDS = frozenset({KIND_CAS_DELETE, KIND_ATTEST_ABSENT})
|
|
31
|
+
|
|
32
|
+
REF_REMOVED_AT_EXPECTED_OID = "REF_REMOVED_AT_EXPECTED_OID"
|
|
33
|
+
REF_ALREADY_ABSENT = "REF_ALREADY_ABSENT"
|
|
34
|
+
REF_STILL_PRESENT = "REF_STILL_PRESENT"
|
|
35
|
+
EXECUTION_FAILED = "EXECUTION_FAILED"
|
|
36
|
+
OUTCOMES = frozenset(
|
|
37
|
+
{REF_REMOVED_AT_EXPECTED_OID, REF_ALREADY_ABSENT, REF_STILL_PRESENT, EXECUTION_FAILED}
|
|
38
|
+
)
|
|
39
|
+
SUCCESS_OUTCOMES = frozenset({REF_REMOVED_AT_EXPECTED_OID, REF_ALREADY_ABSENT})
|
|
40
|
+
|
|
41
|
+
#: The authorization identity is content-addressed over exactly these fields, in
|
|
42
|
+
#: this order. ``token`` is deliberately excluded so the secret capability cannot
|
|
43
|
+
#: change the authorization's own address.
|
|
44
|
+
AUTHORIZATION_IDENTITY_FIELDS = (
|
|
45
|
+
"schema_version",
|
|
46
|
+
"contract",
|
|
47
|
+
"request_id",
|
|
48
|
+
"plan_id",
|
|
49
|
+
"operation",
|
|
50
|
+
"kind",
|
|
51
|
+
"repository_id",
|
|
52
|
+
"allocation_id",
|
|
53
|
+
"branch",
|
|
54
|
+
"ref",
|
|
55
|
+
"expected_oid",
|
|
56
|
+
"integrated_ref",
|
|
57
|
+
"expected_integrated_oid",
|
|
58
|
+
"tracking_ref",
|
|
59
|
+
"observed",
|
|
60
|
+
"controller_state_digest",
|
|
61
|
+
"holder_sha256",
|
|
62
|
+
"issued_at",
|
|
63
|
+
"expires_at",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
#: Every result document carries these; they are the values the Controller can
|
|
67
|
+
#: check against its own plan and against Git afterwards.
|
|
68
|
+
RESULT_FIELDS = frozenset(
|
|
69
|
+
{
|
|
70
|
+
"schema_version",
|
|
71
|
+
"contract",
|
|
72
|
+
"plan_id",
|
|
73
|
+
"operation",
|
|
74
|
+
"kind",
|
|
75
|
+
"outcome",
|
|
76
|
+
"ref",
|
|
77
|
+
"expected_oid",
|
|
78
|
+
"expected_integrated_oid",
|
|
79
|
+
"executor",
|
|
80
|
+
}
|
|
81
|
+
)
|
|
82
|
+
#: Optional because a released consumer summary predates them; when present they are
|
|
83
|
+
#: validated, and the Controller never infers a missing one.
|
|
84
|
+
RESULT_OPTIONAL_FIELDS = frozenset({"integrated_ref", "performed_at", "evidence"})
|
|
85
|
+
|
|
86
|
+
RECIPE_FIELDS = frozenset(
|
|
87
|
+
{"contract", "schema_version", "grant", "settle", "result_contract", "operations"}
|
|
88
|
+
)
|
|
89
|
+
RECIPE_OPERATION_FIELDS = frozenset(
|
|
90
|
+
{
|
|
91
|
+
"ref",
|
|
92
|
+
"mutation",
|
|
93
|
+
"expected_oid",
|
|
94
|
+
"integrated_ref",
|
|
95
|
+
"expected_integrated_oid",
|
|
96
|
+
"tracking_ref",
|
|
97
|
+
"kind",
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
OBSERVATION_FIELDS = frozenset(
|
|
102
|
+
{"refs", "checked_out_paths", "upstream_ref", "upstream_target", "classification", "blockers"}
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
_HEX = frozenset("0123456789abcdef")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def identity(document: Mapping[str, Any]) -> dict[str, Any]:
|
|
109
|
+
"""The authorization fields that must match at settle time."""
|
|
110
|
+
|
|
111
|
+
return {field: document.get(field) for field in AUTHORIZATION_IDENTITY_FIELDS}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _text(value: object, field: str) -> str:
|
|
115
|
+
if not isinstance(value, str) or not value:
|
|
116
|
+
raise ControllerError("INVALID_EXECUTION_RESULT", f"{field} must be a non-empty string")
|
|
117
|
+
return value
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _object_id(value: object, field: str) -> str:
|
|
121
|
+
"""Accept either Git object ID length; the repository decides which one it uses."""
|
|
122
|
+
|
|
123
|
+
text = _text(value, field)
|
|
124
|
+
if len(text) not in {40, 64} or not _HEX.issuperset(text):
|
|
125
|
+
raise ControllerError("INVALID_EXECUTION_RESULT", f"{field} must be a Git object id")
|
|
126
|
+
return text
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _digest(value: object, field: str) -> str:
|
|
130
|
+
text = _text(value, field)
|
|
131
|
+
if len(text) != 64 or not _HEX.issuperset(text):
|
|
132
|
+
raise ControllerError("INVALID_EXECUTION_RESULT", f"{field} must be a sha256 hex digest")
|
|
133
|
+
return text
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def normalize_result(document: Mapping[str, Any]) -> dict[str, Any]:
|
|
137
|
+
"""Validate one execution result document and return it with checked types.
|
|
138
|
+
|
|
139
|
+
A well-formed document proves nothing about the world: the caller must still
|
|
140
|
+
observe the ref. This only guarantees every field the observation is compared
|
|
141
|
+
against is present and shaped.
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
unknown = sorted(set(document) - RESULT_FIELDS - RESULT_OPTIONAL_FIELDS)
|
|
145
|
+
if unknown:
|
|
146
|
+
raise ControllerError("INVALID_EXECUTION_RESULT", f"unknown result fields: {unknown}")
|
|
147
|
+
if not RESULT_FIELDS.issubset(document):
|
|
148
|
+
raise ControllerError(
|
|
149
|
+
"INVALID_EXECUTION_RESULT",
|
|
150
|
+
f"execution result is missing {sorted(RESULT_FIELDS - set(document))}",
|
|
151
|
+
)
|
|
152
|
+
if document.get("schema_version") != RESULT_SCHEMA_VERSION:
|
|
153
|
+
raise ControllerError(
|
|
154
|
+
"INVALID_EXECUTION_RESULT",
|
|
155
|
+
f"unsupported result schema {document.get('schema_version')!r}",
|
|
156
|
+
)
|
|
157
|
+
if document.get("contract") != RESULT_CONTRACT:
|
|
158
|
+
raise ControllerError(
|
|
159
|
+
"INVALID_EXECUTION_RESULT", f"unknown result contract {document.get('contract')!r}"
|
|
160
|
+
)
|
|
161
|
+
operation = _text(document.get("operation"), "operation")
|
|
162
|
+
if operation not in {"local", "remote"}:
|
|
163
|
+
raise ControllerError("INVALID_EXECUTION_RESULT", "operation must be local or remote")
|
|
164
|
+
kind = _text(document.get("kind"), "kind")
|
|
165
|
+
if kind not in EXECUTION_KINDS:
|
|
166
|
+
raise ControllerError("INVALID_EXECUTION_RESULT", f"unknown execution kind {kind!r}")
|
|
167
|
+
outcome = _text(document.get("outcome"), "outcome")
|
|
168
|
+
if outcome not in OUTCOMES:
|
|
169
|
+
raise ControllerError("INVALID_EXECUTION_RESULT", f"unknown outcome {outcome!r}")
|
|
170
|
+
executor = document.get("executor")
|
|
171
|
+
if not isinstance(executor, Mapping):
|
|
172
|
+
raise ControllerError("INVALID_EXECUTION_RESULT", "executor must be an object")
|
|
173
|
+
evidence = document.get("evidence")
|
|
174
|
+
if evidence is not None and not isinstance(evidence, Mapping):
|
|
175
|
+
raise ControllerError("INVALID_EXECUTION_RESULT", "evidence must be an object or null")
|
|
176
|
+
return {
|
|
177
|
+
"schema_version": RESULT_SCHEMA_VERSION,
|
|
178
|
+
"contract": RESULT_CONTRACT,
|
|
179
|
+
"plan_id": _digest(document.get("plan_id"), "plan_id"),
|
|
180
|
+
"operation": operation,
|
|
181
|
+
"kind": kind,
|
|
182
|
+
"outcome": outcome,
|
|
183
|
+
"ref": _text(document.get("ref"), "ref"),
|
|
184
|
+
"expected_oid": _object_id(document.get("expected_oid"), "expected_oid"),
|
|
185
|
+
"integrated_ref": (
|
|
186
|
+
_text(document["integrated_ref"], "integrated_ref")
|
|
187
|
+
if document.get("integrated_ref") is not None
|
|
188
|
+
else None
|
|
189
|
+
),
|
|
190
|
+
"expected_integrated_oid": _object_id(
|
|
191
|
+
document.get("expected_integrated_oid"), "expected_integrated_oid"
|
|
192
|
+
),
|
|
193
|
+
"performed_at": (
|
|
194
|
+
_text(document["performed_at"], "performed_at")
|
|
195
|
+
if document.get("performed_at") is not None
|
|
196
|
+
else None
|
|
197
|
+
),
|
|
198
|
+
"executor": {
|
|
199
|
+
"name": _text(executor.get("name"), "executor.name"),
|
|
200
|
+
"version": _text(executor.get("version"), "executor.version"),
|
|
201
|
+
},
|
|
202
|
+
"evidence": dict(evidence or {}),
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def normalize_observation(value: object) -> dict[str, Any]:
|
|
207
|
+
"""Return an observation vector with a stable shape, or fail closed."""
|
|
208
|
+
|
|
209
|
+
if not isinstance(value, Mapping) or not OBSERVATION_FIELDS.issubset(value):
|
|
210
|
+
raise ControllerError("INVALID_METADATA", "retirement observation vector is incomplete")
|
|
211
|
+
refs = value.get("refs")
|
|
212
|
+
if not isinstance(refs, Mapping):
|
|
213
|
+
raise ControllerError("INVALID_METADATA", "retirement observation refs must be an object")
|
|
214
|
+
paths = value.get("checked_out_paths")
|
|
215
|
+
if not isinstance(paths, list) or not all(isinstance(item, str) for item in paths):
|
|
216
|
+
raise ControllerError("INVALID_METADATA", "retirement observation paths must be a list")
|
|
217
|
+
blockers = value.get("blockers")
|
|
218
|
+
if not isinstance(blockers, list) or not all(isinstance(item, str) for item in blockers):
|
|
219
|
+
raise ControllerError("INVALID_METADATA", "retirement observation blockers must be a list")
|
|
220
|
+
return {
|
|
221
|
+
"refs": {
|
|
222
|
+
key: (oid if oid is None or isinstance(oid, str) else None)
|
|
223
|
+
for key, oid in sorted(refs.items())
|
|
224
|
+
},
|
|
225
|
+
"checked_out_paths": sorted(paths),
|
|
226
|
+
"upstream_ref": value.get("upstream_ref"),
|
|
227
|
+
"upstream_target": value.get("upstream_target"),
|
|
228
|
+
"classification": value.get("classification"),
|
|
229
|
+
"blockers": sorted(blockers),
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def succeeded(result: Mapping[str, Any]) -> bool:
|
|
234
|
+
return result.get("outcome") in SUCCESS_OUTCOMES
|