mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-24 04:33:17 +00:00
Bind maintenance stable releases to their soaked source
Apply existing candidate content-drift validation to every future stable promotion, including v6.4 patches. Preserve patch and minor soak durations and the explicit hotfix reason path. Extend regression cases to maintenance releases and give historical unit fixtures explicit source paths rather than reading the live checkout. Change-source: pulse-maintainer
This commit is contained in:
@@ -10401,7 +10401,21 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"work_claims": [],
|
||||
"work_claims": [
|
||||
{
|
||||
"id": "pulse-maintainer-lane-l1",
|
||||
"agent_id": "pulse-maintainer",
|
||||
"summary": "Bind future maintenance stable promotion to its soaked candidate, preserving explicit hotfix reason and soak checks",
|
||||
"target_id": "v6-release-reliability",
|
||||
"claimed_at": "2026-09-14T23:35:36Z",
|
||||
"heartbeat_at": "2026-09-14T23:35:36Z",
|
||||
"expires_at": "2026-09-15T01:35:36Z",
|
||||
"work_item": {
|
||||
"kind": "lane",
|
||||
"id": "L1"
|
||||
}
|
||||
}
|
||||
],
|
||||
"open_decisions": [],
|
||||
"source_of_truth_file": "docs/release-control/v6/internal/SOURCE_OF_TRUTH.md",
|
||||
"resolved_decisions": [
|
||||
|
||||
@@ -521,16 +521,16 @@ def resolve_metadata(
|
||||
promoted_tag_ts = release_published_unix_fn(promoted_from_tag)
|
||||
soak_hours_value = int((now_unix_fn() - promoted_tag_ts) / 3600)
|
||||
soak_hours = str(soak_hours_value)
|
||||
# The release train governs v6.5.0 and later. Earlier lines shipped
|
||||
# under the previous regime and their recorded exceptions stand.
|
||||
# The seven-day minor-release cadence starts at v6.5.0.
|
||||
# Source binding below applies independently to every future stable.
|
||||
train_governed = bool(stable_version and stable_version >= RELEASE_TRAIN_MIN_VERSION)
|
||||
candidate_content_drift: list[str] = []
|
||||
if train_governed:
|
||||
candidate_content_drift = [
|
||||
path
|
||||
for path in changed_paths_fn(promoted_from_tag)
|
||||
if not RELEASE_METADATA_PATH_RE.match(path)
|
||||
]
|
||||
# Every new stable promotion must ship its observed source, including
|
||||
# maintenance lines predating the minor-release train cadence.
|
||||
candidate_content_drift = [
|
||||
path
|
||||
for path in changed_paths_fn(promoted_from_tag)
|
||||
if not RELEASE_METADATA_PATH_RE.match(path)
|
||||
]
|
||||
|
||||
if hotfix_exception:
|
||||
if not hotfix_reason:
|
||||
|
||||
@@ -268,6 +268,7 @@ class ResolveReleasePromotionTest(unittest.TestCase):
|
||||
|
||||
def test_stable_requires_matching_promoted_rc_and_soak(self) -> None:
|
||||
metadata = resolver.resolve_metadata(
|
||||
changed_paths_fn=lambda tag: ["VERSION"],
|
||||
version="6.0.0",
|
||||
promoted_from_tag_input="6.0.0-rc.2",
|
||||
rollback_version_input="5.1.14",
|
||||
@@ -289,6 +290,7 @@ class ResolveReleasePromotionTest(unittest.TestCase):
|
||||
|
||||
def test_v610_owner_exception_allows_disclosed_unsigned_windows_candidate(self) -> None:
|
||||
metadata = resolver.resolve_metadata(
|
||||
changed_paths_fn=lambda tag: ["VERSION"],
|
||||
version="6.1.0",
|
||||
promoted_from_tag_input="v6.1.0-rc.4",
|
||||
rollback_version_input="v6.0.5",
|
||||
@@ -378,6 +380,7 @@ class ResolveReleasePromotionTest(unittest.TestCase):
|
||||
|
||||
def test_v620_owner_exception_allows_disclosed_stable_promotion(self) -> None:
|
||||
metadata = resolver.resolve_metadata(
|
||||
changed_paths_fn=lambda tag: ["VERSION"],
|
||||
version="6.2.0",
|
||||
promoted_from_tag_input="v6.2.0-rc.11",
|
||||
rollback_version_input="v6.1.2",
|
||||
@@ -437,6 +440,7 @@ class ResolveReleasePromotionTest(unittest.TestCase):
|
||||
|
||||
def test_v630_owner_exception_allows_disclosed_stable_promotion(self) -> None:
|
||||
metadata = resolver.resolve_metadata(
|
||||
changed_paths_fn=lambda tag: ["VERSION"],
|
||||
version="6.3.0",
|
||||
promoted_from_tag_input="v6.3.0-rc.6",
|
||||
rollback_version_input="v6.2.1",
|
||||
@@ -526,6 +530,7 @@ class ResolveReleasePromotionTest(unittest.TestCase):
|
||||
|
||||
def test_future_stable_release_stays_unsigned_until_authenticode_is_restored(self) -> None:
|
||||
metadata = resolver.resolve_metadata(
|
||||
changed_paths_fn=lambda tag: ["VERSION"],
|
||||
version="6.4.0",
|
||||
promoted_from_tag_input="v6.4.0-rc.1",
|
||||
rollback_version_input="v6.3.2",
|
||||
@@ -553,6 +558,7 @@ class ResolveReleasePromotionTest(unittest.TestCase):
|
||||
"approved only for stable v6.1.0, v6.1.1, v6.1.2, v6.2.0, v6.2.1, v6.3.0, v6.3.1, or v6.3.2",
|
||||
):
|
||||
resolver.resolve_metadata(
|
||||
changed_paths_fn=lambda tag: ["VERSION"],
|
||||
version="6.4.0",
|
||||
promoted_from_tag_input="",
|
||||
rollback_version_input="v6.3.2",
|
||||
@@ -602,6 +608,7 @@ class ResolveReleasePromotionTest(unittest.TestCase):
|
||||
"release_notes must include the Pulse v5 maintenance-only support notice",
|
||||
):
|
||||
resolver.resolve_metadata(
|
||||
changed_paths_fn=lambda tag: ["VERSION"],
|
||||
version="6.0.0",
|
||||
promoted_from_tag_input="6.0.0-rc.2",
|
||||
rollback_version_input="5.1.14",
|
||||
@@ -620,6 +627,7 @@ class ResolveReleasePromotionTest(unittest.TestCase):
|
||||
def test_current_stable_v6_packet_resolves_with_publish_dates(self) -> None:
|
||||
release_notes = (REPO_ROOT / "docs/releases/RELEASE_NOTES_v6.md").read_text(encoding="utf-8")
|
||||
metadata = resolver.resolve_metadata(
|
||||
changed_paths_fn=lambda tag: ["VERSION"],
|
||||
version="6.0.0",
|
||||
promoted_from_tag_input="v6.0.0-rc.7",
|
||||
rollback_version_input="v5.1.35",
|
||||
@@ -784,6 +792,7 @@ class ResolveReleasePromotionTest(unittest.TestCase):
|
||||
def test_stable_hotfix_requires_reason(self) -> None:
|
||||
with self.assertRaisesRegex(ValueError, "hotfix_reason is required"):
|
||||
resolver.resolve_metadata(
|
||||
changed_paths_fn=lambda tag: ["VERSION"],
|
||||
version="6.0.2",
|
||||
promoted_from_tag_input="6.0.2-rc.1",
|
||||
rollback_version_input="6.0.1",
|
||||
@@ -802,6 +811,7 @@ class ResolveReleasePromotionTest(unittest.TestCase):
|
||||
def test_stable_rejects_short_soak_without_hotfix(self) -> None:
|
||||
with self.assertRaisesRegex(ValueError, "hours of prerelease soak"):
|
||||
resolver.resolve_metadata(
|
||||
changed_paths_fn=lambda tag: ["VERSION"],
|
||||
version="6.0.2",
|
||||
promoted_from_tag_input="6.0.2-rc.1",
|
||||
rollback_version_input="6.0.1",
|
||||
@@ -888,8 +898,10 @@ class ReleaseTrainPromotionTest(unittest.TestCase):
|
||||
]
|
||||
with self.assertRaisesRegex(ValueError, "never soaked.*2 paths beyond release metadata"):
|
||||
self.promote("6.5.0", changed_paths_fn=lambda base_tag: drift)
|
||||
with self.assertRaisesRegex(ValueError, "never soaked"):
|
||||
self.promote("6.5.1", changed_paths_fn=lambda base_tag: drift)
|
||||
for version in ("6.5.1", "6.4.5", "6.3.2"):
|
||||
with self.subTest(version=version):
|
||||
with self.assertRaisesRegex(ValueError, "never soaked"):
|
||||
self.promote(version, changed_paths_fn=lambda base_tag: drift)
|
||||
|
||||
def test_hotfix_exception_still_requires_a_reason_for_drift(self) -> None:
|
||||
drift = ["VERSION", "internal/api/router.go"]
|
||||
@@ -905,7 +917,7 @@ class ReleaseTrainPromotionTest(unittest.TestCase):
|
||||
self.assertEqual(metadata["hotfix_exception"], "true")
|
||||
|
||||
def test_repaired_candidate_restarts_full_publication_soak(self) -> None:
|
||||
for version, hours in (("6.5.0", 168), ("6.5.1", 72)):
|
||||
for version, hours in (("6.5.0", 168), ("6.5.1", 72), ("6.4.5", 72)):
|
||||
tag = f"v{version}-rc.2"
|
||||
observed = []
|
||||
def publication(candidate):
|
||||
|
||||
Reference in New Issue
Block a user