mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-19 02:28:59 +00:00
Map legacy stable releases in control plane
This commit is contained in:
parent
5e7c6d4c7b
commit
1a0acd310a
6 changed files with 151 additions and 3 deletions
|
|
@ -7,6 +7,13 @@
|
|||
"control_plane_schema": "docs/release-control/control_plane.schema.json",
|
||||
"active_profile_id": "v6",
|
||||
"active_target_id": "v6-rc-stabilization",
|
||||
"legacy_release_lines": [
|
||||
{
|
||||
"version_prefix": "5.1.",
|
||||
"prerelease_branch": "main",
|
||||
"stable_branch": "main"
|
||||
}
|
||||
],
|
||||
"profiles": [
|
||||
{
|
||||
"id": "v6",
|
||||
|
|
|
|||
|
|
@ -49,6 +49,12 @@
|
|||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"legacy_release_lines": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/legacy_release_line"
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
|
|
@ -65,6 +71,29 @@
|
|||
}
|
||||
},
|
||||
"$defs": {
|
||||
"legacy_release_line": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"version_prefix",
|
||||
"prerelease_branch",
|
||||
"stable_branch"
|
||||
],
|
||||
"properties": {
|
||||
"version_prefix": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"prerelease_branch": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"stable_branch": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
|
|
|||
|
|
@ -32,6 +32,10 @@ model that active and future release profiles reuse.
|
|||
The active profile also owns the governed prerelease and stable release
|
||||
branches for that line, so workflows and local release tooling must resolve
|
||||
branch requirements from the control plane instead of hard-coding `main`.
|
||||
When an older maintenance line still needs governed release or demo-routing
|
||||
support after the active profile has moved on, that branch mapping must stay
|
||||
explicit in `control_plane.json` as a legacy release-line override rather
|
||||
than being inferred from the active profile or smuggled into one workflow.
|
||||
4. Profile changes must reuse the same control-plane machinery.
|
||||
Future releases should switch or add profiles rather than fork the guardrail
|
||||
system.
|
||||
|
|
@ -250,10 +254,16 @@ user language should update the control plane.
|
|||
long-term system.
|
||||
8. Until the explicit post-GA branch cutover happens, both prerelease and
|
||||
stable v6 promotions resolve to `pulse/v6-release` via `control_plane.json`.
|
||||
9. `AGENT_VALUES.md` is the evergreen values-only entry point; prompts should
|
||||
9. Legacy maintenance releases that still feed governed automation outside the
|
||||
active v6 line must also resolve through `control_plane.json`.
|
||||
Right now the remaining `5.1.x` stable maintenance line resolves to `main`
|
||||
via an explicit legacy release-line override, so stable demo automation can
|
||||
validate the real shipped stable tag without pretending that v5 tags belong
|
||||
to `pulse/v6-release`.
|
||||
10. `AGENT_VALUES.md` is the evergreen values-only entry point; prompts should
|
||||
stay close to that layer and delegate detailed behavior to the governed
|
||||
control-plane and profile-specific files.
|
||||
10. `python3 scripts/release_control/control_plane.py --agent-entrypoint --pretty`
|
||||
11. `python3 scripts/release_control/control_plane.py --agent-entrypoint --pretty`
|
||||
is the executable entrypoint for that ordered guidance bundle; agents
|
||||
should prefer it over reconstructing the bundle ad hoc.
|
||||
That entrypoint should lead with lightweight startup files plus derived
|
||||
|
|
|
|||
|
|
@ -203,7 +203,10 @@ preview paths in menus, CLI help text, operator diagnostics, and runtime logs
|
|||
rather than as release-candidate promises.
|
||||
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.
|
||||
`pulse/v6-release`, `main`, or any later branch literal inline; when a stable
|
||||
maintenance line such as `5.1.x` remains live after the active profile has
|
||||
moved on, that branch routing must come from an explicit control-plane release
|
||||
line override instead of being guessed inside the workflow.
|
||||
That same branch-policy contract must survive step boundaries inside the
|
||||
workflows themselves: `.github/workflows/create-release.yml` and
|
||||
`.github/workflows/release-dry-run.yml` must pass the resolved
|
||||
|
|
|
|||
|
|
@ -30,6 +30,11 @@ REQUIRED_CONTROL_PLANE_FIELDS = (
|
|||
"profiles",
|
||||
"targets",
|
||||
)
|
||||
REQUIRED_LEGACY_RELEASE_LINE_FIELDS = (
|
||||
"version_prefix",
|
||||
"prerelease_branch",
|
||||
"stable_branch",
|
||||
)
|
||||
REQUIRED_PROFILE_FIELDS = (
|
||||
"id",
|
||||
"lifecycle",
|
||||
|
|
@ -145,6 +150,40 @@ def validate_control_plane_payload(payload: dict[str, Any]) -> dict[str, Any]:
|
|||
if not isinstance(active_target_id, str) or not active_target_id.strip():
|
||||
raise ValueError("control plane active_target_id must be a non-empty string")
|
||||
|
||||
legacy_release_lines: list[dict[str, str]] = []
|
||||
raw_legacy_release_lines = payload.get("legacy_release_lines", [])
|
||||
if not isinstance(raw_legacy_release_lines, list):
|
||||
raise ValueError("control plane legacy_release_lines must be a list when declared")
|
||||
seen_version_prefixes: set[str] = set()
|
||||
for index, raw in enumerate(raw_legacy_release_lines):
|
||||
if not isinstance(raw, dict):
|
||||
raise ValueError(f"control plane legacy_release_lines[{index}] must be an object")
|
||||
line: dict[str, str] = {}
|
||||
for field in REQUIRED_LEGACY_RELEASE_LINE_FIELDS:
|
||||
if field not in raw:
|
||||
raise ValueError(f"control plane legacy_release_lines[{index}] missing {field!r}")
|
||||
value = raw[field]
|
||||
if not isinstance(value, str) or not value.strip():
|
||||
raise ValueError(
|
||||
f"control plane legacy_release_lines[{index}].{field} must be a non-empty string"
|
||||
)
|
||||
line[field] = str(value)
|
||||
version_prefix = line["version_prefix"]
|
||||
if version_prefix in seen_version_prefixes:
|
||||
raise ValueError(
|
||||
f"control plane legacy_release_lines duplicates version_prefix {version_prefix!r}"
|
||||
)
|
||||
seen_version_prefixes.add(version_prefix)
|
||||
_clean_branch_name(
|
||||
line["prerelease_branch"],
|
||||
context=f"legacy_release_line {version_prefix}.prerelease_branch",
|
||||
)
|
||||
_clean_branch_name(
|
||||
line["stable_branch"],
|
||||
context=f"legacy_release_line {version_prefix}.stable_branch",
|
||||
)
|
||||
legacy_release_lines.append(line)
|
||||
|
||||
raw_profiles = payload.get("profiles")
|
||||
if not isinstance(raw_profiles, list) or not raw_profiles:
|
||||
raise ValueError("control plane profiles must be a non-empty list")
|
||||
|
|
@ -261,6 +300,7 @@ def validate_control_plane_payload(payload: dict[str, Any]) -> dict[str, Any]:
|
|||
"agent_values_doc_path": REPO_ROOT / AGENT_VALUES_DOC_REL,
|
||||
"control_plane_doc_path": REPO_ROOT / CONTROL_PLANE_DOC_REL,
|
||||
"control_plane_schema_path": REPO_ROOT / CONTROL_PLANE_SCHEMA_REL,
|
||||
"legacy_release_lines": legacy_release_lines,
|
||||
"profiles": profiles,
|
||||
"profiles_by_id": profiles_by_id,
|
||||
"targets": targets,
|
||||
|
|
@ -383,6 +423,30 @@ def is_prerelease_version(version: str) -> bool:
|
|||
return bool(PRERELEASE_VERSION_PATTERN.search(version))
|
||||
|
||||
|
||||
def legacy_release_line_for_version(
|
||||
version: str,
|
||||
*,
|
||||
control_plane: dict[str, Any] | None = None,
|
||||
staged: bool = False,
|
||||
) -> dict[str, str] | None:
|
||||
resolved = control_plane
|
||||
if resolved is None:
|
||||
resolved = active_control_plane(staged=staged)
|
||||
elif "profiles_by_id" not in resolved:
|
||||
resolved = validate_control_plane_payload(control_plane)
|
||||
|
||||
normalized_version = version[1:] if version.startswith("v") else version
|
||||
legacy_release_lines = sorted(
|
||||
resolved.get("legacy_release_lines", []),
|
||||
key=lambda line: len(line["version_prefix"]),
|
||||
reverse=True,
|
||||
)
|
||||
for line in legacy_release_lines:
|
||||
if normalized_version.startswith(line["version_prefix"]):
|
||||
return line
|
||||
return None
|
||||
|
||||
|
||||
def release_branch_for_version(
|
||||
version: str,
|
||||
*,
|
||||
|
|
@ -394,6 +458,13 @@ def release_branch_for_version(
|
|||
resolved = active_control_plane(staged=staged)
|
||||
elif "profiles_by_id" not in resolved:
|
||||
resolved = validate_control_plane_payload(control_plane)
|
||||
legacy_release_line = legacy_release_line_for_version(version, control_plane=resolved)
|
||||
if legacy_release_line is not None:
|
||||
return (
|
||||
legacy_release_line["prerelease_branch"]
|
||||
if is_prerelease_version(version)
|
||||
else legacy_release_line["stable_branch"]
|
||||
)
|
||||
return resolved["prerelease_branch"] if is_prerelease_version(version) else resolved["stable_branch"]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,13 @@ VALID_PAYLOAD = {
|
|||
"control_plane_schema": "docs/release-control/control_plane.schema.json",
|
||||
"active_profile_id": "v6",
|
||||
"active_target_id": "v6-rc-cut",
|
||||
"legacy_release_lines": [
|
||||
{
|
||||
"version_prefix": "5.1.",
|
||||
"prerelease_branch": "main",
|
||||
"stable_branch": "main",
|
||||
}
|
||||
],
|
||||
"profiles": [
|
||||
{
|
||||
"id": "v6",
|
||||
|
|
@ -167,10 +174,31 @@ class ControlPlaneAuditTest(unittest.TestCase):
|
|||
self.assertFalse(report["summary"]["active_target_completion_met"])
|
||||
|
||||
def test_release_branch_for_version_uses_profile_branch_policy(self) -> None:
|
||||
self.assertEqual(release_branch_for_version("5.1.27", control_plane=VALID_PAYLOAD), "main")
|
||||
self.assertEqual(release_branch_for_version("5.1.27-rc.3", control_plane=VALID_PAYLOAD), "main")
|
||||
self.assertEqual(release_branch_for_version("6.0.0-rc.1", control_plane=VALID_PAYLOAD), "pulse/v6-release")
|
||||
self.assertEqual(release_branch_for_version("6.0.0-dev", control_plane=VALID_PAYLOAD), "pulse/v6-release")
|
||||
self.assertEqual(release_branch_for_version("6.0.0", control_plane=VALID_PAYLOAD), "pulse/v6-release")
|
||||
|
||||
def test_release_branch_for_version_prefers_longest_legacy_prefix(self) -> None:
|
||||
payload = dict(VALID_PAYLOAD)
|
||||
payload["legacy_release_lines"] = [
|
||||
{
|
||||
"version_prefix": "5.",
|
||||
"prerelease_branch": "main",
|
||||
"stable_branch": "main",
|
||||
},
|
||||
{
|
||||
"version_prefix": "5.1.",
|
||||
"prerelease_branch": "pulse/release-5.1.25",
|
||||
"stable_branch": "pulse/release-5.1.25",
|
||||
},
|
||||
]
|
||||
self.assertEqual(
|
||||
release_branch_for_version("5.1.27", control_plane=payload),
|
||||
"pulse/release-5.1.25",
|
||||
)
|
||||
|
||||
def test_audit_flags_stale_active_target(self) -> None:
|
||||
report = audit_control_plane_payload(
|
||||
VALID_PAYLOAD,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue