Govern RC-to-GA rehearsal record capture

This commit is contained in:
rcourtman
2026-04-11 14:08:57 +01:00
parent 3c1f0ba0d9
commit d643b0fb51
13 changed files with 227 additions and 47 deletions
+7
View File
@@ -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"
]
+7 -3
View File
@@ -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")