diff --git a/.github/workflows/intercept-e2e.yml b/.github/workflows/intercept-e2e.yml index 7ca2476..24db38c 100644 --- a/.github/workflows/intercept-e2e.yml +++ b/.github/workflows/intercept-e2e.yml @@ -5,16 +5,30 @@ on: paths: - ".github/workflows/intercept-e2e.yml" - "helm/tekton-dag/**" + - "libs/tekton-dag-common/**" + - "operator/**" + - "orchestrator/**" + - "pipeline/**" + - "stacks/**" + - "tasks/**" - "scripts/bootstrap-namespace.sh" - "scripts/install-tekton.sh" + - "scripts/run-cluster-ci.sh" - "scripts/run-product-intercept-e2e.sh" push: branches: [main] paths: - ".github/workflows/intercept-e2e.yml" - "helm/tekton-dag/**" + - "libs/tekton-dag-common/**" + - "operator/**" + - "orchestrator/**" + - "pipeline/**" + - "stacks/**" + - "tasks/**" - "scripts/bootstrap-namespace.sh" - "scripts/install-tekton.sh" + - "scripts/run-cluster-ci.sh" - "scripts/run-product-intercept-e2e.sh" workflow_dispatch: schedule: diff --git a/helm/tekton-dag/package.sh b/helm/tekton-dag/package.sh index 5f42275..de12335 100755 --- a/helm/tekton-dag/package.sh +++ b/helm/tekton-dag/package.sh @@ -10,10 +10,12 @@ echo " Repo root: $REPO_ROOT" rm -rf "$CHART_DIR/raw" mkdir -p "$CHART_DIR/raw/tasks" "$CHART_DIR/raw/pipelines" "$CHART_DIR/raw/stacks" \ - "$CHART_DIR/raw/stack-crs" "$CHART_DIR/raw/team-crs" + "$CHART_DIR/raw/stack-crs" "$CHART_DIR/raw/team-crs" "$CHART_DIR/raw/scripts" echo " Copying tasks..." cp "$REPO_ROOT"/tasks/*.yaml "$CHART_DIR/raw/tasks/" +cp "$REPO_ROOT/scripts/run-stack-tests-runners.sh" \ + "$CHART_DIR/raw/scripts/run-stack-tests-runners.sh" echo " Copying pipelines and triggers..." cp "$REPO_ROOT"/pipeline/*.yaml "$CHART_DIR/raw/pipelines/" diff --git a/helm/tekton-dag/templates/configmap-run-stack-tests-runners.yaml b/helm/tekton-dag/templates/configmap-run-stack-tests-runners.yaml new file mode 100644 index 0000000..d033c57 --- /dev/null +++ b/helm/tekton-dag/templates/configmap-run-stack-tests-runners.yaml @@ -0,0 +1,14 @@ +{{- $script := .Files.Get "raw/scripts/run-stack-tests-runners.sh" -}} +{{- if $script }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: run-stack-tests-runners + namespace: {{ .Values.namespace }} + labels: + app.kubernetes.io/part-of: tekton-job-standardization + {{- include "tekton-dag.labels" . | nindent 4 }} +data: + run-stack-tests-runners.sh: | + {{- $script | nindent 4 }} +{{- end }} diff --git a/libs/tekton-dag-common/tests/fixtures/pipelineruns/bootstrap.json b/libs/tekton-dag-common/tests/fixtures/pipelineruns/bootstrap.json index 3edb921..f018a5f 100644 --- a/libs/tekton-dag-common/tests/fixtures/pipelineruns/bootstrap.json +++ b/libs/tekton-dag-common/tests/fixtures/pipelineruns/bootstrap.json @@ -70,7 +70,7 @@ { "name": "build-cache", "persistentVolumeClaim": { - "claimName": "build-cache-pvc" + "claimName": "build-cache" } } ] diff --git a/libs/tekton-dag-common/tests/fixtures/pipelineruns/merge.json b/libs/tekton-dag-common/tests/fixtures/pipelineruns/merge.json index d9d7000..b91617f 100644 --- a/libs/tekton-dag-common/tests/fixtures/pipelineruns/merge.json +++ b/libs/tekton-dag-common/tests/fixtures/pipelineruns/merge.json @@ -31,10 +31,6 @@ "name": "image-registry", "value": "localhost:5000" }, - { - "name": "cache-repo", - "value": "localhost:5000/kaniko-cache" - }, { "name": "max-retries", "value": "2" @@ -74,7 +70,7 @@ { "name": "build-cache", "persistentVolumeClaim": { - "claimName": "build-cache-pvc" + "claimName": "build-cache" } } ] diff --git a/libs/tekton-dag-common/tests/fixtures/pipelineruns/pr.json b/libs/tekton-dag-common/tests/fixtures/pipelineruns/pr.json index 20869bc..0663128 100644 --- a/libs/tekton-dag-common/tests/fixtures/pipelineruns/pr.json +++ b/libs/tekton-dag-common/tests/fixtures/pipelineruns/pr.json @@ -94,7 +94,7 @@ { "name": "build-cache", "persistentVolumeClaim": { - "claimName": "build-cache-pvc" + "claimName": "build-cache" } } ] diff --git a/libs/tekton-dag-common/tests/test_m17_intercept_automation.py b/libs/tekton-dag-common/tests/test_m17_intercept_automation.py index 383f9f8..b780b53 100644 --- a/libs/tekton-dag-common/tests/test_m17_intercept_automation.py +++ b/libs/tekton-dag-common/tests/test_m17_intercept_automation.py @@ -13,8 +13,12 @@ def test_intercept_workflow_has_explicit_backend_cadence_and_evidence(): assert "branches: [main]" in workflow assert '".github/workflows/intercept-e2e.yml"' in workflow assert '"helm/tekton-dag/**"' in workflow - assert '"scripts/bootstrap-namespace.sh"' in workflow + assert '"operator/**"' in workflow + assert '"orchestrator/**"' in workflow + assert '"pipeline/**"' in workflow + assert '"tasks/**"' in workflow assert '"scripts/install-tekton.sh"' in workflow + assert '"scripts/bootstrap-namespace.sh"' in workflow assert "workflow_dispatch:" in workflow assert "schedule:" in workflow assert "backend: [telepresence, mirrord]" in workflow @@ -41,6 +45,7 @@ def test_product_script_covers_trigger_stackrun_tests_and_cleanup(): assert "pipeline-results.json" in script assert "tekton.dev/pipelineTask=run-tests" in script assert 'pipeline_status" == "False"' in script + assert "kubectl get pvc build-cache" in script assert "pr-traffic-evidence.log" in script assert "kubectl delete pipelinerun" in script assert "kubectl delete stackrun" in script @@ -71,6 +76,13 @@ def test_tekton_install_allows_source_and_build_cache_pvcs(): assert "kubectl patch configmap feature-flags -n tekton-pipelines" in install assert '''-p '{"data":{"coschedule":"disabled"}}' '''.strip() in install + assert "rollout status deployment/tekton-pipelines-webhook" in install + assert "rollout status deployment/tekton-triggers-webhook" in install + assert ( + install.index("rollout status deployment/tekton-triggers-webhook") + < install.index('apply_with_retry -f "$TEKTON_TRIGGERS_INTERCEPTORS_URL"') + ) + assert 'apply_with_retry -f "$MILESTONE_DIR/tasks/"' in install def test_compile_pipeline_defaults_are_valid_container_images(): @@ -84,3 +96,22 @@ def test_compile_pipeline_defaults_are_valid_container_images(): marker = f"- name: compile-image-{image_param}" default = pipeline.split(marker, 1)[1].split("- name:", 1)[0] assert 'default: "ubuntu:22.04"' in default + + +def test_pipelinerun_builders_use_installed_build_cache_claim(): + go_builder = (ROOT / "operator/internal/pipeline/builder.go").read_text() + python_builder = (ROOT / "orchestrator/pipelinerun_builder.py").read_text() + + assert '"claimName": "build-cache"' in go_builder + assert '"claimName": "build-cache-pvc"' not in go_builder + assert '"claimName": "build-cache"' in python_builder + assert '"claimName": "build-cache-pvc"' not in python_builder + + +def test_pr_comment_token_is_optional_when_commenting_is_not_configured(): + task = (ROOT / "tasks/post-pr-comment.yaml").read_text() + + token_ref = task.split("secretKeyRef:", 1)[1].split("- name: GIT_URL", 1)[0] + assert "key: token" in token_ref + assert "optional: true" in token_ref + assert 'if [ -z "$GITHUB_TOKEN" ]' in task diff --git a/libs/tekton-dag-common/tests/test_m17_stack_test_runners.py b/libs/tekton-dag-common/tests/test_m17_stack_test_runners.py new file mode 100644 index 0000000..9eb09b4 --- /dev/null +++ b/libs/tekton-dag-common/tests/test_m17_stack_test_runners.py @@ -0,0 +1,42 @@ +"""Static acceptance checks for M17.13 stack test runners.""" + +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[3] + + +def test_task_sources_extracted_runner_script(): + task = (ROOT / "tasks/run-stack-tests.yaml").read_text() + installer = (ROOT / "scripts/install-tekton.sh").read_text() + chart = (ROOT / "helm/tekton-dag/templates/configmap-run-stack-tests-runners.yaml").read_text() + packager = (ROOT / "helm/tekton-dag/package.sh").read_text() + script = (ROOT / "scripts/run-stack-tests-runners.sh").read_text() + + assert "name: run-stack-tests-runners" in task + assert "mountPath: /opt/tekton-dag" in task + assert "ERROR: run-stack-tests runners ConfigMap missing" in task + assert "PHASE 2: Per-app tests" not in task + assert "kubectl create configmap run-stack-tests-runners" in installer + assert "raw/scripts/run-stack-tests-runners.sh" in chart + assert "raw/scripts/run-stack-tests-runners.sh" in packager + assert "run_newman" in script + assert "run_playwright" in script + assert "run_artillery" in script + assert "stack-json is not valid JSON" in script + + +def test_runner_fixture_suite_covers_success_and_failure(): + tests = (ROOT / "libs/tekton-dag-common/tests/test_run_stack_tests_runners.py").read_text() + + assert "test_malformed_stack_json_fails" in tests + assert '"{not-json"' in tests + assert "test_runner_success_and_failure_paths" in tests + assert '"newman"' in tests + assert '"playwright"' in tests + assert '"artillery"' in tests + assert "[postman] PASS" in tests + assert "[postman] FAIL" in tests + assert "[playwright] PASS" in tests + assert "[playwright] FAIL" in tests + assert "[artillery] PASS" in tests + assert "[artillery] FAIL" in tests diff --git a/libs/tekton-dag-common/tests/test_run_stack_tests_runners.py b/libs/tekton-dag-common/tests/test_run_stack_tests_runners.py new file mode 100644 index 0000000..b480aff --- /dev/null +++ b/libs/tekton-dag-common/tests/test_run_stack_tests_runners.py @@ -0,0 +1,213 @@ +"""Fixture coverage for run-stack-tests Newman, Playwright, and Artillery branches.""" + +from __future__ import annotations + +import json +import os +import stat +import subprocess +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[3] +RUNNERS = ROOT / "scripts" / "run-stack-tests-runners.sh" + + +def _write_executable(path: Path, body: str) -> None: + path.write_text(body) + path.chmod(path.stat().st_mode | stat.S_IXUSR | stat.S_IRUSR | stat.S_IWUSR) + + +def _stub_bin( + tmp_path: Path, + *, + newman: int = 0, + playwright: int = 0, + artillery: int = 0, +) -> Path: + bindir = tmp_path / "bin" + bindir.mkdir(parents=True) + _write_executable( + bindir / "newman", + f"#!/bin/sh\necho newman-stub \"$@\"\nexit {newman}\n", + ) + _write_executable( + bindir / "artillery", + f"#!/bin/sh\necho artillery-stub \"$@\"\nexit {artillery}\n", + ) + _write_executable( + bindir / "npx", + "#!/bin/sh\n" + 'if [ "$1" = "playwright" ]; then\n' + f' echo playwright-stub "$@"\n exit {playwright}\n' + "fi\n" + "exit 0\n", + ) + _write_executable(bindir / "npm", "#!/bin/sh\nexit 0\n") + _write_executable( + bindir / "curl", + "#!/bin/sh\n" + 'echo \'{"frontend":"ok","api":"ok","sess-1":"present"}\'\n', + ) + return bindir + + +def _stack(*apps: dict) -> str: + return json.dumps( + { + "propagation": {"baggage-key": "dev-session"}, + "defaults": {"namespace": "staging", "service-port": "80"}, + "apps": list(apps), + } + ) + + +def _app(name: str, **tests: str) -> dict: + payload: dict = {"name": name, "namespace": "staging", "service-port": "80"} + if tests: + payload["tests"] = tests + return payload + + +def _prepare_runner_files(source: Path, kind: str) -> None: + if kind == "newman": + collection = source / "tests" / "postman" / "api.json" + collection.parent.mkdir(parents=True, exist_ok=True) + collection.write_text('{"info":{"name":"api"},"item":[]}\n') + return + if kind == "playwright": + suite = source / "tests" / "playwright" + suite.mkdir(parents=True, exist_ok=True) + (source / "tests" / "package.json").write_text('{"name":"api-tests"}\n') + (suite / "example.spec.js").write_text("test('ok', () => {});\n") + return + if kind == "artillery": + script = source / "tests" / "artillery" / "load.yml" + script.parent.mkdir(parents=True, exist_ok=True) + script.write_text("config:\n target: http://localhost\n") + return + raise AssertionError(kind) + + +def _run( + root: Path, + stack_json: str, + *, + newman: int = 0, + playwright: int = 0, + artillery: int = 0, + extra_env: dict[str, str] | None = None, + prepare: str | None = None, +) -> tuple[subprocess.CompletedProcess[str], Path]: + source = root / "source" + source.mkdir(parents=True) + if prepare: + _prepare_runner_files(source, prepare) + summary = root / "summary.json" + bindir = _stub_bin( + root, newman=newman, playwright=playwright, artillery=artillery + ) + env = os.environ.copy() + env.update( + { + "STACK_JSON": stack_json, + "APP_LIST": "frontend api", + "ENTRY_APP": "frontend", + "CHAIN": "frontend api", + "BUILD_APPS": "api", + "INTERCEPT": "x-dev-session:sess-1", + "DEFAULT_NS": "staging", + "TESTS_TO_RUN": "", + "UNMAPPED_AREA": "", + "APPS_TO_TEST": "", + "TEST_SOURCE": str(source), + "TEST_SUMMARY_PATH": str(summary), + "PATH": f"{bindir}{os.pathsep}{env['PATH']}", + } + ) + if extra_env: + env.update(extra_env) + completed = subprocess.run( + ["sh", str(RUNNERS)], + cwd=source, + env=env, + capture_output=True, + text=True, + check=False, + ) + return completed, summary + + +def test_malformed_stack_json_fails(tmp_path: Path) -> None: + completed, _summary = _run(tmp_path, "{not-json") + assert completed.returncode == 1 + assert "stack-json is not valid JSON" in completed.stderr + + +def test_unmapped_area_exits_without_runners(tmp_path: Path) -> None: + completed, summary = _run( + tmp_path, + _stack(_app("frontend"), _app("api")), + extra_env={"UNMAPPED_AREA": "billing"}, + ) + assert completed.returncode == 0 + payload = json.loads(summary.read_text()) + assert payload["unmapped-area"] == "billing" + assert "newman-stub" not in completed.stdout + assert "playwright-stub" not in completed.stdout + assert "artillery-stub" not in completed.stdout + + +@pytest.mark.parametrize( + ("kind", "tests", "pass_token", "fail_token"), + [ + ( + "newman", + {"postman": "tests/postman/api.json"}, + "[postman] PASS", + "[postman] FAIL", + ), + ( + "playwright", + {"playwright": "tests/playwright"}, + "[playwright] PASS", + "[playwright] FAIL", + ), + ( + "artillery", + {"artillery": "tests/artillery/load.yml"}, + "[artillery] PASS", + "[artillery] FAIL", + ), + ], +) +def test_runner_success_and_failure_paths( + tmp_path: Path, + kind: str, + tests: dict[str, str], + pass_token: str, + fail_token: str, +) -> None: + stack = _stack(_app("frontend"), _app("api", **tests)) + + passed, passed_summary = _run( + tmp_path / "success", + stack, + prepare=kind, + ) + assert passed.returncode == 0, passed.stdout + passed.stderr + assert pass_token in passed.stdout + assert json.loads(passed_summary.read_text())["api"] == "pass" + + failed, failed_summary = _run( + tmp_path / "failure", + stack, + prepare=kind, + newman=1 if kind == "newman" else 0, + playwright=1 if kind == "playwright" else 0, + artillery=1 if kind == "artillery" else 0, + ) + assert failed.returncode == 1, failed.stdout + failed.stderr + assert fail_token in failed.stdout + assert json.loads(failed_summary.read_text())["api"] == "fail" diff --git a/operator/internal/pipeline/builder.go b/operator/internal/pipeline/builder.go index b951b36..f0f61a9 100644 --- a/operator/internal/pipeline/builder.go +++ b/operator/internal/pipeline/builder.go @@ -145,7 +145,7 @@ func defaultWorkspaces(storage string) []any { map[string]any{ "name": "build-cache", "persistentVolumeClaim": map[string]any{ - "claimName": "build-cache-pvc", + "claimName": "build-cache", }, }, } @@ -288,7 +288,6 @@ func BuildMerge(opt Options) (*unstructured.Unstructured, error) { param("stack-file", opt.StackFile), param("changed-app", opt.ChangedApp), param("image-registry", opt.ImageRegistry), - param("cache-repo", opt.CacheRepo), } obj := map[string]any{ "apiVersion": TektonAPIVersion, diff --git a/operator/internal/pipeline/testdata/golden/bootstrap.json b/operator/internal/pipeline/testdata/golden/bootstrap.json index 3edb921..f018a5f 100644 --- a/operator/internal/pipeline/testdata/golden/bootstrap.json +++ b/operator/internal/pipeline/testdata/golden/bootstrap.json @@ -70,7 +70,7 @@ { "name": "build-cache", "persistentVolumeClaim": { - "claimName": "build-cache-pvc" + "claimName": "build-cache" } } ] diff --git a/operator/internal/pipeline/testdata/golden/merge.json b/operator/internal/pipeline/testdata/golden/merge.json index d9d7000..b91617f 100644 --- a/operator/internal/pipeline/testdata/golden/merge.json +++ b/operator/internal/pipeline/testdata/golden/merge.json @@ -31,10 +31,6 @@ "name": "image-registry", "value": "localhost:5000" }, - { - "name": "cache-repo", - "value": "localhost:5000/kaniko-cache" - }, { "name": "max-retries", "value": "2" @@ -74,7 +70,7 @@ { "name": "build-cache", "persistentVolumeClaim": { - "claimName": "build-cache-pvc" + "claimName": "build-cache" } } ] diff --git a/operator/internal/pipeline/testdata/golden/pr.json b/operator/internal/pipeline/testdata/golden/pr.json index 20869bc..0663128 100644 --- a/operator/internal/pipeline/testdata/golden/pr.json +++ b/operator/internal/pipeline/testdata/golden/pr.json @@ -94,7 +94,7 @@ { "name": "build-cache", "persistentVolumeClaim": { - "claimName": "build-cache-pvc" + "claimName": "build-cache" } } ] diff --git a/orchestrator/pipelinerun_builder.py b/orchestrator/pipelinerun_builder.py index 861cfd6..5f4b02a 100644 --- a/orchestrator/pipelinerun_builder.py +++ b/orchestrator/pipelinerun_builder.py @@ -102,7 +102,7 @@ def build_pr_pipelinerun( }, { "name": "build-cache", - "persistentVolumeClaim": {"claimName": "build-cache-pvc"}, + "persistentVolumeClaim": {"claimName": "build-cache"}, }, ], "taskRunTemplate": { @@ -177,7 +177,7 @@ def build_bootstrap_pipelinerun( }, { "name": "build-cache", - "persistentVolumeClaim": {"claimName": "build-cache-pvc"}, + "persistentVolumeClaim": {"claimName": "build-cache"}, }, ], "taskRunTemplate": { @@ -233,7 +233,6 @@ def build_merge_pipelinerun( {"name": "stack-file", "value": stack_file}, {"name": "changed-app", "value": changed_app}, {"name": "image-registry", "value": image_registry}, - {"name": "cache-repo", "value": cache_repo}, ], "workspaces": [ { @@ -251,7 +250,7 @@ def build_merge_pipelinerun( }, { "name": "build-cache", - "persistentVolumeClaim": {"claimName": "build-cache-pvc"}, + "persistentVolumeClaim": {"claimName": "build-cache"}, }, ], "taskRunTemplate": { diff --git a/scripts/check-helm-chart.sh b/scripts/check-helm-chart.sh new file mode 100644 index 0000000..09a2cff --- /dev/null +++ b/scripts/check-helm-chart.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# Stage, package, render, and parse the distributable Helm chart. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=common.sh +source "$SCRIPT_DIR/common.sh" + +need helm +need python3 + +TMP_DIR="$(mktemp -d)" +trap 'rm -rf "$TMP_DIR"' EXIT + +"$REPO_ROOT/helm/tekton-dag/package.sh" +helm lint --strict "$REPO_ROOT/helm/tekton-dag" +helm package "$REPO_ROOT/helm/tekton-dag" --destination "$TMP_DIR" + +mapfile -t charts < <(compgen -G "$TMP_DIR/tekton-dag-*.tgz" || true) +((${#charts[@]} == 1)) || + die "expected one packaged chart, found ${#charts[@]}" +chart="${charts[0]}" + +helm lint --strict "$chart" +helm template static-quality-default "$chart" --include-crds \ + >"$TMP_DIR/rendered-default.yaml" +helm template static-quality-cluster-admin "$chart" --include-crds \ + --set rbac.clusterAdmin=true \ + >"$TMP_DIR/rendered-cluster-admin.yaml" + +python3 - "$TMP_DIR/rendered-default.yaml" "$TMP_DIR/rendered-cluster-admin.yaml" <<'PY' +import sys +from pathlib import Path + +import yaml + +for filename in sys.argv[1:]: + path = Path(filename) + documents = [document for document in yaml.safe_load_all(path.read_text()) if document] + if not documents: + raise SystemExit(f"{path}: rendered no resources") + runners = [ + document + for document in documents + if document.get("kind") == "ConfigMap" + and document.get("metadata", {}).get("name") == "run-stack-tests-runners" + ] + if not runners: + raise SystemExit(f"{path}: missing ConfigMap run-stack-tests-runners") + script = runners[0].get("data", {}).get("run-stack-tests-runners.sh", "") + if "run_newman" not in script: + raise SystemExit(f"{path}: runner ConfigMap is missing run_newman") + print(f"{path.name}: parsed {len(documents)} resource(s)") +PY diff --git a/scripts/install-tekton.sh b/scripts/install-tekton.sh index 01e5d72..45be660 100755 --- a/scripts/install-tekton.sh +++ b/scripts/install-tekton.sh @@ -59,14 +59,18 @@ kubectl label namespace "$NAMESPACE" pod-security.kubernetes.io/enforce=privileg kubectl label namespace "$NAMESPACE" pod-security.kubernetes.io/audit=privileged --overwrite 2>/dev/null || true kubectl label namespace "$NAMESPACE" pod-security.kubernetes.io/warn=privileged --overwrite 2>/dev/null || true echo " Waiting for Tekton Pipelines to be ready..." -kubectl wait --for=condition=Ready pods -l app.kubernetes.io/part-of=tekton-pipelines -n "$NAMESPACE" --timeout=120s 2>/dev/null || true +kubectl rollout status deployment/tekton-pipelines-controller -n tekton-pipelines --timeout=120s +kubectl rollout status deployment/tekton-pipelines-webhook -n tekton-pipelines --timeout=120s +kubectl rollout status deployment/tekton-pipelines-remote-resolvers -n tekton-pipelines-resolvers --timeout=120s # 2. Tekton Triggers (required for pipeline/triggers.yaml — EventListener, TriggerBinding, TriggerTemplate) echo " Installing Tekton Triggers..." kubectl apply -f "$TEKTON_TRIGGERS_URL" -kubectl apply -f "$TEKTON_TRIGGERS_INTERCEPTORS_URL" echo " Waiting for Tekton Triggers to be ready..." -kubectl wait --for=condition=Ready pods -l app.kubernetes.io/part-of=tekton-triggers -n "$NAMESPACE" --timeout=120s 2>/dev/null || true +kubectl rollout status deployment/tekton-triggers-controller -n tekton-pipelines --timeout=120s +kubectl rollout status deployment/tekton-triggers-webhook -n tekton-pipelines --timeout=120s +apply_with_retry -f "$TEKTON_TRIGGERS_INTERCEPTORS_URL" +kubectl rollout status deployment/tekton-triggers-core-interceptors -n tekton-pipelines --timeout=120s # 3. git-clone task (into target namespace so our pipelines can reference it) echo " Installing git-clone task..." @@ -75,7 +79,10 @@ kubectl apply -f "$TEKTON_GIT_CLONE_URL" -n "$NAMESPACE" 2>/dev/null || \ # 4. This repo's tasks and pipelines (kubectl apply is idempotent; triggers apply now that Triggers is installed) echo " Applying stack tasks and pipelines..." -kubectl apply -f "$MILESTONE_DIR/tasks/" -n "$NAMESPACE" +kubectl create configmap run-stack-tests-runners \ + --from-file=run-stack-tests-runners.sh="$MILESTONE_DIR/scripts/run-stack-tests-runners.sh" \ + -n "$NAMESPACE" --dry-run=client -o yaml | kubectl apply -f - +apply_with_retry -f "$MILESTONE_DIR/tasks/" -n "$NAMESPACE" # EventListener reconciliation creates el-* Services. It can race the explicit # Service in triggers.yaml between kubectl's read and create operations. apply_with_retry -f "$MILESTONE_DIR/pipeline/" -n "$NAMESPACE" diff --git a/scripts/run-product-intercept-e2e.sh b/scripts/run-product-intercept-e2e.sh index 4e0bb7b..c3e967b 100755 --- a/scripts/run-product-intercept-e2e.sh +++ b/scripts/run-product-intercept-e2e.sh @@ -48,6 +48,8 @@ need kubectl need curl need jq mkdir -p "$ARTIFACT_DIR" +kubectl get pvc build-cache -n "$NAMESPACE" >/dev/null 2>&1 \ + || die "required PVC $NAMESPACE/build-cache is missing" resolve_api_token() { if [[ -n "${API_MUTATION_TOKEN:-}" ]]; then diff --git a/scripts/run-stack-tests-runners.sh b/scripts/run-stack-tests-runners.sh new file mode 100755 index 0000000..203c661 --- /dev/null +++ b/scripts/run-stack-tests-runners.sh @@ -0,0 +1,358 @@ +#!/bin/sh +# Newman / Playwright / Artillery runner used by tasks/run-stack-tests. +# Source from the Task after Tekton params are exported, or execute locally +# with the same environment variables. +# shellcheck shell=sh +set -eu + +STACK_JSON="${STACK_JSON:-}" +APP_LIST="${APP_LIST:-}" +ENTRY_APP="${ENTRY_APP:-}" +CHAIN="${CHAIN:-}" +BUILD_APPS="${BUILD_APPS:-}" +INTERCEPT="${INTERCEPT:-}" +DEFAULT_NS="${DEFAULT_NS:-staging}" +TESTS_TO_RUN="${TESTS_TO_RUN:-}" +UNMAPPED_AREA="${UNMAPPED_AREA:-}" +APPS_TO_TEST="${APPS_TO_TEST:-}" +TEST_SOURCE="${TEST_SOURCE:-.}" +TEST_SUMMARY_PATH="${TEST_SUMMARY_PATH:-/tmp/run-stack-tests-summary.json}" + +cd "$TEST_SOURCE" + +echo '{}' > /tmp/test-summary.json +OVERALL_PASS=true + +if [ -n "$UNMAPPED_AREA" ] && [ -z "$TESTS_TO_RUN" ]; then + echo "" + echo "####################################################" + echo " NO MAPPED REGRESSION for area: $UNMAPPED_AREA" + echo " This area needs regression tests to be built." + echo "####################################################" + echo "" + jq --arg area "$UNMAPPED_AREA" \ + '. + {"unmapped-area": $area, "message": "No mapped regression; area needs tests"}' \ + /tmp/test-summary.json > /tmp/test-summary-tmp.json + mv /tmp/test-summary-tmp.json /tmp/test-summary.json + tee "$TEST_SUMMARY_PATH" < /tmp/test-summary.json + exit 0 +fi + +if ! printf '%s' "$STACK_JSON" | jq -e 'type == "object"' >/dev/null 2>&1; then + echo "ERROR: stack-json is not valid JSON" >&2 + exit 1 +fi + +HEADER_NAME="" +HEADER_VAL="" +BAGGAGE_KEY=$(printf '%s' "$STACK_JSON" | jq -r '.propagation."baggage-key" // "dev-session"') +if [ -n "$INTERCEPT" ]; then + HEADER_NAME=$(printf '%s' "$INTERCEPT" | cut -d: -f1) + HEADER_VAL=$(printf '%s' "$INTERCEPT" | cut -d: -f2-) +fi + +FILTERED=false +HAS_E2E=false +if [ -n "$TESTS_TO_RUN" ]; then + FILTERED=true + echo "" + echo "####################################################" + echo " FILTERED TEST RUN (from test-plan graph)" + echo " Tests: $TESTS_TO_RUN" + echo " Apps: $APPS_TO_TEST" + echo "####################################################" + echo "" + if printf '%s' "$TESTS_TO_RUN" | grep -q "e2e/"; then + HAS_E2E=true + fi +fi + +app_in_test_plan() { + [ "$FILTERED" = "false" ] && return 0 + [ -z "$APPS_TO_TEST" ] && return 0 + printf '%s' ",$APPS_TO_TEST," | grep -q ",$1," +} + +run_newman() { + collection="$1" + base_url="$2" + label="$3" + echo " [$label] Running: $collection" + if [ -n "$HEADER_NAME" ]; then + newman run "$collection" --env-var "baseUrl=$base_url" --reporters cli,json \ + --global-var "${HEADER_NAME}=${HEADER_VAL}" + else + newman run "$collection" --env-var "baseUrl=$base_url" --reporters cli,json + fi +} + +run_playwright() { + suite_dir="$1" + base_url="$2" + label="$3" + echo " [$label] Running: $suite_dir" + package_dir=$(dirname "$suite_dir") + if [ ! -f "${package_dir}/package.json" ]; then + echo " [$label] No package.json found (skipping)" + return 0 + fi + cd "$package_dir" + npm install --legacy-peer-deps 2>/dev/null || true + BASE_URL="$base_url" + export BASE_URL + if [ -n "$HEADER_NAME" ]; then + INTERCEPT_HEADER_NAME="$HEADER_NAME" + INTERCEPT_HEADER_VALUE="$HEADER_VAL" + export INTERCEPT_HEADER_NAME + export INTERCEPT_HEADER_VALUE + fi + rel_path=$(printf '%s' "$suite_dir" | sed "s|^${package_dir}/||") + status=0 + npx playwright test "$rel_path" --reporter=list 2>/dev/null || status=$? + cd "$TEST_SOURCE" + return "$status" +} + +run_artillery() { + script_path="$1" + base_url="$2" + label="$3" + echo " [$label] Running: $script_path" + if [ -n "$HEADER_NAME" ]; then + cat > /tmp/artillery-override.yml < /tmp/test-summary-tmp.json + mv /tmp/test-summary-tmp.json /tmp/test-summary.json +else + echo "" + echo "####################################################" + echo " PHASE 1: E2E test through entry point ($ENTRY_APP)" + echo "####################################################" + echo "" + echo " Chain: $CHAIN" + echo " Intercepted: $BUILD_APPS" + echo " Header: $HEADER_NAME=$HEADER_VAL" + echo "" + + ENTRY_NS=$(printf '%s' "$STACK_JSON" | jq -r --arg a "$ENTRY_APP" --arg d "$DEFAULT_NS" \ + '(.defaults.namespace // $d) as $dn | .apps[]|select(.name==$a)|.namespace // $dn') + ENTRY_SPORT=$(printf '%s' "$STACK_JSON" | jq -r --arg a "$ENTRY_APP" \ + '(.defaults."service-port" // "80") as $dp | .apps[]|select(.name==$a)|."service-port" // $dp') + ENTRY_URL="http://${ENTRY_APP}.${ENTRY_NS}.svc.cluster.local:${ENTRY_SPORT}" + + echo " Entry URL: $ENTRY_URL" + echo "" + + E2E_PASS=true + E2E_RESULT=$(curl -s \ + -H "${HEADER_NAME}: ${HEADER_VAL}" \ + -H "baggage: ${BAGGAGE_KEY}=${HEADER_VAL}" \ + --connect-timeout 15 --max-time 60 \ + "$ENTRY_URL" 2>/dev/null || echo '{"error":"unreachable"}') + + echo " Response:" + printf '%s\n' "$E2E_RESULT" | jq '.' 2>/dev/null || echo " $E2E_RESULT" + echo "" + + HOP=0 + for APP in $CHAIN; do + HOP=$((HOP + 1)) + IS_INTERCEPTED=false + for BA in $BUILD_APPS; do + [ "$BA" = "$APP" ] && IS_INTERCEPTED=true + done + + MARKER="" + if [ "$IS_INTERCEPTED" = "true" ]; then + MARKER=" [INTERCEPTED → PR build]" + fi + + if printf '%s' "$E2E_RESULT" | grep -qi "$APP"; then + echo " HOP $HOP ($APP)${MARKER}: REACHED" + else + echo " HOP $HOP ($APP)${MARKER}: NOT CONFIRMED" + fi + done + + if [ -n "$HEADER_VAL" ] && printf '%s' "$E2E_RESULT" | grep -qi "$HEADER_VAL"; then + echo "" + echo " Header value '$HEADER_VAL' found in response" + fi + + echo "" + if [ "$E2E_PASS" = "true" ]; then + echo " E2E: PASS" + else + echo " E2E: FAIL" + OVERALL_PASS=false + fi + + jq --arg s "$([ "$E2E_PASS" = "true" ] && echo pass || echo fail)" \ + '. + {"e2e-entry-point": $s}' \ + /tmp/test-summary.json > /tmp/test-summary-tmp.json + mv /tmp/test-summary-tmp.json /tmp/test-summary.json + + ENTRY_POSTMAN=$(printf '%s' "$STACK_JSON" | jq -r --arg a "$ENTRY_APP" \ + '.apps[]|select(.name==$a)|.tests.postman // ""') + ENTRY_PLAYWRIGHT=$(printf '%s' "$STACK_JSON" | jq -r --arg a "$ENTRY_APP" \ + '.apps[]|select(.name==$a)|.tests.playwright // ""') + + if [ -n "$ENTRY_POSTMAN" ] && [ -f "$ENTRY_POSTMAN" ]; then + echo "" + if run_newman "$ENTRY_POSTMAN" "$ENTRY_URL" "e2e/postman"; then + echo " [e2e/postman] PASS" + else + echo " [e2e/postman] FAIL" + OVERALL_PASS=false + fi + fi + + if [ -n "$ENTRY_PLAYWRIGHT" ] && [ -d "$ENTRY_PLAYWRIGHT" ]; then + echo "" + if run_playwright "$ENTRY_PLAYWRIGHT" "$ENTRY_URL" "e2e/playwright"; then + echo " [e2e/playwright] PASS" + else + echo " [e2e/playwright] FAIL" + OVERALL_PASS=false + fi + fi +fi + +# =========================================================== +# Phase 2: Per-app tests +# =========================================================== +echo "" +echo "####################################################" +echo " PHASE 2: Per-app tests" +echo "####################################################" + +for APP in $APP_LIST; do + if [ "$APP" = "$ENTRY_APP" ]; then + echo "" + echo " Skipping $APP (already tested in e2e phase)" + continue + fi + + if ! app_in_test_plan "$APP"; then + echo "" + echo " Skipping $APP (not in test plan)" + jq --arg a "$APP" '. + {($a): "skipped (not in plan)"}' \ + /tmp/test-summary.json > /tmp/test-summary-tmp.json + mv /tmp/test-summary-tmp.json /tmp/test-summary.json + continue + fi + + echo "" + echo "=========================================" + echo " Testing: $APP" + + IS_INTERCEPTED=false + for BA in $BUILD_APPS; do + [ "$BA" = "$APP" ] && IS_INTERCEPTED=true + done + if [ "$IS_INTERCEPTED" = "true" ]; then + echo " (INTERCEPTED — hitting PR build via Telepresence)" + else + echo " (normal cluster deployment)" + fi + + echo "=========================================" + + NS=$(printf '%s' "$STACK_JSON" | jq -r --arg a "$APP" --arg d "$DEFAULT_NS" \ + '(.defaults.namespace // $d) as $dn | .apps[]|select(.name==$a)|.namespace // $dn') + SPORT=$(printf '%s' "$STACK_JSON" | jq -r --arg a "$APP" \ + '(.defaults."service-port" // "80") as $dp | .apps[]|select(.name==$a)|."service-port" // $dp') + SERVICE_URL="http://${APP}.${NS}.svc.cluster.local:${SPORT}" + + POSTMAN=$(printf '%s' "$STACK_JSON" | jq -r --arg a "$APP" \ + '.apps[]|select(.name==$a)|.tests.postman // ""') + PLAYWRIGHT=$(printf '%s' "$STACK_JSON" | jq -r --arg a "$APP" \ + '.apps[]|select(.name==$a)|.tests.playwright // ""') + ARTILLERY=$(printf '%s' "$STACK_JSON" | jq -r --arg a "$APP" \ + '.apps[]|select(.name==$a)|.tests.artillery // ""') + + APP_PASS=true + + if [ -n "$POSTMAN" ] && [ -f "$POSTMAN" ]; then + if run_newman "$POSTMAN" "$SERVICE_URL" "postman"; then + echo " [postman] PASS" + else + echo " [postman] FAIL" + APP_PASS=false + fi + elif [ -n "$POSTMAN" ]; then + echo " [postman] Collection not found: $POSTMAN (skipping)" + fi + + if [ -n "$PLAYWRIGHT" ] && [ -d "$PLAYWRIGHT" ]; then + if run_playwright "$PLAYWRIGHT" "$SERVICE_URL" "playwright"; then + echo " [playwright] PASS" + else + echo " [playwright] FAIL" + APP_PASS=false + fi + elif [ -n "$PLAYWRIGHT" ]; then + echo " [playwright] Test dir not found: $PLAYWRIGHT (skipping)" + fi + + if [ -n "$ARTILLERY" ] && [ -f "$ARTILLERY" ]; then + if run_artillery "$ARTILLERY" "$SERVICE_URL" "artillery"; then + echo " [artillery] PASS" + else + echo " [artillery] FAIL" + APP_PASS=false + fi + elif [ -n "$ARTILLERY" ]; then + echo " [artillery] Script not found: $ARTILLERY (skipping)" + fi + + STATUS="pass" + if [ "$APP_PASS" = "false" ]; then + STATUS="fail" + OVERALL_PASS=false + fi + + jq --arg a "$APP" --arg s "$STATUS" '. + {($a): $s}' \ + /tmp/test-summary.json > /tmp/test-summary-tmp.json + mv /tmp/test-summary-tmp.json /tmp/test-summary.json +done + +echo "" +echo "=========================================" +echo " Test Summary" +echo "=========================================" +jq '.' /tmp/test-summary.json +tee "$TEST_SUMMARY_PATH" < /tmp/test-summary.json + +if [ "$OVERALL_PASS" = "false" ]; then + echo "" + echo "FAILED: One or more test phases failed." + exit 1 +fi diff --git a/tasks/post-pr-comment.yaml b/tasks/post-pr-comment.yaml index 7bdbf33..773c493 100644 --- a/tasks/post-pr-comment.yaml +++ b/tasks/post-pr-comment.yaml @@ -1,5 +1,5 @@ # Task that posts a comment on the GitHub PR with pipeline run status and optional link to Tekton Dashboard. -# Requires a Kubernetes secret with key 'token' (GitHub token with repo scope for creating issue comments). +# Uses a Kubernetes secret with key 'token' when configured; without one, the Task skips posting. # Pipeline should pass: git-url, pr-number, dashboard-url (optional), and optionally test-summary. # Status and pipeline run name/namespace can come from pipeline context. apiVersion: tekton.dev/v1 @@ -44,6 +44,7 @@ spec: secretKeyRef: name: $(params.github-secret-name) key: token + optional: true - name: GIT_URL value: $(params.git-url) - name: PR_REPO_URL diff --git a/tasks/run-stack-tests.yaml b/tasks/run-stack-tests.yaml index cc39f72..0071675 100644 --- a/tasks/run-stack-tests.yaml +++ b/tasks/run-stack-tests.yaml @@ -22,6 +22,10 @@ spec: Artillery) with the intercept header attached. For intercepted apps this validates the PR build directly. For non-intercepted apps this validates they still work with the header present. + + Runner logic lives in scripts/run-stack-tests-runners.sh and is + mounted from ConfigMap run-stack-tests-runners so the Task does + not depend on the cloned platform git-revision. params: - name: stack-json type: string @@ -65,6 +69,9 @@ spec: - name: run-tests image: node:22-alpine workingDir: $(workspaces.test-source.path) + volumeMounts: + - name: runners-script + mountPath: /opt/tekton-dag script: | #!/bin/sh set -e @@ -79,338 +86,23 @@ spec: BUILD_APPS="$(params.build-apps)" INTERCEPT="$(params.intercept-header-value)" DEFAULT_NS="$(params.default-namespace)" - - HEADER_NAME="" - HEADER_VAL="" - BAGGAGE_KEY=$(echo "$STACK_JSON" | jq -r '.propagation."baggage-key" // "dev-session"') - if [ -n "$INTERCEPT" ]; then - HEADER_NAME=$(echo "$INTERCEPT" | cut -d: -f1) - HEADER_VAL=$(echo "$INTERCEPT" | cut -d: -f2-) - fi - TESTS_TO_RUN="$(params.tests-to-run)" UNMAPPED_AREA="$(params.unmapped-area)" APPS_TO_TEST="$(params.apps-to-test)" - - echo '{}' > /tmp/test-summary.json - OVERALL_PASS=true - - if [ -n "$UNMAPPED_AREA" ] && [ -z "$TESTS_TO_RUN" ]; then - echo "" - echo "####################################################" - echo " NO MAPPED REGRESSION for area: $UNMAPPED_AREA" - echo " This area needs regression tests to be built." - echo "####################################################" - echo "" - jq --arg area "$UNMAPPED_AREA" \ - '. + {"unmapped-area": $area, "message": "No mapped regression; area needs tests"}' \ - /tmp/test-summary.json > /tmp/test-summary-tmp.json - mv /tmp/test-summary-tmp.json /tmp/test-summary.json - cat /tmp/test-summary.json | tee $(results.test-summary.path) - exit 0 - fi - - FILTERED=false - HAS_E2E=false - if [ -n "$TESTS_TO_RUN" ]; then - FILTERED=true - echo "" - echo "####################################################" - echo " FILTERED TEST RUN (from test-plan graph)" - echo " Tests: $TESTS_TO_RUN" - echo " Apps: $APPS_TO_TEST" - echo "####################################################" - echo "" - if echo "$TESTS_TO_RUN" | grep -q "e2e/"; then - HAS_E2E=true - fi - fi - - app_in_test_plan() { - [ "$FILTERED" = "false" ] && return 0 - [ -z "$APPS_TO_TEST" ] && return 0 - echo ",$APPS_TO_TEST," | grep -q ",$1," - } - - # =========================================================== - # Phase 1: E2E through entry point - # =========================================================== - if [ "$FILTERED" = "true" ] && [ "$HAS_E2E" = "false" ]; then - echo "" - echo "####################################################" - echo " PHASE 1: SKIPPED (no e2e tests in test plan)" - echo "####################################################" - jq '. + {"e2e-entry-point": "skipped (no e2e in plan)"}' \ - /tmp/test-summary.json > /tmp/test-summary-tmp.json - mv /tmp/test-summary-tmp.json /tmp/test-summary.json - else - echo "" - echo "####################################################" - echo " PHASE 1: E2E test through entry point ($ENTRY_APP)" - echo "####################################################" - echo "" - echo " Chain: $CHAIN" - echo " Intercepted: $BUILD_APPS" - echo " Header: $HEADER_NAME=$HEADER_VAL" - echo "" - - ENTRY_NS=$(echo "$STACK_JSON" | jq -r --arg a "$ENTRY_APP" --arg d "$DEFAULT_NS" \ - '(.defaults.namespace // $d) as $dn | .apps[]|select(.name==$a)|.namespace // $dn') - ENTRY_SPORT=$(echo "$STACK_JSON" | jq -r --arg a "$ENTRY_APP" \ - '(.defaults."service-port" // "80") as $dp | .apps[]|select(.name==$a)|."service-port" // $dp') - ENTRY_URL="http://${ENTRY_APP}.${ENTRY_NS}.svc.cluster.local:${ENTRY_SPORT}" - - echo " Entry URL: $ENTRY_URL" - echo "" - - # Send request through the full chain with the intercept header - E2E_PASS=true - E2E_RESULT=$(curl -s \ - -H "${HEADER_NAME}: ${HEADER_VAL}" \ - -H "baggage: ${BAGGAGE_KEY}=${HEADER_VAL}" \ - --connect-timeout 15 --max-time 60 \ - "$ENTRY_URL" 2>/dev/null || echo '{"error":"unreachable"}') - - echo " Response:" - echo "$E2E_RESULT" | jq '.' 2>/dev/null || echo " $E2E_RESULT" - echo "" - - # Verify each hop in the chain was reached - HOP=0 - for APP in $CHAIN; do - HOP=$((HOP + 1)) - IS_INTERCEPTED=false - for BA in $BUILD_APPS; do - [ "$BA" = "$APP" ] && IS_INTERCEPTED=true - done - - MARKER="" - $IS_INTERCEPTED && MARKER=" [INTERCEPTED → PR build]" - - if echo "$E2E_RESULT" | grep -qi "$APP"; then - echo " HOP $HOP ($APP)${MARKER}: REACHED" - else - echo " HOP $HOP ($APP)${MARKER}: NOT CONFIRMED" - fi - done - - # Verify the intercept header propagated - if echo "$E2E_RESULT" | grep -qi "$HEADER_VAL"; then - echo "" - echo " Header value '$HEADER_VAL' found in response" - fi - - echo "" - if [ "$E2E_PASS" = "true" ]; then - echo " E2E: PASS" - else - echo " E2E: FAIL" - OVERALL_PASS=false - fi - - jq --arg s "$([ "$E2E_PASS" = "true" ] && echo pass || echo fail)" \ - '. + {"e2e-entry-point": $s}' \ - /tmp/test-summary.json > /tmp/test-summary-tmp.json - mv /tmp/test-summary-tmp.json /tmp/test-summary.json - - # Also run entry app's Postman/Playwright tests through the full chain - ENTRY_POSTMAN=$(echo "$STACK_JSON" | jq -r --arg a "$ENTRY_APP" \ - '.apps[]|select(.name==$a)|.tests.postman // ""') - ENTRY_PLAYWRIGHT=$(echo "$STACK_JSON" | jq -r --arg a "$ENTRY_APP" \ - '.apps[]|select(.name==$a)|.tests.playwright // ""') - - if [ -n "$ENTRY_POSTMAN" ] && [ -f "$ENTRY_POSTMAN" ]; then - echo "" - echo " [e2e/postman] Running entry-point collection: $ENTRY_POSTMAN" - NEWMAN_ARGS="newman run $ENTRY_POSTMAN --env-var baseUrl=$ENTRY_URL --reporters cli,json" - if [ -n "$HEADER_NAME" ]; then - NEWMAN_ARGS="$NEWMAN_ARGS --global-var ${HEADER_NAME}=${HEADER_VAL}" - fi - if eval $NEWMAN_ARGS; then - echo " [e2e/postman] PASS" - else - echo " [e2e/postman] FAIL" - OVERALL_PASS=false - fi - fi - - if [ -n "$ENTRY_PLAYWRIGHT" ] && [ -d "$ENTRY_PLAYWRIGHT" ]; then - echo "" - echo " [e2e/playwright] Running entry-point e2e tests: $ENTRY_PLAYWRIGHT" - PACKAGE_DIR=$(dirname "$ENTRY_PLAYWRIGHT") - if [ -f "${PACKAGE_DIR}/package.json" ]; then - cd "$PACKAGE_DIR" - npm install --legacy-peer-deps 2>/dev/null || true - export BASE_URL="$ENTRY_URL" - if [ -n "$HEADER_NAME" ]; then - export INTERCEPT_HEADER_NAME="$HEADER_NAME" - export INTERCEPT_HEADER_VALUE="$HEADER_VAL" - fi - REL_PATH=$(echo "$ENTRY_PLAYWRIGHT" | sed "s|^${PACKAGE_DIR}/||") - if npx playwright test "$REL_PATH" --reporter=list 2>/dev/null; then - echo " [e2e/playwright] PASS" - else - echo " [e2e/playwright] FAIL" - OVERALL_PASS=false - fi - cd "$(workspaces.test-source.path)" - fi - fi - - fi - - # =========================================================== - # Phase 2: Per-app tests - # =========================================================== - echo "" - echo "####################################################" - echo " PHASE 2: Per-app tests" - echo "####################################################" - - for APP in $APP_LIST; do - if [ "$APP" = "$ENTRY_APP" ]; then - echo "" - echo " Skipping $APP (already tested in e2e phase)" - continue - fi - - if ! app_in_test_plan "$APP"; then - echo "" - echo " Skipping $APP (not in test plan)" - jq --arg a "$APP" '. + {($a): "skipped (not in plan)"}' \ - /tmp/test-summary.json > /tmp/test-summary-tmp.json - mv /tmp/test-summary-tmp.json /tmp/test-summary.json - continue - fi - - echo "" - echo "=========================================" - echo " Testing: $APP" - - IS_INTERCEPTED=false - for BA in $BUILD_APPS; do - [ "$BA" = "$APP" ] && IS_INTERCEPTED=true - done - $IS_INTERCEPTED && echo " (INTERCEPTED — hitting PR build via Telepresence)" - $IS_INTERCEPTED || echo " (normal cluster deployment)" - - echo "=========================================" - - NS=$(echo "$STACK_JSON" | jq -r --arg a "$APP" --arg d "$DEFAULT_NS" \ - '(.defaults.namespace // $d) as $dn | .apps[]|select(.name==$a)|.namespace // $dn') - SPORT=$(echo "$STACK_JSON" | jq -r --arg a "$APP" \ - '(.defaults."service-port" // "80") as $dp | .apps[]|select(.name==$a)|."service-port" // $dp') - SERVICE_URL="http://${APP}.${NS}.svc.cluster.local:${SPORT}" - - POSTMAN=$(echo "$STACK_JSON" | jq -r --arg a "$APP" \ - '.apps[]|select(.name==$a)|.tests.postman // ""') - PLAYWRIGHT=$(echo "$STACK_JSON" | jq -r --arg a "$APP" \ - '.apps[]|select(.name==$a)|.tests.playwright // ""') - ARTILLERY=$(echo "$STACK_JSON" | jq -r --arg a "$APP" \ - '.apps[]|select(.name==$a)|.tests.artillery // ""') - - APP_PASS=true - - # --- Postman / Newman --- - if [ -n "$POSTMAN" ] && [ -f "$POSTMAN" ]; then - echo " [postman] Running: $POSTMAN" - NEWMAN_ARGS="newman run $POSTMAN --env-var baseUrl=$SERVICE_URL --reporters cli,json" - if [ -n "$HEADER_NAME" ]; then - NEWMAN_ARGS="$NEWMAN_ARGS --global-var ${HEADER_NAME}=${HEADER_VAL}" - fi - if eval $NEWMAN_ARGS; then - echo " [postman] PASS" - else - echo " [postman] FAIL" - APP_PASS=false - fi - elif [ -n "$POSTMAN" ]; then - echo " [postman] Collection not found: $POSTMAN (skipping)" - fi - - # --- Playwright --- - if [ -n "$PLAYWRIGHT" ] && [ -d "$PLAYWRIGHT" ]; then - echo " [playwright] Running: $PLAYWRIGHT" - PACKAGE_DIR=$(dirname "$PLAYWRIGHT") - if [ -f "${PACKAGE_DIR}/package.json" ]; then - cd "$PACKAGE_DIR" - npm install --legacy-peer-deps 2>/dev/null || true - export BASE_URL="$SERVICE_URL" - if [ -n "$HEADER_NAME" ]; then - export INTERCEPT_HEADER_NAME="$HEADER_NAME" - export INTERCEPT_HEADER_VALUE="$HEADER_VAL" - fi - REL_PATH=$(echo "$PLAYWRIGHT" | sed "s|^${PACKAGE_DIR}/||") - if npx playwright test "$REL_PATH" --reporter=list 2>/dev/null; then - echo " [playwright] PASS" - else - echo " [playwright] FAIL" - APP_PASS=false - fi - cd "$(workspaces.test-source.path)" - else - echo " [playwright] No package.json found (skipping)" - fi - elif [ -n "$PLAYWRIGHT" ]; then - echo " [playwright] Test dir not found: $PLAYWRIGHT (skipping)" - fi - - # --- Artillery --- - if [ -n "$ARTILLERY" ] && [ -f "$ARTILLERY" ]; then - echo " [artillery] Running: $ARTILLERY" - ARTILLERY_ARGS="" - if [ -n "$HEADER_NAME" ]; then - cat > /tmp/artillery-override.yml < /tmp/test-summary-tmp.json - mv /tmp/test-summary-tmp.json /tmp/test-summary.json - done - - # =========================================================== - # Summary - # =========================================================== - echo "" - echo "=========================================" - echo " Test Summary" - echo "=========================================" - cat /tmp/test-summary.json | jq '.' - cat /tmp/test-summary.json | tee $(results.test-summary.path) - - if [ "$OVERALL_PASS" = "false" ]; then - echo "" - echo "FAILED: One or more test phases failed." + TEST_SOURCE="$(workspaces.test-source.path)" + TEST_SUMMARY_PATH="$(results.test-summary.path)" + export STACK_JSON APP_LIST ENTRY_APP CHAIN BUILD_APPS INTERCEPT \ + DEFAULT_NS TESTS_TO_RUN UNMAPPED_AREA APPS_TO_TEST \ + TEST_SOURCE TEST_SUMMARY_PATH + + RUNNERS="/opt/tekton-dag/run-stack-tests-runners.sh" + if [ ! -f "$RUNNERS" ]; then + echo "ERROR: run-stack-tests runners ConfigMap missing: $RUNNERS" >&2 exit 1 fi + # shellcheck disable=SC1090 + . "$RUNNERS" + volumes: + - name: runners-script + configMap: + name: run-stack-tests-runners