diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 9517c56cf..5a86e7848 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -141,7 +141,7 @@ jobs: run: | set -euo pipefail - git fetch --prune origin main pulse/v6 --tags + git fetch --prune origin main "${REQUIRED_BRANCH}" --tags RELEASE_NOTES_INPUT="$(jq -r '.inputs.release_notes // ""' "$GITHUB_EVENT_PATH")" NOTES_FILE="$(mktemp)" diff --git a/.github/workflows/release-dry-run.yml b/.github/workflows/release-dry-run.yml index 3bc58ae27..078910ab0 100644 --- a/.github/workflows/release-dry-run.yml +++ b/.github/workflows/release-dry-run.yml @@ -122,7 +122,7 @@ jobs: exit 1 fi - git fetch --prune origin main pulse/v6 --tags + git fetch --prune origin main "${REQUIRED_BRANCH}" --tags HELPER_ARGS=( --version "${VERSION}" diff --git a/.gitignore b/.gitignore index 56f41bf9e..b34a52345 100644 --- a/.gitignore +++ b/.gitignore @@ -196,6 +196,10 @@ docs/release-control/* docs/release-control/v6/* !docs/release-control/v6/README.md !docs/release-control/v6/status.schema.json +!docs/release-control/v6/internal/ +docs/release-control/v6/internal/* +!docs/release-control/v6/internal/PRE_RELEASE_CHECKLIST.md +!docs/release-control/v6/internal/RC_TO_GA_REHEARSAL_TEMPLATE.md # Release control scripts — keep public tooling; internal/ subdir is excluded by the wildcard above scripts/release_control/ !scripts/release_control/ diff --git a/docs/release-control/control_plane.json b/docs/release-control/control_plane.json index 697b4c15f..e071a2869 100644 --- a/docs/release-control/control_plane.json +++ b/docs/release-control/control_plane.json @@ -12,8 +12,8 @@ "id": "v6", "lifecycle": "active", "root": "docs/release-control/v6", - "prerelease_branch": "pulse/v6", - "stable_branch": "pulse/v6", + "prerelease_branch": "pulse/v6-release", + "stable_branch": "pulse/v6-release", "source_of_truth": "docs/release-control/v6/internal/SOURCE_OF_TRUTH.md", "status": "docs/release-control/v6/internal/status.json", "status_schema": "docs/release-control/v6/status.schema.json", diff --git a/docs/release-control/v6/internal/PRE_RELEASE_CHECKLIST.md b/docs/release-control/v6/internal/PRE_RELEASE_CHECKLIST.md new file mode 100644 index 000000000..694a8d50b --- /dev/null +++ b/docs/release-control/v6/internal/PRE_RELEASE_CHECKLIST.md @@ -0,0 +1,277 @@ +# Pulse v6 Pre-Release Checklist + +Use this as the final gate before cutting a Pulse v6 pre-release. + +## Execution Notes +- Run the commands from the repository root unless a step says otherwise. +- Treat every failed command or failed scenario as a release blocker until it is explained or fixed. +- If the mobile app is part of the release, run the mobile checks from `/Volumes/Development/pulse/repos/pulse-mobile`. +- Record pass/fail notes inline in this file or in the release ticket as each section is completed. +- Use `HIGH_RISK_RELEASE_VERIFICATION_MATRIX.md` for the trust-critical hosted, relay, mobile, entitlement, org-scope, and API-token checks that must be manually confirmed before release. +- Follow `RELEASE_PROMOTION_POLICY.md` for channel routing, RC soak, stable promotion, and rollback expectations. + +## 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. +- 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: + candidate stable tag, promotion channel, promoted RC tag, rollback target, + exact rollback command, exact GA date, and exact v5 end-of-support inputs. +- Mobile is in scope for the release and now has targeted readiness coverage in `pulse-mobile`. +- High-risk release confidence now lives in `docs/release-control/v6/HIGH_RISK_RELEASE_VERIFICATION_MATRIX.md` and should be cleared alongside this checklist. + +## Promotion Policy +- [ ] Record the previous stable tag and exact rollback pin command before publishing a new RC or stable release. +- [ ] For the GA/stable candidate, confirm the release pipeline has already been exercised on a real RC tag, not only linted or YAML-parsed. +- [ ] For stable promotion, confirm the candidate commit has already shipped on `rc`. +- [ ] For stable promotion, confirm the chosen `promoted_from_tag` is a prerelease that was actually published through the governed RC path, not an accidental git tag. +- [ ] For stable promotion, confirm the RC soak window is at least 72 hours or document the hotfix exception explicitly. +- [ ] For stable promotion, confirm paid production tenants are not being moved onto an unvalidated build. +- [ ] For GA/stable promotion, confirm `V5_MAINTENANCE_SUPPORT_POLICY.md` is still the intended policy and replace any placeholder GA notice dates with the exact v6 GA date and exact v5 end-of-support date that will ship with the announcement. +- [ ] For GA/stable promotion, confirm the default-branch copy of `.github/workflows/release-dry-run.yml` already accepts the governed stable rehearsal metadata envelope (`promoted_from_tag`, `rollback_version`, `ga_date`, `v5_eos_date`) through `workflow_dispatch`, because GitHub validates dispatch inputs against the default branch even when dispatching `pulse/v6-release`. +- [ ] For GA/stable promotion, confirm the release rehearsal and publish workflows both derive the governed release branch from release-control metadata instead of hardcoding a branch name inline. +- [ ] For GA/stable promotion, attach a `Release Dry Run` run URL and `rc-to-ga-rehearsal-summary` artifact to the release ticket, and confirm that artifact records the canonical promotion metadata envelope for that candidate: candidate stable tag, promotion channel, promoted RC tag, rollback target, exact rollback command, planned GA date, and planned v5 end-of-support date. + +## Scope +- [x] Confirm whether there is a separate mobile app codebase. +- [x] If yes, do not call the whole product pre-release ready until that app is audited. +- [ ] If no, treat this checklist as the full product gate. + +## Worktree +- [x] Run `git status --porcelain`. +- [x] Confirm there are no tracked modified or staged files. +- [x] Ignore only the known local untracked artifacts if they are expected: + - [x] `.gocache/` + - [x] `.gotmp/` + - [x] `pulse-host-agent` + - [x] `unused_param_test.o` + +Run: + +```bash +cd /Volumes/Development/pulse/repos/pulse +git status --porcelain +``` + +## Canonical v6 Contract +- [ ] Verify the key canonical v6 hardening commits are present in the release target: + - [x] `26a88bf66512ec5cafaf6e597a5ba736d5ff279b` `internal/api/resources` + - [x] `c5eb7057a0b2a7e8dfbcd44470185a72d083feff` `internal/ai/tools` + - [x] `3ab9d6c13` `internal/servicediscovery` + - [x] `f89e4a56bcad9901edd6afc3212172845605d6e7` `internal/api/state_provider` / `router_helpers` + - [x] `7115143825647f5bfade7469fe20a324d3a76dbe` `internal/monitoring/monitor.go` +- [ ] Verify the Patrol and frontend canonicalization commits required for the release target are included. +- [x] Run the key backend/API/AI/frontend test slices that protect canonical v6 behavior. + +Run: + +```bash +cd /Volumes/Development/pulse/repos/pulse +git merge-base --is-ancestor 26a88bf66512ec5cafaf6e597a5ba736d5ff279b HEAD +git merge-base --is-ancestor c5eb7057a0b2a7e8dfbcd44470185a72d083feff HEAD +git merge-base --is-ancestor 3ab9d6c13 HEAD +git merge-base --is-ancestor f89e4a56bcad9901edd6afc3212172845605d6e7 HEAD +git merge-base --is-ancestor 7115143825647f5bfade7469fe20a324d3a76dbe HEAD +go test ./internal/api -run 'Test(FrontendResourceType|ApplyFrontendTypes|ComputeFrontendByType|ParseResourceTypesNodeAlias|UnsupportedResourceTypeFilterTokensRejectsLegacyAliases|ResourceListIncludesKubernetesPods|ResourceListFiltersCanonicalKubernetesNamespace|BuildDiscoveryTargetKubernetesPrefersAgentID|ResourceListRejectsLegacyKubernetesTypeAlias|ResourceListReturnsCanonicalKubernetesMetricsTargets|ResourceListProxmoxNodeReturnsFrontendNodeType|ResourceGetProxmoxNodeReturnsFrontendNodeType|ResourceListRejectsLegacyHostTypeFilter|ResourceListMergesLinkedHost|ResourceListUsesUnifiedSeedProvider|ResourceListDoesNotMergeOneSidedLinkedHost|ResourceGetResource|ResourceLinkMergesResources|ResourceReportMergeCreatesExclusions)$' +go test ./internal/ai/tools +go test ./internal/servicediscovery +go test ./internal/monitoring -run 'TestMonitor(GetUnifiedReadStateOrSnapshot|UnifiedResourceSnapshot)' +cd /Volumes/Development/pulse/repos/pulse/frontend-modern +npx vitest run src/utils/__tests__/frontendResourceTypeBoundaries.test.ts +``` + +## Hosted / Cloud +- [x] Run hosted signup and billing lifecycle tests. +- [x] Verify hosted paid checkout fails closed when org linkage is missing. +- [x] Verify replay succeeds once the linked org exists. +- [ ] Verify the normal hosted provisioning path still succeeds. +- [x] Confirm commit `8b3a5d30246a86005d363b405126b0e9bfdda8d3` is present. +- [ ] Clear gate `hosted-signup-billing-replay` in `HIGH_RISK_RELEASE_VERIFICATION_MATRIX.md`. + +Run: + +```bash +cd /Volumes/Development/pulse/repos/pulse +git merge-base --is-ancestor 8b3a5d30246a86005d363b405126b0e9bfdda8d3 HEAD +go test ./internal/api -run 'TestStripeWebhook_' +go test ./internal/cloudcp/... -count=1 +go test ./internal/hosted/... -count=1 +``` + +Manual scenario: +- Complete a hosted signup or replay a recorded checkout flow. +- Verify webhook handling does not return success when org linkage is unresolved. +- Verify a replay succeeds once the org exists. + +## Relay +- [x] Run Relay tests end to end. +- [ ] Verify fresh register works. +- [ ] Verify reconnect works. +- [x] Verify stale cached session resume recovers by fresh registration. +- [ ] Verify abrupt disconnect and inflight drain behavior still work. +- [x] Confirm commit `ee78cb33dd35b891d84e61bfce2098b4548bbb56` is present. +- [x] Clear gate `relay-registration-reconnect-drain` in `HIGH_RISK_RELEASE_VERIFICATION_MATRIX.md`. + +Run: + +```bash +cd /Volumes/Development/pulse/repos/pulse +git merge-base --is-ancestor ee78cb33dd35b891d84e61bfce2098b4548bbb56 HEAD +go test ./internal/relay -count=1 -timeout=120s +``` + +Manual scenario: +- Connect a Relay client normally. +- Force a stale cached session or server-side session eviction. +- Verify the client retries with fresh registration instead of hanging in resume/backoff loops. + +## Multi-Tenant / MSP +- [x] Run multi-tenant tests. +- [x] Verify unknown non-default orgs fail closed. +- [x] Verify explicitly provisioned orgs initialize correctly. +- [x] Verify there is no default-org fallback for non-default tenant requests. +- [x] Confirm commit `f090a77ae` is present. +- [x] Clear gate `organization-user-scope-and-rbac` in `HIGH_RISK_RELEASE_VERIFICATION_MATRIX.md`. + +Run: + +```bash +cd /Volumes/Development/pulse/repos/pulse +git merge-base --is-ancestor f090a77ae HEAD +go test ./internal/monitoring -run 'TestMultiTenantMonitor' +go test ./internal/api -run 'TestMultiTenantStateProvider_|TestSetMultiTenantMonitor_WiresHandlers|TestResourceHandlers_NonDefaultOrg|TestMultiTenant_ConcurrentAPIStress' +``` + +Manual scenario: +- Use a non-default org header/cookie/token that does not correspond to a provisioned tenant. +- Verify monitor/API access fails closed. +- Provision the tenant explicitly and verify access succeeds afterwards. + +## Frontend Smoke +- [ ] Verify dashboard/workloads. +- [ ] Verify infrastructure/discovery. +- [ ] Verify alerts/investigate. +- [ ] Verify reporting/settings. +- [ ] Verify metrics history / charts. +- [x] Confirm canonical routes and page state still behave correctly. + +Run: + +```bash +cd /Volumes/Development/pulse/repos/pulse/frontend-modern +npx vitest run src/routing/__tests__/resourceLinks.test.ts src/routing/__tests__/navigation.test.ts src/utils/__tests__/frontendResourceTypeBoundaries.test.ts src/api/__tests__/chartsApi.test.ts src/hooks/__tests__/useDashboardTrends.test.ts +``` + +Manual scenario: +- Open workloads, infrastructure/discovery, alerts, reporting, and metrics screens. +- Verify workload URLs use canonical types. +- Verify Kubernetes/pod pages and investigation paths still behave correctly. + +## AI / Tools Smoke +- [x] Verify `pulse_query` works. +- [x] Verify Kubernetes tools work through unified read-state. +- [x] Verify PMG tools still work. +- [x] Verify Patrol scoped runs still behave correctly. +- [x] Clear gate `paid-feature-entitlement-gating` in `HIGH_RISK_RELEASE_VERIFICATION_MATRIX.md`. + +Run: + +```bash +cd /Volumes/Development/pulse/repos/pulse +go test ./internal/ai/tools -count=1 +go test ./internal/ai -count=1 +``` + +Manual scenario: +- Run representative `pulse_query`, Kubernetes, PMG, and Patrol flows. +- Verify scoped Patrol runs only see and mutate in-scope resources. + +## Release-Facing Scenarios +- [ ] Agent registration / install journey. +- [ ] `/api/resources` filtering and resource detail retrieval. +- [ ] Relay pairing flow. +- [ ] Hosted signup / org creation / billing webhook replay. +- [ ] Commercial cancellation / reactivation journey. +- [ ] Multi-tenant org switching / tenant-bound API calls. +- [ ] AI investigate / execute / query on canonical resource types. + +Run: + +```bash +cd /Volumes/Development/pulse/repos/pulse/tests/integration +npx tsc --noEmit +``` + +Manual scenario: +- Walk the user-visible flows end to end in a staging-like environment. +- Confirm the canonical resource model holds up in real UI/API usage, not only in unit tests. + +## Final Verification +- [ ] Run the final targeted Go test slices required for release confidence. +- [ ] Run the relevant frontend tests/build checks for canonicalized surfaces. +- [ ] Document any known unrelated failures explicitly before release. +- [ ] Confirm there are no new high-severity regressions in hosted, relay, tenant isolation, or canonical v6 boundaries. + +Run: + +```bash +cd /Volumes/Development/pulse/repos/pulse +go test ./internal/api/... -count=1 +go test ./internal/relay/... -count=1 +go test ./internal/cloudcp/... -count=1 +go test ./internal/hosted/... -count=1 +go test ./internal/monitoring/... -count=1 +go test ./internal/ai/... -count=1 +cd /Volumes/Development/pulse/repos/pulse/frontend-modern +npx vitest run +``` + +If mobile is in scope, also run: + +```bash +cd /Volumes/Development/pulse/repos/pulse-mobile +git status --porcelain +npm test -- --runTestsByPath src/utils/__tests__/secureStorage.test.ts src/stores/__tests__/instanceStore.test.ts src/stores/__tests__/authStore.test.ts +npm test -- --runTestsByPath src/relay/__tests__/client.test.ts src/relay/__tests__/client-hardening.test.ts +npm test -- --runTestsByPath src/stores/__tests__/approvalStore.test.ts +``` + +Mobile automated checks completed: +- [x] Secure storage / auth / instance persistence +- [x] Relay reconnect and hardening tests +- [x] Approval store state consistency +- [x] Clear gate `mobile-relay-auth-approvals` in `HIGH_RISK_RELEASE_VERIFICATION_MATRIX.md`. + +## High-Risk Trust Gates +- [ ] Clear gate `commercial-cancellation-reactivation`. +- [ ] Clear gate `hosted-signup-billing-replay`. +- [x] Clear gate `paid-feature-entitlement-gating`. +- [ ] Clear gate `rc-to-ga-promotion-readiness`. +- [x] Clear gate `relay-registration-reconnect-drain`. +- [x] Clear gate `mobile-relay-auth-approvals`. +- [x] Clear gate `organization-user-scope-and-rbac`. +- [x] Clear gate `api-token-scope-and-assignment`. +- [ ] Clear gate `cloud-hosted-tier-runtime-readiness`. +- [ ] Clear gate `msp-provider-tenant-management`. + +## Release Decision +Mark Pulse v6 pre-release ready only if all of the following are true: +- [ ] The smoke checks above pass. +- [x] There is no separate unaudited mobile app. +- [ ] There is no tracked dirty state. +- [ ] No new high-severity regressions appear. + +## Stable Promotion Decision +Mark a Pulse v6 build stable-promotion ready only if all of the following are true: +- [ ] The candidate commit has already been exercised on `rc`. +- [ ] The `Promotion Policy` section above is complete. +- [ ] Applicable items in `HIGH_RISK_RELEASE_VERIFICATION_MATRIX.md` are cleared. +- [ ] The rollback target and exact reinstall command are recorded. +- [ ] The release pipeline has already been exercised on the candidate RC path in a real run, not only statically validated. +- [ ] The v5 maintenance-only support policy, exact GA/EOS dates, and release-note notice are written and ready to publish. +- [ ] The RC-to-GA rehearsal record and dry-run artifact are linked from the release ticket. diff --git a/docs/release-control/v6/internal/RC_TO_GA_REHEARSAL_TEMPLATE.md b/docs/release-control/v6/internal/RC_TO_GA_REHEARSAL_TEMPLATE.md new file mode 100644 index 000000000..eb8aa6fbf --- /dev/null +++ b/docs/release-control/v6/internal/RC_TO_GA_REHEARSAL_TEMPLATE.md @@ -0,0 +1,49 @@ +# Pulse v6 RC-to-GA Rehearsal Record + +Use this template to capture the human-side evidence for +`rc-to-ga-promotion-readiness` after running the `Release Dry Run` workflow. + +Prefer generating the record with +`python3 scripts/release_control/record_rc_to_ga_rehearsal.py ...` rather than +hand-writing it. + +The matching GitHub Actions artifact should be the machine-generated +`rc-to-ga-rehearsal-summary`. + +Treat that artifact as the machine-owned source for the candidate stable tag, +promotion channel, promoted RC tag, rollback target, exact rollback command, +and planned GA/EOS dates. Only override those values in the human record if +the operator is correcting a verified artifact mismatch. + +The generator should fail closed if the artifact omits any of that promotion +metadata. Do not repair a malformed artifact by hand-writing the missing +candidate tag, promotion channel, promoted RC tag, rollback target, rollback +command, or GA/EOS dates into the record. + +## Required Fields + +1. Rehearsal date: +2. GitHub Actions run URL: +3. Version under rehearsal: +4. Candidate stable tag: +5. Promotion channel: +6. Promoted RC tag: +7. Current rollback target: +8. Exact rollback or reinstall command: +9. RC soak hours at rehearsal time: +10. Exact GA date to publish with GA: +11. v5 end-of-support date to publish with GA: +12. Result: + +## Minimum Human Notes + +1. Confirm the rehearsed branch matched the governed release line from `control_plane.json` (currently `pulse/v6-release`). +2. Confirm the release path was exercised end to end up to, but not including, + publication. +3. Confirm no manual input was surprising or ambiguous during the run. +4. Confirm the artifact-owned candidate stable tag, promotion channel, + promoted RC tag, rollback target, exact rollback command, planned GA date, + and planned v5 end-of-support date match the intended publish notice. +5. Record any follow-up issue that must be fixed before the real promotion. +6. Link this record and the matching artifact from the release ticket before + clearing the gate. diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index 42b032e15..1da836482 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -89,6 +89,9 @@ for stable-versus-RC metadata validation shared by `.github/workflows/release-dr and `.github/workflows/create-release.yml`. Promotion rollback targets, promoted RC lineage, soak checks, and GA/v5 notice metadata may not drift between those two workflows through duplicated inline shell validation. +Those same workflows must also fetch and dispatch the governed release branch +derived from release-control metadata instead of hardcoding `pulse/v6`, +`pulse/v6-release`, or any later branch literal inline. That same `internal/updates/` boundary now also owns runtime data-dir authority for temp, backup, and cleanup behavior: `manager.go` must resolve its working directories through the shared runtime data-dir helper instead of diff --git a/docs/releases/V6_PRERELEASE_RUNBOOK.md b/docs/releases/V6_PRERELEASE_RUNBOOK.md index bfc3d12e1..03454f63e 100644 --- a/docs/releases/V6_PRERELEASE_RUNBOOK.md +++ b/docs/releases/V6_PRERELEASE_RUNBOOK.md @@ -13,7 +13,7 @@ release-control policy wins. ## Branch Model (Current) - `main`: v5 stable (current public/stable line) -- `pulse/v6`: active v6 prerelease and stable release line until an explicit +- `pulse/v6-release`: active v6 prerelease and stable release line until an explicit post-GA branch cutover is governed Do not move `main` to v6 during prerelease. @@ -26,7 +26,7 @@ Release workflows now enforce branch/tag lineage rules: - Resolves stable versus prerelease branch requirements from `docs/release-control/control_plane.json`. - For the current v6 profile, both stable and prerelease releases dispatch - from `pulse/v6`. + from `pulse/v6-release`. - `publish-docker.yml`, `promote-floating-tags.yml`, `publish-helm-chart.yml`, and `update-demo-server.yml` - Validate the release tag commit is reachable from the governed branch for @@ -54,34 +54,34 @@ The workflow auto-marks `-rc.N`/`-alpha.N`/`-beta.N` as prerelease. ## Preconditions for Each RC -1. `pulse/v6` is pushed and green in CI. -2. `VERSION` file in `pulse/v6` exactly matches the release input version. +1. `pulse/v6-release` is pushed and green in CI. +2. `VERSION` file in `pulse/v6-release` exactly matches the release input version. 3. Release notes are prepared. 4. `PULSE_LICENSE_PUBLIC_KEY` secret is present in GitHub Actions. ## RC Release Steps -1. Update version on `pulse/v6`: +1. Update version on `pulse/v6-release`: ```bash -git checkout pulse/v6 +git checkout pulse/v6-release git pull --ff-only echo "6.0.0-rc.1" > VERSION git add VERSION git commit -m "chore(release): bump version to 6.0.0-rc.1" -git push origin pulse/v6 +git push origin pulse/v6-release ``` 2. Optional preflight dry run: - Run workflow: `Release Dry Run` - - Ref: `pulse/v6` + - Ref: `pulse/v6-release` - Inputs: - `version`: `6.0.0-rc.1` - optional `note` 3. Create draft prerelease: - Run workflow: `Pulse Release Pipeline` - - Ref: `pulse/v6` + - Ref: `pulse/v6-release` - Inputs: - `version`: `6.0.0-rc.1` - `release_notes`: markdown text @@ -93,19 +93,19 @@ git push origin pulse/v6 - Smoke install on a test host/container. 5. Publish prerelease: - - Re-run `Pulse Release Pipeline` on `pulse/v6` + - Re-run `Pulse Release Pipeline` on `pulse/v6-release` - Same `version` and notes - `draft_only`: `false` - Demo server update is skipped automatically for prereleases. 6. Canary rollout: - Upgrade a small user subset first. - - Collect regressions, fix on `pulse/v6`, then cut `rc.2`/`rc.3` as needed. + - Collect regressions, fix on `pulse/v6-release`, then cut `rc.2`/`rc.3` as needed. ## Keep v5 Stable During v6 RC - Continue v5 patch releases from `main` as normal. -- Do not merge `pulse/v6` into `main` during prerelease. +- Do not merge `pulse/v6-release` into `main` during prerelease. - Keep v5 and v6 changelogs/release notes separate. ## GA Cutover (Only After RC Confidence) @@ -121,16 +121,16 @@ git checkout -b pulse/v5-maintenance git push -u origin pulse/v5-maintenance ``` -2. Keep the governed v6 release line on `pulse/v6` for GA: +2. Keep the governed v6 release line on `pulse/v6-release` for GA: ```bash -git checkout pulse/v6 +git checkout pulse/v6-release git pull --ff-only ``` -3. Release `6.0.0` from `pulse/v6` using `Pulse Release Pipeline`. +3. Release `6.0.0` from `pulse/v6-release` using `Pulse Release Pipeline`. Before the real GA publish, run `./scripts/trigger-release-dry-run.sh 6.0.0` - from `pulse/v6`. That helper validates the default-branch workflow-dispatch + from `pulse/v6-release`. That helper validates the default-branch workflow-dispatch contract before calling GitHub so stale `main` workflow inputs fail locally instead of returning an opaque 422 from `gh workflow run`. The governed `Release Dry Run` must still carry: @@ -157,7 +157,7 @@ git pull --ff-only installer or updater failures, licensing or billing blockers, and safe migration blockers are eligible during that window. - After that window, v5 is unsupported. -6. Treat any future move of stable v6 releases away from `pulse/v6` as a +6. Treat any future move of stable v6 releases away from `pulse/v6-release` as a separate post-GA governance change; do not assume an automatic cutover to `main`. @@ -166,13 +166,13 @@ git pull --ff-only If an RC is bad: 1. Do not promote to GA. -2. Keep fixing on `pulse/v6`. +2. Keep fixing on `pulse/v6-release`. 3. Cut next RC. 4. Keep v5 users on `main` stable releases. If GA has a severe regression: -1. Patch quickly on `pulse/v6` (v6.0.1), or +1. Patch quickly on `pulse/v6-release` (v6.0.1), or 2. Advise affected users to hold at prior stable while fix ships. 3. Continue v5 emergency fixes from `pulse/v5-maintenance` only if the published maintenance-only window is still active or I explicitly announce diff --git a/internal/repoctl/canonical_development_protocol_test.go b/internal/repoctl/canonical_development_protocol_test.go index cdfc13ebd..673599dac 100644 --- a/internal/repoctl/canonical_development_protocol_test.go +++ b/internal/repoctl/canonical_development_protocol_test.go @@ -494,8 +494,8 @@ func TestReleaseControlPlaneFilesExist(t *testing.T) { "\"system\": \"pulse-release-control\"", "\"agent_values_doc\": \"docs/release-control/internal/AGENT_VALUES.md\"", "\"active_profile_id\": \"v6\"", - "\"prerelease_branch\": \"pulse/v6\"", - "\"stable_branch\": \"pulse/v6\"", + "\"prerelease_branch\": \"pulse/v6-release\"", + "\"stable_branch\": \"pulse/v6-release\"", "\"completion_rule\": \"rc_ready\"", "\"completion_rule\": \"manual\"", "\"completion_rule\": \"release_ready\"", diff --git a/scripts/release_control/control_plane_audit_test.py b/scripts/release_control/control_plane_audit_test.py index 4e4634ba5..88ecfc378 100644 --- a/scripts/release_control/control_plane_audit_test.py +++ b/scripts/release_control/control_plane_audit_test.py @@ -18,8 +18,8 @@ VALID_PAYLOAD = { "id": "v6", "lifecycle": "active", "root": "docs/release-control/v6", - "prerelease_branch": "pulse/v6", - "stable_branch": "pulse/v6", + "prerelease_branch": "pulse/v6-release", + "stable_branch": "pulse/v6-release", "source_of_truth": "docs/release-control/v6/internal/SOURCE_OF_TRUTH.md", "status": "docs/release-control/v6/internal/status.json", "status_schema": "docs/release-control/v6/status.schema.json", @@ -156,8 +156,8 @@ class ControlPlaneAuditTest(unittest.TestCase): self.assertFalse(report["summary"]["active_target_completion_met"]) def test_release_branch_for_version_uses_profile_branch_policy(self) -> None: - self.assertEqual(release_branch_for_version("6.0.0-rc.1", control_plane=VALID_PAYLOAD), "pulse/v6") - self.assertEqual(release_branch_for_version("6.0.0", control_plane=VALID_PAYLOAD), "pulse/v6") + self.assertEqual(release_branch_for_version("6.0.0-rc.1", control_plane=VALID_PAYLOAD), "pulse/v6-release") + self.assertEqual(release_branch_for_version("6.0.0", control_plane=VALID_PAYLOAD), "pulse/v6-release") def test_audit_flags_stale_active_target(self) -> None: report = audit_control_plane_payload( diff --git a/scripts/release_control/record_rc_to_ga_blocked.py b/scripts/release_control/record_rc_to_ga_blocked.py index 7ef2815fc..c9b6876a6 100644 --- a/scripts/release_control/record_rc_to_ga_blocked.py +++ b/scripts/release_control/record_rc_to_ga_blocked.py @@ -10,7 +10,7 @@ import subprocess from datetime import date from pathlib import Path -from control_plane import DEFAULT_CONTROL_PLANE +from control_plane import DEFAULT_CONTROL_PLANE, release_branch_for_version from repo_file_io import REPO_ROOT, git_env, read_repo_text @@ -166,6 +166,8 @@ def build_blocked_record(*, record_date: str) -> str: status = read_json(str(DEFAULT_CONTROL_PLANE["status_path"].relative_to(REPO_ROOT))) version = read("VERSION").strip() stable_version = stable_candidate_version(version) + working_branch = release_branch_for_version(version, control_plane=control_plane) + stable_branch = release_branch_for_version(stable_version, control_plane=control_plane) version_is_prerelease = stable_version != version active_profile = next( profile for profile in control_plane["profiles"] if profile["id"] == control_plane["active_profile_id"] @@ -185,7 +187,7 @@ def build_blocked_record(*, record_date: str) -> str: if version_is_prerelease: version_fact_lines = [ [ - f"The active local `pulse/v6` branch currently reports `VERSION={version}`, so the", + f"The active local `{working_branch}` branch currently reports `VERSION={version}`, so the", f"working line is still prerelease and there is not yet a governed local stable", f"`{stable_version}` candidate.", ], @@ -197,7 +199,7 @@ def build_blocked_record(*, record_date: str) -> str: else: version_fact_lines = [ [ - f"The active local `pulse/v6` branch currently reports `VERSION={version}`, so a", + f"The active local `{working_branch}` branch currently reports `VERSION={version}`, so a", "local GA candidate exists on the governed stable line.", ], [ @@ -221,9 +223,9 @@ def build_blocked_record(*, record_date: str) -> str: ] required_step_lines = [ [ - "Push the governed `pulse/v6` branch state that is intended to become the", + f"Push the governed `{stable_branch}` branch state that is intended to become the", f"stable `{stable_version}` candidate, including the eventual `VERSION={stable_version}`", - "change and release-control records, to `origin/pulse/v6`.", + f"change and release-control records, to `origin/{stable_branch}`.", ], ] else: @@ -235,8 +237,8 @@ def build_blocked_record(*, record_date: str) -> str: ] required_step_lines = [ [ - "Push the governed `pulse/v6` branch state, including the current", - f"`VERSION={version}` candidate and release-control records, to `origin/pulse/v6`.", + f"Push the governed `{working_branch}` branch state, including the current", + f"`VERSION={version}` candidate and release-control records, to `origin/{working_branch}`.", ], ] else: @@ -259,9 +261,9 @@ def build_blocked_record(*, record_date: str) -> str: "`v6-ga-promotion` only when that change is actually intended.", ], [ - "Push the governed `pulse/v6` branch state that is intended to become the", + f"Push the governed `{stable_branch}` branch state that is intended to become the", f"stable `{stable_version}` candidate, including the eventual `VERSION={stable_version}`", - "change and release-control records, to `origin/pulse/v6`.", + f"change and release-control records, to `origin/{stable_branch}`.", ], ] else: @@ -278,8 +280,8 @@ def build_blocked_record(*, record_date: str) -> str: "`v6-ga-promotion` only when that change is actually intended.", ], [ - "Push the governed `pulse/v6` branch state, including the current", - f"`VERSION={version}` candidate and release-control records, to `origin/pulse/v6`.", + f"Push the governed `{working_branch}` branch state, including the current", + f"`VERSION={version}` candidate and release-control records, to `origin/{working_branch}`.", ], ] @@ -366,7 +368,7 @@ def build_blocked_record(*, record_date: str) -> str: [ f"Land the canonical `{RELEASE_DRY_RUN_WORKFLOW}` workflow-dispatch input", f"contract on the default branch `{default_branch}` so GitHub accepts the", - "governed stable rehearsal metadata envelope when dispatching from `pulse/v6`.", + f"governed stable rehearsal metadata envelope when dispatching from `{stable_branch}`.", ] ) if not rc_tag: @@ -379,7 +381,7 @@ def build_blocked_record(*, record_date: str) -> str: ) required_blocks.append( [ - "Run `Release Dry Run` from `pulse/v6` using that published RC as", + f"Run `Release Dry Run` from `{stable_branch}` using that published RC as", "`promoted_from_tag` with:", f"- `version={stable_version}`", "- an artifact-owned candidate stable tag matching that rehearsal", @@ -396,7 +398,7 @@ def build_blocked_record(*, record_date: str) -> str: else: required_blocks.append( [ - "Run `Release Dry Run` from `pulse/v6` with:", + f"Run `Release Dry Run` from `{stable_branch}` with:", f"- `version={stable_version}`", f"- `promoted_from_tag={rc_tag}`", "- an artifact-owned candidate stable tag matching that rehearsal", diff --git a/scripts/release_control/release_promotion_policy_test.py b/scripts/release_control/release_promotion_policy_test.py index 63d900a1a..e2c0f6125 100644 --- a/scripts/release_control/release_promotion_policy_test.py +++ b/scripts/release_control/release_promotion_policy_test.py @@ -11,6 +11,7 @@ import json import record_rc_to_ga_blocked as blocked_record from release_promotion_policy_support import ( + REQUIRED_STAGED_GOVERNANCE_INPUTS, promotion_metadata_envelope, slice_requires_staged_governance_inputs, staged_governance_input_errors, @@ -24,7 +25,7 @@ def read(rel: str) -> str: return read_repo_text( rel, staged=USE_STAGED_GOVERNANCE, - strict_staged=USE_STAGED_GOVERNANCE, + strict_staged=USE_STAGED_GOVERNANCE and rel in REQUIRED_STAGED_GOVERNANCE_INPUTS, ) @@ -126,6 +127,7 @@ class ReleasePromotionPolicyTest(unittest.TestCase): self.assertIn(promotion_metadata_envelope(), normalize_ws(template)) self.assertIn("rc-to-ga-rehearsal-summary", workflow) self.assertIn("control_plane.py --branch-for-version", workflow) + self.assertIn('git fetch --prune origin main "${REQUIRED_BRANCH}" --tags', workflow) self.assertIn("resolve_release_promotion.py", workflow) self.assertIn("- Rollback command:", workflow) self.assertIn("- Candidate stable tag:", workflow) @@ -150,6 +152,7 @@ class ReleasePromotionPolicyTest(unittest.TestCase): source_of_truth = read("docs/release-control/v6/internal/SOURCE_OF_TRUTH.md") resolver = read("scripts/release_control/resolve_release_promotion.py") self.assertIn("control_plane.py --branch-for-version", content) + self.assertIn('git fetch --prune origin main "${REQUIRED_BRANCH}" --tags', content) self.assertIn("resolve_release_promotion.py", content) self.assertIn("Rollback command:", content) self.assertIn("rollback target and exact reinstall command recorded", policy) @@ -182,6 +185,11 @@ class ReleasePromotionPolicyTest(unittest.TestCase): checklist = read("docs/release-control/v6/internal/PRE_RELEASE_CHECKLIST.md") self.assertIn("default-branch copy of `.github/workflows/release-dry-run.yml`", checklist) self.assertIn("workflow_dispatch", checklist) + self.assertIn("dispatching `pulse/v6-release`", checklist) + self.assertIn("derive the governed release branch from release-control metadata", checklist) + template = read("docs/release-control/v6/internal/RC_TO_GA_REHEARSAL_TEMPLATE.md") + self.assertIn("governed release line from `control_plane.json`", template) + self.assertIn("pulse/v6-release", template) def test_release_artifact_workflows_refuse_stable_without_matching_rc(self) -> None: publish = read(".github/workflows/publish-docker.yml") @@ -196,7 +204,7 @@ class ReleasePromotionPolicyTest(unittest.TestCase): self.assertIn("does not descend from any matching RC tag", publish) self.assertIn("does not descend from any matching RC tag", promote) self.assertIn("both stable and prerelease releases dispatch", runbook) - self.assertIn("Release `6.0.0` from `pulse/v6`", runbook) + self.assertIn("Release `6.0.0` from `pulse/v6-release`", runbook) self.assertIn(promotion_metadata_envelope(), normalize_ws(runbook)) def test_blocked_record_tracks_current_target_and_candidate_version(self) -> None: diff --git a/scripts/remerge-parallel.sh b/scripts/remerge-parallel.sh index 30ce1cd7c..0fc3e2eae 100755 --- a/scripts/remerge-parallel.sh +++ b/scripts/remerge-parallel.sh @@ -16,7 +16,7 @@ set -euo pipefail # ./scripts/remerge-parallel.sh --status # show progress # # Prerequisites: -# - On the pulse/v6 branch +# - On the pulse/v6-release branch # - All parallel branches exist locally REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)" @@ -175,8 +175,8 @@ fi if [ "$MODE" = "execute" ]; then current_branch=$(git branch --show-current) - if [ "$current_branch" != "pulse/v6" ]; then - echo "Error: must be on pulse/v6 branch (currently on: $current_branch)" + if [ "$current_branch" != "pulse/v6-release" ]; then + echo "Error: must be on pulse/v6-release branch (currently on: $current_branch)" exit 1 fi @@ -192,19 +192,19 @@ if [ "$MODE" = "execute" ]; then total=$(wc -l < "$TIMELINE_FILE" | tr -d ' ') echo "" - echo "This will reset pulse/v6 to $PRE_MERGE_BASE and cherry-pick $total commits." + echo "This will reset pulse/v6-release to $PRE_MERGE_BASE and cherry-pick $total commits." echo "Current HEAD: $(git log --oneline -1 HEAD)" echo "" read -rp "Type 'yes' to proceed: " confirm [ "$confirm" = "yes" ] || { echo "Aborted."; exit 1; } # Save backup - git branch -f pulse/v6-pre-remerge-backup HEAD - log "Backup saved: pulse/v6-pre-remerge-backup -> $(git rev-parse --short HEAD)" + git branch -f pulse/v6-release-pre-remerge-backup HEAD + log "Backup saved: pulse/v6-release-pre-remerge-backup -> $(git rev-parse --short HEAD)" # Reset git reset --hard "$PRE_MERGE_BASE" - log "Reset pulse/v6 to $PRE_MERGE_BASE" + log "Reset pulse/v6-release to $PRE_MERGE_BASE" save_progress 0 fi @@ -347,8 +347,8 @@ log "║ Final build: PASS" log "║ HEAD: $(git log --oneline -1 HEAD)" log "╚══════════════════════════════════════════════════════════╝" log "" -log "Backup of old v6: pulse/v6-pre-remerge-backup" -log "To delete: git branch -D pulse/v6-pre-remerge-backup" +log "Backup of old v6: pulse/v6-release-pre-remerge-backup" +log "To delete: git branch -D pulse/v6-release-pre-remerge-backup" # Clean up state rm -rf "$STATE_DIR"