mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Govern RC-to-GA rehearsal record capture
This commit is contained in:
@@ -313,6 +313,13 @@ jobs:
|
||||
echo ""
|
||||
echo "This run exercised the non-publish release path and validated the current promotion contract on the selected branch."
|
||||
echo "Record this run URL in the release ticket when clearing \`rc-to-ga-promotion-readiness\`."
|
||||
echo ""
|
||||
echo "## Governed Record"
|
||||
echo ""
|
||||
echo "Materialize the dated rehearsal record from this exact run with:"
|
||||
echo "\`python3 scripts/release_control/record_rc_to_ga_rehearsal.py --run-id ${{ github.run_id }}\`"
|
||||
echo ""
|
||||
echo "If you do not pass \`--output\`, the recorder writes to \`docs/release-control/v6/internal/records/rc-to-ga-promotion-readiness-rehearsal-<record-date>.md\`."
|
||||
} > "$SUMMARY_FILE"
|
||||
fi
|
||||
|
||||
|
||||
@@ -41,7 +41,8 @@ Use this as the final gate before cutting a Pulse v6 pre-release.
|
||||
- [ ] For GA/stable promotion, confirm the pushed governed release-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 executes the selected remote ref and does not see local-only governance state.
|
||||
- [ ] For GA/stable promotion, confirm the local rehearsal branch exactly matches `origin` before dispatching `Release Dry Run`, so the run exercises the intended governed branch state instead of stale remote control-plane metadata.
|
||||
- [ ] 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 prerelease tag, rollback target, exact rollback command, planned GA date, and planned v5 end-of-support date.
|
||||
- [ ] For GA/stable promotion, materialize the final rehearsal record with `python3 scripts/release_control/record_rc_to_ga_rehearsal.py --run-id <run-id>` and, unless an explicit `--output` is chosen, confirm it lands at `docs/release-control/v6/internal/records/rc-to-ga-promotion-readiness-rehearsal-<record-date>.md`.
|
||||
- [ ] For GA/stable promotion, attach the dated rehearsal record, `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.
|
||||
|
||||
@@ -4,8 +4,9 @@ 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.
|
||||
`python3 scripts/release_control/record_rc_to_ga_rehearsal.py --run-id <run-id>`
|
||||
rather than hand-writing it. If `--output` is omitted, the recorder writes to
|
||||
`docs/release-control/v6/internal/records/rc-to-ga-promotion-readiness-rehearsal-<record-date>.md`.
|
||||
|
||||
The matching GitHub Actions artifact should be the machine-generated
|
||||
`rc-to-ga-rehearsal-summary`.
|
||||
|
||||
@@ -144,7 +144,9 @@ Cloud, and self-hosted production users.
|
||||
for the non-publish dry run and the canonical promotion metadata envelope:
|
||||
candidate stable tag, promotion channel, promoted prerelease tag, rollback target,
|
||||
exact rollback command, planned GA date, and planned v5 end-of-support
|
||||
date.
|
||||
date. Materialize that dated record with
|
||||
`python3 scripts/release_control/record_rc_to_ga_rehearsal.py --run-id <run-id>`
|
||||
unless an explicitly different output path is needed.
|
||||
8. The pushed governed release-branch copy of `.github/workflows/release-dry-run.yml`
|
||||
must already accept that stable rehearsal metadata envelope through
|
||||
`workflow_dispatch`, and the local release branch must match `origin` before
|
||||
|
||||
@@ -36,39 +36,45 @@ server-side update execution surfaces.
|
||||
14. `.github/workflows/release-dry-run.yml`
|
||||
15. `.github/workflows/update-demo-server.yml`
|
||||
16. `docs/releases/V6_PRERELEASE_RUNBOOK.md`
|
||||
17. `package.json`
|
||||
18. `package-lock.json`
|
||||
19. `frontend-modern/package.json`
|
||||
20. `frontend-modern/package-lock.json`
|
||||
21. `frontend-modern/vite.config.ts`
|
||||
22. `go.mod`
|
||||
23. `go.sum`
|
||||
24. `scripts/build-release.sh`
|
||||
25. `scripts/check-workflow-dispatch-inputs.py`
|
||||
26. `scripts/clean-mock-alerts.sh`
|
||||
27. `scripts/com.pulse.hot-dev.plist.template`
|
||||
28. `scripts/dev-check.sh`
|
||||
29. `scripts/dev-launchd-setup.sh`
|
||||
30. `scripts/dev-launchd-wrapper.sh`
|
||||
31. `scripts/hot-dev-bg.sh`
|
||||
32. `scripts/hot-dev.sh`
|
||||
33. `scripts/install-container-agent.sh`
|
||||
34. `scripts/install.ps1`
|
||||
35. `scripts/install.sh`
|
||||
36. `scripts/pulse-auto-update.sh`
|
||||
37. `scripts/release_control/resolve_release_promotion.py`
|
||||
38. `scripts/release_ldflags.sh`
|
||||
39. `scripts/run_demo_public_browser_smoke.sh`
|
||||
40. `scripts/demo_public_browser_smoke.cjs`
|
||||
41. `scripts/trigger-release-dry-run.sh`
|
||||
42. `scripts/trigger-release.sh`
|
||||
43. `scripts/toggle-mock.sh`
|
||||
44. `tests/integration/playwright.config.ts`
|
||||
45. `tests/integration/QUICK_START.md`
|
||||
46. `tests/integration/README.md`
|
||||
47. `tests/integration/scripts/managed-dev-runtime.mjs`
|
||||
48. `tests/integration/tests/helpers.ts`
|
||||
49. `tests/integration/tests/runtime-defaults.ts`
|
||||
17. `docs/release-control/v6/internal/PRE_RELEASE_CHECKLIST.md`
|
||||
18. `docs/release-control/v6/internal/RC_TO_GA_REHEARSAL_TEMPLATE.md`
|
||||
19. `docs/release-control/v6/internal/RELEASE_PROMOTION_POLICY.md`
|
||||
20. `package.json`
|
||||
21. `package-lock.json`
|
||||
22. `frontend-modern/package.json`
|
||||
23. `frontend-modern/package-lock.json`
|
||||
24. `frontend-modern/vite.config.ts`
|
||||
25. `go.mod`
|
||||
26. `go.sum`
|
||||
27. `scripts/build-release.sh`
|
||||
28. `scripts/check-workflow-dispatch-inputs.py`
|
||||
29. `scripts/clean-mock-alerts.sh`
|
||||
30. `scripts/com.pulse.hot-dev.plist.template`
|
||||
31. `scripts/dev-check.sh`
|
||||
32. `scripts/dev-launchd-setup.sh`
|
||||
33. `scripts/dev-launchd-wrapper.sh`
|
||||
34. `scripts/hot-dev-bg.sh`
|
||||
35. `scripts/hot-dev.sh`
|
||||
36. `scripts/install-container-agent.sh`
|
||||
37. `scripts/install.ps1`
|
||||
38. `scripts/install.sh`
|
||||
39. `scripts/pulse-auto-update.sh`
|
||||
40. `scripts/release_control/internal/record_rc_to_ga_rehearsal.py`
|
||||
41. `scripts/release_control/record_rc_to_ga_rehearsal.py`
|
||||
42. `scripts/release_control/release_promotion_policy_support.py`
|
||||
43. `scripts/release_control/resolve_release_promotion.py`
|
||||
44. `scripts/release_ldflags.sh`
|
||||
45. `scripts/run_demo_public_browser_smoke.sh`
|
||||
46. `scripts/demo_public_browser_smoke.cjs`
|
||||
47. `scripts/trigger-release-dry-run.sh`
|
||||
48. `scripts/trigger-release.sh`
|
||||
49. `scripts/toggle-mock.sh`
|
||||
50. `tests/integration/playwright.config.ts`
|
||||
51. `tests/integration/QUICK_START.md`
|
||||
52. `tests/integration/README.md`
|
||||
53. `tests/integration/scripts/managed-dev-runtime.mjs`
|
||||
54. `tests/integration/tests/helpers.ts`
|
||||
55. `tests/integration/tests/runtime-defaults.ts`
|
||||
|
||||
## Shared Boundaries
|
||||
|
||||
@@ -80,11 +86,11 @@ server-side update execution surfaces.
|
||||
## Extension Points
|
||||
|
||||
1. Add or change deployment-type detection, update planning, or apply behavior through `internal/updates/`
|
||||
2. Add or change release-build metadata injection, Docker build-context allowlists, release artifact assembly, or governed promotion metadata resolution through `scripts/build-release.sh`, `scripts/release_ldflags.sh`, `scripts/check-workflow-dispatch-inputs.py`, `scripts/release_control/resolve_release_promotion.py`, `.dockerignore`, `Dockerfile`, `docs/releases/V6_PRERELEASE_RUNBOOK.md`, the operator dispatch helpers `scripts/trigger-release.sh` and `scripts/trigger-release-dry-run.sh`, and the governed release workflows `.github/workflows/create-release.yml`, `.github/workflows/deploy-demo-server.yml`, `.github/workflows/helm-pages.yml`, `.github/workflows/publish-docker.yml`, `.github/workflows/publish-helm-chart.yml`, `.github/workflows/promote-floating-tags.yml`, `.github/workflows/release-dry-run.yml`, and `.github/workflows/update-demo-server.yml`
|
||||
2. Add or change release-build metadata injection, Docker build-context allowlists, release artifact assembly, or governed promotion metadata resolution through `scripts/build-release.sh`, `scripts/release_ldflags.sh`, `scripts/check-workflow-dispatch-inputs.py`, `scripts/release_control/resolve_release_promotion.py`, `scripts/release_control/record_rc_to_ga_rehearsal.py`, `scripts/release_control/internal/record_rc_to_ga_rehearsal.py`, `scripts/release_control/release_promotion_policy_support.py`, `.dockerignore`, `Dockerfile`, `docs/releases/V6_PRERELEASE_RUNBOOK.md`, `docs/release-control/v6/internal/RELEASE_PROMOTION_POLICY.md`, `docs/release-control/v6/internal/PRE_RELEASE_CHECKLIST.md`, `docs/release-control/v6/internal/RC_TO_GA_REHEARSAL_TEMPLATE.md`, the operator dispatch helpers `scripts/trigger-release.sh` and `scripts/trigger-release-dry-run.sh`, and the governed release workflows `.github/workflows/create-release.yml`, `.github/workflows/deploy-demo-server.yml`, `.github/workflows/helm-pages.yml`, `.github/workflows/publish-docker.yml`, `.github/workflows/publish-helm-chart.yml`, `.github/workflows/promote-floating-tags.yml`, `.github/workflows/release-dry-run.yml`, and `.github/workflows/update-demo-server.yml`
|
||||
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, dependency manifest floors, frontend build chunking, or dev-runtime helper control surfaces through `scripts/hot-dev.sh`, `scripts/hot-dev-bg.sh`, `Makefile`, `package.json`, `package-lock.json`, `frontend-modern/package.json`, `frontend-modern/package-lock.json`, `frontend-modern/vite.config.ts`, `go.mod`, `go.sum`, `scripts/dev-check.sh`, `scripts/toggle-mock.sh`, `scripts/clean-mock-alerts.sh`, `scripts/dev-launchd-setup.sh`, `scripts/dev-launchd-wrapper.sh`, `scripts/run_demo_public_browser_smoke.sh`, `scripts/demo_public_browser_smoke.cjs`, `scripts/com.pulse.hot-dev.plist.template`, `tests/integration/scripts/managed-dev-runtime.mjs`, `tests/integration/playwright.config.ts`, `tests/integration/tests/helpers.ts`, `tests/integration/tests/runtime-defaults.ts`, `tests/integration/README.md`, and `tests/integration/QUICK_START.md`
|
||||
6. Add or change governed release-promotion workflow inputs, operator-facing promotion metadata, artifact publication lineage enforcement, or stable-promotion rehearsal summaries through `.github/workflows/create-release.yml`, `.github/workflows/helm-pages.yml`, `.github/workflows/publish-docker.yml`, `.github/workflows/publish-helm-chart.yml`, `.github/workflows/promote-floating-tags.yml`, `.github/workflows/release-dry-run.yml`, `.github/workflows/update-demo-server.yml`, `docs/releases/V6_PRERELEASE_RUNBOOK.md`, `scripts/check-workflow-dispatch-inputs.py`, `scripts/trigger-release.sh`, and `scripts/trigger-release-dry-run.sh`
|
||||
6. Add or change governed release-promotion workflow inputs, operator-facing promotion metadata, artifact publication lineage enforcement, or stable-promotion rehearsal summaries through `.github/workflows/create-release.yml`, `.github/workflows/helm-pages.yml`, `.github/workflows/publish-docker.yml`, `.github/workflows/publish-helm-chart.yml`, `.github/workflows/promote-floating-tags.yml`, `.github/workflows/release-dry-run.yml`, `.github/workflows/update-demo-server.yml`, `docs/releases/V6_PRERELEASE_RUNBOOK.md`, `docs/release-control/v6/internal/RELEASE_PROMOTION_POLICY.md`, `docs/release-control/v6/internal/PRE_RELEASE_CHECKLIST.md`, `docs/release-control/v6/internal/RC_TO_GA_REHEARSAL_TEMPLATE.md`, `scripts/check-workflow-dispatch-inputs.py`, `scripts/release_control/record_rc_to_ga_rehearsal.py`, `scripts/release_control/internal/record_rc_to_ga_rehearsal.py`, `scripts/release_control/release_promotion_policy_support.py`, `scripts/trigger-release.sh`, and `scripts/trigger-release-dry-run.sh`
|
||||
7. Preserve release-matched installer and Helm operator documentation links through `scripts/install.sh`, `.github/workflows/helm-pages.yml`, `.github/workflows/publish-helm-chart.yml`, and the chart metadata itself so deployment guidance and packaged chart metadata do not drift back to branch-tip `main` docs when a release line or promoted tag already exists.
|
||||
8. Add or change operator-facing hosted tenant runtime canary rollout, batch runtime contract reconciliation, canonical hosted route/public URL generation, or control-plane runtime-registry reconciliation through `cmd/pulse-control-plane/main.go`, `internal/cloudcp/docker/manager.go`, `internal/cloudcp/docker/labels.go`, and `internal/cloudcp/tenant_runtime_rollout.go`
|
||||
|
||||
@@ -210,6 +216,13 @@ 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.
|
||||
That same promotion-governance package also owns the dated rehearsal-record
|
||||
materialization path. The public recorder
|
||||
`scripts/release_control/record_rc_to_ga_rehearsal.py` and its internal module
|
||||
must remain the canonical route from a `Release Dry Run` run ID or summary
|
||||
artifact to `docs/release-control/v6/internal/records/`, and they must fail
|
||||
closed on missing artifact metadata or silent record overwrites rather than
|
||||
encouraging hand-written repair of governed promotion fields.
|
||||
That same prerelease framing requirement also applies to installer and update
|
||||
runtime copy: `install.sh`, `scripts/pulse-auto-update.sh`, and
|
||||
`internal/updates/manager.go` must present `rc`-tagged builds as prerelease or
|
||||
|
||||
@@ -2337,6 +2337,9 @@
|
||||
".github/workflows/update-demo-server.yml",
|
||||
"cmd/pulse-control-plane/main.go",
|
||||
"Dockerfile",
|
||||
"docs/release-control/v6/internal/PRE_RELEASE_CHECKLIST.md",
|
||||
"docs/release-control/v6/internal/RC_TO_GA_REHEARSAL_TEMPLATE.md",
|
||||
"docs/release-control/v6/internal/RELEASE_PROMOTION_POLICY.md",
|
||||
"docs/releases/V6_PRERELEASE_RUNBOOK.md",
|
||||
"frontend-modern/package-lock.json",
|
||||
"frontend-modern/package.json",
|
||||
@@ -2365,6 +2368,9 @@
|
||||
"scripts/install.ps1",
|
||||
"scripts/install.sh",
|
||||
"scripts/pulse-auto-update.sh",
|
||||
"scripts/release_control/internal/record_rc_to_ga_rehearsal.py",
|
||||
"scripts/release_control/record_rc_to_ga_rehearsal.py",
|
||||
"scripts/release_control/release_promotion_policy_support.py",
|
||||
"scripts/release_control/resolve_release_promotion.py",
|
||||
"scripts/release_ldflags.sh",
|
||||
"scripts/run_demo_public_browser_smoke.sh",
|
||||
@@ -2424,8 +2430,14 @@
|
||||
".github/workflows/publish-helm-chart.yml",
|
||||
".github/workflows/release-dry-run.yml",
|
||||
".github/workflows/update-demo-server.yml",
|
||||
"docs/release-control/v6/internal/PRE_RELEASE_CHECKLIST.md",
|
||||
"docs/release-control/v6/internal/RC_TO_GA_REHEARSAL_TEMPLATE.md",
|
||||
"docs/release-control/v6/internal/RELEASE_PROMOTION_POLICY.md",
|
||||
"docs/releases/V6_PRERELEASE_RUNBOOK.md",
|
||||
"scripts/check-workflow-dispatch-inputs.py",
|
||||
"scripts/release_control/internal/record_rc_to_ga_rehearsal.py",
|
||||
"scripts/release_control/record_rc_to_ga_rehearsal.py",
|
||||
"scripts/release_control/release_promotion_policy_support.py",
|
||||
"scripts/release_control/resolve_release_promotion.py",
|
||||
"scripts/trigger-release-dry-run.sh",
|
||||
"scripts/trigger-release.sh"
|
||||
@@ -2433,6 +2445,8 @@
|
||||
"allow_same_subsystem_tests": false,
|
||||
"test_prefixes": [],
|
||||
"exact_files": [
|
||||
"scripts/release_control/internal/record_rc_to_ga_rehearsal_test.py",
|
||||
"scripts/release_control/release_promotion_policy_support_test.py",
|
||||
"scripts/release_control/release_promotion_policy_test.py",
|
||||
"scripts/release_control/resolve_release_promotion_test.py"
|
||||
]
|
||||
|
||||
@@ -145,9 +145,13 @@ git pull --ff-only
|
||||
- `ga_date`: exact published v6 GA date
|
||||
- `v5_eos_date`: exact published v5 end-of-support date
|
||||
- optional `hotfix_exception` and `hotfix_reason`
|
||||
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,
|
||||
After the run passes, materialize the governed dated rehearsal record with
|
||||
`python3 scripts/release_control/record_rc_to_ga_rehearsal.py --run-id <run-id>`.
|
||||
If `--output` is omitted, that recorder writes to
|
||||
`docs/release-control/v6/internal/records/rc-to-ga-promotion-readiness-rehearsal-<record-date>.md`.
|
||||
Attach that record, the `rc-to-ga-rehearsal-summary` artifact, and the 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 prerelease tag, rollback target, exact rollback command, planned GA date,
|
||||
and planned v5 end-of-support date.
|
||||
|
||||
|
||||
@@ -15,6 +15,9 @@ from typing import Any
|
||||
from repo_file_io import REPO_ROOT, git_env
|
||||
from release_promotion_policy_support import PROMOTION_METADATA_FIELDS
|
||||
|
||||
DEFAULT_RECORDS_DIR = Path("docs/release-control/v6/internal/records")
|
||||
DEFAULT_RECORD_PREFIX = "rc-to-ga-promotion-readiness-rehearsal"
|
||||
|
||||
|
||||
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
@@ -25,8 +28,11 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
source.add_argument("--summary-file", help="Existing rc-to-ga-rehearsal-summary.md file.")
|
||||
parser.add_argument(
|
||||
"--output",
|
||||
required=True,
|
||||
help="Repo-relative or absolute path for the generated record markdown.",
|
||||
help=(
|
||||
"Repo-relative or absolute path for the generated record markdown. "
|
||||
"Defaults to docs/release-control/v6/internal/records/"
|
||||
"rc-to-ga-promotion-readiness-rehearsal-<record-date>.md."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--record-date",
|
||||
@@ -63,6 +69,11 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
default=[],
|
||||
help="Additional operator note to include in the record. Repeatable.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--force",
|
||||
action="store_true",
|
||||
help="Overwrite the output path if it already exists.",
|
||||
)
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
@@ -176,6 +187,10 @@ def normalize_input_path(path_text: str) -> Path:
|
||||
return REPO_ROOT / path
|
||||
|
||||
|
||||
def default_output_path(record_date: str) -> Path:
|
||||
return REPO_ROOT / DEFAULT_RECORDS_DIR / f"{DEFAULT_RECORD_PREFIX}-{record_date}.md"
|
||||
|
||||
|
||||
def render_record(
|
||||
*,
|
||||
record_date: str,
|
||||
@@ -296,7 +311,16 @@ def main(argv: list[str] | None = None) -> int:
|
||||
summary_metadata = parse_summary_markdown(summary_markdown)
|
||||
validate_required_summary_metadata(summary_metadata)
|
||||
rollback_command = args.rollback_command or normalize_summary_command(summary_metadata["rollback_command"])
|
||||
output_path = normalize_output_path(args.output)
|
||||
output_path = (
|
||||
normalize_output_path(args.output)
|
||||
if args.output
|
||||
else default_output_path(args.record_date)
|
||||
)
|
||||
if output_path.exists() and not args.force:
|
||||
raise FileExistsError(
|
||||
f"output path already exists: {output_path}. "
|
||||
"Pass --force to overwrite or choose a different --output."
|
||||
)
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
output_path.write_text(
|
||||
render_record(
|
||||
|
||||
@@ -10,7 +10,9 @@ import unittest
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
import record_rc_to_ga_rehearsal as mod
|
||||
import record_rc_to_ga_rehearsal as wrapper_mod
|
||||
|
||||
mod = wrapper_mod._INTERNAL
|
||||
|
||||
|
||||
SUMMARY = """# Prerelease-to-GA Rehearsal Summary
|
||||
@@ -90,6 +92,66 @@ class RecordRcToGaRehearsalTest(unittest.TestCase):
|
||||
self.assertIn("2026-03-15", content)
|
||||
self.assertIn("Exact rollback or reinstall command: `./scripts/install.sh --version v5.1.23`", content)
|
||||
|
||||
def test_main_defaults_output_to_canonical_record_path(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
tmp_path = Path(tmp)
|
||||
summary_path = tmp_path / "summary.md"
|
||||
summary_path.write_text(SUMMARY, encoding="utf-8")
|
||||
|
||||
with mock.patch.object(mod, "REPO_ROOT", tmp_path):
|
||||
exit_code = mod.main(
|
||||
[
|
||||
"--summary-file",
|
||||
str(summary_path),
|
||||
"--record-date",
|
||||
"2026-03-12",
|
||||
]
|
||||
)
|
||||
|
||||
self.assertEqual(exit_code, 0)
|
||||
output_path = tmp_path / "docs/release-control/v6/internal/records/rc-to-ga-promotion-readiness-rehearsal-2026-03-12.md"
|
||||
self.assertTrue(output_path.is_file())
|
||||
self.assertIn("Prerelease-to-GA Rehearsal Record", output_path.read_text(encoding="utf-8"))
|
||||
|
||||
def test_main_refuses_to_overwrite_existing_record_without_force(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
tmp_path = Path(tmp)
|
||||
summary_path = tmp_path / "summary.md"
|
||||
summary_path.write_text(SUMMARY, encoding="utf-8")
|
||||
output_path = tmp_path / "record.md"
|
||||
output_path.write_text("existing\n", encoding="utf-8")
|
||||
|
||||
with self.assertRaisesRegex(FileExistsError, "output path already exists"):
|
||||
mod.main(
|
||||
[
|
||||
"--summary-file",
|
||||
str(summary_path),
|
||||
"--output",
|
||||
str(output_path),
|
||||
]
|
||||
)
|
||||
|
||||
def test_main_force_overwrites_existing_record(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
tmp_path = Path(tmp)
|
||||
summary_path = tmp_path / "summary.md"
|
||||
summary_path.write_text(SUMMARY, encoding="utf-8")
|
||||
output_path = tmp_path / "record.md"
|
||||
output_path.write_text("existing\n", encoding="utf-8")
|
||||
|
||||
exit_code = mod.main(
|
||||
[
|
||||
"--summary-file",
|
||||
str(summary_path),
|
||||
"--output",
|
||||
str(output_path),
|
||||
"--force",
|
||||
]
|
||||
)
|
||||
|
||||
self.assertEqual(exit_code, 0)
|
||||
self.assertIn("Prerelease-to-GA Rehearsal Record", output_path.read_text(encoding="utf-8"))
|
||||
|
||||
def test_download_summary_artifact_reads_named_artifact(self) -> None:
|
||||
artifact_text = "# summary\n"
|
||||
|
||||
@@ -267,6 +329,31 @@ class RecordRcToGaRehearsalTest(unittest.TestCase):
|
||||
result.stderr,
|
||||
)
|
||||
|
||||
def test_public_wrapper_reports_existing_output_path(self) -> None:
|
||||
wrapper = Path(__file__).resolve().parent.parent / "record_rc_to_ga_rehearsal.py"
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
tmp_path = Path(tmp)
|
||||
summary_path = tmp_path / "summary.md"
|
||||
summary_path.write_text(SUMMARY, encoding="utf-8")
|
||||
output_path = tmp_path / "record.md"
|
||||
output_path.write_text("existing\n", encoding="utf-8")
|
||||
result = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(wrapper),
|
||||
"--summary-file",
|
||||
str(summary_path),
|
||||
"--output",
|
||||
str(output_path),
|
||||
],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
self.assertEqual(result.returncode, 1)
|
||||
self.assertIn("output path already exists", result.stderr)
|
||||
|
||||
def test_parse_summary_markdown_accepts_legacy_artifact_labels(self) -> None:
|
||||
legacy_summary = (
|
||||
SUMMARY.replace("- Candidate stable tag: v6.0.0\n", "- Tag: v6.0.0\n")
|
||||
|
||||
@@ -32,6 +32,7 @@ normalize_summary_command = _INTERNAL.normalize_summary_command
|
||||
validate_required_summary_metadata = _INTERNAL.validate_required_summary_metadata
|
||||
normalize_output_path = _INTERNAL.normalize_output_path
|
||||
normalize_input_path = _INTERNAL.normalize_input_path
|
||||
default_output_path = _INTERNAL.default_output_path
|
||||
render_record = _INTERNAL.render_record
|
||||
main = _INTERNAL.main
|
||||
|
||||
@@ -39,6 +40,6 @@ main = _INTERNAL.main
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except (FileNotFoundError, ValueError) as exc:
|
||||
except (FileExistsError, FileNotFoundError, ValueError) as exc:
|
||||
print(f"error: {exc}", file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
|
||||
@@ -14,9 +14,13 @@ PROMOTION_PROOF_TRIGGER_PATHS: tuple[str, ...] = (
|
||||
"docs/release-control/v6/internal/HIGH_RISK_RELEASE_VERIFICATION_MATRIX.md",
|
||||
"docs/release-control/v6/internal/PRE_RELEASE_CHECKLIST.md",
|
||||
"docs/release-control/v6/internal/RC_TO_GA_REHEARSAL_TEMPLATE.md",
|
||||
"docs/release-control/v6/internal/RELEASE_PROMOTION_POLICY.md",
|
||||
"docs/release-control/v6/internal/SOURCE_OF_TRUTH.md",
|
||||
"docs/release-control/v6/internal/V5_MAINTENANCE_SUPPORT_POLICY.md",
|
||||
"docs/releases/V6_PRERELEASE_RUNBOOK.md",
|
||||
"scripts/check-workflow-dispatch-inputs.py",
|
||||
"scripts/release_control/internal/record_rc_to_ga_rehearsal.py",
|
||||
"scripts/release_control/record_rc_to_ga_rehearsal.py",
|
||||
"scripts/trigger-release.sh",
|
||||
"scripts/trigger-release-dry-run.sh",
|
||||
)
|
||||
|
||||
@@ -84,6 +84,13 @@ on:
|
||||
]
|
||||
)
|
||||
)
|
||||
self.assertTrue(
|
||||
slice_requires_staged_governance_inputs(
|
||||
[
|
||||
"scripts/release_control/record_rc_to_ga_rehearsal.py",
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
def test_slice_skips_staged_governance_inputs_for_unrelated_paths(self) -> None:
|
||||
self.assertFalse(
|
||||
|
||||
@@ -97,6 +97,8 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
|
||||
self.assertIn("replace any placeholder GA notice dates", content)
|
||||
self.assertIn("rc-to-ga-rehearsal-summary", content)
|
||||
self.assertIn("rc-to-ga-promotion-readiness", content)
|
||||
self.assertIn("record_rc_to_ga_rehearsal.py --run-id <run-id>", content)
|
||||
self.assertIn("rc-to-ga-promotion-readiness-rehearsal-<record-date>.md", content)
|
||||
self.assertIn(promotion_metadata_envelope(), normalize_ws(content))
|
||||
|
||||
def test_v5_support_policy_and_release_notes_publish_exact_notice(self) -> None:
|
||||
@@ -125,11 +127,17 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
|
||||
workflow = read(".github/workflows/release-dry-run.yml")
|
||||
release_workflow = read(".github/workflows/create-release.yml")
|
||||
dry_run_trigger = read("scripts/trigger-release-dry-run.sh")
|
||||
recorder = read("scripts/release_control/record_rc_to_ga_rehearsal.py")
|
||||
internal_recorder = read("scripts/release_control/internal/record_rc_to_ga_rehearsal.py")
|
||||
resolver = read("scripts/release_control/resolve_release_promotion.py")
|
||||
self.assertIn("GitHub Actions run URL", template)
|
||||
self.assertIn("Exact GA date to publish with GA", template)
|
||||
self.assertIn("record_rc_to_ga_rehearsal.py --run-id <run-id>", template)
|
||||
self.assertIn("rc-to-ga-promotion-readiness-rehearsal-<record-date>.md", template)
|
||||
self.assertIn(promotion_metadata_envelope(), normalize_ws(template))
|
||||
self.assertIn("rc-to-ga-rehearsal-summary", workflow)
|
||||
self.assertIn("record_rc_to_ga_rehearsal.py --run-id ${{ github.run_id }}", workflow)
|
||||
self.assertIn("rc-to-ga-promotion-readiness-rehearsal-<record-date>.md", 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)
|
||||
@@ -142,6 +150,9 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
|
||||
self.assertIn("Planned v5 end-of-support date", workflow)
|
||||
self.assertIn("resolve_release_promotion.py", release_workflow)
|
||||
self.assertIn("- Rollback command:", release_workflow)
|
||||
self.assertIn("default_output_path", internal_recorder)
|
||||
self.assertIn("output path already exists", internal_recorder)
|
||||
self.assertIn("default_output_path", recorder)
|
||||
self.assertIn("rollback_version is required for every release rehearsal and promotion", resolver)
|
||||
self.assertIn("Stable promotion requires promoted_from_tag", resolver)
|
||||
self.assertIn("Stable v6.0.0 requires ga_date in YYYY-MM-DD form", resolver)
|
||||
@@ -161,6 +172,7 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
|
||||
helper = read("scripts/trigger-release.sh")
|
||||
policy = read("docs/release-control/v6/internal/RELEASE_PROMOTION_POLICY.md")
|
||||
source_of_truth = read("docs/release-control/v6/internal/SOURCE_OF_TRUTH.md")
|
||||
runbook = read("docs/releases/V6_PRERELEASE_RUNBOOK.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)
|
||||
@@ -169,6 +181,7 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
|
||||
self.assertIn("Rollback command:", content)
|
||||
self.assertIn("rollback target and exact reinstall command recorded", policy)
|
||||
self.assertIn("rc-to-ga-rehearsal-summary", policy)
|
||||
self.assertIn("record_rc_to_ga_rehearsal.py --run-id <run-id>", policy)
|
||||
self.assertIn(promotion_metadata_envelope(), normalize_ws(policy))
|
||||
self.assertIn("recorded rollback target plus exact", source_of_truth)
|
||||
self.assertIn("hours of prerelease soak", resolver)
|
||||
@@ -208,6 +221,8 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
|
||||
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)
|
||||
self.assertIn("record_rc_to_ga_rehearsal.py --run-id <run-id>", runbook)
|
||||
self.assertIn("rc-to-ga-promotion-readiness-rehearsal-<record-date>.md", runbook)
|
||||
|
||||
def test_release_artifact_workflows_refuse_stable_without_matching_rc(self) -> None:
|
||||
publish = read(".github/workflows/publish-docker.yml")
|
||||
|
||||
Reference in New Issue
Block a user