diff --git a/src/agentready/assessors/testing.py b/src/agentready/assessors/testing.py index d16a108f..aa9a9347 100644 --- a/src/agentready/assessors/testing.py +++ b/src/agentready/assessors/testing.py @@ -786,6 +786,7 @@ def _detect_ci_configs(self, repository: Repository) -> list: repository.path / ".circleci" / "config.yml", # CircleCI repository.path / ".travis.yml", # Travis CI repository.path / "Jenkinsfile", # Jenkins + repository.path / ".tekton", # Pipelines-as-Code ] configs = [] @@ -815,6 +816,25 @@ def _has_pr_trigger(self, config: Path) -> bool: return bool(re.search(r"\bpull_request", content)) except (OSError, UnicodeDecodeError): return False + elif ".tekton" in str(config): + # Check each pipeline definition for annotation: pipelinesascode.tekton.dev/on-event, + # note, values could be just "pull_request" or be an array of events "[pull_request,push]" + # Also, the matcher could be configured as CEL expression, so check for + # pipelinesascode.tekton.dev/on-cel-expression: ... event == "pull_request" ... + try: + content = config.read_text() + # Check for on-event annotation with pull_request value + on_event_pattern = ( + r"pipelinesascode\.tekton\.dev/on-event[:\s]+.*\bpull_request\b" + ) + # Check for CEL expression with event == "pull_request" (use DOTALL to match across newlines) + cel_pattern = r'pipelinesascode\.tekton\.dev/on-cel-expression.*event\s*==\s*["\']pull_request["\']' + return bool( + re.search(on_event_pattern, content) + or re.search(cel_pattern, content, re.DOTALL) + ) + except (OSError, UnicodeDecodeError): + return False return True def _assess_quality_gates(self, ci_configs: list) -> tuple: diff --git a/tests/unit/test_assessors_testing.py b/tests/unit/test_assessors_testing.py index 30c6ba84..ce4fc854 100644 --- a/tests/unit/test_assessors_testing.py +++ b/tests/unit/test_assessors_testing.py @@ -529,6 +529,147 @@ def test_config_quality_adds_score(self, tmp_path): # 50 (CI) + 30 (all gates) + config quality bonus assert finding.score > 80 + # --- Tekton Pipelines as Code tests --- + + def test_tekton_on_event_simple_pull_request(self, tmp_path): + """Test Tekton pipeline with simple pull_request on-event annotation.""" + tekton_dir = tmp_path / ".tekton" + tekton_dir.mkdir() + (tekton_dir / "pull-request.yaml").write_text( + "apiVersion: tekton.dev/v1beta1\n" + "kind: PipelineRun\n" + "metadata:\n" + " name: pr-pipeline\n" + " annotations:\n" + ' pipelinesascode.tekton.dev/on-event: "pull_request"\n' + "spec:\n" + " pipelineRef:\n" + " name: test-pipeline\n" + " tasks:\n" + " - name: test\n" + " taskRef:\n" + " name: pytest\n" + ) + + assessor = CIQualityGatesAssessor() + # Use internal method to verify PR trigger detection + assert assessor._has_pr_trigger(tekton_dir / "pull-request.yaml") + + def test_tekton_on_event_array_single(self, tmp_path): + """Test Tekton pipeline with on-event as array containing only pull_request.""" + tekton_dir = tmp_path / ".tekton" + tekton_dir.mkdir() + (tekton_dir / "pull-request.yaml").write_text( + "apiVersion: tekton.dev/v1beta1\n" + "kind: PipelineRun\n" + "metadata:\n" + " name: pr-pipeline\n" + " annotations:\n" + ' pipelinesascode.tekton.dev/on-event: "[pull_request]"\n' + "spec:\n" + " pipelineRef:\n" + " name: test-pipeline\n" + ) + + assessor = CIQualityGatesAssessor() + assert assessor._has_pr_trigger(tekton_dir / "pull-request.yaml") + + def test_tekton_on_event_array_multiple(self, tmp_path): + """Test Tekton pipeline with on-event as array containing pull_request and push.""" + tekton_dir = tmp_path / ".tekton" + tekton_dir.mkdir() + (tekton_dir / "pull-request.yaml").write_text( + "apiVersion: tekton.dev/v1beta1\n" + "kind: PipelineRun\n" + "metadata:\n" + " name: pr-pipeline\n" + " annotations:\n" + ' pipelinesascode.tekton.dev/on-event: "[push,pull_request]"\n' + "spec:\n" + " pipelineRef:\n" + " name: test-pipeline\n" + ) + + assessor = CIQualityGatesAssessor() + assert assessor._has_pr_trigger(tekton_dir / "pull-request.yaml") + + def test_tekton_cel_expression_simple(self, tmp_path): + """Test Tekton pipeline with CEL expression checking for pull_request event.""" + tekton_dir = tmp_path / ".tekton" + tekton_dir.mkdir() + (tekton_dir / "pull-request.yaml").write_text( + "apiVersion: tekton.dev/v1beta1\n" + "kind: PipelineRun\n" + "metadata:\n" + " name: pr-pipeline\n" + " annotations:\n" + ' pipelinesascode.tekton.dev/on-cel-expression: event == "pull_request"\n' + "spec:\n" + " pipelineRef:\n" + " name: test-pipeline\n" + ) + + assessor = CIQualityGatesAssessor() + assert assessor._has_pr_trigger(tekton_dir / "pull-request.yaml") + + def test_tekton_cel_expression_complex(self, tmp_path): + """Test Tekton pipeline with complex CEL expression containing pull_request check.""" + tekton_dir = tmp_path / ".tekton" + tekton_dir.mkdir() + (tekton_dir / "pull-request.yaml").write_text( + "apiVersion: tekton.dev/v1beta1\n" + "kind: PipelineRun\n" + "metadata:\n" + " name: pr-pipeline\n" + " annotations:\n" + " pipelinesascode.tekton.dev/on-cel-expression: |\n" + ' target_branch == "main" && (event == "push" || event == "pull_request") && \n' + ' ( "my-component/***".pathChanged() || ".tekton/my-component-sample-pull-request.yaml".pathChanged() )\n' + "spec:\n" + " pipelineRef:\n" + " name: test-pipeline\n" + ) + + assessor = CIQualityGatesAssessor() + assert assessor._has_pr_trigger(tekton_dir / "pull-request.yaml") + + def test_tekton_no_pr_trigger_push_only(self, tmp_path): + """Test Tekton pipeline without pull_request trigger (push only).""" + tekton_dir = tmp_path / ".tekton" + tekton_dir.mkdir() + (tekton_dir / "push-only.yaml").write_text( + "apiVersion: tekton.dev/v1beta1\n" + "kind: PipelineRun\n" + "metadata:\n" + " name: push-pipeline\n" + " annotations:\n" + ' pipelinesascode.tekton.dev/on-event: "push"\n' + "spec:\n" + " pipelineRef:\n" + " name: test-pipeline\n" + ) + + assessor = CIQualityGatesAssessor() + assert not assessor._has_pr_trigger(tekton_dir / "push-only.yaml") + + def test_tekton_cel_expression_no_pull_request(self, tmp_path): + """Test Tekton pipeline with CEL expression that doesn't check for pull_request.""" + tekton_dir = tmp_path / ".tekton" + tekton_dir.mkdir() + (tekton_dir / "push-only.yaml").write_text( + "apiVersion: tekton.dev/v1beta1\n" + "kind: PipelineRun\n" + "metadata:\n" + " name: push-pipeline\n" + " annotations:\n" + ' pipelinesascode.tekton.dev/on-cel-expression: event == "push" && target_branch == "main"\n' + "spec:\n" + " pipelineRef:\n" + " name: test-pipeline\n" + ) + + assessor = CIQualityGatesAssessor() + assert not assessor._has_pr_trigger(tekton_dir / "push-only.yaml") class TestDeterministicEnforcementAssessor: """Test DeterministicEnforcementAssessor."""