Restructure solution layout by module
	
		
			
	
		
	
	
		
	
		
			Some checks failed
		
		
	
	
		
			
				
	
				Docs CI / lint-and-preview (push) Has been cancelled
				
			
		
		
	
	
				
					
				
			
		
			Some checks failed
		
		
	
	Docs CI / lint-and-preview (push) Has been cancelled
				
			This commit is contained in:
		| @@ -1,130 +1,130 @@ | ||||
| #!/usr/bin/env python3 | ||||
| """ | ||||
| Ensure deployment bundles reference the images defined in a release manifest. | ||||
|  | ||||
| Usage: | ||||
|   ./deploy/tools/check-channel-alignment.py \ | ||||
|       --release deploy/releases/2025.10-edge.yaml \ | ||||
|       --target deploy/helm/stellaops/values-dev.yaml \ | ||||
|       --target deploy/compose/docker-compose.dev.yaml | ||||
|  | ||||
| For every target file, the script scans `image:` declarations and verifies that | ||||
| any image belonging to a repository listed in the release manifest matches the | ||||
| exact digest or tag recorded there. Images outside of the manifest (for example, | ||||
| supporting services such as `nats`) are ignored. | ||||
| """ | ||||
|  | ||||
| from __future__ import annotations | ||||
|  | ||||
| import argparse | ||||
| import pathlib | ||||
| import re | ||||
| import sys | ||||
| from typing import Dict, Iterable, List, Optional, Set | ||||
|  | ||||
| IMAGE_LINE = re.compile(r"^\s*image:\s*['\"]?(?P<image>\S+)['\"]?\s*$") | ||||
|  | ||||
|  | ||||
| def extract_images(path: pathlib.Path) -> List[str]: | ||||
|     images: List[str] = [] | ||||
|     for line in path.read_text(encoding="utf-8").splitlines(): | ||||
|         match = IMAGE_LINE.match(line) | ||||
|         if match: | ||||
|             images.append(match.group("image")) | ||||
|     return images | ||||
|  | ||||
|  | ||||
| def image_repo(image: str) -> str: | ||||
|     if "@" in image: | ||||
|         return image.split("@", 1)[0] | ||||
|     # Split on the last colon to preserve registries with ports (e.g. localhost:5000) | ||||
|     if ":" in image: | ||||
|         prefix, tag = image.rsplit(":", 1) | ||||
|         if "/" in tag: | ||||
|             # handle digestive colon inside path (unlikely) | ||||
|             return image | ||||
|         return prefix | ||||
|     return image | ||||
|  | ||||
|  | ||||
| def load_release_map(release_path: pathlib.Path) -> Dict[str, str]: | ||||
|     release_map: Dict[str, str] = {} | ||||
|     for image in extract_images(release_path): | ||||
|         repo = image_repo(image) | ||||
|         release_map[repo] = image | ||||
|     return release_map | ||||
|  | ||||
|  | ||||
| def check_target( | ||||
|     target_path: pathlib.Path, | ||||
|     release_map: Dict[str, str], | ||||
|     ignore_repos: Set[str], | ||||
| ) -> List[str]: | ||||
|     errors: List[str] = [] | ||||
|     for image in extract_images(target_path): | ||||
|         repo = image_repo(image) | ||||
|         if repo in ignore_repos: | ||||
|             continue | ||||
|         if repo not in release_map: | ||||
|             continue | ||||
|         expected = release_map[repo] | ||||
|         if image != expected: | ||||
|             errors.append( | ||||
|                 f"{target_path}: {image} does not match release value {expected}" | ||||
|             ) | ||||
|     return errors | ||||
|  | ||||
|  | ||||
| def parse_args(argv: Optional[Iterable[str]] = None) -> argparse.Namespace: | ||||
|     parser = argparse.ArgumentParser(description=__doc__) | ||||
|     parser.add_argument( | ||||
|         "--release", | ||||
|         required=True, | ||||
|         type=pathlib.Path, | ||||
|         help="Path to the release manifest (YAML)", | ||||
|     ) | ||||
|     parser.add_argument( | ||||
|         "--target", | ||||
|         action="append", | ||||
|         required=True, | ||||
|         type=pathlib.Path, | ||||
|         help="Deployment profile to validate against the release manifest", | ||||
|     ) | ||||
|     parser.add_argument( | ||||
|         "--ignore-repo", | ||||
|         action="append", | ||||
|         default=[], | ||||
|         help="Repository prefix to ignore (may be repeated)", | ||||
|     ) | ||||
|     return parser.parse_args(argv) | ||||
|  | ||||
|  | ||||
| def main(argv: Optional[Iterable[str]] = None) -> int: | ||||
|     args = parse_args(argv) | ||||
|  | ||||
|     release_map = load_release_map(args.release) | ||||
|     ignore_repos = {repo.rstrip("/") for repo in args.ignore_repo} | ||||
|  | ||||
|     if not release_map: | ||||
|         print(f"error: no images found in release manifest {args.release}", file=sys.stderr) | ||||
|         return 2 | ||||
|  | ||||
|     total_errors: List[str] = [] | ||||
|     for target in args.target: | ||||
|         if not target.exists(): | ||||
|             total_errors.append(f"{target}: file not found") | ||||
|             continue | ||||
|         total_errors.extend(check_target(target, release_map, ignore_repos)) | ||||
|  | ||||
|     if total_errors: | ||||
|         print("✖ channel alignment check failed:", file=sys.stderr) | ||||
|         for err in total_errors: | ||||
|             print(f"  - {err}", file=sys.stderr) | ||||
|         return 1 | ||||
|  | ||||
|     print("✓ deployment profiles reference release images for the inspected repositories.") | ||||
|     return 0 | ||||
|  | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|     raise SystemExit(main()) | ||||
| #!/usr/bin/env python3 | ||||
| """ | ||||
| Ensure deployment bundles reference the images defined in a release manifest. | ||||
|  | ||||
| Usage: | ||||
|   ./deploy/tools/check-channel-alignment.py \ | ||||
|       --release deploy/releases/2025.10-edge.yaml \ | ||||
|       --target deploy/helm/stellaops/values-dev.yaml \ | ||||
|       --target deploy/compose/docker-compose.dev.yaml | ||||
|  | ||||
| For every target file, the script scans `image:` declarations and verifies that | ||||
| any image belonging to a repository listed in the release manifest matches the | ||||
| exact digest or tag recorded there. Images outside of the manifest (for example, | ||||
| supporting services such as `nats`) are ignored. | ||||
| """ | ||||
|  | ||||
| from __future__ import annotations | ||||
|  | ||||
| import argparse | ||||
| import pathlib | ||||
| import re | ||||
| import sys | ||||
| from typing import Dict, Iterable, List, Optional, Set | ||||
|  | ||||
| IMAGE_LINE = re.compile(r"^\s*image:\s*['\"]?(?P<image>\S+)['\"]?\s*$") | ||||
|  | ||||
|  | ||||
| def extract_images(path: pathlib.Path) -> List[str]: | ||||
|     images: List[str] = [] | ||||
|     for line in path.read_text(encoding="utf-8").splitlines(): | ||||
|         match = IMAGE_LINE.match(line) | ||||
|         if match: | ||||
|             images.append(match.group("image")) | ||||
|     return images | ||||
|  | ||||
|  | ||||
| def image_repo(image: str) -> str: | ||||
|     if "@" in image: | ||||
|         return image.split("@", 1)[0] | ||||
|     # Split on the last colon to preserve registries with ports (e.g. localhost:5000) | ||||
|     if ":" in image: | ||||
|         prefix, tag = image.rsplit(":", 1) | ||||
|         if "/" in tag: | ||||
|             # handle digestive colon inside path (unlikely) | ||||
|             return image | ||||
|         return prefix | ||||
|     return image | ||||
|  | ||||
|  | ||||
| def load_release_map(release_path: pathlib.Path) -> Dict[str, str]: | ||||
|     release_map: Dict[str, str] = {} | ||||
|     for image in extract_images(release_path): | ||||
|         repo = image_repo(image) | ||||
|         release_map[repo] = image | ||||
|     return release_map | ||||
|  | ||||
|  | ||||
| def check_target( | ||||
|     target_path: pathlib.Path, | ||||
|     release_map: Dict[str, str], | ||||
|     ignore_repos: Set[str], | ||||
| ) -> List[str]: | ||||
|     errors: List[str] = [] | ||||
|     for image in extract_images(target_path): | ||||
|         repo = image_repo(image) | ||||
|         if repo in ignore_repos: | ||||
|             continue | ||||
|         if repo not in release_map: | ||||
|             continue | ||||
|         expected = release_map[repo] | ||||
|         if image != expected: | ||||
|             errors.append( | ||||
|                 f"{target_path}: {image} does not match release value {expected}" | ||||
|             ) | ||||
|     return errors | ||||
|  | ||||
|  | ||||
| def parse_args(argv: Optional[Iterable[str]] = None) -> argparse.Namespace: | ||||
|     parser = argparse.ArgumentParser(description=__doc__) | ||||
|     parser.add_argument( | ||||
|         "--release", | ||||
|         required=True, | ||||
|         type=pathlib.Path, | ||||
|         help="Path to the release manifest (YAML)", | ||||
|     ) | ||||
|     parser.add_argument( | ||||
|         "--target", | ||||
|         action="append", | ||||
|         required=True, | ||||
|         type=pathlib.Path, | ||||
|         help="Deployment profile to validate against the release manifest", | ||||
|     ) | ||||
|     parser.add_argument( | ||||
|         "--ignore-repo", | ||||
|         action="append", | ||||
|         default=[], | ||||
|         help="Repository prefix to ignore (may be repeated)", | ||||
|     ) | ||||
|     return parser.parse_args(argv) | ||||
|  | ||||
|  | ||||
| def main(argv: Optional[Iterable[str]] = None) -> int: | ||||
|     args = parse_args(argv) | ||||
|  | ||||
|     release_map = load_release_map(args.release) | ||||
|     ignore_repos = {repo.rstrip("/") for repo in args.ignore_repo} | ||||
|  | ||||
|     if not release_map: | ||||
|         print(f"error: no images found in release manifest {args.release}", file=sys.stderr) | ||||
|         return 2 | ||||
|  | ||||
|     total_errors: List[str] = [] | ||||
|     for target in args.target: | ||||
|         if not target.exists(): | ||||
|             total_errors.append(f"{target}: file not found") | ||||
|             continue | ||||
|         total_errors.extend(check_target(target, release_map, ignore_repos)) | ||||
|  | ||||
|     if total_errors: | ||||
|         print("✖ channel alignment check failed:", file=sys.stderr) | ||||
|         for err in total_errors: | ||||
|             print(f"  - {err}", file=sys.stderr) | ||||
|         return 1 | ||||
|  | ||||
|     print("✓ deployment profiles reference release images for the inspected repositories.") | ||||
|     return 0 | ||||
|  | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|     raise SystemExit(main()) | ||||
|   | ||||
| @@ -1,61 +1,61 @@ | ||||
| #!/usr/bin/env bash | ||||
| set -euo pipefail | ||||
|  | ||||
| ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" | ||||
| COMPOSE_DIR="$ROOT_DIR/compose" | ||||
| HELM_DIR="$ROOT_DIR/helm/stellaops" | ||||
|  | ||||
| compose_profiles=( | ||||
|   "docker-compose.dev.yaml:env/dev.env.example" | ||||
|   "docker-compose.stage.yaml:env/stage.env.example" | ||||
|   "docker-compose.prod.yaml:env/prod.env.example" | ||||
|   "docker-compose.airgap.yaml:env/airgap.env.example" | ||||
|   "docker-compose.mirror.yaml:env/mirror.env.example" | ||||
|   "docker-compose.telemetry.yaml:" | ||||
|   "docker-compose.telemetry-storage.yaml:" | ||||
| ) | ||||
|  | ||||
| docker_ready=false | ||||
| if command -v docker >/dev/null 2>&1; then | ||||
|   if docker compose version >/dev/null 2>&1; then | ||||
|     docker_ready=true | ||||
|   else | ||||
|     echo "⚠️  docker CLI present but Compose plugin unavailable; skipping compose validation" >&2 | ||||
|   fi | ||||
| else | ||||
|   echo "⚠️  docker CLI not found; skipping compose validation" >&2 | ||||
| fi | ||||
|  | ||||
| if [[ "$docker_ready" == "true" ]]; then | ||||
|   for entry in "${compose_profiles[@]}"; do | ||||
|     IFS=":" read -r compose_file env_file <<<"$entry" | ||||
|     printf '→ validating %s with %s\n' "$compose_file" "$env_file" | ||||
|     if [[ -n "$env_file" ]]; then | ||||
|       docker compose \ | ||||
|         --env-file "$COMPOSE_DIR/$env_file" \ | ||||
|         -f "$COMPOSE_DIR/$compose_file" config >/dev/null | ||||
|     else | ||||
|       docker compose -f "$COMPOSE_DIR/$compose_file" config >/dev/null | ||||
|     fi | ||||
|   done | ||||
| fi | ||||
|  | ||||
| helm_values=( | ||||
|   "$HELM_DIR/values-dev.yaml" | ||||
|   "$HELM_DIR/values-stage.yaml" | ||||
|   "$HELM_DIR/values-prod.yaml" | ||||
|   "$HELM_DIR/values-airgap.yaml" | ||||
|   "$HELM_DIR/values-mirror.yaml" | ||||
| ) | ||||
|  | ||||
| if command -v helm >/dev/null 2>&1; then | ||||
|   for values in "${helm_values[@]}"; do | ||||
|     printf '→ linting Helm chart with %s\n' "$(basename "$values")" | ||||
|     helm lint "$HELM_DIR" -f "$values" | ||||
|     helm template test-release "$HELM_DIR" -f "$values" >/dev/null | ||||
|   done | ||||
| else | ||||
|   echo "⚠️  helm CLI not found; skipping Helm lint/template" >&2 | ||||
| fi | ||||
|  | ||||
| printf 'Profiles validated (where tooling was available).\n' | ||||
| #!/usr/bin/env bash | ||||
| set -euo pipefail | ||||
|  | ||||
| ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" | ||||
| COMPOSE_DIR="$ROOT_DIR/compose" | ||||
| HELM_DIR="$ROOT_DIR/helm/stellaops" | ||||
|  | ||||
| compose_profiles=( | ||||
|   "docker-compose.dev.yaml:env/dev.env.example" | ||||
|   "docker-compose.stage.yaml:env/stage.env.example" | ||||
|   "docker-compose.prod.yaml:env/prod.env.example" | ||||
|   "docker-compose.airgap.yaml:env/airgap.env.example" | ||||
|   "docker-compose.mirror.yaml:env/mirror.env.example" | ||||
|   "docker-compose.telemetry.yaml:" | ||||
|   "docker-compose.telemetry-storage.yaml:" | ||||
| ) | ||||
|  | ||||
| docker_ready=false | ||||
| if command -v docker >/dev/null 2>&1; then | ||||
|   if docker compose version >/dev/null 2>&1; then | ||||
|     docker_ready=true | ||||
|   else | ||||
|     echo "⚠️  docker CLI present but Compose plugin unavailable; skipping compose validation" >&2 | ||||
|   fi | ||||
| else | ||||
|   echo "⚠️  docker CLI not found; skipping compose validation" >&2 | ||||
| fi | ||||
|  | ||||
| if [[ "$docker_ready" == "true" ]]; then | ||||
|   for entry in "${compose_profiles[@]}"; do | ||||
|     IFS=":" read -r compose_file env_file <<<"$entry" | ||||
|     printf '→ validating %s with %s\n' "$compose_file" "$env_file" | ||||
|     if [[ -n "$env_file" ]]; then | ||||
|       docker compose \ | ||||
|         --env-file "$COMPOSE_DIR/$env_file" \ | ||||
|         -f "$COMPOSE_DIR/$compose_file" config >/dev/null | ||||
|     else | ||||
|       docker compose -f "$COMPOSE_DIR/$compose_file" config >/dev/null | ||||
|     fi | ||||
|   done | ||||
| fi | ||||
|  | ||||
| helm_values=( | ||||
|   "$HELM_DIR/values-dev.yaml" | ||||
|   "$HELM_DIR/values-stage.yaml" | ||||
|   "$HELM_DIR/values-prod.yaml" | ||||
|   "$HELM_DIR/values-airgap.yaml" | ||||
|   "$HELM_DIR/values-mirror.yaml" | ||||
| ) | ||||
|  | ||||
| if command -v helm >/dev/null 2>&1; then | ||||
|   for values in "${helm_values[@]}"; do | ||||
|     printf '→ linting Helm chart with %s\n' "$(basename "$values")" | ||||
|     helm lint "$HELM_DIR" -f "$values" | ||||
|     helm template test-release "$HELM_DIR" -f "$values" >/dev/null | ||||
|   done | ||||
| else | ||||
|   echo "⚠️  helm CLI not found; skipping Helm lint/template" >&2 | ||||
| fi | ||||
|  | ||||
| printf 'Profiles validated (where tooling was available).\n' | ||||
|   | ||||
		Reference in New Issue
	
	Block a user