mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-23 20:25:56 +00:00
Soak release candidates for 24 hours before stable (#2307)
The resolver refused a stable promotion until its candidate had soaked for 72 hours, or 168 for a minor. Few installs run previews: 35 of about 7,900 active installs ran v6.4.5-rc.3 in the week to 2026-09-28, while 75 percent sat on a month-old stable. The minimums drop to 24 hours for minor and patch alike; the content-drift check and the hotfix exception are unchanged. The workflow and trigger prompts, the promotion policy and the deployment-installability contract say the same.
This commit is contained in:
@@ -194,7 +194,7 @@ Use this decision sequence within the existing release authority:
|
||||
This is supersession, not evidence that the old publication was successful
|
||||
or that customers pinned to its container version have migrated.
|
||||
3. Keep patch scope to named regression/security fixes. A changed patch RC
|
||||
restarts 72 hours of clean soak; an older immutable RC or green main cannot
|
||||
restarts 24 hours of clean soak; an older immutable RC or green main cannot
|
||||
lend its qualification to changed bytes. Retain the exact candidate packet
|
||||
for release-steward judgment and exact-source admission before stable
|
||||
publication. Under standing authority granted on 8 September 2026, the
|
||||
|
||||
@@ -46,7 +46,7 @@ on:
|
||||
required: false
|
||||
type: string
|
||||
hotfix_exception:
|
||||
description: 'Stable only: bypass the 72-hour prerelease soak for urgent customer harm'
|
||||
description: 'Stable only: bypass the 24-hour prerelease soak for urgent customer harm'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
@@ -34,7 +34,7 @@ on:
|
||||
required: false
|
||||
type: string
|
||||
hotfix_exception:
|
||||
description: 'Stable rehearsal only: bypass 72-hour prerelease soak for urgent customer harm'
|
||||
description: 'Stable rehearsal only: bypass 24-hour prerelease soak for urgent customer harm'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
@@ -566,7 +566,7 @@ Companion drill:
|
||||
2. Confirm the candidate commit has already shipped on `rc` through a real
|
||||
release-pipeline run, not only workflow lint or static YAML validation.
|
||||
Accidental prerelease git tags do not count as shipped prerelease lineage.
|
||||
3. Confirm the candidate satisfies the minimum 72-hour prerelease soak or that a
|
||||
3. Confirm the candidate satisfies the minimum 24-hour prerelease soak or that a
|
||||
hotfix exception and reason are recorded explicitly before promotion.
|
||||
4. Confirm the previous stable rollback target and exact reinstall or pin
|
||||
command are recorded in the release notes or release ticket.
|
||||
|
||||
@@ -278,9 +278,12 @@ without the other lanes changing the candidate underneath it.
|
||||
upgrade guide, release records) unless `hotfix_exception` names active
|
||||
customer harm. The v6.4.0 promotion, which shipped 64 changed files that
|
||||
`v6.4.0-rc.12` had not soaked, is the case this rule prevents.
|
||||
4. A minor release (`X.Y.0`) requires a seven day soak of the promoted
|
||||
candidate; patch releases keep the 72 hour minimum. Patch releases are for
|
||||
a named regression or security issue only.
|
||||
4. A minor release (`X.Y.0`) and a patch release both require a 24 hour soak
|
||||
of the promoted candidate (founder direction of 28 September 2026; it was
|
||||
seven days for a minor and 72 hours for a patch). Few installs run
|
||||
previews, so the longer soak saw little beyond the first day while stable
|
||||
users waited. Patch releases are for a named regression or security issue
|
||||
only.
|
||||
5. "Soaked clean" means all of: the soak has elapsed since the candidate's
|
||||
release was published; no open issue reports an unresolved regression,
|
||||
security or data-loss problem in the candidate, including any issue
|
||||
@@ -435,7 +438,7 @@ without the other lanes changing the candidate underneath it.
|
||||
- The locked 90-day v5 maintenance-only policy in
|
||||
`V5_MAINTENANCE_SUPPORT_POLICY.md` and the exact end-of-support notice
|
||||
ready to publish with the promotion.
|
||||
7. RC-derived stable promotions require a minimum 72-hour prerelease soak after
|
||||
7. RC-derived stable promotions require a minimum 24-hour prerelease soak after
|
||||
the candidate is available to internal or staging-like users.
|
||||
8. Hotfix exception:
|
||||
- Bypassing an RC requirement or shortening an RC soak is allowed only for
|
||||
|
||||
@@ -5601,12 +5601,19 @@ in release metadata: `VERSION`, the Helm chart version and README, the compose
|
||||
default image, `docs/RELEASE_NOTES.md`, `docs/UPGRADE_v6.md` and its shipped
|
||||
docs mirror, `docs/releases/`, release-control records, this contract, and
|
||||
`status.json`. Any other path refuses the promotion unless `hotfix_exception`
|
||||
names active customer harm, because that content was never soaked. Minor
|
||||
releases (`X.Y.0`) additionally require a seven day soak; patches keep 72
|
||||
hours. `docs/release-control/control_plane.json` declares `release/v6.5` for
|
||||
names active customer harm, because that content was never soaked. Since
|
||||
the founder direction of 28 September 2026 minor releases (`X.Y.0`) and
|
||||
patches both require a 24 hour soak (`MIN_STABLE_SOAK_HOURS` and
|
||||
`MIN_MINOR_STABLE_SOAK_HOURS`); it was seven days and 72 hours. The workflow
|
||||
and trigger prompts describe the hotfix exception against the same 24 hours.
|
||||
A release line cut earlier keeps its own resolver's longer minimum, which the
|
||||
maintainer's release admission relaxes to the release plan's soak only
|
||||
through that exception and only once the stable source is shown to add
|
||||
nothing beyond release metadata. `docs/release-control/control_plane.json` declares `release/v6.5` for
|
||||
the first release train so the workflow refuses a v6.5 dispatch from any other
|
||||
branch. `scripts/release_control/resolve_release_promotion_test.py` pins the
|
||||
allowlist, the drift refusal, the hotfix path, and the minor soak;
|
||||
allowlist, the drift refusal, the hotfix path, and the 24 hour minor and
|
||||
patch soak;
|
||||
`release_promotion_policy_test.py` pins the policy's Release Train section.
|
||||
|
||||
### Provider docs name the Patrol weekly summary schedule kind
|
||||
|
||||
@@ -1767,7 +1767,7 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
|
||||
self.assertIn("release-dry-run.yml", dry_run_trigger)
|
||||
self.assertIn("gh workflow run release-dry-run.yml", dry_run_trigger)
|
||||
self.assertIn("Release Dry Run executes the selected remote ref", dry_run_trigger)
|
||||
self.assertIn("Hotfix exception to bypass 72-hour prerelease soak? [y/N]", dry_run_trigger)
|
||||
self.assertIn("Hotfix exception to bypass 24-hour prerelease soak? [y/N]", dry_run_trigger)
|
||||
self.assertIn("blank only for approved hotfix", dry_run_trigger)
|
||||
self.assertIn('if [ -z "$PROMOTED_FROM_TAG" ] && [ "$HOTFIX_EXCEPTION" != "true" ]; then', dry_run_trigger)
|
||||
self.assertNotIn("Continue anyway?", dry_run_trigger)
|
||||
@@ -1863,7 +1863,7 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
|
||||
self.assertIn(promotion_metadata_envelope(), normalize_ws(policy))
|
||||
self.assertIn("recorded rollback target plus exact", source_of_truth)
|
||||
self.assertIn("hours of prerelease soak", resolver)
|
||||
self.assertIn("minimum is 72 hours unless hotfix_exception is true", resolver)
|
||||
self.assertIn("minimum is {MIN_STABLE_SOAK_HOURS} hours unless hotfix_exception is true", resolver)
|
||||
self.assertIn("MIN_PRERELEASE_OBSERVATION_HOURS = 24", resolver)
|
||||
self.assertIn("cohort checkpoint, not a delivery vehicle", normalize_ws(policy))
|
||||
self.assertIn("at least 24 hours of public observation", normalize_ws(source_of_truth))
|
||||
|
||||
@@ -24,10 +24,12 @@ SEMVER_PUBLISHED_PRERELEASE_RE = re.compile(
|
||||
)
|
||||
SEMVER_RC_RE = re.compile(r"^(\d+)\.(\d+)\.(\d+)-rc\.(\d+)$")
|
||||
MIN_PRERELEASE_OBSERVATION_HOURS = 24
|
||||
# Release train (RELEASE_PROMOTION_POLICY.md, "Release Train"): a patch keeps
|
||||
# the 72 hour candidate soak; a minor release soaks its candidate for a week.
|
||||
MIN_STABLE_SOAK_HOURS = 72
|
||||
MIN_MINOR_STABLE_SOAK_HOURS = 168
|
||||
# Release train (RELEASE_PROMOTION_POLICY.md, "Release Train"): since the
|
||||
# founder direction of 28 September 2026 every candidate, minor or patch, soaks
|
||||
# for 24 hours. Few installs run previews, so a longer soak saw little more
|
||||
# than the first day while keeping stable users on an older build.
|
||||
MIN_STABLE_SOAK_HOURS = 24
|
||||
MIN_MINOR_STABLE_SOAK_HOURS = 24
|
||||
RELEASE_TRAIN_MIN_VERSION = (6, 5, 0)
|
||||
# Paths a stable promotion may change relative to its promoted candidate.
|
||||
# Everything else is content the candidate never soaked, so the resolver
|
||||
@@ -586,11 +588,12 @@ def resolve_metadata(
|
||||
raise ValueError(
|
||||
f"Minor stable promotion {tag} has only {soak_hours_value} hours of prerelease soak since "
|
||||
f"{promoted_from_tag}; the release train requires {MIN_MINOR_STABLE_SOAK_HOURS} hours "
|
||||
"(seven days) for a minor release unless hotfix_exception is true."
|
||||
"for a minor release unless hotfix_exception is true."
|
||||
)
|
||||
elif soak_hours_value < MIN_STABLE_SOAK_HOURS:
|
||||
raise ValueError(
|
||||
f"Stable promotion {tag} has only {soak_hours_value} hours of prerelease soak since {promoted_from_tag}; minimum is 72 hours unless hotfix_exception is true."
|
||||
f"Stable promotion {tag} has only {soak_hours_value} hours of prerelease soak since "
|
||||
f"{promoted_from_tag}; minimum is {MIN_STABLE_SOAK_HOURS} hours unless hotfix_exception is true."
|
||||
)
|
||||
|
||||
if version == "6.0.0":
|
||||
|
||||
@@ -1003,7 +1003,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), ("6.4.5", 72)):
|
||||
for version, hours in (("6.5.0", 24), ("6.5.1", 24), ("6.4.5", 24)):
|
||||
tag = f"v{version}-rc.2"
|
||||
observed = []
|
||||
def publication(candidate):
|
||||
@@ -1023,13 +1023,15 @@ class ReleaseTrainPromotionTest(unittest.TestCase):
|
||||
self.assertEqual(metadata["soak_hours"], str(hours))
|
||||
self.assertEqual(observed, [tag, tag])
|
||||
|
||||
def test_minor_releases_soak_seven_days_and_patches_seventy_two_hours(self) -> None:
|
||||
with self.assertRaisesRegex(ValueError, "release train requires 168 hours"):
|
||||
self.promote("6.5.0", now_unix_fn=lambda: 100 + (100 * 3600))
|
||||
metadata = self.promote("6.5.1", now_unix_fn=lambda: 100 + (73 * 3600))
|
||||
self.assertEqual(metadata["soak_hours"], "73")
|
||||
with self.assertRaisesRegex(ValueError, "minimum is 72 hours"):
|
||||
self.promote("6.5.1", now_unix_fn=lambda: 100 + (71 * 3600))
|
||||
def test_minor_and_patch_releases_soak_twenty_four_hours(self) -> None:
|
||||
with self.assertRaisesRegex(ValueError, "release train requires 24 hours"):
|
||||
self.promote("6.5.0", now_unix_fn=lambda: 100 + (23 * 3600))
|
||||
metadata = self.promote("6.5.0", now_unix_fn=lambda: 100 + (24 * 3600))
|
||||
self.assertEqual(metadata["soak_hours"], "24")
|
||||
metadata = self.promote("6.5.1", now_unix_fn=lambda: 100 + (25 * 3600))
|
||||
self.assertEqual(metadata["soak_hours"], "25")
|
||||
with self.assertRaisesRegex(ValueError, "minimum is 24 hours"):
|
||||
self.promote("6.5.1", now_unix_fn=lambda: 100 + (23 * 3600))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -146,7 +146,7 @@ fi
|
||||
|
||||
if [ "$IS_PRERELEASE" != "true" ]; then
|
||||
echo ""
|
||||
read -r -p "Hotfix exception to bypass 72-hour prerelease soak? [y/N] " HOTFIX_REPLY
|
||||
read -r -p "Hotfix exception to bypass 24-hour prerelease soak? [y/N] " HOTFIX_REPLY
|
||||
if [[ "$HOTFIX_REPLY" =~ ^[Yy]$ ]]; then
|
||||
HOTFIX_EXCEPTION="true"
|
||||
read -r -p "Hotfix reason: " HOTFIX_REASON
|
||||
|
||||
@@ -280,7 +280,7 @@ echo "Derived rollback command: ${ROLLBACK_COMMAND}"
|
||||
|
||||
if [ "$IS_PRERELEASE" != "true" ]; then
|
||||
echo ""
|
||||
read -r -p "Hotfix exception to bypass 72-hour prerelease soak? [y/N] " HOTFIX_REPLY
|
||||
read -r -p "Hotfix exception to bypass 24-hour prerelease soak? [y/N] " HOTFIX_REPLY
|
||||
if [[ "$HOTFIX_REPLY" =~ ^[Yy]$ ]]; then
|
||||
HOTFIX_EXCEPTION="true"
|
||||
read -r -p "Hotfix reason: " HOTFIX_REASON
|
||||
|
||||
Reference in New Issue
Block a user