|
76 | 76 | ".github/workflows/release-candidate-cleanup.yml", |
77 | 77 | } |
78 | 78 | ) |
| 79 | +REPO_ROOT = Path(__file__).resolve().parents[2] |
| 80 | +STANDARD_JOURNEY_MANIFEST = ( |
| 81 | + REPO_ROOT / "docs/site/src/data/standard-journeys.yaml" |
| 82 | +) |
| 83 | +AUTHORING_REFERENCE_MANIFEST = ( |
| 84 | + REPO_ROOT / "docs/site/scripts/authoring-reference-sources.json" |
| 85 | +) |
| 86 | + |
| 87 | + |
| 88 | +def standard_journey_sources( |
| 89 | + manifest_path: Path = STANDARD_JOURNEY_MANIFEST, |
| 90 | +) -> frozenset[str]: |
| 91 | + """Extract the exact canonical source paths from the journey manifest. |
| 92 | +
|
| 93 | + The manifest owns this routing inventory. Keeping the deliberately small |
| 94 | + parser here avoids adding a YAML dependency to the CI classifier. |
| 95 | + """ |
| 96 | + |
| 97 | + sources: list[str] = [] |
| 98 | + in_canonical_sources = False |
| 99 | + for line in manifest_path.read_text(encoding="utf-8").splitlines(): |
| 100 | + if line == " canonical_sources:": |
| 101 | + in_canonical_sources = True |
| 102 | + continue |
| 103 | + if in_canonical_sources and line.startswith(" - "): |
| 104 | + source = line.removeprefix(" - ").strip() |
| 105 | + if ( |
| 106 | + not source |
| 107 | + or source[0] in {"'", '"', "{", "["} |
| 108 | + or source.startswith(("/", "../")) |
| 109 | + ): |
| 110 | + raise ValueError( |
| 111 | + "standard journey canonical_sources must contain " |
| 112 | + f"unquoted repository-relative paths: {source!r}" |
| 113 | + ) |
| 114 | + sources.append(source) |
| 115 | + continue |
| 116 | + if in_canonical_sources and line and not line.startswith(" "): |
| 117 | + in_canonical_sources = False |
| 118 | + |
| 119 | + if not sources: |
| 120 | + raise ValueError( |
| 121 | + f"no standard journey canonical_sources found in {manifest_path}" |
| 122 | + ) |
| 123 | + return frozenset(sources) |
| 124 | + |
| 125 | + |
| 126 | +STANDARD_JOURNEY_SOURCES = standard_journey_sources() |
| 127 | + |
| 128 | + |
| 129 | +def authoring_reference_contract_sources( |
| 130 | + manifest_path: Path = AUTHORING_REFERENCE_MANIFEST, |
| 131 | +) -> tuple[str, ...]: |
| 132 | + """Derive repository inputs from the published reference source contract.""" |
| 133 | + |
| 134 | + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) |
| 135 | + schema_sources = manifest.get("schema_sources") |
| 136 | + field_knowledge = manifest.get("field_knowledge") |
| 137 | + human_intent = manifest.get("human_intent") |
| 138 | + runtime_intent = manifest.get("runtime_intent") |
| 139 | + if ( |
| 140 | + not isinstance(schema_sources, list) |
| 141 | + or not schema_sources |
| 142 | + or not all(isinstance(source, str) and source for source in schema_sources) |
| 143 | + or not isinstance(field_knowledge, str) |
| 144 | + or not field_knowledge |
| 145 | + or not isinstance(human_intent, str) |
| 146 | + or not human_intent |
| 147 | + or not isinstance(runtime_intent, list) |
| 148 | + or not runtime_intent |
| 149 | + or not all(isinstance(source, str) and source for source in runtime_intent) |
| 150 | + ): |
| 151 | + raise ValueError( |
| 152 | + f"authoring-reference manifest has an invalid source contract: {manifest_path}" |
| 153 | + ) |
| 154 | + |
| 155 | + sources = [ |
| 156 | + ( |
| 157 | + f"schemas/{source}" |
| 158 | + if source.startswith("registry-") |
| 159 | + else f"crates/registryctl/schemas/project-authoring/{source}" |
| 160 | + ) |
| 161 | + for source in schema_sources |
| 162 | + ] |
| 163 | + for source in (field_knowledge.split("#", 1)[0], human_intent): |
| 164 | + sources.append( |
| 165 | + source if source.startswith("crates/") else f"crates/registryctl/{source}" |
| 166 | + ) |
| 167 | + sources.extend(runtime_intent) |
| 168 | + if any(source.startswith(("/", "../")) for source in sources): |
| 169 | + raise ValueError( |
| 170 | + "authoring-reference source contract must use repository-relative paths" |
| 171 | + ) |
| 172 | + if len(sources) != len(set(sources)): |
| 173 | + raise ValueError("authoring-reference source contract paths must be unique") |
| 174 | + return tuple(sources) |
| 175 | + |
| 176 | + |
| 177 | +def validate_authoring_reference_routing( |
| 178 | + contract_sources: tuple[str, ...], |
| 179 | + inputs: tuple[tuple[str, str], ...], |
| 180 | +) -> None: |
| 181 | + """Fail when a source-contract input can change without rebuilding docs.""" |
| 182 | + |
| 183 | + missing = [ |
| 184 | + source |
| 185 | + for source in contract_sources |
| 186 | + if not any(fnmatch.fnmatchcase(source, pattern) for pattern, _ in inputs) |
| 187 | + ] |
| 188 | + if missing: |
| 189 | + raise ValueError( |
| 190 | + "authoring-reference CI inputs do not route source-contract paths: " |
| 191 | + f"{missing}" |
| 192 | + ) |
| 193 | + |
| 194 | + |
| 195 | +def authoring_reference_inputs( |
| 196 | + manifest_path: Path = AUTHORING_REFERENCE_MANIFEST, |
| 197 | +) -> tuple[tuple[str, str], ...]: |
| 198 | + """Load the authoring-reference CI routing inventory from its owner.""" |
| 199 | + |
| 200 | + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) |
| 201 | + inputs = manifest.get("ci_inputs") |
| 202 | + if not isinstance(inputs, list) or not inputs: |
| 203 | + raise ValueError( |
| 204 | + f"authoring-reference manifest has no ci_inputs: {manifest_path}" |
| 205 | + ) |
| 206 | + |
| 207 | + parsed: list[tuple[str, str]] = [] |
| 208 | + for index, entry in enumerate(inputs): |
| 209 | + if not isinstance(entry, dict): |
| 210 | + raise ValueError( |
| 211 | + f"authoring-reference ci_inputs[{index}] must be an object" |
| 212 | + ) |
| 213 | + pattern = entry.get("pattern") |
| 214 | + sample = entry.get("sample") |
| 215 | + if ( |
| 216 | + not isinstance(pattern, str) |
| 217 | + or not pattern |
| 218 | + or not isinstance(sample, str) |
| 219 | + or not sample |
| 220 | + or pattern.startswith(("/", "../")) |
| 221 | + or sample.startswith(("/", "../")) |
| 222 | + or not fnmatch.fnmatchcase(sample, pattern) |
| 223 | + ): |
| 224 | + raise ValueError( |
| 225 | + "authoring-reference CI inputs must have matching " |
| 226 | + f"repository-relative pattern/sample pairs: {entry!r}" |
| 227 | + ) |
| 228 | + if not (REPO_ROOT / sample).is_file(): |
| 229 | + raise ValueError( |
| 230 | + "authoring-reference CI input samples must name existing " |
| 231 | + f"repository files: {sample!r}" |
| 232 | + ) |
| 233 | + parsed.append((pattern, sample)) |
| 234 | + |
| 235 | + patterns = [pattern for pattern, _ in parsed] |
| 236 | + samples = [sample for _, sample in parsed] |
| 237 | + if len(patterns) != len(set(patterns)) or len(samples) != len(set(samples)): |
| 238 | + raise ValueError("authoring-reference CI patterns and samples must be unique") |
| 239 | + result = tuple(parsed) |
| 240 | + validate_authoring_reference_routing( |
| 241 | + authoring_reference_contract_sources(manifest_path), |
| 242 | + result, |
| 243 | + ) |
| 244 | + return result |
| 245 | + |
| 246 | + |
| 247 | +AUTHORING_REFERENCE_CONTRACT_SOURCES = authoring_reference_contract_sources() |
| 248 | +AUTHORING_REFERENCE_INPUTS = authoring_reference_inputs() |
| 249 | +AUTHORING_REFERENCE_PATTERNS = tuple( |
| 250 | + pattern for pattern, _ in AUTHORING_REFERENCE_INPUTS |
| 251 | +) |
79 | 252 |
|
80 | 253 |
|
81 | 254 | class Workspace: |
@@ -256,12 +429,42 @@ def classify( |
256 | 429 | path, |
257 | 430 | "crates/registry-relay/docs/*", |
258 | 431 | "crates/registry-relay/openapi/*", |
| 432 | + "crates/registry-relay/src/api/openapi.rs", |
| 433 | + "crates/registryctl/assets/project-starters/*", |
| 434 | + "crates/registry-notary-server/src/standalone/activation.rs", |
| 435 | + "crates/registry-platform-ops/src/lib.rs", |
| 436 | + "crates/registry-relay/src/consultation/*", |
| 437 | + "crates/registryctl/schemas/project-reports/*", |
| 438 | + "crates/registryctl/src/templates/*", |
| 439 | + "crates/registryctl/tests/fixtures/project-authoring/*", |
| 440 | + "crates/registryctl/tests/fixtures/project-reports/*", |
259 | 441 | "docs/site/*", |
260 | 442 | "products/manifest/docs/*", |
261 | 443 | "products/notary/docs/*", |
262 | 444 | "products/notary/openapi/*", |
| 445 | + *AUTHORING_REFERENCE_PATTERNS, |
263 | 446 | ) |
264 | | - or path == ".github/workflows/docs-pages.yml" |
| 447 | + or path |
| 448 | + in { |
| 449 | + ".github/workflows/docs-pages.yml", |
| 450 | + "crates/registry-relay/src/main.rs", |
| 451 | + "crates/registry-relay/src/process_startup.rs", |
| 452 | + "crates/registry-relay/src/server.rs", |
| 453 | + "crates/registryctl/src/main.rs", |
| 454 | + "crates/registryctl/src/project_authoring/capability_inventory.rs", |
| 455 | + "crates/registryctl/src/project_authoring/diagnostic_reference.rs", |
| 456 | + "crates/registryctl/src/project_authoring/diagnostics.rs", |
| 457 | + "crates/registryctl/src/project_authoring/fixture_diagnostics.rs", |
| 458 | + "crates/registryctl/src/project_authoring/fixture_coverage.rs", |
| 459 | + "crates/registryctl/src/project_authoring/migration.rs", |
| 460 | + "crates/registryctl/src/project_authoring/output.rs", |
| 461 | + "crates/registryctl/src/project_authoring/preflight.rs", |
| 462 | + "crates/registryctl/src/project_authoring/promotion.rs", |
| 463 | + "crates/registryctl/src/project_authoring/report_contract.rs", |
| 464 | + "crates/registryctl/src/project_authoring/semantic_comparison.rs", |
| 465 | + "crates/registryctl/tests/fixtures/project-authoring-journeys.yaml", |
| 466 | + } |
| 467 | + or path in STANDARD_JOURNEY_SOURCES |
265 | 468 | for path in paths |
266 | 469 | ) |
267 | 470 | docs_archives = any( |
@@ -310,8 +513,29 @@ def classify( |
310 | 513 | or path.startswith("products/notary/") |
311 | 514 | for path in paths |
312 | 515 | ) |
| 516 | + tutorial_source_under_test = any( |
| 517 | + matches( |
| 518 | + path, |
| 519 | + "crates/registry-notary/src/*", |
| 520 | + "crates/registry-notary-core/src/*", |
| 521 | + "crates/registry-notary-server/src/*", |
| 522 | + "crates/registryctl/src/templates/*", |
| 523 | + ) |
| 524 | + or path |
| 525 | + in { |
| 526 | + "crates/registry-relay/src/api/openapi.rs", |
| 527 | + "crates/registry-relay/src/main.rs", |
| 528 | + "crates/registry-relay/src/server.rs", |
| 529 | + "crates/registryctl/src/main.rs", |
| 530 | + "crates/registryctl/src/project_authoring/output.rs", |
| 531 | + } |
| 532 | + for path in paths |
| 533 | + ) |
313 | 534 | registryctl_tutorial = ( |
314 | | - complete or tutorial_infrastructure or bool(affected & TUTORIAL_PACKAGES) |
| 535 | + complete |
| 536 | + or tutorial_infrastructure |
| 537 | + or tutorial_source_under_test |
| 538 | + or bool(affected & TUTORIAL_PACKAGES) |
315 | 539 | ) |
316 | 540 |
|
317 | 541 | matrix = [] |
|
0 commit comments