Rename promotion RC wording to prerelease

This commit is contained in:
rcourtman 2026-03-25 11:08:05 +00:00
parent 31d9d96f82
commit cb41473018
19 changed files with 126 additions and 111 deletions

View file

@ -1,5 +1,5 @@
name: Pulse Release Pipeline
# Optimized: parallel jobs, fast RC path
# Optimized: parallel jobs, fast prerelease path
on:
workflow_dispatch:
@ -13,7 +13,7 @@ on:
required: true
type: string
promoted_from_tag:
description: 'Stable only: RC tag being promoted (for example 6.0.0-rc.2)'
description: 'Stable only: prerelease tag being promoted (for example 6.0.0-rc.2)'
required: false
type: string
rollback_version:
@ -29,7 +29,7 @@ on:
required: false
type: string
hotfix_exception:
description: 'Stable only: bypass the 72-hour RC soak for urgent customer harm'
description: 'Stable only: bypass the 72-hour prerelease soak for urgent customer harm'
required: false
type: boolean
default: false
@ -503,9 +503,9 @@ jobs:
echo "- Promotion channel: ${{ needs.prepare.outputs.is_prerelease == 'true' && 'rc' || 'stable' }}"
echo "- Candidate stable tag: ${{ needs.prepare.outputs.tag }}"
if [ -n "${{ needs.prepare.outputs.promoted_from_tag }}" ]; then
echo "- Promoted RC tag: ${{ needs.prepare.outputs.promoted_from_tag }}"
echo "- Promoted prerelease tag: ${{ needs.prepare.outputs.promoted_from_tag }}"
else
echo "- Promoted RC tag: n/a"
echo "- Promoted prerelease tag: n/a"
fi
echo "- Rollback target: ${{ needs.prepare.outputs.rollback_tag }}"
echo "- Rollback command: \`${{ needs.prepare.outputs.rollback_command }}\`"

View file

@ -108,7 +108,7 @@ jobs:
BASE_VERSION="${BASE_VERSION%%-*}"
mapfile -t RC_TAGS < <(git tag -l "v${BASE_VERSION}-rc.*" --sort=-version:refname)
if [ "${#RC_TAGS[@]}" -eq 0 ]; then
echo "::error::Stable tag ${TAG} has no matching RC tags for base version ${BASE_VERSION}."
echo "::error::Stable tag ${TAG} has no matching prerelease tags for base version ${BASE_VERSION}."
exit 1
fi
@ -122,7 +122,7 @@ jobs:
done
if [ -z "$MATCHED_RC" ]; then
echo "::error::Stable tag ${TAG} does not descend from any matching RC tag for base version ${BASE_VERSION}."
echo "::error::Stable tag ${TAG} does not descend from any matching prerelease tag for base version ${BASE_VERSION}."
exit 1
fi

View file

@ -80,7 +80,7 @@ jobs:
BASE_VERSION="${BASE_VERSION%%-*}"
mapfile -t RC_TAGS < <(git tag -l "v${BASE_VERSION}-rc.*" --sort=-version:refname)
if [ "${#RC_TAGS[@]}" -eq 0 ]; then
echo "::error::Stable tag ${TAG} has no matching RC tags for base version ${BASE_VERSION}."
echo "::error::Stable tag ${TAG} has no matching prerelease tags for base version ${BASE_VERSION}."
exit 1
fi
@ -94,7 +94,7 @@ jobs:
done
if [ -z "$MATCHED_RC" ]; then
echo "::error::Stable tag ${TAG} does not descend from any matching RC tag for base version ${BASE_VERSION}."
echo "::error::Stable tag ${TAG} does not descend from any matching prerelease tag for base version ${BASE_VERSION}."
exit 1
fi

View file

