Add release_cycle_artifact_globs so RC ceremony skips contract-update requirement

This commit is contained in:
rcourtman 2026-05-11 22:55:29 +01:00
parent 9a20bbd0b2
commit 7951da526b
6 changed files with 154 additions and 16 deletions

View file

@ -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": [],

View file

@ -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"
}

View file

@ -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\":",

View file

@ -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:

View file

@ -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()

View file

@ -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",