Align v6 release branch governance

This commit is contained in:
rcourtman
2026-03-19 10:18:45 +00:00
parent 43cdc30f9b
commit 11c50eb7a0
13 changed files with 397 additions and 54 deletions
+1 -1
View File
@@ -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)"
+1 -1
View File
@@ -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}"
+4
View File
@@ -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/
+2 -2
View File
@@ -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",
@@ -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.
@@ -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.
@@ -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
+19 -19
View File
@@ -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
@@ -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\"",
@@ -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(
@@ -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",
@@ -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:
+9 -9
View File
@@ -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"