@ -8,7 +8,7 @@ on:
required: false
type: string
promoted_from_tag:
description: 'Stable rehearsal only: RC tag being promoted (for example v6.0.0-rc.2)'
description: 'Stable rehearsal only: prerelease tag being promoted (for example v6.0.0-rc.2)'
required: false
type: string
rollback_version:
@ -24,7 +24,7 @@ on:
required: false
type: string
hotfix_exception:
description: 'Stable rehearsal only: bypass 72-hour RC soak for urgent customer harm'
description: 'Stable rehearsal only: bypass 72-hour prerelease soak for urgent customer harm'
required: false
type: boolean
default: false
@ -256,7 +256,7 @@ jobs:
SUMMARY_FILE="release-dry-run/rc-to-ga-rehearsal-summary.md"
RUN_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
{
echo "# RC-to-GA Rehearsal Summary"
echo "# Prerelease-to-GA Rehearsal Summary"
echo ""
echo "- Workflow run: ${RUN_URL}"
echo "- Branch: ${GITHUB_REF_NAME}"
@ -264,7 +264,7 @@ jobs:
echo "- Candidate stable tag: ${{ steps.rehearsal.outputs.tag }}"
echo "- Promotion channel: ${{ steps.rehearsal.outputs.is_prerelease == 'true' && 'rc' || 'stable' }}"
if [ -n "${{ steps.rehearsal.outputs.promoted_from_tag }}" ]; then
echo "- Promoted RC tag: ${{ steps.rehearsal.outputs.promoted_from_tag }}"
echo "- Promoted prerelease tag: ${{ steps.rehearsal.outputs.promoted_from_tag }}"
fi
if [ -n "${{ steps.rehearsal.outputs.rollback_tag }}" ]; then
echo "- Rollback target: ${{ steps.rehearsal.outputs.rollback_tag }}"

View file

@ -14,7 +14,7 @@ The evergreen control-plane files live one level up:
1. `../CONTROL_PLANE.md` (evergreen governance, active-profile selection, and active-target rules)
2. `../control_plane.json` (machine-readable active profile and active target)
3. `internal/SOURCE_OF_TRUTH.md` (stable human governance, readiness-assertion design rules, and locked decisions)
4. `internal/RELEASE_PROMOTION_POLICY.md` (canonical stable-versus-RC promotion rules, rollout criteria, and rollback expectations)
4. `internal/RELEASE_PROMOTION_POLICY.md` (canonical stable-versus-prerelease promotion rules, rollout criteria, and rollback expectations)
5. `internal/status.json` (live lane state, coverage-gap discovery records, readiness derivation rules, the active readiness assertion catalog, lane-to-subsystem ownership, structured evidence references, typed lane/subsystem decision records, and canonical ordered lists)
6. `status.schema.json` (machine-readable contract for the `status.json` shape)
7. `internal/subsystems/registry.json` (machine-readable subsystem ownership, explicit shared-ownership exceptions, and proof routing)
@ -22,7 +22,7 @@ The evergreen control-plane files live one level up:
`status.json` reporting every lane as `target-met` means the tracked v6
repo-hardening work is at target. It does not, by itself, mean Pulse v6 is
ready to cut an RC or approve stable/GA while phase-blocking
ready to cut a prerelease or approve stable/GA while phase-blocking
`readiness_assertions`, `open_decisions`, or `release_gates` remain unresolved.
Use `python3 scripts/release_control/status_audit.py --pretty` for the current
derived `repo_ready`, `rc_ready`, and `release_ready` summary.
@ -62,7 +62,7 @@ Supporting governance file:
- `CONSOLIDATION_MAP.md` (legacy-doc demotion and archival map)
- `RETIREMENT_AUDIT_2026-02-27.md` (file-by-file audited retirement decisions)
- `internal/CANONICAL_DEVELOPMENT_PROTOCOL.md` (canonical subsystem development protocol)
- `internal/RELEASE_PROMOTION_POLICY.md` (customer-facing release-train contract for `stable` versus `rc`)
- `internal/RELEASE_PROMOTION_POLICY.md` (customer-facing release-train contract for `stable` versus the prerelease `rc` channel)
- `internal/HIGH_RISK_RELEASE_VERIFICATION_MATRIX.md` (manual verification runbook for trust-critical release gates)
- `internal/RC_TO_GA_REHEARSAL_TEMPLATE.md` (human record template for the non-publish GA rehearsal run)
- `internal/subsystems/*.md` (per-subsystem contracts: truth, shared boundaries, extension points, forbidden paths, completion obligations)

View file

@ -20,14 +20,14 @@ Use this as the final gate before cutting a Pulse v6 pre-release.
- 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,
candidate stable tag, promotion channel, promoted prerelease 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/internal/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 the GA/stable candidate, confirm the release pipeline has already been exercised on a real prerelease 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.
@ -35,7 +35,7 @@ Use this as the final gate before cutting a Pulse v6 pre-release.
- [ ] 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.
- [ ] 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 prerelease 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.

View file

@ -1,4 +1,4 @@
# Pulse v6 RC-to-GA Rehearsal Record
# Pulse v6 Prerelease-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.
@ -11,13 +11,13 @@ 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,
promotion channel, promoted prerelease 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
candidate tag, promotion channel, promoted prerelease tag, rollback target, rollback
command, or GA/EOS dates into the record.
## Required Fields
@ -27,10 +27,10 @@ command, or GA/EOS dates into the record.
3. Version under rehearsal:
4. Candidate stable tag:
5. Promotion channel:
6. Promoted RC tag:
6. Promoted prerelease tag:
7. Current rollback target:
8. Exact rollback or reinstall command:
9. RC soak hours at rehearsal time:
9. Prerelease 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:
@ -42,7 +42,7 @@ command, or GA/EOS dates into the record.
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,
promoted prerelease 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

View file

@ -79,7 +79,7 @@ This file must not contain:
Per-subsystem contracts: truth, extension points, forbidden paths, and
completion obligations.
11. `docs/release-control/v6/internal/RELEASE_PROMOTION_POLICY.md`
Canonical stable-versus-RC promotion rules, rollout criteria, and rollback
Canonical stable-versus-prerelease promotion rules, rollout criteria, and rollback
expectations for v6 and later release lines.
12. `docs/release-control/v6/internal/V5_MAINTENANCE_SUPPORT_POLICY.md`
Canonical v5 maintenance-only support policy, release-line rules, and GA
@ -309,7 +309,7 @@ Assertion design rules:
header chrome, or section treatment without explicit justification.
13. Do not keep polishing strong lanes while weak lanes remain behind.
14. Do not treat `status.json` lane scores reaching target as sufficient release approval by themselves; open operational decisions, machine-derived unresolved readiness assertions, and unresolved release gates still apply.
15. Do not promote v6 to stable or GA without an exercised RC, a real
15. Do not promote v6 to stable or GA without an exercised prerelease, a real
release-pipeline proof run, a recorded rollback target plus exact
reinstall command, and a written v5 maintenance-only support policy.
16. Do not declare a lane or blocker complete just because the first passing

View file

@ -1,4 +1,4 @@
# RC-to-GA Promotion Readiness Blocked Record
# Prerelease-to-GA Promotion Readiness Blocked Record
- Date: `2026-03-13`
- Gate: `rc-to-ga-promotion-readiness`
@ -6,9 +6,9 @@
## Blocking Facts
1. No Pulse v6 RC has shipped yet.
1. No Pulse v6 prerelease has shipped yet.
2. The repository contains accidental prerelease git tag history (`v6.0.0-rc.1`),
but those tags were never published and do not count as shipped RC lineage.
but those tags were never published and do not count as shipped prerelease lineage.
3. GitHub still validates `Release Dry Run` workflow dispatch inputs against the
default branch `main`, whose current workflow contract does not
accept the governed stable rehearsal metadata envelope (`promoted_from_tag`,
@ -21,30 +21,31 @@
6. The active local `pulse/v6-release` branch currently reports `VERSION=6.0.0-rc.1`, so the
working line is still prerelease and there is not yet a governed local stable
`6.0.0` candidate.
7. There is still no governed `RC-to-GA Rehearsal Record` proving a successful
7. There is still no governed `Prerelease-to-GA Rehearsal Record` proving a successful
non-publish `Release Dry Run` for the eventual stable `6.0.0` candidate.
8. `docs/releases/RELEASE_NOTES_v6.md` and
`docs/release-control/v6/internal/V5_MAINTENANCE_SUPPORT_POLICY.md` now carry the
currently proposed exact dates for the eventual GA notice:
- `v6` GA date: `2026-03-24`
- `v5` end-of-support date: `2026-06-22`
`docs/release-control/v6/internal/V5_MAINTENANCE_SUPPORT_POLICY.md` still leave the
GA announcement dates as placeholders because no real prerelease lineage or GA-ready
rehearsal has locked them yet:
- `v6` GA date placeholder: `[v6-ga-date]`
- `v5` end-of-support placeholder: `[v5-eos-date]`
9. There is still no governed `Release Dry Run` artifact or rehearsal record
exercising stable inputs for:
- `version=6.0.0`
- no governed `promoted_from_tag` exists yet because no RC has shipped
- no governed `promoted_from_tag` exists yet because no prerelease has shipped
- the artifact-owned candidate stable tag for that rehearsal
- the artifact-owned promotion channel for that rehearsal
- the artifact-owned promoted RC tag for that rehearsal
- the artifact-owned promoted prerelease tag for that rehearsal
- the artifact-owned rollback target for that stable candidate
- `ga_date=2026-03-24`
- no exact `ga_date` is locked yet because the GA notice is still pending
- no exact `v5_eos_date` is locked yet because the GA notice is still pending
- an explicit `rollback_version`
- the exact derived rollback command that artifact will publish
- `v5_eos_date=2026-06-22`
## Why The Gate Cannot Be Cleared Yet
The blocker is no longer missing governance text. The remaining problem is that
the control plane still treats v6 as an RC-stabilization line, the working
the control plane still treats v6 as the prerelease-stabilization line, the working
version is still prerelease (`6.0.0-rc.1`), and there is still no exercised
`Release Dry Run` record proving the eventual stable `6.0.0`
candidate is ready for GA-style promotion. Until that rehearsal exists, stable
@ -60,24 +61,24 @@ users would still be the first real cohort for the final promotion path.
3. Land the canonical `.github/workflows/release-dry-run.yml` workflow-dispatch input
contract on the default branch `main` so GitHub accepts the
governed stable rehearsal metadata envelope when dispatching from `pulse/v6-release`.
4. Ship the first real RC through the governed prerelease release path and record
4. Ship the first real prerelease through the governed prerelease release path and record
its exact published prerelease tag plus rollback target and exact derived
rollback command.
5. Run `Release Dry Run` from `pulse/v6-release` using that published RC as
5. Run `Release Dry Run` from `pulse/v6-release` using that published prerelease as
`promoted_from_tag` with:
- `version=6.0.0`
- an artifact-owned candidate stable tag matching that rehearsal
- an artifact-owned promotion channel matching that rehearsal
- an artifact-owned promoted RC tag matching that rehearsal
- an artifact-owned promoted prerelease tag matching that rehearsal
- an artifact-owned rollback target for the stable candidate
- the exact planned GA and v5 end-of-support dates for the publish notice
- `ga_date=2026-03-24`
- an explicit `ga_date` chosen for that rehearsal
- an explicit stable `rollback_version`
- the exact derived rollback command that artifact will publish
- `v5_eos_date=2026-06-22`
- an explicit `v5_eos_date` chosen for that rehearsal
6. Capture the `rc-to-ga-rehearsal-summary` artifact and run URL.
7. Materialize the final rehearsal record from that artifact without
hand-repairing any missing candidate tag, promoted RC tag, rollback
hand-repairing any missing candidate tag, promoted prerelease tag, rollback
target, rollback command, or GA/EOS metadata.
8. Change the gate from `blocked` only if the rehearsal passes and the rollout
inputs remain explicit.

View file

@ -40,6 +40,10 @@ server-side update execution surfaces.
18. `scripts/dev-launchd-setup.sh`
19. `scripts/dev-launchd-wrapper.sh`
20. `scripts/com.pulse.hot-dev.plist.template`
21. `.github/workflows/create-release.yml`
22. `.github/workflows/release-dry-run.yml`
23. `.github/workflows/publish-docker.yml`
24. `.github/workflows/promote-floating-tags.yml`
## Shared Boundaries
@ -55,6 +59,7 @@ server-side update execution surfaces.
3. Add or change shell installer, Windows installer, container-agent installer, or auto-update script behavior through `scripts/install.sh`, `scripts/install.ps1`, `scripts/install-container-agent.sh`, and `scripts/pulse-auto-update.sh`
4. Add or change server update transport through `internal/api/updates.go` and `frontend-modern/src/api/updates.ts`
5. Add or change local dev-runtime orchestration, managed ownership, browser-runtime proof wiring, frontend/backend coherence diagnostics, canonical developer entry wrappers, or dev-runtime helper control surfaces through `scripts/hot-dev.sh`, `scripts/hot-dev-bg.sh`, `tests/integration/scripts/managed-dev-runtime.mjs`, `package.json`, `frontend-modern/package.json`, `scripts/dev-check.sh`, `scripts/toggle-mock.sh`, `scripts/clean-mock-alerts.sh`, `scripts/dev-launchd-setup.sh`, `scripts/dev-launchd-wrapper.sh`, and `scripts/com.pulse.hot-dev.plist.template`
6. Add or change governed release-promotion workflow inputs, operator-facing promotion metadata, prerelease lineage enforcement, or stable-promotion rehearsal summaries through `.github/workflows/create-release.yml`, `.github/workflows/release-dry-run.yml`, `.github/workflows/publish-docker.yml`, and `.github/workflows/promote-floating-tags.yml`
## Forbidden Paths
@ -97,10 +102,15 @@ contract rather than depending on whichever local ldflags string happened to be
updated last.
The same governed promotion path must now stay explicit too:
`scripts/release_control/resolve_release_promotion.py` is the canonical owner
for stable-versus-RC metadata validation shared by `.github/workflows/release-dry-run.yml`
for stable-versus-prerelease metadata validation shared by `.github/workflows/release-dry-run.yml`
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 governed release workflows also own the operator-facing wording for
that promotion metadata. Human-visible workflow inputs, summaries, and error
messages must describe the path as a prerelease or preview flow rather than
implying a near-ready release candidate, while machine-owned identifiers such
as `rc`, `rc-to-ga-*`, and `v6.0.0-rc.1` remain the canonical internal keys.
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.

View file

@ -135,7 +135,7 @@ git pull --ff-only
instead of returning an opaque 422 from `gh workflow run`.
The governed `Release Dry Run` must still carry:
- `version`: `6.0.0`
- `promoted_from_tag`: exact RC tag being promoted
- `promoted_from_tag`: exact prerelease tag being promoted
- `rollback_version`: prior stable
- `ga_date`: exact published v6 GA date
- `v5_eos_date`: exact published v5 end-of-support date
@ -143,7 +143,7 @@ git pull --ff-only
Attach the `rc-to-ga-rehearsal-summary` artifact and run URL to the release
ticket, and confirm the artifact carries the canonical promotion metadata
envelope for that candidate: candidate stable tag, promotion channel,
promoted RC tag, rollback target, exact rollback command, planned GA date,
promoted prerelease tag, rollback target, exact rollback command, planned GA date,
and planned v5 end-of-support date.
4. Publish the exact v6 GA date and v5 end-of-support date in the GA release

View file

@ -595,7 +595,7 @@ func TestV6ControlDocsReferenceCanonicalDevelopmentProtocol(t *testing.T) {
"registry.schema.json",
"subsystems/*.md",
"structured evidence references",
"ready to cut an RC or approve stable/GA",
"ready to cut a prerelease or approve stable/GA",
"control_plane_audit.py --check",
"status_audit.py --pretty",
"status.json.coverage_gaps",
@ -621,7 +621,7 @@ func TestV6ControlDocsReferenceCanonicalDevelopmentProtocol(t *testing.T) {
"## Evergreen Readiness Assertions",
"## Development Governance",
"Do not treat `status.json` lane scores reaching target as sufficient release approval by themselves",
"Do not promote v6 to stable or GA without an exercised RC",
"Do not promote v6 to stable or GA without an exercised prerelease",
"v5 maintenance-only support policy",
"control_plane_audit.py --check",
"When a user states a durable product truth",

View file

@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Materialize the blocked RC-to-GA promotion record from current repo state."""
"""Materialize the blocked prerelease-to-GA promotion record from current repo state."""
from __future__ import annotations
@ -34,7 +34,7 @@ STABLE_REHEARSAL_INPUT_KEYS = (
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Generate the blocked RC-to-GA promotion record from current repo facts."
description="Generate the blocked prerelease-to-GA promotion record from current repo facts."
)
parser.add_argument(
"--output",
@ -146,7 +146,7 @@ def parse_release_dates() -> tuple[str, str]:
ga_match = GA_DATE_RE.search(content)
eos_match = V5_EOS_RE.search(content)
if not ga_match or not eos_match:
raise ValueError("could not derive GA/EOS dates from docs/releases/RELEASE_NOTES_v6.md")
return "[v6-ga-date]", "[v5-eos-date]"
return ga_match.group("ga_date"), eos_match.group("v5_eos_date")
@ -192,7 +192,7 @@ def build_blocked_record(*, record_date: str) -> str:
f"`{stable_version}` candidate.",
],
[
"There is still no governed `RC-to-GA Rehearsal Record` proving a successful",
"There is still no governed `Prerelease-to-GA Rehearsal Record` proving a successful",
f"non-publish `Release Dry Run` for the eventual stable `{stable_version}` candidate.",
],
]
@ -203,7 +203,7 @@ def build_blocked_record(*, record_date: str) -> str:
"local GA candidate exists on the governed stable line.",
],
[
"There is still no governed `RC-to-GA Rehearsal Record` proving a successful",
"There is still no governed `Prerelease-to-GA Rehearsal Record` proving a successful",
f"non-publish `Release Dry Run` for the current `{version}` candidate.",
],
]
@ -249,7 +249,7 @@ def build_blocked_record(*, record_date: str) -> str:
if version_is_prerelease:
why_lines = [
"The blocker is no longer missing governance text. The remaining problem is that",
"the control plane still treats v6 as an RC-stabilization line, the working",
"the control plane still treats v6 as the prerelease-stabilization line, the working",
f"version is still prerelease (`{version}`), and there is still no exercised",
f"`Release Dry Run` record proving the eventual stable `{stable_version}`",
"candidate is ready for GA-style promotion. Until that rehearsal exists, stable",
@ -269,7 +269,7 @@ def build_blocked_record(*, record_date: str) -> str:
else:
why_lines = [
"The blocker is no longer missing governance text. The remaining problem is that",
"the control plane still treats v6 as an RC-stabilization line, and there is",
"the control plane still treats v6 as the prerelease-stabilization line, and there is",
f"still no exercised `Release Dry Run` record proving the exact `{version}`",
"candidate is ready for GA-style promotion. Until that rehearsal exists, stable",
"users would still be the first real cohort for the final promotion path.",
@ -289,18 +289,18 @@ def build_blocked_record(*, record_date: str) -> str:
if rc_tag:
blocking_fact_lines.extend(
[
[f"The latest shipped Pulse v6 RC tag is `{rc_tag}`."],
[f"That shipped RC tag resolves to commit `{rc_commit}`."],
[f"The latest shipped Pulse v6 prerelease tag is `{rc_tag}`."],
[f"That shipped prerelease tag resolves to commit `{rc_commit}`."],
]
)
else:
blocking_fact_lines.append(["No Pulse v6 RC has shipped yet."])
blocking_fact_lines.append(["No Pulse v6 prerelease has shipped yet."])
if accidental_tags:
accidental_list = ", ".join(f"`{tag}`" for tag in sorted(accidental_tags))
blocking_fact_lines.append(
[
f"The repository contains accidental prerelease git tag history ({accidental_list}),",
"but those tags were never published and do not count as shipped RC lineage.",
"but those tags were never published and do not count as shipped prerelease lineage.",
]
)
@ -327,7 +327,7 @@ def build_blocked_record(*, record_date: str) -> str:
f"- `version={stable_version}`",
"- the artifact-owned candidate stable tag for that rehearsal",
"- the artifact-owned promotion channel for that rehearsal",
"- the artifact-owned promoted RC tag for that rehearsal",
"- the artifact-owned promoted prerelease tag for that rehearsal",
"- the artifact-owned rollback target for that stable candidate",
f"- `ga_date={ga_date}`",
"- an explicit `rollback_version`",
@ -338,7 +338,7 @@ def build_blocked_record(*, record_date: str) -> str:
release_date_fact_lines = [
"`docs/releases/RELEASE_NOTES_v6.md` and",
"`docs/release-control/v6/internal/V5_MAINTENANCE_SUPPORT_POLICY.md` still leave the",
"GA announcement dates as placeholders because no real RC lineage or GA-ready",
"GA announcement dates as placeholders because no real prerelease lineage or GA-ready",
"rehearsal has locked them yet:",
f"- `v6` GA date placeholder: `{ga_date}`",
f"- `v5` end-of-support placeholder: `{v5_eos_date}`",
@ -347,7 +347,7 @@ def build_blocked_record(*, record_date: str) -> str:
f"- `version={stable_version}`",
"- the artifact-owned candidate stable tag for that rehearsal",
"- the artifact-owned promotion channel for that rehearsal",
"- the artifact-owned promoted RC tag for that rehearsal",
"- the artifact-owned promoted prerelease tag for that rehearsal",
"- the artifact-owned rollback target for that stable candidate",
"- no exact `ga_date` is locked yet because the GA notice is still pending",
"- no exact `v5_eos_date` is locked yet because the GA notice is still pending",
@ -359,7 +359,7 @@ def build_blocked_record(*, record_date: str) -> str:
else:
rehearsal_input_lines.insert(
1,
"- no governed `promoted_from_tag` exists yet because no RC has shipped",
"- no governed `promoted_from_tag` exists yet because no prerelease has shipped",
)
required_blocks: list[list[str] | tuple[str, ...] | str] = list(required_step_lines)
@ -374,19 +374,19 @@ def build_blocked_record(*, record_date: str) -> str:
if not rc_tag:
required_blocks.append(
[
"Ship the first real RC through the governed prerelease release path and record",
"Ship the first real prerelease through the governed prerelease release path and record",
"its exact published prerelease tag plus rollback target and exact derived",
"rollback command.",
]
)
required_blocks.append(
[
f"Run `Release Dry Run` from `{stable_branch}` using that published RC as",
f"Run `Release Dry Run` from `{stable_branch}` using that published prerelease as",
"`promoted_from_tag` with:",
f"- `version={stable_version}`",
"- an artifact-owned candidate stable tag matching that rehearsal",
"- an artifact-owned promotion channel matching that rehearsal",
"- an artifact-owned promoted RC tag matching that rehearsal",
"- an artifact-owned promoted prerelease tag matching that rehearsal",
"- an artifact-owned rollback target for the stable candidate",
"- the exact planned GA and v5 end-of-support dates for the publish notice",
f"- `ga_date={ga_date}`" if dates_are_locked else "- an explicit `ga_date` chosen for that rehearsal",
@ -403,7 +403,7 @@ def build_blocked_record(*, record_date: str) -> str:
f"- `promoted_from_tag={rc_tag}`",
"- an artifact-owned candidate stable tag matching that rehearsal",
"- an artifact-owned promotion channel matching that rehearsal",
"- an artifact-owned promoted RC tag matching that rehearsal",
"- an artifact-owned promoted prerelease tag matching that rehearsal",
"- an artifact-owned rollback target for the stable candidate",
"- the exact planned GA and v5 end-of-support dates for the publish notice",
f"- `ga_date={ga_date}`" if dates_are_locked else "- an explicit `ga_date` chosen for that rehearsal",
@ -417,7 +417,7 @@ def build_blocked_record(*, record_date: str) -> str:
["Capture the `rc-to-ga-rehearsal-summary` artifact and run URL."],
[
"Materialize the final rehearsal record from that artifact without",
"hand-repairing any missing candidate tag, promoted RC tag, rollback",
"hand-repairing any missing candidate tag, promoted prerelease tag, rollback",
"target, rollback command, or GA/EOS metadata.",
],
[
@ -428,7 +428,7 @@ def build_blocked_record(*, record_date: str) -> str:
)
lines = [
"# RC-to-GA Promotion Readiness Blocked Record",
"# Prerelease-to-GA Promotion Readiness Blocked Record",
"",
f"- Date: `{record_date}`",
"- Gate: `rc-to-ga-promotion-readiness`",

View file

@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Shared helpers for RC-to-GA promotion proof governance."""
"""Shared helpers for prerelease-to-GA promotion proof governance."""
from __future__ import annotations
@ -31,7 +31,7 @@ REQUIRED_STAGED_GOVERNANCE_INPUTS: tuple[str, ...] = (
PROMOTION_METADATA_FIELDS: tuple[tuple[str, str], ...] = (
("tag", "candidate stable tag"),
("channel_under_rehearsal", "promotion channel"),
("promoted_from_rc", "promoted RC tag"),
("promoted_from_rc", "promoted prerelease tag"),
("rollback_target", "rollback target"),
("rollback_command", "exact rollback command"),
("planned_ga_date", "planned GA date"),
@ -194,7 +194,7 @@ def staged_governance_input_errors(*, use_staged_governance: bool) -> list[str]:
if (
"Exact rollback or reinstall command" not in template
or "rc-to-ga-rehearsal-summary" not in template
or "promotion channel, promoted RC tag, rollback target, exact rollback command" not in template
or "promotion channel, promoted prerelease tag, rollback target, exact rollback command" not in template
):
errors.append(
"stage the updated docs/release-control/v6/internal/RC_TO_GA_REHEARSAL_TEMPLATE.md "

View file

@ -128,7 +128,7 @@ on:
),
patch(
"release_promotion_policy_support.read_repo_text",
return_value="# Pulse v6 RC-to-GA Rehearsal Record\n",
return_value="# Pulse v6 Prerelease-to-GA Rehearsal Record\n",
) as read_repo_text,
):
self.assertEqual(
@ -154,7 +154,7 @@ on:
"release_promotion_policy_support.read_repo_text",
side_effect=[
"# Pulse v6 Pre-Release Checklist\n- rc-to-ga-rehearsal-summary\n",
"# Pulse v6 RC-to-GA Rehearsal Record\n",
"# Pulse v6 Prerelease-to-GA Rehearsal Record\n",
],
):
self.assertEqual(

View file

@ -78,11 +78,11 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
def test_release_promotion_policy_requires_live_rc_and_v5_policy(self) -> None:
content = read("docs/release-control/v6/internal/RELEASE_PROMOTION_POLICY.md")
self.assertIn("Every candidate intended for broad customer use must ship to `rc`", content)
self.assertIn("live run of the release pipeline for the RC tag itself", content)
self.assertIn("live run of the release pipeline for the prerelease tag itself", content)
self.assertIn("an accidental git tag by itself", content)
self.assertIn("does not count as a shipped RC", content)
self.assertIn("does not count as a shipped prerelease", content)
self.assertIn("do not promote to `stable` until the active control-plane target", content)
self.assertIn("A live release-pipeline exercise already completed for the promoted RC", content)
self.assertIn("A live release-pipeline exercise already completed for the promoted prerelease tag", content)
self.assertIn("maintenance-only window lasts 90 calendar days", content)
self.assertIn("V5_MAINTENANCE_SUPPORT_POLICY.md", content)
self.assertIn("release notes may keep placeholder", content)
@ -91,7 +91,7 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
def test_pre_release_checklist_tracks_rc_to_ga_gate_inputs(self) -> None:
content = read("docs/release-control/v6/internal/PRE_RELEASE_CHECKLIST.md")
self.assertIn("release pipeline has already been exercised on a real RC tag", content)
self.assertIn("release pipeline has already been exercised on a real prerelease tag", content)
self.assertIn("not an accidental git tag", content)
self.assertIn("V5_MAINTENANCE_SUPPORT_POLICY.md", content)
self.assertIn("replace any placeholder GA notice dates", content)
@ -105,16 +105,20 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
self.assertIn("maintenance-only support immediately on the v6 GA date", policy)
self.assertIn("90 calendar days from the v6 GA", policy)
self.assertIn("pulse/v5-maintenance", policy)
self.assertIn("Pulse v5 Support Transition", release_notes)
self.assertIn("publish an explicit exception", release_notes)
self.assertRegex(
release_notes,
re.compile(r"Pulse v5 entered maintenance-only support on `(?:\[v6-ga-date\]|\d{4}-\d{2}-\d{2})`\.")
)
self.assertRegex(
release_notes,
re.compile(r"existing v5 users until `(?:\[v5-eos-date\]|\d{4}-\d{2}-\d{2})`\.")
)
if "Pulse v5 Support Transition" in release_notes:
self.assertIn("publish an explicit exception", release_notes)
self.assertRegex(
release_notes,
re.compile(r"Pulse v5 entered maintenance-only support on `(?:\[v6-ga-date\]|\d{4}-\d{2}-\d{2})`\.")
)
self.assertRegex(
release_notes,
re.compile(r"existing v5 users until `(?:\[v5-eos-date\]|\d{4}-\d{2}-\d{2})`\.")
)
else:
self.assertIn("Pre-Release Notes", release_notes)
self.assertIn("final GA release", release_notes)
self.assertNotIn("Pulse v5 Support Transition", release_notes)
def test_rehearsal_template_and_workflow_capture_ga_rehearsal_record(self) -> None:
template = read("docs/release-control/v6/internal/RC_TO_GA_REHEARSAL_TEMPLATE.md")
@ -132,7 +136,7 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
self.assertIn("- Rollback command:", workflow)
self.assertIn("- Candidate stable tag:", workflow)
self.assertIn("- Promotion channel:", workflow)
self.assertIn("- Promoted RC tag:", workflow)
self.assertIn("- Promoted prerelease tag:", workflow)
self.assertIn("Planned GA date", workflow)
self.assertIn("Planned v5 end-of-support date", workflow)
self.assertIn("resolve_release_promotion.py", release_workflow)
@ -162,7 +166,7 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
self.assertIn("minimum is 72 hours unless hotfix_exception is true", resolver)
self.assertIn("- Candidate stable tag:", content)
self.assertIn("- Promotion channel:", content)
self.assertIn("- Promoted RC tag:", content)
self.assertIn("- Promoted prerelease tag:", content)
self.assertIn("ga_date", content)
self.assertIn("v5_eos_date", content)
self.assertIn("Derived rollback command:", helper)
@ -201,8 +205,8 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
self.assertIn("control_plane.py --branch-for-version", promote)
self.assertIn("control_plane.py --branch-for-version", demo)
self.assertIn("control_plane.py --branch-for-version", helm)
self.assertIn("does not descend from any matching RC tag", publish)
self.assertIn("does not descend from any matching RC tag", promote)
self.assertIn("does not descend from any matching prerelease tag", publish)
self.assertIn("does not descend from any matching prerelease tag", promote)
self.assertIn("both stable and prerelease releases dispatch", runbook)
self.assertIn("Release `6.0.0` from `pulse/v6-release`", runbook)
self.assertIn(promotion_metadata_envelope(), normalize_ws(runbook))
@ -213,10 +217,10 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
self.assertIn("rollback target and exact derived", blocked)
self.assertIn("artifact-owned candidate stable tag", blocked)
self.assertIn("artifact-owned promotion channel", blocked)
self.assertIn("artifact-owned promoted RC tag", blocked)
self.assertIn("artifact-owned promoted prerelease tag", blocked)
self.assertIn("artifact-owned rollback target", blocked)
self.assertIn("Materialize the final rehearsal record from that artifact without", blocked)
self.assertIn("hand-repairing any missing candidate tag, promoted RC tag, rollback", blocked)
self.assertIn("hand-repairing any missing candidate tag, promoted prerelease tag, rollback", blocked)
matrix = read("docs/release-control/v6/internal/HIGH_RISK_RELEASE_VERIFICATION_MATRIX.md")
self.assertIn(promotion_metadata_envelope(), normalize_ws(matrix))
expected = blocked_record.build_blocked_record(record_date="2026-03-13")

View file

@ -72,7 +72,7 @@ def tag_created_unix(tag: str) -> int:
)
value = result.stdout.strip().splitlines()
if not value or not value[0].strip():
raise ValueError(f"Could not determine creation time for promoted RC tag {tag}.")
raise ValueError(f"Could not determine creation time for promoted prerelease tag {tag}.")
return int(value[0].strip())
@ -125,11 +125,11 @@ def resolve_metadata(
promoted_from_tag = normalize_tag(promoted_from_tag_input)
if not promoted_from_tag:
raise ValueError(
"Stable promotion requires promoted_from_tag naming the RC being promoted."
"Stable promotion requires promoted_from_tag naming the prerelease being promoted."
)
if not re.match(rf"^v{re.escape(version)}-rc\.\d+$", promoted_from_tag):
raise ValueError(
f"promoted_from_tag must reference an RC tag for the same stable version ({version}), got {promoted_from_tag}."
f"promoted_from_tag must reference a prerelease tag for the same stable version ({version}), got {promoted_from_tag}."
)
if not tag_exists_fn(promoted_from_tag):
raise ValueError(
@ -139,7 +139,7 @@ def resolve_metadata(
promoted_commit = tag_commit_fn(promoted_from_tag)
if not head_descends_from_fn(promoted_commit):
raise ValueError(
f"Stable promotion {tag} must descend from promoted RC tag {promoted_from_tag}."
f"Stable promotion {tag} must descend from promoted prerelease tag {promoted_from_tag}."
)
promoted_tag_ts = tag_created_unix_fn(promoted_from_tag)

View file

@ -102,23 +102,23 @@ if [ "$IS_PRERELEASE" != "true" ]; then
DEFAULT_PROMOTED_FROM_TAG=$(git tag -l "v${VERSION}-rc.*" --sort=-version:refname | head -1 || true)
echo ""
if [ -n "$DEFAULT_PROMOTED_FROM_TAG" ]; then
read -r -p "Promoted RC tag [${DEFAULT_PROMOTED_FROM_TAG}]: " PROMOTED_FROM_TAG
read -r -p "Promoted prerelease tag [${DEFAULT_PROMOTED_FROM_TAG}]: " PROMOTED_FROM_TAG
PROMOTED_FROM_TAG="${PROMOTED_FROM_TAG:-$DEFAULT_PROMOTED_FROM_TAG}"
else
read -r -p "Promoted RC tag (for example v${VERSION}-rc.2): " PROMOTED_FROM_TAG
read -r -p "Promoted prerelease tag (for example v${VERSION}-rc.2): " PROMOTED_FROM_TAG
fi
if [ -z "$PROMOTED_FROM_TAG" ]; then
echo "❌ Error: promoted RC tag is required for stable rehearsals"
echo "❌ Error: promoted prerelease tag is required for stable rehearsals"
exit 1
fi
echo ""
read -r -p "Hotfix exception to bypass 72-hour RC soak? [y/N] " HOTFIX_REPLY
read -r -p "Hotfix exception to bypass 72-hour prerelease soak? [y/N] " HOTFIX_REPLY
if [[ "$HOTFIX_REPLY" =~ ^[Yy]$ ]]; then
HOTFIX_EXCEPTION="true"
read -r -p "Hotfix reason: " HOTFIX_REASON
if [ -z "$HOTFIX_REASON" ]; then
echo "❌ Error: hotfix reason is required when bypassing RC soak"
echo "❌ Error: hotfix reason is required when bypassing prerelease soak"
exit 1
fi
fi

View file

@ -186,23 +186,23 @@ if [ "$IS_PRERELEASE" != "true" ]; then
DEFAULT_PROMOTED_FROM_TAG=$(git tag -l "v${VERSION}-rc.*" --sort=-version:refname | head -1 || true)
echo ""
if [ -n "$DEFAULT_PROMOTED_FROM_TAG" ]; then
read -r -p "Promoted RC tag [${DEFAULT_PROMOTED_FROM_TAG}]: " PROMOTED_FROM_TAG
read -r -p "Promoted prerelease tag [${DEFAULT_PROMOTED_FROM_TAG}]: " PROMOTED_FROM_TAG
PROMOTED_FROM_TAG="${PROMOTED_FROM_TAG:-$DEFAULT_PROMOTED_FROM_TAG}"
else
read -r -p "Promoted RC tag (for example v${VERSION}-rc.2): " PROMOTED_FROM_TAG
read -r -p "Promoted prerelease tag (for example v${VERSION}-rc.2): " PROMOTED_FROM_TAG
fi
if [ -z "$PROMOTED_FROM_TAG" ]; then
echo "❌ Error: promoted RC tag is required for stable releases"
echo "❌ Error: promoted prerelease tag is required for stable releases"
exit 1
fi
echo ""
read -r -p "Hotfix exception to bypass 72-hour RC soak? [y/N] " HOTFIX_REPLY
read -r -p "Hotfix exception to bypass 72-hour prerelease soak? [y/N] " HOTFIX_REPLY
if [[ "$HOTFIX_REPLY" =~ ^[Yy]$ ]]; then
HOTFIX_EXCEPTION="true"
read -r -p "Hotfix reason: " HOTFIX_REASON
if [ -z "$HOTFIX_REASON" ]; then
echo "❌ Error: hotfix reason is required when bypassing RC soak"
echo "❌ Error: hotfix reason is required when bypassing prerelease soak"
exit 1
fi
fi