mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-19 07:54:10 +00:00
Add release_cycle_artifact_globs so RC ceremony skips contract-update requirement
This commit is contained in:
parent
9a20bbd0b2
commit
7951da526b
6 changed files with 154 additions and 16 deletions
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": 12,
|
||||
"version": 13,
|
||||
"shared_ownerships": [
|
||||
{
|
||||
"path": "frontend-modern/src/api/agentProfiles.ts",
|
||||
|
|
@ -2742,6 +2742,13 @@
|
|||
"tests/integration/tests/runtime-defaults.ts",
|
||||
"VERSION"
|
||||
],
|
||||
"release_cycle_artifact_globs": [
|
||||
"docs/RELEASE_NOTES.md",
|
||||
"docs/UPGRADE_v6.md",
|
||||
"docs/releases/RELEASE_NOTES_v6_RC*_DRAFT.md",
|
||||
"docs/releases/V6_CHANGELOG_RC*_DRAFT.md",
|
||||
"docs/releases/V6_RC*_OPERATOR_SUPPORT_PACK_DRAFT.md"
|
||||
],
|
||||
"verification": {
|
||||
"allow_same_subsystem_tests": false,
|
||||
"test_prefixes": [],
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
"properties": {
|
||||
"version": {
|
||||
"type": "integer",
|
||||
"const": 12
|
||||
"const": 13
|
||||
},
|
||||
"shared_ownerships": {
|
||||
"type": "array",
|
||||
|
|
@ -69,6 +69,15 @@
|
|||
"minLength": 1
|
||||
}
|
||||
},
|
||||
"release_cycle_artifact_globs": {
|
||||
"description": "Glob patterns for files this subsystem owns but that are per-release-cycle artifacts (RC packet drafts, version-pointer docs, regenerated record files) rather than contract-shape surfaces. Changes matching any of these globs skip the contract-update requirement check for this subsystem; the file is still considered subsystem-tracked otherwise. Use sparingly: contract-shape files (Dockerfiles, workflow YAMLs, runbook procedures) belong in owned_files, not here.",
|
||||
"type": "array",
|
||||
"uniqueItems": true,
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
},
|
||||
"verification": {
|
||||
"$ref": "#/$defs/verification"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -374,7 +374,7 @@ func TestSubsystemRegistryExistsAndReferencesContracts(t *testing.T) {
|
|||
rel := "docs/release-control/v6/internal/subsystems/registry.json"
|
||||
content := readRepoFile(t, rel)
|
||||
assertContainsAll(t, rel, content, []string{
|
||||
"\"version\": 12",
|
||||
"\"version\": 13",
|
||||
"\"shared_ownerships\":",
|
||||
"\"subsystems\":",
|
||||
"\"verification\":",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ staged in the same commit.
|
|||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import fnmatch
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
|
@ -154,6 +155,19 @@ def subsystem_matches_path(rule: dict, path: str) -> bool:
|
|||
)
|
||||
|
||||
|
||||
def is_release_cycle_artifact(rule: dict, path: str) -> bool:
|
||||
"""Return True if path matches one of this subsystem's release_cycle_artifact_globs.
|
||||
|
||||
Release-cycle artifacts (RC packet drafts, version-pointer docs, regenerated
|
||||
record files) are tracked by the subsystem but are per-release-cycle content
|
||||
rather than contract-shape surfaces. They skip the contract-update
|
||||
requirement and verification requirements so routine RC-cycle maintenance
|
||||
doesn't need the contract-neutral bypass.
|
||||
"""
|
||||
globs = tuple(rule.get("release_cycle_artifact_globs", []))
|
||||
return any(fnmatch.fnmatchcase(path, glob) for glob in globs)
|
||||
|
||||
|
||||
def path_policy_matches(policy: dict, path: str) -> bool:
|
||||
prefixes = tuple(policy.get("match_prefixes", []))
|
||||
exact_files = tuple(policy.get("match_files", []))
|
||||
|
|
@ -253,20 +267,36 @@ def infer_impacted_subsystems(
|
|||
for rule in rules:
|
||||
if not subsystem_matches_path(rule, path):
|
||||
continue
|
||||
impacted.setdefault(
|
||||
entry = impacted.setdefault(
|
||||
str(rule["id"]),
|
||||
{
|
||||
"id": str(rule["id"]),
|
||||
"contract": str(rule["contract"]),
|
||||
"touched_runtime_files": [],
|
||||
"cycle_artifact_files": [],
|
||||
"verification": dict(rule.get("verification", {})),
|
||||
},
|
||||
)["touched_runtime_files"].append(path)
|
||||
)
|
||||
entry["touched_runtime_files"].append(path)
|
||||
if is_release_cycle_artifact(rule, path):
|
||||
# Path is owned by this subsystem but is a per-release-cycle
|
||||
# artifact (RC packet draft, version-pointer doc, regenerated
|
||||
# record). Tracked here so the lookup tool can still report
|
||||
# ownership; contract-update / verification requirement
|
||||
# builders consult this list to exclude these paths.
|
||||
entry["cycle_artifact_files"].append(path)
|
||||
|
||||
for subsystem_id, data in impacted.items():
|
||||
# Verification requirements derive from non-cycle-artifact touches only.
|
||||
# Release-cycle artifacts (RC packet drafts, version-pointer docs) don't
|
||||
# trigger verification proof requirements.
|
||||
cycle_artifacts = set(data.get("cycle_artifact_files", []))
|
||||
non_artifact_files = [
|
||||
path for path in data["touched_runtime_files"] if path not in cycle_artifacts
|
||||
]
|
||||
data["verification_requirements"] = build_verification_requirements(
|
||||
rules_by_id[subsystem_id],
|
||||
data["touched_runtime_files"],
|
||||
non_artifact_files,
|
||||
)
|
||||
|
||||
return impacted
|
||||
|
|
@ -283,19 +313,38 @@ def required_contract_updates(
|
|||
contract_index = load_contract_index(staged=use_staged_contract_index)
|
||||
|
||||
for subsystem_id, data in impacted_subsystems.items():
|
||||
# Contract update requirement triggers on non-cycle-artifact touches
|
||||
# only. If every touched file is a release-cycle artifact (RC packet
|
||||
# draft, version-pointer doc, regenerated record), there's no
|
||||
# contract-shape change to require — the contract describes the
|
||||
# release process, not the per-RC content of any specific cycle.
|
||||
cycle_artifacts = set(data.get("cycle_artifact_files", []))
|
||||
contract_impacting = sorted(
|
||||
set(data["touched_runtime_files"]) - cycle_artifacts
|
||||
)
|
||||
if not contract_impacting:
|
||||
continue
|
||||
required[data["contract"]] = {
|
||||
"subsystem": subsystem_id,
|
||||
"contract": data["contract"],
|
||||
"reason": "owner",
|
||||
"touched_runtime_files": sorted(set(data["touched_runtime_files"])),
|
||||
"touched_runtime_files": contract_impacting,
|
||||
"matched_references": [],
|
||||
}
|
||||
|
||||
# Dependent-reference contracts also skip cycle artifacts — the contract
|
||||
# describes the release process, not specific RC packet contents.
|
||||
cycle_artifact_paths = {
|
||||
path
|
||||
for data in impacted_subsystems.values()
|
||||
for path in data.get("cycle_artifact_files", [])
|
||||
}
|
||||
touched_runtime_files = sorted(
|
||||
{
|
||||
path
|
||||
for data in impacted_subsystems.values()
|
||||
for path in data.get("touched_runtime_files", [])
|
||||
if path not in cycle_artifact_paths
|
||||
}
|
||||
)
|
||||
for path in touched_runtime_files:
|
||||
|
|
|
|||
|
|
@ -4069,5 +4069,78 @@ class ContractNeutralOverrideTest(unittest.TestCase):
|
|||
self.assertIn("contract-neutral", stderr_value)
|
||||
|
||||
|
||||
class ReleaseCycleArtifactGuardTest(unittest.TestCase):
|
||||
"""The release_cycle_artifact_globs field in a subsystem rule lets
|
||||
per-RC-cycle artifacts (packet drafts, version-pointer docs, regenerated
|
||||
record files) skip the contract-update + verification requirements
|
||||
without losing the subsystem-tracked status. The lookup tool still
|
||||
reports ownership; the shape guard just doesn't demand a contract delta.
|
||||
Contract-shape files (Dockerfile, workflow YAMLs, runbook) still
|
||||
trigger as normal."""
|
||||
|
||||
def test_rc_draft_packets_register_as_cycle_artifacts(self):
|
||||
impacted = infer_impacted_subsystems(
|
||||
[
|
||||
"docs/releases/RELEASE_NOTES_v6_RC5_DRAFT.md",
|
||||
"docs/releases/V6_CHANGELOG_RC5_DRAFT.md",
|
||||
"docs/releases/V6_RC5_OPERATOR_SUPPORT_PACK_DRAFT.md",
|
||||
]
|
||||
)
|
||||
self.assertIn("deployment-installability", impacted)
|
||||
entry = impacted["deployment-installability"]
|
||||
# All three files are tracked as touches AND as cycle artifacts.
|
||||
self.assertEqual(set(entry["touched_runtime_files"]), set(entry["cycle_artifact_files"]))
|
||||
# No contract-update requirement since every touch is a cycle artifact.
|
||||
self.assertEqual(required_contract_updates([
|
||||
"docs/releases/RELEASE_NOTES_v6_RC5_DRAFT.md",
|
||||
"docs/releases/V6_CHANGELOG_RC5_DRAFT.md",
|
||||
"docs/releases/V6_RC5_OPERATOR_SUPPORT_PACK_DRAFT.md",
|
||||
]), {})
|
||||
|
||||
def test_version_pointer_docs_register_as_cycle_artifacts(self):
|
||||
impacted = infer_impacted_subsystems(
|
||||
["docs/RELEASE_NOTES.md", "docs/UPGRADE_v6.md"]
|
||||
)
|
||||
self.assertIn("deployment-installability", impacted)
|
||||
self.assertEqual(
|
||||
required_contract_updates(["docs/RELEASE_NOTES.md", "docs/UPGRADE_v6.md"]),
|
||||
{},
|
||||
)
|
||||
|
||||
def test_contract_shape_files_still_require_contract_update(self):
|
||||
impacted = infer_impacted_subsystems(["docker-compose.yml"])
|
||||
self.assertIn("deployment-installability", impacted)
|
||||
self.assertEqual(
|
||||
impacted["deployment-installability"]["cycle_artifact_files"],
|
||||
[],
|
||||
"docker-compose.yml is a contract-shape file, not a cycle artifact",
|
||||
)
|
||||
required = required_contract_updates(["docker-compose.yml"])
|
||||
self.assertIn(
|
||||
"docs/release-control/v6/internal/subsystems/deployment-installability.md",
|
||||
required,
|
||||
)
|
||||
|
||||
def test_mixed_artifact_and_shape_only_requires_contract_update_for_shape(self):
|
||||
paths = [
|
||||
"docs/releases/RELEASE_NOTES_v6_RC5_DRAFT.md",
|
||||
"scripts/install-docker.sh",
|
||||
]
|
||||
impacted = infer_impacted_subsystems(paths)
|
||||
entry = impacted["deployment-installability"]
|
||||
self.assertEqual(
|
||||
set(entry["cycle_artifact_files"]),
|
||||
{"docs/releases/RELEASE_NOTES_v6_RC5_DRAFT.md"},
|
||||
)
|
||||
required = required_contract_updates(paths)
|
||||
contract_path = "docs/release-control/v6/internal/subsystems/deployment-installability.md"
|
||||
self.assertIn(contract_path, required)
|
||||
# The contract update requirement carries the non-artifact file only.
|
||||
self.assertEqual(
|
||||
required[contract_path]["touched_runtime_files"],
|
||||
["scripts/install-docker.sh"],
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ class RegistryAuditTest(unittest.TestCase):
|
|||
|
||||
def test_audit_registry_payload_accepts_valid_minimal_registry(self) -> None:
|
||||
payload = {
|
||||
"version": 12,
|
||||
"version": 13,
|
||||
"shared_ownerships": [],
|
||||
"subsystems": [
|
||||
{
|
||||
|
|
@ -55,7 +55,7 @@ class RegistryAuditTest(unittest.TestCase):
|
|||
|
||||
def test_audit_registry_payload_accepts_cross_repo_owned_prefixes(self) -> None:
|
||||
payload = {
|
||||
"version": 12,
|
||||
"version": 13,
|
||||
"shared_ownerships": [],
|
||||
"subsystems": [
|
||||
{
|
||||
|
|
@ -101,7 +101,7 @@ class RegistryAuditTest(unittest.TestCase):
|
|||
|
||||
def test_audit_registry_payload_flags_unknown_lane_and_missing_contract(self) -> None:
|
||||
payload = {
|
||||
"version": 12,
|
||||
"version": 13,
|
||||
"shared_ownerships": [],
|
||||
"subsystems": [
|
||||
{
|
||||
|
|
@ -130,7 +130,7 @@ class RegistryAuditTest(unittest.TestCase):
|
|||
|
||||
def test_audit_registry_payload_flags_explicit_coverage_gap(self) -> None:
|
||||
payload = {
|
||||
"version": 12,
|
||||
"version": 13,
|
||||
"shared_ownerships": [],
|
||||
"subsystems": [
|
||||
{
|
||||
|
|
@ -173,7 +173,7 @@ class RegistryAuditTest(unittest.TestCase):
|
|||
|
||||
def test_audit_registry_payload_requires_explicit_coverage_flag_true(self) -> None:
|
||||
payload = {
|
||||
"version": 12,
|
||||
"version": 13,
|
||||
"shared_ownerships": [],
|
||||
"subsystems": [
|
||||
{
|
||||
|
|
@ -218,7 +218,7 @@ class RegistryAuditTest(unittest.TestCase):
|
|||
|
||||
def test_audit_registry_payload_rejects_uncanonical_ordering(self) -> None:
|
||||
payload = {
|
||||
"version": 12,
|
||||
"version": 13,
|
||||
"shared_ownerships": [],
|
||||
"subsystems": [
|
||||
{
|
||||
|
|
@ -318,7 +318,7 @@ class RegistryAuditTest(unittest.TestCase):
|
|||
|
||||
def test_audit_registry_payload_rejects_fully_shadowed_path_policy(self) -> None:
|
||||
payload = {
|
||||
"version": 12,
|
||||
"version": 13,
|
||||
"shared_ownerships": [],
|
||||
"subsystems": [
|
||||
{
|
||||
|
|
@ -371,7 +371,7 @@ class RegistryAuditTest(unittest.TestCase):
|
|||
|
||||
def test_audit_registry_payload_requires_declared_shared_ownership(self) -> None:
|
||||
payload = {
|
||||
"version": 12,
|
||||
"version": 13,
|
||||
"shared_ownerships": [],
|
||||
"subsystems": [
|
||||
{
|
||||
|
|
@ -441,7 +441,7 @@ class RegistryAuditTest(unittest.TestCase):
|
|||
|
||||
def test_audit_registry_payload_rejects_stale_or_wrong_shared_ownership(self) -> None:
|
||||
payload = {
|
||||
"version": 12,
|
||||
"version": 13,
|
||||
"shared_ownerships": [
|
||||
{
|
||||
"path": "internal/api/resources.go",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue