Skip to content

Commit f032f2a

Browse files
Prove beta continuity with the workspace unavailable
1 parent 1ad2bc7 commit f032f2a

2 files changed

Lines changed: 81 additions & 0 deletions

File tree

scripts/ci/component-release-recovery.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
STATE_SCHEMA = "durable-workflow.component-release-recovery/v1"
3737
CONTROL_REPOSITORY = "durable-workflow/.github"
3838
PLAN_TAG_PREFIX = "release-plan/"
39+
CONTINUITY_TAG_PREFIX = "beta-continuity/"
3940
FOUNDATION_TAG = "beta-candidate/beta-continuity-foundation"
4041
FOUNDATION_COMMIT = "4995052410bd4301c5796ffba54e0b6d2f490ed1"
4142
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
12391240
}
12401241

12411242

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+
12421263
def resolution_failure_state(
12431264
component_name: str,
12441265
tag: str | None,
@@ -1453,6 +1474,32 @@ def main() -> int:
14531474
args.plan_output.write_bytes(canonical_json(plan))
14541475
if preparation is not None:
14551476
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
14561503
state, outputs = resolve_component(
14571504
client,
14581505
args.component,

scripts/ci/test-component-release-recovery.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,40 @@ def load_recovery_for_retry_tests():
6868
return loader()
6969

7070

71+
class ContinuityGateTest(unittest.TestCase):
72+
@classmethod
73+
def setUpClass(cls) -> None:
74+
cls.recovery = load_recovery_for_retry_tests()
75+
76+
def test_scheduled_recovery_pauses_until_remote_resume(self) -> None:
77+
plan = {"plan": "workspace-unavailable-test"}
78+
with (
79+
mock.patch.object(
80+
self.recovery,
81+
"resolve_tag",
82+
side_effect=["a" * 40, None],
83+
),
84+
mock.patch.object(self.recovery, "read_record", return_value=plan),
85+
mock.patch.object(self.recovery, "validate_plan"),
86+
):
87+
paused = self.recovery.scheduled_continuity_pause(mock.Mock(), plan)
88+
89+
self.assertEqual(
90+
"beta-continuity/workspace-unavailable-test/resumed",
91+
paused["resumed_tag"],
92+
)
93+
with (
94+
mock.patch.object(
95+
self.recovery,
96+
"resolve_tag",
97+
side_effect=["a" * 40, "b" * 40],
98+
),
99+
mock.patch.object(self.recovery, "read_record", return_value=plan),
100+
mock.patch.object(self.recovery, "validate_plan"),
101+
):
102+
self.assertIsNone(self.recovery.scheduled_continuity_pause(mock.Mock(), plan))
103+
104+
71105
class PublicClientRetryTest(unittest.TestCase):
72106
@classmethod
73107
def setUpClass(cls) -> None:

0 commit comments

Comments
 (0)