Add Policy DSL Validator, Schema Exporter, and Simulation Smoke tools
	
		
			
	
		
	
	
		
	
		
			Some checks failed
		
		
	
	
		
			
				
	
				Docs CI / lint-and-preview (push) Has been cancelled
				
			
		
		
	
	
				
					
				
			
		
			Some checks failed
		
		
	
	Docs CI / lint-and-preview (push) Has been cancelled
				
			- Implemented PolicyDslValidator with command-line options for strict mode and JSON output. - Created PolicySchemaExporter to generate JSON schemas for policy-related models. - Developed PolicySimulationSmoke tool to validate policy simulations against expected outcomes. - Added project files and necessary dependencies for each tool. - Ensured proper error handling and usage instructions across tools.
This commit is contained in:
		
							
								
								
									
										183
									
								
								ops/devops/validate_restore_sources.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										183
									
								
								ops/devops/validate_restore_sources.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,183 @@ | ||||
| #!/usr/bin/env python3 | ||||
|  | ||||
| """ | ||||
| Validate NuGet source ordering for StellaOps. | ||||
|  | ||||
| Ensures `local-nuget` is the highest priority feed in both NuGet.config and the | ||||
| Directory.Build.props restore configuration. Fails fast with actionable errors | ||||
| so CI/offline kit workflows can assert deterministic restore ordering. | ||||
| """ | ||||
|  | ||||
| from __future__ import annotations | ||||
|  | ||||
| import argparse | ||||
| import subprocess | ||||
| import sys | ||||
| import xml.etree.ElementTree as ET | ||||
| from pathlib import Path | ||||
|  | ||||
|  | ||||
| REPO_ROOT = Path(__file__).resolve().parents[2] | ||||
| NUGET_CONFIG = REPO_ROOT / "NuGet.config" | ||||
| ROOT_PROPS = REPO_ROOT / "Directory.Build.props" | ||||
| EXPECTED_SOURCE_KEYS = ["local", "dotnet-public", "nuget.org"] | ||||
|  | ||||
|  | ||||
| class ValidationError(Exception): | ||||
|     """Raised when validation fails.""" | ||||
|  | ||||
|  | ||||
| def _fail(message: str) -> None: | ||||
|     raise ValidationError(message) | ||||
|  | ||||
|  | ||||
| def _parse_xml(path: Path) -> ET.ElementTree: | ||||
|     try: | ||||
|         return ET.parse(path) | ||||
|     except FileNotFoundError as exc: | ||||
|         _fail(f"Missing required file: {path}") | ||||
|     except ET.ParseError as exc: | ||||
|         _fail(f"Could not parse XML for {path}: {exc}") | ||||
|  | ||||
|  | ||||
| def validate_nuget_config() -> None: | ||||
|     tree = _parse_xml(NUGET_CONFIG) | ||||
|     root = tree.getroot() | ||||
|  | ||||
|     package_sources = root.find("packageSources") | ||||
|     if package_sources is None: | ||||
|         _fail("NuGet.config must declare a <packageSources> section.") | ||||
|  | ||||
|     children = list(package_sources) | ||||
|     if not children or children[0].tag != "clear": | ||||
|         _fail("NuGet.config packageSources must begin with a <clear /> element.") | ||||
|  | ||||
|     adds = [child for child in children if child.tag == "add"] | ||||
|     if not adds: | ||||
|         _fail("NuGet.config packageSources must define at least one <add> entry.") | ||||
|  | ||||
|     keys = [add.attrib.get("key") for add in adds] | ||||
|     if keys[: len(EXPECTED_SOURCE_KEYS)] != EXPECTED_SOURCE_KEYS: | ||||
|         formatted = ", ".join(keys) or "<empty>" | ||||
|         _fail( | ||||
|             "NuGet.config packageSources must list feeds in the order " | ||||
|             f"{EXPECTED_SOURCE_KEYS}. Found: {formatted}" | ||||
|         ) | ||||
|  | ||||
|     local_value = adds[0].attrib.get("value", "") | ||||
|     if Path(local_value).name != "local-nuget": | ||||
|         _fail( | ||||
|             "NuGet.config local feed should point at the repo-local mirror " | ||||
|             f"'local-nuget', found value '{local_value}'." | ||||
|         ) | ||||
|  | ||||
|     clear = package_sources.find("clear") | ||||
|     if clear is None: | ||||
|         _fail("NuGet.config packageSources must start with <clear /> to avoid inherited feeds.") | ||||
|  | ||||
|  | ||||
| def validate_directory_build_props() -> None: | ||||
|     tree = _parse_xml(ROOT_PROPS) | ||||
|     root = tree.getroot() | ||||
|     defaults = None | ||||
|     for element in root.findall(".//_StellaOpsDefaultRestoreSources"): | ||||
|         defaults = [fragment.strip() for fragment in element.text.split(";") if fragment.strip()] | ||||
|         break | ||||
|  | ||||
|     if defaults is None: | ||||
|         _fail("Directory.Build.props must define _StellaOpsDefaultRestoreSources.") | ||||
|  | ||||
|     expected_props = [ | ||||
|         "$(StellaOpsLocalNuGetSource)", | ||||
|         "$(StellaOpsDotNetPublicSource)", | ||||
|         "$(StellaOpsNuGetOrgSource)", | ||||
|     ] | ||||
|     if defaults != expected_props: | ||||
|         _fail( | ||||
|             "Directory.Build.props _StellaOpsDefaultRestoreSources must list feeds " | ||||
|             f"in the order {expected_props}. Found: {defaults}" | ||||
|         ) | ||||
|  | ||||
|     restore_nodes = root.findall(".//RestoreSources") | ||||
|     if not restore_nodes: | ||||
|         _fail("Directory.Build.props must override RestoreSources to force deterministic ordering.") | ||||
|  | ||||
|     uses_default_first = any( | ||||
|         node.text | ||||
|         and node.text.strip().startswith("$(_StellaOpsDefaultRestoreSources)") | ||||
|         for node in restore_nodes | ||||
|     ) | ||||
|     if not uses_default_first: | ||||
|         _fail( | ||||
|             "Directory.Build.props RestoreSources override must place " | ||||
|             "$(_StellaOpsDefaultRestoreSources) at the beginning." | ||||
|         ) | ||||
|  | ||||
|  | ||||
| def assert_single_nuget_config() -> None: | ||||
|     extra_configs: list[Path] = [] | ||||
|     configs: set[Path] = set() | ||||
|     for glob in ("NuGet.config", "nuget.config"): | ||||
|         try: | ||||
|             result = subprocess.run( | ||||
|                 ["rg", "--files", f"-g{glob}"], | ||||
|                 check=False, | ||||
|                 capture_output=True, | ||||
|                 text=True, | ||||
|                 cwd=REPO_ROOT, | ||||
|             ) | ||||
|         except FileNotFoundError as exc: | ||||
|             _fail("ripgrep (rg) is required for validation but was not found on PATH.") | ||||
|         if result.returncode not in (0, 1): | ||||
|             _fail( | ||||
|                 f"ripgrep failed while searching for {glob}: {result.stderr.strip() or result.returncode}" | ||||
|             ) | ||||
|         for line in result.stdout.splitlines(): | ||||
|             configs.add((REPO_ROOT / line).resolve()) | ||||
|  | ||||
|     configs.discard(NUGET_CONFIG.resolve()) | ||||
|     extra_configs.extend(sorted(configs)) | ||||
|     if extra_configs: | ||||
|         formatted = "\n  ".join(str(path.relative_to(REPO_ROOT)) for path in extra_configs) | ||||
|         _fail( | ||||
|             "Unexpected additional NuGet.config files detected. " | ||||
|             "Consolidate feed configuration in the repo root:\n  " | ||||
|             f"{formatted}" | ||||
|         ) | ||||
|  | ||||
|  | ||||
| def parse_args(argv: list[str]) -> argparse.Namespace: | ||||
|     parser = argparse.ArgumentParser( | ||||
|         description="Verify StellaOps NuGet feeds prioritise the local mirror." | ||||
|     ) | ||||
|     parser.add_argument( | ||||
|         "--skip-rg", | ||||
|         action="store_true", | ||||
|         help="Skip ripgrep discovery of extra NuGet.config files (useful for focused runs).", | ||||
|     ) | ||||
|     return parser.parse_args(argv) | ||||
|  | ||||
|  | ||||
| def main(argv: list[str]) -> int: | ||||
|     args = parse_args(argv) | ||||
|     validations = [ | ||||
|         ("NuGet.config ordering", validate_nuget_config), | ||||
|         ("Directory.Build.props restore override", validate_directory_build_props), | ||||
|     ] | ||||
|     if not args.skip_rg: | ||||
|         validations.append(("single NuGet.config", assert_single_nuget_config)) | ||||
|  | ||||
|     for label, check in validations: | ||||
|         try: | ||||
|             check() | ||||
|         except ValidationError as exc: | ||||
|             sys.stderr.write(f"[FAIL] {label}: {exc}\n") | ||||
|             return 1 | ||||
|         else: | ||||
|             sys.stdout.write(f"[OK] {label}\n") | ||||
|  | ||||
|     return 0 | ||||
|  | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|     sys.exit(main(sys.argv[1:])) | ||||
		Reference in New Issue
	
	Block a user