mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Align v6 release branch governance
This commit is contained in:
@@ -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)"
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user