diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 7af1fd8569..4c398c5875 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -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 diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index d1c4f7aeef..1a315d4e9e 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -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 diff --git a/.github/workflows/release-dry-run.yml b/.github/workflows/release-dry-run.yml index 1458389768..eb9d71eb39 100644 --- a/.github/workflows/release-dry-run.yml +++ b/.github/workflows/release-dry-run.yml @@ -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 diff --git a/docs/release-control/v6/internal/HIGH_RISK_RELEASE_VERIFICATION_MATRIX.md b/docs/release-control/v6/internal/HIGH_RISK_RELEASE_VERIFICATION_MATRIX.md index e1163e1f37..37839089d1 100644 --- a/docs/release-control/v6/internal/HIGH_RISK_RELEASE_VERIFICATION_MATRIX.md +++ b/docs/release-control/v6/internal/HIGH_RISK_RELEASE_VERIFICATION_MATRIX.md @@ -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. diff --git a/docs/release-control/v6/internal/RELEASE_PROMOTION_POLICY.md b/docs/release-control/v6/internal/RELEASE_PROMOTION_POLICY.md index 04f22548de..a26453a16f 100644 --- a/docs/release-control/v6/internal/RELEASE_PROMOTION_POLICY.md +++ b/docs/release-control/v6/internal/RELEASE_PROMOTION_POLICY.md @@ -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 diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index b9ec38638f..86624b192d 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -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 diff --git a/scripts/release_control/release_promotion_policy_test.py b/scripts/release_control/release_promotion_policy_test.py index 438288a408..799ada8b88 100644 --- a/scripts/release_control/release_promotion_policy_test.py +++ b/scripts/release_control/release_promotion_policy_test.py @@ -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)) diff --git a/scripts/release_control/resolve_release_promotion.py b/scripts/release_control/resolve_release_promotion.py index b19da00995..d3f8fb57a2 100644 --- a/scripts/release_control/resolve_release_promotion.py +++ b/scripts/release_control/resolve_release_promotion.py @@ -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": diff --git a/scripts/release_control/resolve_release_promotion_test.py b/scripts/release_control/resolve_release_promotion_test.py index 460543a159..c58c825bdf 100644 --- a/scripts/release_control/resolve_release_promotion_test.py +++ b/scripts/release_control/resolve_release_promotion_test.py @@ -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__": diff --git a/scripts/trigger-release-dry-run.sh b/scripts/trigger-release-dry-run.sh index a62844317d..98d1432dd7 100755 --- a/scripts/trigger-release-dry-run.sh +++ b/scripts/trigger-release-dry-run.sh @@ -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 diff --git a/scripts/trigger-release.sh b/scripts/trigger-release.sh index 79a901e792..79626ab037 100755 --- a/scripts/trigger-release.sh +++ b/scripts/trigger-release.sh @@ -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