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
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, so a longer soak saw little the first day did not, while 75 percent of installs sat on a stable build a month old. Richard set a 24 hour soak for every release on 2026-09-28. The minimums drop to 24 hours for minor and patch alike, the error text reads the constants instead of repeating numbers, and the workflow and trigger prompts, the promotion policy and the deployment contract say the same. The content-drift check and the hotfix exception are unchanged.
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