diff --git a/docs/release-control/control_plane.json b/docs/release-control/control_plane.json index 8fc7c3fcc..af5a4cd92 100644 --- a/docs/release-control/control_plane.json +++ b/docs/release-control/control_plane.json @@ -6,7 +6,7 @@ "control_plane_doc": "docs/release-control/internal/CONTROL_PLANE.md", "control_plane_schema": "docs/release-control/control_plane.schema.json", "active_profile_id": "v6", - "active_target_id": "v6-ga-promotion", + "active_target_id": "v6-rc-cut", "profiles": [ { "id": "v6", @@ -30,16 +30,16 @@ "id": "v6-rc-cut", "profile_id": "v6", "kind": "release", - "status": "completed", - "summary": "Drive Pulse v6 to a governed RC cut by clearing prerelease blockers, proving migration safety, and exercising the pre-RC high-risk gates.", + "status": "active", + "summary": "Hold Pulse v6 on the governed prerelease-cut target until RC publication is explicitly judged ready, while continuing to clear prerelease blockers, prove migration safety, and exercise the pre-RC high-risk gates.", "completion_rule": "rc_ready" }, { "id": "v6-rc-stabilization", "profile_id": "v6", "kind": "stabilization", - "status": "completed", - "summary": "Keep Pulse v6 simple, accurate, and cleanly readable around the proven monitoring-and-alerting RC floor, using resource-change intelligence only where it materially improves investigation or safe AI flows while broader control-plane expansion stays out of the default target until it is proven.", + "status": "planned", + "summary": "Once a real governed RC exists, keep Pulse v6 simple, accurate, and cleanly readable around that proven monitoring-first floor while broader control-plane expansion stays out of the default target until it is proven.", "completion_rule": "manual", "proof_scope": "none" }, @@ -47,8 +47,8 @@ "id": "v6-ga-promotion", "profile_id": "v6", "kind": "release", - "status": "active", - "summary": "Promote Pulse v6 from a validated RC to governed GA with exercised promotion proof, rollback clarity, and only GA-blocking decisions remaining.", + "status": "planned", + "summary": "Promote Pulse v6 from a validated, actually shipped RC to governed GA with exercised promotion proof, rollback clarity, and only GA-blocking decisions remaining.", "completion_rule": "release_ready" }, { diff --git a/docs/release-control/internal/CONTROL_PLANE.md b/docs/release-control/internal/CONTROL_PLANE.md index 6d951a919..d9c870842 100644 --- a/docs/release-control/internal/CONTROL_PLANE.md +++ b/docs/release-control/internal/CONTROL_PLANE.md @@ -232,18 +232,18 @@ user language should update the control plane. ## Current State 1. v6 is the current active release profile. -2. `v6-ga-promotion` is the current active engineering target. +2. `v6-rc-cut` is the current active engineering target. It is a release target, so default slice selection should stay centered on - exercised prerelease lineage, release-pipeline proof, rollback metadata, - and the remaining GA-blocking documentation inputs until `release_ready` - is actually met. + prerelease blockers, RC-publication judgment, and the remaining proof needed + before a real governed RC should actually be cut. 3. `v6-product-lane-expansion` remains planned and is still blocked on the broader surfaced product case being proven. Its candidate-lane surface remains available in `available_candidate_lane_queue` plus the linked `candidate_lanes` and `coverage_gaps`. -4. `v6-rc-stabilization` is complete and remains the predecessor - stabilization target for the current v6 line. -5. `v6-rc-cut` is complete and remains the predecessor release-cut target. +4. `v6-ga-promotion` remains planned and must stay dormant until a real + governed RC has actually shipped and been validated. +5. `v6-rc-stabilization` remains planned and should only become active once + the first governed RC exists. 6. Its files remain under `docs/release-control/v6/`. 7. The existing v6 control surfaces are still live, but they now sit underneath an evergreen Pulse control plane rather than pretending to be the whole diff --git a/docs/release-control/v6/internal/PRE_RELEASE_CHECKLIST.md b/docs/release-control/v6/internal/PRE_RELEASE_CHECKLIST.md index 848f35e98..b75fc1583 100644 --- a/docs/release-control/v6/internal/PRE_RELEASE_CHECKLIST.md +++ b/docs/release-control/v6/internal/PRE_RELEASE_CHECKLIST.md @@ -13,10 +13,13 @@ Use this as the final gate before cutting a Pulse v6 pre-release. ## Current Status - Automated command-driven checks completed on 2026-03-06 are marked `[x]` below. - `status.json` is no longer overclaiming threshold-unmet gates as passed. -- There are no remaining `rc-ready` high-risk blockers; `rc_ready` now derives - true in `status.json`. -- The active target is back on `v6-rc-stabilization`, so GA rehearsal remains - intentionally out of scope until promotion is resumed explicitly. +- There are no remaining `rc-ready` high-risk release-gate blockers, but + `rc_ready` is intentionally held false in `status.json` by the open decision + `rc-publication-judgment` until the current candidate is explicitly judged + ready for a real governed RC. +- The active target is back on `v6-rc-cut`, so GA rehearsal remains + intentionally out of scope until a real RC has actually shipped and + promotion is resumed explicitly. - The remaining release-ready blocker is `rc-to-ga-promotion-readiness`, which stays blocked until a later stable `6.0.0` candidate completes a matching `Release Dry Run` rehearsal with the canonical promotion artifact envelope: diff --git a/docs/release-control/v6/internal/SOURCE_OF_TRUTH.md b/docs/release-control/v6/internal/SOURCE_OF_TRUTH.md index 1ba0f7d5e..0476250d1 100644 --- a/docs/release-control/v6/internal/SOURCE_OF_TRUTH.md +++ b/docs/release-control/v6/internal/SOURCE_OF_TRUTH.md @@ -154,8 +154,8 @@ Pulse v6 is ready when these outcomes land together: customer exposure. Pulse v6 is still a bridge release toward that unified operations layer, while -the current active target remains governed GA promotion rather than broad new -platform execution. +the current active target remains governed prerelease-cut readiness with an +explicit RC-publication hold rather than broad new platform execution. For this profile, "bridge release" means the product should stop reading as a monitoring tool growing sideways and instead make the mixed-estate operator surface legible through canonical resources, investigation context, governed diff --git a/docs/release-control/v6/internal/records/rc-to-ga-promotion-readiness-blocked-2026-04-04.md b/docs/release-control/v6/internal/records/rc-to-ga-promotion-readiness-blocked-2026-04-04.md new file mode 100644 index 000000000..d1a1dd48c --- /dev/null +++ b/docs/release-control/v6/internal/records/rc-to-ga-promotion-readiness-blocked-2026-04-04.md @@ -0,0 +1,80 @@ +# Prerelease-to-GA Promotion Readiness Blocked Record + +- Date: `2026-04-04` +- Gate: `rc-to-ga-promotion-readiness` +- Result: `blocked` + +## Blocking Facts + +1. No Pulse v6 prerelease has shipped yet. +2. The repository contains accidental prerelease git tag history (`v6.0.0-rc.1`), + but those tags were never published and do not count as shipped prerelease lineage. +3. The selected remote ref `origin/pulse/v6-release` is still behind the current + local governed branch state, so `Release Dry Run` would exercise stale remote + control-plane metadata instead of the intended candidate. +4. The governed release profile in `docs/release-control/control_plane.json` + currently declares both `prerelease_branch` and `stable_branch` as + `pulse/v6-release`. +5. The active control-plane target is still `v6-rc-cut`, not + `v6-ga-promotion`. +6. The active local `pulse/v6-release` branch currently reports `VERSION=6.0.0-rc.1`, so the + working line is still prerelease and there is not yet a governed local stable + `6.0.0` candidate. +7. There is still no governed `Prerelease-to-GA Rehearsal Record` proving a successful + non-publish `Release Dry Run` for the eventual stable `6.0.0` candidate. +8. `docs/releases/RELEASE_NOTES_v6.md` and + `docs/release-control/v6/internal/V5_MAINTENANCE_SUPPORT_POLICY.md` still leave the + GA announcement dates as placeholders because no real prerelease lineage or GA-ready + rehearsal has locked them yet: + - `v6` GA date placeholder: `[v6-ga-date]` + - `v5` end-of-support placeholder: `[v5-eos-date]` +9. There is still no governed `Release Dry Run` artifact or rehearsal record + exercising stable inputs for: + - `version=6.0.0` + - no governed `promoted_from_tag` exists yet because no prerelease has shipped + - the artifact-owned candidate stable tag for that rehearsal + - the artifact-owned promotion channel for that rehearsal + - the artifact-owned promoted prerelease tag for that rehearsal + - the artifact-owned rollback target for that stable candidate + - no exact `ga_date` is locked yet because the GA notice is still pending + - no exact `v5_eos_date` is locked yet because the GA notice is still pending + - an explicit `rollback_version` + - the exact derived rollback command that artifact will publish + +## Why The Gate Cannot Be Cleared Yet + +The blocker is no longer missing governance text. The remaining problem is that +the control plane still holds v6 on the pre-GA prerelease line, the working +version is still prerelease (`6.0.0-rc.1`), and there is still no exercised +`Release Dry Run` record proving the eventual stable `6.0.0` +candidate is ready for GA-style promotion. Until that rehearsal exists, stable +users would still be the first real cohort for the final promotion path. + +## Required Unblock Steps + +1. Promote the active target from `v6-rc-cut` to + `v6-ga-promotion` only when that change is actually intended. +2. Push the governed `pulse/v6-release` branch state that is intended to become the + stable `6.0.0` candidate, including the eventual `VERSION=6.0.0` + change and release-control records, to `origin/pulse/v6-release`. +3. Ship the first real prerelease through the governed prerelease release path and record + its exact published prerelease tag plus rollback target and exact derived + rollback command. +4. Run `Release Dry Run` from `pulse/v6-release` using that published prerelease as + `promoted_from_tag` with: + - `version=6.0.0` + - an artifact-owned candidate stable tag matching that rehearsal + - an artifact-owned promotion channel matching that rehearsal + - an artifact-owned promoted prerelease tag matching that rehearsal + - an artifact-owned rollback target for the stable candidate + - the exact planned GA and v5 end-of-support dates for the publish notice + - an explicit `ga_date` chosen for that rehearsal + - an explicit stable `rollback_version` + - the exact derived rollback command that artifact will publish + - an explicit `v5_eos_date` chosen for that rehearsal +5. Capture the `rc-to-ga-rehearsal-summary` artifact and run URL. +6. Materialize the final rehearsal record from that artifact without + hand-repairing any missing candidate tag, promoted prerelease tag, rollback + target, rollback command, or GA/EOS metadata. +7. Change the gate from `blocked` only if the rehearsal passes and the rollout + inputs remain explicit. diff --git a/docs/release-control/v6/internal/status.json b/docs/release-control/v6/internal/status.json index e99da4f4e..fa20491cd 100644 --- a/docs/release-control/v6/internal/status.json +++ b/docs/release-control/v6/internal/status.json @@ -2698,7 +2698,7 @@ "status": "partial", "completion": { "state": "bounded-residual", - "summary": "First-session UX reached the RC floor with the runtime wizard reduced to welcome and security, direct handoff into Infrastructure Operations install, and setup completion retained only as a separate preview surface; broader polish and parity work remain intentionally outside the current stabilization target.", + "summary": "First-session UX reached the RC floor with the runtime wizard reduced to welcome and security, direct handoff into Infrastructure Operations install, and setup completion retained only as a separate preview surface; broader polish and parity work remain intentionally outside the current prerelease target.", "tracking": [ { "kind": "lane-followup", @@ -3624,6 +3624,12 @@ "kind": "file", "evidence_tier": "local-rehearsal" }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/rc-to-ga-promotion-readiness-blocked-2026-04-04.md", + "kind": "file", + "evidence_tier": "local-rehearsal" + }, { "repo": "pulse", "path": "docs/release-control/v6/internal/records/rc-to-ga-promotion-readiness-rehearsal-blocked-2026-03-26.md", @@ -4216,7 +4222,23 @@ } ], "work_claims": [], - "open_decisions": [], + "open_decisions": [ + { + "id": "rc-publication-judgment", + "summary": "Pulse v6 prerelease publication remains an explicit product judgment: even if the machine-derived RC floor looks close, `rc_ready` stays blocked until the current candidate is explicitly judged ready for a real governed RC and not inferred from accidental `6.0.0-rc.1` lineage or checklist completion.", + "owner": "project-owner", + "blocking_level": "rc-ready", + "status": "open", + "opened_at": "2026-04-04", + "lane_ids": [ + "L1", + "L8", + "L9", + "L12" + ], + "subsystem_ids": [] + } + ], "source_of_truth_file": "docs/release-control/v6/internal/SOURCE_OF_TRUTH.md", "resolved_decisions": [ { diff --git a/internal/repoctl/canonical_development_protocol_test.go b/internal/repoctl/canonical_development_protocol_test.go index 58de6dd3a..e5844fc47 100644 --- a/internal/repoctl/canonical_development_protocol_test.go +++ b/internal/repoctl/canonical_development_protocol_test.go @@ -510,7 +510,7 @@ func TestReleaseControlPlaneFilesExist(t *testing.T) { if !ok || activeTargetID == "" { t.Fatalf("%s missing active_target_id", jsonRel) } - if activeTargetID != "v6-rc-stabilization" && activeTargetID != "v6-ga-promotion" && activeTargetID != "v6-product-lane-expansion" { + if activeTargetID != "v6-rc-cut" && activeTargetID != "v6-rc-stabilization" && activeTargetID != "v6-ga-promotion" && activeTargetID != "v6-product-lane-expansion" { t.Fatalf("%s has unexpected active_target_id %q", jsonRel, activeTargetID) } @@ -557,8 +557,8 @@ func TestReleaseControlPlaneFilesExist(t *testing.T) { if status, _ := targetsByID[activeTargetID]["status"].(string); status != "active" { t.Fatalf("%s active_target_id %q is not marked active", jsonRel, activeTargetID) } - if status, _ := targetsByID["v6-rc-cut"]["status"].(string); status != "completed" { - t.Fatalf("%s v6-rc-cut must remain completed", jsonRel) + if status, _ := targetsByID["v6-rc-cut"]["status"].(string); status != "active" && status != "completed" { + t.Fatalf("%s v6-rc-cut must remain either active or completed", jsonRel) } if rule, _ := targetsByID["v6-rc-stabilization"]["completion_rule"].(string); rule != "manual" { t.Fatalf("%s v6-rc-stabilization must keep completion_rule=manual", jsonRel) diff --git a/scripts/release_control/record_rc_to_ga_blocked.py b/scripts/release_control/record_rc_to_ga_blocked.py index fedb4fbc0..4f7ca061f 100644 --- a/scripts/release_control/record_rc_to_ga_blocked.py +++ b/scripts/release_control/record_rc_to_ga_blocked.py @@ -253,7 +253,7 @@ def build_blocked_record(*, record_date: str) -> str: if version_is_prerelease: why_lines = [ "The blocker is no longer missing governance text. The remaining problem is that", - "the control plane still treats v6 as the prerelease-stabilization line, the working", + "the control plane still holds v6 on the pre-GA prerelease line, the working", f"version is still prerelease (`{version}`), and there is still no exercised", f"`Release Dry Run` record proving the eventual stable `{stable_version}`", "candidate is ready for GA-style promotion. Until that rehearsal exists, stable", @@ -273,7 +273,7 @@ def build_blocked_record(*, record_date: str) -> str: else: why_lines = [ "The blocker is no longer missing governance text. The remaining problem is that", - "the control plane still treats v6 as the prerelease-stabilization line, and there is", + "the control plane still holds v6 on the pre-GA prerelease line, and there is", f"still no exercised `Release Dry Run` record proving the exact `{version}`", "candidate is ready for GA-style promotion. Until that rehearsal exists, stable", "users would still be the first real cohort for the final promotion path.", diff --git a/scripts/release_control/release_promotion_policy_test.py b/scripts/release_control/release_promotion_policy_test.py index 4ab24dcf7..6cdb989c5 100644 --- a/scripts/release_control/release_promotion_policy_test.py +++ b/scripts/release_control/release_promotion_policy_test.py @@ -239,7 +239,7 @@ class ReleasePromotionPolicyTest(unittest.TestCase): self.assertIn(promotion_metadata_envelope(), normalize_ws(runbook)) def test_blocked_record_tracks_current_target_and_candidate_version(self) -> None: - blocked = read("docs/release-control/v6/internal/records/rc-to-ga-promotion-readiness-blocked-2026-03-28.md") + blocked = read("docs/release-control/v6/internal/records/rc-to-ga-promotion-readiness-blocked-2026-04-04.md") self.assertIn("origin/pulse/v6-release", blocked) self.assertIn("VERSION=6.0.0-rc.1", blocked) self.assertIn("artifact-owned candidate stable tag", blocked) @@ -248,10 +248,10 @@ class ReleasePromotionPolicyTest(unittest.TestCase): self.assertIn("artifact-owned rollback target", blocked) self.assertIn("Materialize the final rehearsal record from that artifact without", blocked) self.assertIn("hand-repairing any missing candidate tag, promoted prerelease tag, rollback", blocked) - self.assertIn("The active control-plane target is `v6-ga-promotion`", blocked) + self.assertIn("The active control-plane target is still `v6-rc-cut`, not", blocked) matrix = read("docs/release-control/v6/internal/HIGH_RISK_RELEASE_VERIFICATION_MATRIX.md") self.assertIn(promotion_metadata_envelope(), normalize_ws(matrix)) - expected = blocked_record.build_blocked_record(record_date="2026-03-28") + expected = blocked_record.build_blocked_record(record_date="2026-04-04") self.assertEqual(blocked, expected) diff --git a/scripts/release_control/status_audit_test.py b/scripts/release_control/status_audit_test.py index c69b896f7..60dbd1d79 100644 --- a/scripts/release_control/status_audit_test.py +++ b/scripts/release_control/status_audit_test.py @@ -1,5 +1,6 @@ from __future__ import annotations +import copy from datetime import datetime, timezone import os import tempfile @@ -1354,9 +1355,15 @@ class StatusAuditTest(unittest.TestCase): ], ) + control_plane = copy.deepcopy(status_audit.DEFAULT_CONTROL_PLANE) + control_plane["targets_by_id"]["v6-rc-cut"]["status"] = "completed" + with mock.patch.dict(os.environ, {"PULSE_REPO_ROOT_PULSE": str(pulse)}, clear=False), mock.patch( "status_audit.load_subsystem_rules", return_value=[], + ), mock.patch( + "status_audit.DEFAULT_CONTROL_PLANE", + control_plane, ): report = audit_status_payload(payload) diff --git a/scripts/release_control/subsystem_lookup_test.py b/scripts/release_control/subsystem_lookup_test.py index 5c12b0562..64112de0d 100644 --- a/scripts/release_control/subsystem_lookup_test.py +++ b/scripts/release_control/subsystem_lookup_test.py @@ -76,7 +76,7 @@ class SubsystemLookupTest(unittest.TestCase): self.assertEqual(result["status_audit_errors"], []) self.assertIn( result["control_plane"]["active_target"]["id"], - {"v6-rc-stabilization", "v6-ga-promotion", "v6-product-lane-expansion"}, + {"v6-rc-cut", "v6-rc-stabilization", "v6-ga-promotion", "v6-product-lane-expansion"}, ) self.assertEqual(result["scope"]["control_plane_repo"], "pulse") self.assertEqual(result["status_summary"]["lane_count"], 16)