|
36 | 36 | STATE_SCHEMA = "durable-workflow.component-release-recovery/v1" |
37 | 37 | CONTROL_REPOSITORY = "durable-workflow/.github" |
38 | 38 | PLAN_TAG_PREFIX = "release-plan/" |
| 39 | +CONTINUITY_TAG_PREFIX = "beta-continuity/" |
39 | 40 | FOUNDATION_TAG = "beta-candidate/beta-continuity-foundation" |
40 | 41 | FOUNDATION_COMMIT = "4995052410bd4301c5796ffba54e0b6d2f490ed1" |
41 | 42 | COMMIT_PATTERN = re.compile(r"^[0-9a-f]{40}$") |
@@ -1239,6 +1240,26 @@ def base_state(component: str, tag: str | None = None, plan: dict[str, Any] | No |
1239 | 1240 | } |
1240 | 1241 |
|
1241 | 1242 |
|
| 1243 | +def scheduled_continuity_pause(client: PublicClient, plan: dict[str, Any]) -> dict[str, str] | None: |
| 1244 | + accepted_tag = f"{CONTINUITY_TAG_PREFIX}{plan['plan']}/accepted" |
| 1245 | + accepted_commit = resolve_tag(client, CONTROL_REPOSITORY, accepted_tag) |
| 1246 | + if accepted_commit is None: |
| 1247 | + return None |
| 1248 | + accepted_plan = read_record(client, accepted_tag, accepted_commit, "release-plan.json") |
| 1249 | + validate_plan(accepted_plan) |
| 1250 | + if canonical_json(accepted_plan) != canonical_json(plan): |
| 1251 | + raise RecoveryError("continuity acceptance record names a different release plan", "continuity-gate") |
| 1252 | + resumed_tag = f"{CONTINUITY_TAG_PREFIX}{plan['plan']}/resumed" |
| 1253 | + resumed_commit = resolve_tag(client, CONTROL_REPOSITORY, resumed_tag) |
| 1254 | + if resumed_commit is not None: |
| 1255 | + resumed_plan = read_record(client, resumed_tag, resumed_commit, "release-plan.json") |
| 1256 | + validate_plan(resumed_plan) |
| 1257 | + if canonical_json(resumed_plan) != canonical_json(plan): |
| 1258 | + raise RecoveryError("continuity resume record names a different release plan", "continuity-gate") |
| 1259 | + return None |
| 1260 | + return {"accepted_tag": accepted_tag, "accepted_commit": accepted_commit, "resumed_tag": resumed_tag} |
| 1261 | + |
| 1262 | + |
1242 | 1263 | def resolution_failure_state( |
1243 | 1264 | component_name: str, |
1244 | 1265 | tag: str | None, |
@@ -1453,6 +1474,32 @@ def main() -> int: |
1453 | 1474 | args.plan_output.write_bytes(canonical_json(plan)) |
1454 | 1475 | if preparation is not None: |
1455 | 1476 | args.preparation_output.write_bytes(canonical_json(preparation)) |
| 1477 | + continuity_pause = scheduled_continuity_pause(client, plan) if args.plan_tag is None else None |
| 1478 | + if continuity_pause is not None: |
| 1479 | + paused = base_state(args.component, tag, plan) |
| 1480 | + paused.update( |
| 1481 | + { |
| 1482 | + "phase": "continuity-gate", |
| 1483 | + "outcome": "paused", |
| 1484 | + "plan_record_commit": record_commit, |
| 1485 | + "continuity": continuity_pause, |
| 1486 | + "resume_action": ( |
| 1487 | + f"Wait for {continuity_pause['resumed_tag']} or explicitly recover exact plan {tag}" |
| 1488 | + ), |
| 1489 | + } |
| 1490 | + ) |
| 1491 | + args.evidence.write_bytes(canonical_json(paused)) |
| 1492 | + write_output( |
| 1493 | + args.github_output, |
| 1494 | + { |
| 1495 | + "action": "none", |
| 1496 | + "plan": str(plan["plan"]), |
| 1497 | + "channel": str(plan["channel"]), |
| 1498 | + "plan_tag": tag, |
| 1499 | + "plan_record_commit": record_commit, |
| 1500 | + }, |
| 1501 | + ) |
| 1502 | + return 0 |
1456 | 1503 | state, outputs = resolve_component( |
1457 | 1504 | client, |
1458 | 1505 | args.component, |
|
0 commit comments