From 4becc99e7a635fe59ae87cb16eae45366d2be4e6 Mon Sep 17 00:00:00 2001 From: Jorge Romero Date: Thu, 5 Feb 2026 15:26:35 +0100 Subject: [PATCH 1/6] Feature ods quickstarters tests (#1144) * Adapt Golden Test for the new features of the test framework * Add Quickstarters Automated Tests documentation and update navigation * update Changelog.md --- .gitignore | 1 + CHANGELOG.md | 2 +- .../testdata/golden/jenkins-build-stages.json | 4 + be-fe-mono-repo-plain/testdata/steps.yml | 3 + .../testdata/golden/jenkins-build-stages.json | 4 + be-gateway-nginx/testdata/steps.yml | 3 + .../testdata/golden/sonar-scan.json | 8 +- be-golang-plain/testdata/steps.yml | 3 +- .../testdata/golden/sonar-scan.json | 9 +- be-java-springboot/testdata/steps.yml | 3 +- .../functional/api/health-response.json | 1 + .../functional/integration/smoke_test.sh | 25 + .../testdata/golden/sonar-scan.json | 8 +- be-python-flask/testdata/steps.yml | 100 +- .../testdata/golden/jenkins-build-stages.json | 2 +- be-rust-axum/testdata/golden/sonar-scan.json | 15 +- be-rust-axum/testdata/steps.yml | 3 +- be-scala-play/testdata/golden/sonar-scan.json | 23 +- be-scala-play/testdata/steps.yml | 3 +- .../testdata/golden/sonar-scan.json | 8 +- be-typescript-express/testdata/steps.yml | 3 +- .../testdata/functional/nginx-test.sh | 52 + .../testdata/functional/performance-test.sh | 61 + .../testdata/golden/jenkins-build-stages.json | 6 +- .../golden/jenkins-provision-stages.json | 4 - .../testdata/hooks/cleanup-debug-logs.sh | 15 + .../testdata/hooks/validate-cluster.sh | 27 + docker-plain/testdata/steps.yml | 148 +- docs/modules/quickstarters/nav.adoc | 1 + .../pages/authoring-quickstarters.adoc | 4 + .../pages/quickstarters-automated-tests.adoc | 1230 +++++++++++++++++ .../testdata/golden/jenkins-build-stages.json | 8 + ds-jupyter-lab/testdata/steps.yml | 6 +- .../testdata/golden/jenkins-build-stages.json | 8 + ds-rshiny/testdata/steps.yml | 2 + ds-streamlit/testdata/golden/sonar-scan.json | 13 +- ds-streamlit/testdata/steps.yml | 3 +- .../testdata/golden/jenkins-build-stages.json | 12 +- e2e-cypress/testdata/golden/sonar-scan.json | 8 +- e2e-cypress/testdata/steps.yml | 4 +- .../testdata/golden/sonar-scan.json | 3 +- e2e-etl-python/testdata/steps.yml | 3 +- .../testdata/golden/jenkins-build-stages.json | 2 +- .../golden/jenkins-provision-stages.json | 4 + e2e-spock-geb/testdata/golden/sonar-scan.json | 8 +- e2e-spock-geb/testdata/steps.yml | 5 +- .../fixtures/angular-cypress/Jenkinsfile | 134 ++ .../tests/acceptance/angular-smoke.spec.cy.ts | 11 + .../angular-installation.spec.cy.ts | 65 + .../angular-integration.spec.cy.ts | 133 ++ .../golden/jenkins-build-cypress.json | 30 + .../testdata/golden/sonar-scan-cypress.json | 31 + fe-angular/testdata/golden/sonar-scan.json | 23 +- fe-angular/testdata/steps.yml | 144 +- fe-ionic/testdata/golden/sonar-scan.json | 26 +- fe-ionic/testdata/steps.yml | 3 +- .../testdata/golden/sonar-scan.json | 3 +- ods-document-gen-svc/testdata/steps.yml | 3 +- .../testdata/golden/sonar-scan.json | 3 +- ods-provisioning-app/testdata/steps.yml | 3 +- release-manager/testdata/steps.yml | 11 +- 61 files changed, 2364 insertions(+), 127 deletions(-) create mode 100644 be-python-flask/testdata/functional/api/health-response.json create mode 100755 be-python-flask/testdata/functional/integration/smoke_test.sh create mode 100755 docker-plain/testdata/functional/nginx-test.sh create mode 100755 docker-plain/testdata/functional/performance-test.sh create mode 100755 docker-plain/testdata/hooks/cleanup-debug-logs.sh create mode 100755 docker-plain/testdata/hooks/validate-cluster.sh create mode 100644 docs/modules/quickstarters/pages/quickstarters-automated-tests.adoc create mode 100644 fe-angular/testdata/fixtures/angular-cypress/Jenkinsfile create mode 100644 fe-angular/testdata/fixtures/angular-cypress/tests/acceptance/angular-smoke.spec.cy.ts create mode 100644 fe-angular/testdata/fixtures/angular-cypress/tests/installation/angular-installation.spec.cy.ts create mode 100644 fe-angular/testdata/fixtures/angular-cypress/tests/integration/angular-integration.spec.cy.ts create mode 100644 fe-angular/testdata/golden/jenkins-build-cypress.json create mode 100644 fe-angular/testdata/golden/sonar-scan-cypress.json diff --git a/.gitignore b/.gitignore index 22095f024..77948d539 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,4 @@ test-setup.bin /tests/ +e2e-spock-geb/files/.gradle diff --git a/CHANGELOG.md b/CHANGELOG.md index 0997f2613..b111a7210 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # Changelog ## Unreleased - +- Update Quickstarter Tests to Support New Framework Test Capabilities ([#1144](https://github.com/opendevstack/ods-quickstarters/pull/1144)) ### Added ### Changed diff --git a/be-fe-mono-repo-plain/testdata/golden/jenkins-build-stages.json b/be-fe-mono-repo-plain/testdata/golden/jenkins-build-stages.json index 6f0299093..10ae60a8f 100644 --- a/be-fe-mono-repo-plain/testdata/golden/jenkins-build-stages.json +++ b/be-fe-mono-repo-plain/testdata/golden/jenkins-build-stages.json @@ -3,6 +3,10 @@ "stage": "odsPipeline start", "status": "SUCCESS" }, + { + "stage": "SonarQube Analysis", + "status": "SUCCESS" + }, { "stage": "Build OpenShift Image (monorepo-iq-test-frontend)", "status": "SUCCESS" diff --git a/be-fe-mono-repo-plain/testdata/steps.yml b/be-fe-mono-repo-plain/testdata/steps.yml index 747f3a0cf..b236ca817 100644 --- a/be-fe-mono-repo-plain/testdata/steps.yml +++ b/be-fe-mono-repo-plain/testdata/steps.yml @@ -22,3 +22,6 @@ steps: services: - "{{.ComponentID}}-backend" - "{{.ComponentID}}-frontend" + runAttachments: + - sonarqube-report-{{.ProjectID}}-{{.ComponentID}}.pdf + diff --git a/be-gateway-nginx/testdata/golden/jenkins-build-stages.json b/be-gateway-nginx/testdata/golden/jenkins-build-stages.json index 441b776ea..9fd104112 100644 --- a/be-gateway-nginx/testdata/golden/jenkins-build-stages.json +++ b/be-gateway-nginx/testdata/golden/jenkins-build-stages.json @@ -3,6 +3,10 @@ "stage": "odsPipeline start", "status": "SUCCESS" }, + { + "stage": "SonarQube Analysis", + "status": "SUCCESS" + }, { "stage": "Build OpenShift Image", "status": "SUCCESS" diff --git a/be-gateway-nginx/testdata/steps.yml b/be-gateway-nginx/testdata/steps.yml index 01575101a..c75209278 100644 --- a/be-gateway-nginx/testdata/steps.yml +++ b/be-gateway-nginx/testdata/steps.yml @@ -18,3 +18,6 @@ steps: - "{{.ComponentID}}" services: - "{{.ComponentID}}" + runAttachments: + - sonarqube-report-{{.ProjectID}}-{{.ComponentID}}.pdf + diff --git a/be-golang-plain/testdata/golden/sonar-scan.json b/be-golang-plain/testdata/golden/sonar-scan.json index d82f45d73..285dd275f 100644 --- a/be-golang-plain/testdata/golden/sonar-scan.json +++ b/be-golang-plain/testdata/golden/sonar-scan.json @@ -2,9 +2,15 @@ "key": "{{.ProjectID}}-{{.ComponentID}}", "name": "{{.ProjectID}}-{{.ComponentID}}", "isFavorite": false, + "branch": "master", "visibility": "public", "extensions": [], "qualityProfiles": [ + { + "name": "Sonar way", + "language": "docker", + "deleted": false + }, { "name": "{{.SonarQualityProfile}}", "language": "go", @@ -12,7 +18,7 @@ } ], "qualityGate": { - "name": "Sonar way", + "name": "{{.SonarQualityGate}}", "isDefault": true }, "breadcrumbs": [ diff --git a/be-golang-plain/testdata/steps.yml b/be-golang-plain/testdata/steps.yml index 6cf939cb7..b6cf971ad 100644 --- a/be-golang-plain/testdata/steps.yml +++ b/be-golang-plain/testdata/steps.yml @@ -10,8 +10,7 @@ steps: jenkinsStages: golden/jenkins-build-stages.json sonarScan: golden/sonar-scan.json runAttachments: - - SCRR-{{.ProjectID}}-{{.ComponentID}}.docx - - SCRR-{{.ProjectID}}-{{.ComponentID}}.md + - sonarqube-report-{{.ProjectID}}-{{.ComponentID}}.pdf testResults: 1 openShiftResources: imageTags: diff --git a/be-java-springboot/testdata/golden/sonar-scan.json b/be-java-springboot/testdata/golden/sonar-scan.json index e7ecc4ec3..8f6484192 100644 --- a/be-java-springboot/testdata/golden/sonar-scan.json +++ b/be-java-springboot/testdata/golden/sonar-scan.json @@ -2,17 +2,24 @@ "key": "{{.ProjectID}}-{{.ComponentID}}", "name": "{{.ProjectID}}-{{.ComponentID}}", "isFavorite": false, + "branch": "master", "visibility": "public", "extensions": [], "qualityProfiles": [ + { + "name": "Sonar way", + "language": "docker", + "deleted": false + }, { "name": "{{.SonarQualityProfile}}", "language": "java", "deleted": false + } ], "qualityGate": { - "name": "Sonar way", + "name": "{{.SonarQualityGate}}", "isDefault": true }, "breadcrumbs": [ diff --git a/be-java-springboot/testdata/steps.yml b/be-java-springboot/testdata/steps.yml index 051fb4dbf..d2a0733f1 100644 --- a/be-java-springboot/testdata/steps.yml +++ b/be-java-springboot/testdata/steps.yml @@ -10,8 +10,7 @@ steps: jenkinsStages: golden/jenkins-build-stages.json sonarScan: golden/sonar-scan.json runAttachments: - - SCRR-{{.ProjectID}}-{{.ComponentID}}.docx - - SCRR-{{.ProjectID}}-{{.ComponentID}}.md + - sonarqube-report-{{.ProjectID}}-{{.ComponentID}}.pdf testResults: 1 openShiftResources: imageTags: diff --git a/be-python-flask/testdata/functional/api/health-response.json b/be-python-flask/testdata/functional/api/health-response.json new file mode 100644 index 000000000..cefd6d74c --- /dev/null +++ b/be-python-flask/testdata/functional/api/health-response.json @@ -0,0 +1 @@ +{"msg":"hello world!"} diff --git a/be-python-flask/testdata/functional/integration/smoke_test.sh b/be-python-flask/testdata/functional/integration/smoke_test.sh new file mode 100755 index 000000000..e52d1080c --- /dev/null +++ b/be-python-flask/testdata/functional/integration/smoke_test.sh @@ -0,0 +1,25 @@ +#!/bin/bash +set -e + +echo "Running smoke tests..." + +# APP_SERVICE_URL is automatically injected by the test framework +# with the correct resolved URL (route, port-forward, or service DNS) +if [ -z "$APP_SERVICE_URL" ]; then + echo "ERROR: APP_SERVICE_URL not set by test framework" + exit 1 +fi + +echo "Testing service at: $APP_SERVICE_URL" + +# Test 1: Health check +echo "Test 1: Health endpoint" +response=$(curl -f -s "$APP_SERVICE_URL/") +if [ -z "$response" ]; then + echo "FAIL: Health check failed - empty response" + exit 1 +fi +echo "PASS: Health endpoint returned: $response" + + +echo "All smoke tests passed!" diff --git a/be-python-flask/testdata/golden/sonar-scan.json b/be-python-flask/testdata/golden/sonar-scan.json index 559ca033b..b3efd5a2c 100644 --- a/be-python-flask/testdata/golden/sonar-scan.json +++ b/be-python-flask/testdata/golden/sonar-scan.json @@ -2,9 +2,15 @@ "key": "{{.ProjectID}}-{{.ComponentID}}", "name": "{{.ProjectID}}-{{.ComponentID}}", "isFavorite": false, + "branch": "master", "visibility": "public", "extensions": [], "qualityProfiles": [ + { + "name": "Sonar way", + "language": "docker", + "deleted": false + }, { "name": "{{.SonarQualityProfile}}", "language": "py", @@ -12,7 +18,7 @@ } ], "qualityGate": { - "name": "Sonar way", + "name": "{{.SonarQualityGate}}", "isDefault": true }, "breadcrumbs": [ diff --git a/be-python-flask/testdata/steps.yml b/be-python-flask/testdata/steps.yml index f7755d2c6..7e72e8fd3 100644 --- a/be-python-flask/testdata/steps.yml +++ b/be-python-flask/testdata/steps.yml @@ -1,25 +1,79 @@ componentID: python-flask-iq-test steps: -- type: provision - provisionParams: - verify: - jenkinsStages: golden/jenkins-provision-stages.json -- type: build - buildParams: - verify: - jenkinsStages: golden/jenkins-build-stages.json - sonarScan: golden/sonar-scan.json - runAttachments: - - SCRR-{{.ProjectID}}-{{.ComponentID}}.docx - - SCRR-{{.ProjectID}}-{{.ComponentID}}.md - testResults: 1 - openShiftResources: - imageTags: - - name: "{{.ComponentID}}" - tag: latest - imageStreams: - - "{{.ComponentID}}" - deploymentConfigs: - - "{{.ComponentID}}" - services: - - "{{.ComponentID}}" + - type: provision + provisionParams: + verify: + jenkinsStages: golden/jenkins-provision-stages.json + - type: build + buildParams: + verify: + jenkinsStages: golden/jenkins-build-stages.json + sonarScan: golden/sonar-scan.json + runAttachments: + - sonarqube-report-{{.ProjectID}}-{{.ComponentID}}.pdf + testResults: 1 + openShiftResources: + imageTags: + - name: "{{.ComponentID}}" + tag: latest + imageStreams: + - "{{.ComponentID}}" + deploymentConfigs: + - "{{.ComponentID}}" + services: + - "{{.ComponentID}}" + # Step 3: Wait for deployment to be ready + - type: wait + description: Wait for application pod to be ready + waitParams: + condition: pod-ready + resource: "-l app={{.ProjectID}}-{{.ComponentID}}" + namespace: "{{.ProjectID}}-dev" + timeout: "300s" + interval: "5s" + + # Step 4: Verify logs don't contain errors (NEW) + - type: inspect + description: Verify container logs + inspectParams: + resource: "deploymentconfig/{{.ComponentID}}" + namespace: "{{.ProjectID}}-dev" + checks: + logs: + contains: + - "Starting gunicorn 23.0.0" + - "Listening at: http://0.0.0.0:8080" + - "Using worker: sync" + - "Booting worker with pid:" + notContains: + - "panic:" + - "fatal error" + - "ERROR" + + # Step 5: Expose services + - type: "expose-service" + description: "Expose service with defaults" + exposeServiceParams: + services: + - serviceName: "{{.ComponentID}}" + + # Step 6: Test health endpoint + - type: http + description: Verify health endpoint returns 200 + httpParams: + url: "http://{{.ComponentID}}.{{.ProjectID}}-dev.svc.cluster.local:8080/" + method: GET + expectedStatus: 200 + expectedBody: "functional/api/health-response.json" + timeout: 30 + retry: + attempts: 10 + delay: "5s" + + # Step 7: Run integration smoke tests + - type: run + description: Run integration smoke tests + runParams: + file: "functional/integration/smoke_test.sh" + services: + app: "{{.ComponentID}}" diff --git a/be-rust-axum/testdata/golden/jenkins-build-stages.json b/be-rust-axum/testdata/golden/jenkins-build-stages.json index 0db2cef39..9335a681b 100644 --- a/be-rust-axum/testdata/golden/jenkins-build-stages.json +++ b/be-rust-axum/testdata/golden/jenkins-build-stages.json @@ -32,7 +32,7 @@ "status": "SUCCESS" }, { - "stage": "Deploy to OpenShift (app=rust-axum-iq-test)", + "stage": "Deploy to OpenShift (app.kubernetes.io/name=rust-axum-iq-test)", "status": "SUCCESS" }, { diff --git a/be-rust-axum/testdata/golden/sonar-scan.json b/be-rust-axum/testdata/golden/sonar-scan.json index fd32c1471..6cd7b7388 100644 --- a/be-rust-axum/testdata/golden/sonar-scan.json +++ b/be-rust-axum/testdata/golden/sonar-scan.json @@ -3,16 +3,27 @@ "name": "{{.ProjectID}}-{{.ComponentID}}", "isFavorite": false, "visibility": "public", + "branch": "master", "extensions": [], "qualityProfiles": [ { - "name": "Community Rust", + "name": "Sonar way", + "language": "docker", + "deleted": false + }, + { + "name": "Sonar way", "language": "rust", "deleted": false + }, + { + "name": "Sonar way", + "language": "yaml", + "deleted": false } ], "qualityGate": { - "name": "Sonar way", + "name": "{{.SonarQualityGate}}", "isDefault": true }, "breadcrumbs": [ diff --git a/be-rust-axum/testdata/steps.yml b/be-rust-axum/testdata/steps.yml index a668dc1b9..4d8261cc7 100644 --- a/be-rust-axum/testdata/steps.yml +++ b/be-rust-axum/testdata/steps.yml @@ -10,8 +10,7 @@ steps: jenkinsStages: golden/jenkins-build-stages.json sonarScan: golden/sonar-scan.json runAttachments: - - SCRR-{{.ProjectID}}-{{.ComponentID}}.docx - - SCRR-{{.ProjectID}}-{{.ComponentID}}.md + - sonarqube-report-{{.ProjectID}}-{{.ComponentID}}.pdf testResults: 1 openShiftResources: imageStreams: diff --git a/be-scala-play/testdata/golden/sonar-scan.json b/be-scala-play/testdata/golden/sonar-scan.json index 125379060..cd8a7a5cf 100644 --- a/be-scala-play/testdata/golden/sonar-scan.json +++ b/be-scala-play/testdata/golden/sonar-scan.json @@ -2,17 +2,38 @@ "key": "{{.ProjectID}}-{{.ComponentID}}", "name": "{{.ProjectID}}-{{.ComponentID}}", "isFavorite": false, + "branch": "master", "visibility": "public", "extensions": [], "qualityProfiles": [ + { + "name": "Sonar way", + "language": "css", + "deleted": false + }, + { + "name": "Sonar way", + "language": "docker", + "deleted": false + }, + { + "name": "{{.SonarQualityProfile}}", + "language": "js", + "deleted": false + }, { "name": "{{.SonarQualityProfile}}", "language": "scala", "deleted": false + }, + { + "name": "Sonar way", + "language": "web", + "deleted": false } ], "qualityGate": { - "name": "Sonar way", + "name": "{{.SonarQualityGate}}", "isDefault": true }, "breadcrumbs": [ diff --git a/be-scala-play/testdata/steps.yml b/be-scala-play/testdata/steps.yml index bac51e4f2..255d0a303 100644 --- a/be-scala-play/testdata/steps.yml +++ b/be-scala-play/testdata/steps.yml @@ -10,8 +10,7 @@ steps: jenkinsStages: golden/jenkins-build-stages.json sonarScan: golden/sonar-scan.json runAttachments: - - SCRR-{{.ProjectID}}-{{.ComponentID}}.docx - - SCRR-{{.ProjectID}}-{{.ComponentID}}.md + - sonarqube-report-{{.ProjectID}}-{{.ComponentID}}.pdf testResults: 3 openShiftResources: imageTags: diff --git a/be-typescript-express/testdata/golden/sonar-scan.json b/be-typescript-express/testdata/golden/sonar-scan.json index f1e75ed46..273ef8f26 100644 --- a/be-typescript-express/testdata/golden/sonar-scan.json +++ b/be-typescript-express/testdata/golden/sonar-scan.json @@ -2,9 +2,15 @@ "key": "{{.ProjectID}}-{{.ComponentID}}", "name": "{{.ProjectID}}-{{.ComponentID}}", "isFavorite": false, + "branch": "master", "visibility": "public", "extensions": [], "qualityProfiles": [ + { + "name": "Sonar way", + "language": "docker", + "deleted": false + }, { "name": "{{.SonarQualityProfile}}", "language": "ts", @@ -12,7 +18,7 @@ } ], "qualityGate": { - "name": "Sonar way", + "name": "{{.SonarQualityGate}}", "isDefault": true }, "breadcrumbs": [ diff --git a/be-typescript-express/testdata/steps.yml b/be-typescript-express/testdata/steps.yml index beb3cfad2..4dd389624 100644 --- a/be-typescript-express/testdata/steps.yml +++ b/be-typescript-express/testdata/steps.yml @@ -10,8 +10,7 @@ steps: jenkinsStages: golden/jenkins-build-stages.json sonarScan: golden/sonar-scan.json runAttachments: - - SCRR-{{.ProjectID}}-{{.ComponentID}}.docx - - SCRR-{{.ProjectID}}-{{.ComponentID}}.md + - sonarqube-report-{{.ProjectID}}-{{.ComponentID}}.pdf testResults: 1 openShiftResources: imageTags: diff --git a/docker-plain/testdata/functional/nginx-test.sh b/docker-plain/testdata/functional/nginx-test.sh new file mode 100755 index 000000000..c38bd367f --- /dev/null +++ b/docker-plain/testdata/functional/nginx-test.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +set -euo pipefail + +echo "=== Nginx Functional Tests ===" + +# Ensure we have the service URL +: "${WEB_SERVICE_URL:?WEB_SERVICE_URL environment variable is required}" + +echo "Testing nginx at: $WEB_SERVICE_URL" + +# Test 1: Health endpoint +echo "Test 1: Health endpoint..." +HEALTH_RESPONSE=$(curl -sS "${WEB_SERVICE_URL}/health") +if echo "$HEALTH_RESPONSE" | jq -e '.status == "UP"' >/dev/null; then + echo "✓ Health endpoint returns correct status" +else + echo "✗ Health endpoint failed" + echo "Response: $HEALTH_RESPONSE" + exit 1 +fi + +# Test 2: Default page returns 200 +echo "Test 2: Default page..." +STATUS=$(curl -sS -o /dev/null -w "%{http_code}" "${WEB_SERVICE_URL}/") +if [ "$STATUS" = "200" ]; then + echo "✓ Default page returns 200" +else + echo "✗ Default page returned $STATUS (expected 200)" + exit 1 +fi + +# Test 3: Verify nginx is serving HTML +echo "Test 3: Content verification..." +CONTENT=$(curl -sS "${WEB_SERVICE_URL}/") +if echo "$CONTENT" | grep -qi "nginx"; then + echo "✓ Page contains expected content" +else + echo "✗ Page does not contain expected content" + exit 1 +fi + +# Test 4: Verify 404 for non-existent path +echo "Test 4: 404 handling..." +STATUS=$(curl -sS -o /dev/null -w "%{http_code}" "${WEB_SERVICE_URL}/nonexistent") +if [ "$STATUS" = "404" ]; then + echo "✓ Returns 404 for non-existent paths" +else + echo "✗ Returned $STATUS (expected 404)" + exit 1 +fi + +echo "=== All Tests Passed ===" diff --git a/docker-plain/testdata/functional/performance-test.sh b/docker-plain/testdata/functional/performance-test.sh new file mode 100755 index 000000000..a9b3257b4 --- /dev/null +++ b/docker-plain/testdata/functional/performance-test.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +set -euo pipefail + +echo "=== Performance Tests ===" + +: "${WEB_SERVICE_URL:?WEB_SERVICE_URL environment variable is required}" + +echo "Running basic performance checks on: $WEB_SERVICE_URL" + +# Test 1: Response time check +echo "Test 1: Response time check..." +RESPONSE_TIME=$(curl -sS -o /dev/null -w "%{time_total}" "${WEB_SERVICE_URL}/health" || echo "999") +THRESHOLD=1.0 # 1 second (increased from 0.5s for more realistic threshold) + +# Simple decimal comparison without bc +if awk "BEGIN {exit !($RESPONSE_TIME < $THRESHOLD)}"; then + echo "✓ Response time: ${RESPONSE_TIME}s (under ${THRESHOLD}s threshold)" +else + echo "⚠ Response time: ${RESPONSE_TIME}s (above ${THRESHOLD}s threshold, but continuing)" +fi + +# Test 2: Concurrent requests +echo "Test 2: Concurrent request handling..." +CONCURRENT=5 +SUCCESS_COUNT=0 + +for i in $(seq 1 $CONCURRENT); do + if curl -sS -f "${WEB_SERVICE_URL}/health" >/dev/null 2>&1; then + SUCCESS_COUNT=$((SUCCESS_COUNT + 1)) + fi +done + +if [ "$SUCCESS_COUNT" -eq "$CONCURRENT" ]; then + echo "✓ All $CONCURRENT concurrent requests succeeded" +elif [ "$SUCCESS_COUNT" -gt 0 ]; then + echo "⚠ Only $SUCCESS_COUNT/$CONCURRENT concurrent requests succeeded (but continuing)" +else + echo "✗ All concurrent requests failed" + exit 1 +fi + +# Test 3: Connection stability +echo "Test 3: Connection stability..." +FAILED_COUNT=0 +for i in {1..10}; do + if ! curl -sS -f "${WEB_SERVICE_URL}/health" >/dev/null 2>&1; then + FAILED_COUNT=$((FAILED_COUNT + 1)) + echo "⚠ Request $i failed" + fi +done + +if [ "$FAILED_COUNT" -eq 0 ]; then + echo "✓ All 10 sequential requests succeeded" +elif [ "$FAILED_COUNT" -lt 3 ]; then + echo "⚠ $FAILED_COUNT/10 requests failed (acceptable for performance test)" +else + echo "✗ Too many failures: $FAILED_COUNT/10 requests failed" + exit 1 +fi + +echo "=== Performance Tests Complete ===" diff --git a/docker-plain/testdata/golden/jenkins-build-stages.json b/docker-plain/testdata/golden/jenkins-build-stages.json index 441b776ea..eb49385cc 100644 --- a/docker-plain/testdata/golden/jenkins-build-stages.json +++ b/docker-plain/testdata/golden/jenkins-build-stages.json @@ -3,12 +3,16 @@ "stage": "odsPipeline start", "status": "SUCCESS" }, + { + "stage": "SonarQube Analysis", + "status": "SUCCESS" + }, { "stage": "Build OpenShift Image", "status": "SUCCESS" }, { - "stage": "Deploy to OpenShift", + "stage": "Deploy to OpenShift (app.kubernetes.io/instance=docker-plain-test,app.kubernetes.io/name=docker-plain-test)", "status": "SUCCESS" }, { diff --git a/docker-plain/testdata/golden/jenkins-provision-stages.json b/docker-plain/testdata/golden/jenkins-provision-stages.json index e828790f0..9e9ae3cb8 100644 --- a/docker-plain/testdata/golden/jenkins-provision-stages.json +++ b/docker-plain/testdata/golden/jenkins-provision-stages.json @@ -11,10 +11,6 @@ "stage": "Copy files from quickstarter", "status": "SUCCESS" }, - { - "stage": "Create OpenShift resources", - "status": "SUCCESS" - }, { "stage": "Create Jenkinsfile", "status": "SUCCESS" diff --git a/docker-plain/testdata/hooks/cleanup-debug-logs.sh b/docker-plain/testdata/hooks/cleanup-debug-logs.sh new file mode 100755 index 000000000..36b0a4b9c --- /dev/null +++ b/docker-plain/testdata/hooks/cleanup-debug-logs.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -euo pipefail + +echo "=== Cleaning Up Debug Logs ===" + +# Example cleanup - remove temporary debug files if they exist +if [ -d "/tmp/debug-logs-${COMPONENT_ID}" ]; then + echo "Removing debug logs directory..." + rm -rf "/tmp/debug-logs-${COMPONENT_ID}" + echo "✓ Debug logs cleaned up" +else + echo "No debug logs to clean up" +fi + +echo "=== Cleanup Complete ===" diff --git a/docker-plain/testdata/hooks/validate-cluster.sh b/docker-plain/testdata/hooks/validate-cluster.sh new file mode 100755 index 000000000..0ee9f067e --- /dev/null +++ b/docker-plain/testdata/hooks/validate-cluster.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -euo pipefail + +echo "=== Pre-Provision Validation ===" +echo "Checking cluster connectivity..." + +# Verify we're logged in to the cluster +if ! oc whoami &>/dev/null; then + echo "ERROR: Not logged in to OpenShift cluster" + exit 1 +fi + +echo "✓ Logged in as: $(oc whoami)" +echo "✓ Current server: $(oc whoami --show-server)" + +# Check if we have basic permissions (check in current context or use --all-namespaces) +echo "Checking permissions..." +if oc auth can-i create deployments --all-namespaces &>/dev/null; then + echo "✓ Have cluster-level permissions to create deployments" +elif oc auth can-i create deployments &>/dev/null; then + echo "✓ Have permissions to create deployments in current context" +else + echo "⚠️ WARNING: Limited permissions detected - tests may fail" + echo " This is not necessarily a blocker, proceeding anyway..." +fi + +echo "=== Validation Complete ===" diff --git a/docker-plain/testdata/steps.yml b/docker-plain/testdata/steps.yml index b89e746f0..727cd2020 100644 --- a/docker-plain/testdata/steps.yml +++ b/docker-plain/testdata/steps.yml @@ -1,15 +1,137 @@ +# Enhanced test example for docker-plain quickstarter +# This demonstrates the new testing framework features: +# - Lifecycle hooks (beforeStep, afterStep) +# - Retry logic with transient error detection +# - Conditional step execution (skip, skipIf) +# - Structured test reporting (enable with EXPORT_TEST_REPORTS=true) + componentID: docker-plain-test + steps: -- type: provision - provisionParams: - verify: - jenkinsStages: golden/jenkins-provision-stages.json -- type: build - buildParams: - verify: - jenkinsStages: golden/jenkins-build-stages.json - openShiftResources: - imageStreams: - - "{{.ComponentID}}" - services: - - "{{.ComponentID}}" + # Step 1: Provision with pre-check hook + - type: provision + description: Provision docker-plain quickstarter with validation + beforeStep: hooks/validate-cluster.sh + provisionParams: + quickstarter: docker-plain + verify: + jenkinsStages: golden/jenkins-provision-stages.json + + # Step 2: Build and deploy with retry + - type: build + description: Build container image and deploy + retry: + attempts: 2 + delay: "10s" + onlyTransient: true + buildParams: + verify: + strategy: aggregate + jenkinsStages: golden/jenkins-build-stages.json + openShiftResources: + imageStreams: + - "{{.ComponentID}}" + services: + - "{{.ComponentID}}" + deployments: + - "{{.ComponentID}}" + runAttachments: + - sonarqube-report-{{.ProjectID}}-{{.ComponentID}}.pdf + + # Step 3: Wait for deployment to complete + - type: wait + description: Wait for nginx deployment to be ready + waitParams: + condition: deployment-complete + resource: "deployment/{{.ComponentID}}" + namespace: "{{.ProjectID}}-dev" + timeout: "5m" + interval: "5s" + + # Step 4: Wait for pod to be ready + - type: wait + description: Wait for pod to be running + waitParams: + condition: pod-ready + resource: "-l app.kubernetes.io/name={{.ComponentID}}" + namespace: "{{.ProjectID}}-dev" + timeout: "3m" + interval: "5s" + + # Step 5: Expose service for testing + - type: expose-service + description: Make nginx service accessible for tests + exposeServiceParams: + services: + - serviceName: "{{.ComponentID}}" + namespace: "{{.ProjectID}}-dev" + port: "8080" + + # Step 6: Test health endpoint with retry + - type: http + description: Verify health endpoint returns OK + retry: + attempts: 10 + delay: "2s" + onlyTransient: true + httpParams: + url: "http://{{.ComponentID}}.{{.ProjectID}}-dev.svc.cluster.local:8080/health" + method: GET + expectedStatus: 200 + assertions: + - path: "status" + equals: "UP" + + # Step 7: Test default nginx page + - type: http + description: Verify nginx serves default page + httpParams: + url: "http://{{.ComponentID}}.{{.ProjectID}}-dev.svc.cluster.local:8080/" + method: GET + expectedStatus: 200 + timeout: 30 + + # Step 8: Inspect deployment runtime + - type: inspect + description: Verify container configuration + inspectParams: + resource: "deployment/{{.ComponentID}}" + namespace: "{{.ProjectID}}-dev" + checks: + logs: + contains: + - "nginx" + notContains: + - "error" + - "fatal" + + + # Step 9: Run functional tests (optional, can be skipped in CI) + - type: run + description: Execute custom functional tests + skip: false # Set to true to skip this step + runParams: + file: functional/nginx-test.sh + services: + web: "{{.ComponentID}}" + + # Step 10: Performance check (conditional - skip in dev) + - type: run + description: Run performance tests + skipIf: "{{eq .Environment \"dev\"}}" + runParams: + file: functional/performance-test.sh + services: + web: "{{.ComponentID}}" + + # Step 11: Diagnostic inspection (always skipped, enable for debugging) + - type: inspect + description: Debug pod details (diagnostic only) + skip: true + afterStep: hooks/cleanup-debug-logs.sh + inspectParams: + resource: "deployment/{{.ComponentID}}" + namespace: "{{.ProjectID}}-dev" + checks: + logs: + contains: [] diff --git a/docs/modules/quickstarters/nav.adoc b/docs/modules/quickstarters/nav.adoc index 73963fe42..2ba427c30 100644 --- a/docs/modules/quickstarters/nav.adoc +++ b/docs/modules/quickstarters/nav.adoc @@ -31,3 +31,4 @@ *** xref:jenkins-agents:rust.adoc[Rust] *** xref:jenkins-agents:scala.adoc[Scala] ** xref:quickstarters:authoring-quickstarters.adoc[Authoring Quickstarters] +** xref:quickstarters:quickstarters-automated-tests.adoc[Quickstarters Automated Tests] diff --git a/docs/modules/quickstarters/pages/authoring-quickstarters.adoc b/docs/modules/quickstarters/pages/authoring-quickstarters.adoc index 9b5cba6ef..382ba84f3 100644 --- a/docs/modules/quickstarters/pages/authoring-quickstarters.adoc +++ b/docs/modules/quickstarters/pages/authoring-quickstarters.adoc @@ -33,3 +33,7 @@ It is highly recommended to use the xref:jenkins:agent-base.adoc[jenkins-agent-b . Reference the built image from your `Jenkinsfile` like this: `imageStreamTag: 'foo-cd/jenkins-agent-bar:latest'` If you want to contribute your Jenkins agent image to OpenDevStack, make sure to add documentation according to the https://github.com/opendevstack/ods-quickstarters/blob/master/docs/modules/jenkins-agents/pages/__JENKINS_agent_TEMPLATE_README.adoc[agent image template]. + +== Testing Quickstarters + +Every quickstarter must include a complete end-to-end (e2e) test that exercises the generated project; these tests are executed using the testing framework provided in the ods-core/test project. See xref:quickstarters:quickstarters-automated-tests.adoc[Quickstarter automated tests] for additional details and guidelines. diff --git a/docs/modules/quickstarters/pages/quickstarters-automated-tests.adoc b/docs/modules/quickstarters/pages/quickstarters-automated-tests.adoc new file mode 100644 index 000000000..4b00f93a3 --- /dev/null +++ b/docs/modules/quickstarters/pages/quickstarters-automated-tests.adoc @@ -0,0 +1,1230 @@ += Quickstarters Test Framework +:toc: macro +:toclevels: 3 + +toc::[] + +== Overview +Step-based tests driven by `testdata/steps.yml` to validate quickstarters. + +A test is a sequence of steps such as: + +* provision a quickstarter +* upload config/fixtures +* build/deploy +* wait for readiness +* expose service URLs +* call HTTP endpoints +* inspect logs/env/resources +* run an end-to-end shell script + +== Prerequisites + +* Logged in to the target OpenShift cluster (`oc whoami`). +* `oc`, `curl`, and `jq` available locally. +* Quickstarter repo contains a `testdata` folder with `steps.yml`. + +== How to Run + +*Wrapper (recommended):* + +[source,bash] +---- +cd ods-core/tests +./dev-test.sh +---- + +*Make:* + +[source,bash] +---- +cd ods-core/tests +make test-quickstarter QS= PROJECT_NAME= +---- + +*Go test directly:* + +[source,bash] +---- +cd ods-core/tests/quickstarter +go test -v -run TestQuickstarter -timeout 30m \ + -args -quickstarter= -project= -testPhase=devtest +---- + +== Logging and Output + +=== Structured Logging with Colors and Emojis + +The test framework uses link:https://github.com/charmbracelet/log[charmbracelet/log] to provide structured, readable logging with colors and emojis for better visibility and ease of following test execution. + +==== Output Features + +* *🚀 Sections*: Major test milestones are marked with visual section headers +* *📋 Sub-sections*: Logical groupings within a test use sub-section headers +* *▶️ Step Markers*: Each step execution is prefixed with the step number and type +* *✅ Success Messages*: Completed operations are marked with green checkmarks +* *❌ Error Messages*: Failed operations are marked with red X symbols +* *⚙️ Running Operations*: Ongoing operations show a gear symbol +* *⏳ Waiting Indicators*: Operations in waiting states show a hourglass +* *⚠️ Warnings*: Important warnings use the warning symbol + +==== Example Log Output + +[source,text] +---- +🚀 ╔═════════════════════════════════════════════════════════════╗ + 🚀 Starting Quickstarter Test Framework +🚀 ╚═════════════════════════════════════════════════════════════╝ + +🚀 ╔═════════════════════════════════════════════════════════════╗ + 🚀 Test Paths +🚀 ╚═════════════════════════════════════════════════════════════╝ + + • Found 2 quickstarter(s) to test: + • ./quickstarters/be-java-springboot + • ./quickstarters/fe-angular + +🚀 ╔═════════════════════════════════════════════════════════════╗ + 🚀 Testing Quickstarter: be-java-springboot +🚀 ╚═════════════════════════════════════════════════════════════╝ + +┌───────────────────────────────────────────────────────────────── + 📋 Component: myapp +┌───────────────────────────────────────────────────────────────── + + • Total steps to execute: 3 + +▶️ Step 1/3 [provision]: Provision quickstarter + +⚙️ Running: Provision for test-myapp +✅ Success: BitBucket repo created/updated + +⏳ Waiting: Jenkins pipeline execution +✅ Success: Build triggered with name jenkins-1234 + +▶️ Step 2/3 [build]: Trigger build pipeline + +... +---- + +==== Environment Variable Support + +You can control logging verbosity by setting the `LOG_LEVEL` environment variable: + +[source,bash] +---- +LOG_LEVEL=debug go test -v -run TestQuickstarter -timeout 30m \ + -args -quickstarter= -project= +---- + +==== Color Legend + +* *Cyan* (#00d7ff): Information messages +* *Yellow* (#ffaf00): Warning messages +* *Red* (#ff005f): Error messages +* *Green* (#00ff00): Success indicators + +== Templates and Variables + +All string fields in `steps.yml` support Go-template rendering. + +*Common template variables:* + +* `{{.ProjectID}}`: the project passed to the test +* `{{.ComponentID}}`: the component passed to the test (or overridden per step via `componentID`) + +*Common environment variables passed to `run` scripts:* + +* `PROJECT_ID`, `COMPONENT_ID`, `NAMESPACE` (defaults to `-dev`) +* `ODS_NAMESPACE`, `ODS_GIT_REF`, `ODS_IMAGE_TAG` (when available) +* `{ALIAS}_SERVICE_URL` for each entry under `runParams.services` (e.g. `API_SERVICE_URL`) + +== Testdata Layout + +Typical structure in a quickstarter repo: + +[source,text] +---- +testdata/ +├── steps.yml +├── golden/ +│ ├── jenkins-provision-stages.json +│ ├── jenkins-build-stages.json +│ └── sonar-scan.json +└── functional/ + ├── api/ + │ └── health-response.json + └── integration/ + └── e2e_test.sh +---- + +*Key principles:* + +* Use templates and internal service DNS in `steps.yml` URLs. +* Avoid hardcoding localhost and manual port-forwarding. +* For `run` steps that need URLs, declare services in `runParams.services` and consume `{ALIAS}_SERVICE_URL`. + +== Step Types (How to Use) + +The YAML file is a list under `steps:`: + +[source,yaml] +---- +steps: + - type: + description: Optional human-friendly description + componentID: Optional override for this step + Params: + ... +---- + +=== provision + +Provision via ODS; optionally verify Jenkins provision stages. + +*Minimal example:* + +[source,yaml] +---- +- type: provision + provisionParams: + quickstarter: be-python-flask +---- + +*With common options:* + +[source,yaml] +---- +- type: provision + description: Provision quickstarter + provisionParams: + quickstarter: be-python-flask + pipeline: "{{.ProjectID}}-{{.ComponentID}}" + branch: "master" + env: + - key: SOME_PARAM + value: "some-value" + verify: + strategy: fail-fast + jenkinsStages: golden/jenkins-provision-stages.json +---- + +=== build + +Build/deploy; optionally verify Jenkins stages, Sonar scan, test results, and OpenShift resources. + +*Minimal example:* + +[source,yaml] +---- +- type: build + buildParams: {} +---- + +*With verification:* + +[source,yaml] +---- +- type: build + description: Build and deploy + buildParams: + verify: + strategy: aggregate + jenkinsStages: golden/jenkins-build-stages.json + sonarScan: golden/sonar-scan.json + runAttachments: ["metadata.json"] + testResults: 5 + openShiftResources: + deployments: ["{{.ComponentID}}"] + services: ["{{.ComponentID}}", "{{.ComponentID}}-backend"] + routes: ["{{.ComponentID}}"] +---- + +=== upload + +Add a file into the created repository. + +[source,yaml] +---- +- type: upload + description: Upload config into repo + uploadParams: + file: fixtures/app-config.json + filename: config/app-config.json + render: true +---- + +*Notes:* + +* `file` is relative to `testdata/`. +* `filename` is the destination path inside the provisioned repository. +* `render: true` applies templating to the file contents. + +=== wait + +Poll for readiness/conditions. + +*Supported conditions:* + +* `pod-ready` (resource: selector like `-l app=...` or a pod name) +* `deployment-complete` (resource: `deployment/` or `dc/`) +* `job-complete` (resource: `job/`) +* `route-accessible` (resource: `route/`) +* `http-accessible` (url: `...`) +* `log-contains` (resource: `pod/`, `deployment/`, `dc/`; message: `...`) + +*Examples:* + +*Deployment rollout:* + +[source,yaml] +---- +- type: wait + waitParams: + condition: deployment-complete + resource: "deployment/{{.ComponentID}}" + timeout: "10m" + interval: "5s" +---- + +*Pod readiness by label:* + +[source,yaml] +---- +- type: wait + waitParams: + condition: pod-ready + resource: "-l app={{.ProjectID}}-{{.ComponentID}}" + timeout: "5m" + interval: "5s" +---- + +*Log message appears:* + +[source,yaml] +---- +- type: wait + waitParams: + condition: log-contains + resource: "deployment/{{.ComponentID}}" + message: "Server listening" + timeout: "5m" + interval: "10s" +---- + +*Route exists and is reachable:* + +[source,yaml] +---- +- type: wait + waitParams: + condition: route-accessible + resource: "route/{{.ComponentID}}" + timeout: "5m" + interval: "5s" +---- + +*Arbitrary URL becomes reachable:* + +[source,yaml] +---- +- type: wait + waitParams: + condition: http-accessible + url: "http://{{.ComponentID}}.{{.ProjectID}}-dev.svc.cluster.local:8080/health" + timeout: "5m" + interval: "2s" +---- + +*Job completes:* + +[source,yaml] +---- +- type: wait + waitParams: + condition: job-complete + resource: "job/{{.ProjectID}}-{{.ComponentID}}-migration" + timeout: "10m" + interval: "5s" +---- + +=== http + +Call endpoints with status/body/assertions and optional retries. + +*Status + golden JSON body:* + +[source,yaml] +---- +- type: http + description: Health endpoint returns expected JSON + httpParams: + url: "http://{{.ComponentID}}.{{.ProjectID}}-dev.svc.cluster.local:8080/health" + method: GET + headers: + Accept: application/json + expectedStatus: 200 + expectedBody: functional/api/health-response.json + retry: + attempts: 10 + delay: "2s" +---- + +*Assertions (JSONPath via `path`):* + +[source,yaml] +---- +- type: http + description: Assert JSON fields + httpParams: + url: "http://{{.ComponentID}}.{{.ProjectID}}-dev.svc.cluster.local:8080/health" + expectedStatus: 200 + assertions: + - path: "status" + equals: "ok" + - path: "version" + exists: true + - path: "message" + contains: "ready" + - path: "commit" + matches: "^[a-f0-9]{7,}$" +---- + +*POST with JSON body and custom timeout:* + +[source,yaml] +---- +- type: http + description: Create resource + httpParams: + url: "http://{{.ComponentID}}.{{.ProjectID}}-dev.svc.cluster.local:8080/api/v1/items" + method: POST + headers: + Content-Type: application/json + Accept: application/json + body: '{"name":"example"}' + expectedStatus: 201 + timeout: 60 +---- + +=== inspect + +Check logs/env/resources for a resource. + +[source,yaml] +---- +- type: inspect + description: Verify runtime signals + inspectParams: + resource: "deployment/{{.ComponentID}}" + namespace: "{{.ProjectID}}-dev" + checks: + logs: + contains: ["Server listening on :8080"] + notContains: ["Traceback", "panic:"] + matches: ["Listening on.*8080"] + env: + APP_ENV: "dev" + ODS_PROJECT: "{{.ProjectID}}" + resources: + limits: + cpu: "500m" + memory: "512Mi" + requests: + cpu: "100m" + memory: "128Mi" +---- + +=== expose-service + +Resolve stable URLs for one or more services and make them available to later `http` / `run` steps. + +[source,yaml] +---- +- type: expose-service + description: Expose services for local/Jenkins runs + exposeServiceParams: + services: + - serviceName: "{{.ComponentID}}" + namespace: "{{.ProjectID}}-dev" + port: "8080" + - serviceName: "{{.ComponentID}}-backend" + # namespace defaults to "-dev" if omitted + # port defaults to 8080 if omitted +---- + +*Notes:* + +* Use one entry per Kubernetes/OpenShift Service. +* If you use `runParams.services`, ensure those service names are exposed here first. + +=== run + +Execute a shell script. If `runParams.services` is set, the script receives one env var per alias: `{ALIAS}_SERVICE_URL`. + +[source,yaml] +---- +- type: run + description: End-to-end tests + runParams: + file: functional/integration/e2e_test.sh + services: + api: "{{.ComponentID}}" + backend: "{{.ComponentID}}-backend" +---- + +*Minimal script pattern:* + +[source,bash] +---- +#!/usr/bin/env bash +set -euo pipefail + +: "${API_SERVICE_URL:?missing API_SERVICE_URL}" +: "${BACKEND_SERVICE_URL:?missing BACKEND_SERVICE_URL}" + +curl -fsS "$API_SERVICE_URL/health" | jq -e '.status == "ok"' >/dev/null +curl -fsS "$BACKEND_SERVICE_URL/metrics" >/dev/null +---- + +== Advanced Features + +=== Test Lifecycle Hooks + +Each step can execute shell scripts before and after execution. This is useful for setup, cleanup, or custom validation logic. + +==== beforeStep Hook + +Executes a script before the main step. Useful for setup operations: + +[source,yaml] +---- +- type: build + description: Build and deploy with custom setup + beforeStep: "hooks/pre-build-setup.sh" + buildParams: + verify: + jenkinsStages: golden/jenkins-build-stages.json +---- + +Example `testdata/hooks/pre-build-setup.sh`: + +[source,bash] +---- +#!/usr/bin/env bash +set -euo pipefail + +echo "Setting up build environment..." +export CUSTOM_BUILD_VAR="custom-value" +# Additional setup logic +---- + +==== afterStep Hook + +Executes a script after the main step, even if the step fails. Useful for cleanup: + +[source,yaml] +---- +- type: http + description: Call API endpoint + afterStep: "hooks/post-http-cleanup.sh" + httpParams: + url: "http://{{.ComponentID}}.{{.ProjectID}}-dev.svc.cluster.local:8080/health" + expectedStatus: 200 +---- + +Example `testdata/hooks/post-http-cleanup.sh`: + +[source,bash] +---- +#!/usr/bin/env bash +set -euo pipefail + +echo "Cleaning up HTTP test artifacts..." +# Cleanup logic that always runs +---- + +*Notes:* + +* Hooks are executed relative to the `testdata/` directory. +* Hooks receive environment variables from template data. +* Hook failures in `beforeStep` will prevent the main step from executing. +* Hook failures in `afterStep` are logged but don't fail the test (useful for cleanup). +* Hooks support full bash scripting, including conditional logic. + +=== Step Execution Control + +==== Skip Steps Conditionally + +You can skip steps based on static conditions or template expressions: + +*Static Skip:* + +[source,yaml] +---- +- type: inspect + description: Optional diagnostic step (skipped in CI) + skip: true + inspectParams: + resource: "deployment/{{.ComponentID}}" +---- + +*Conditional Skip (Template Expression):* + +[source,yaml] +---- +- type: build + description: Only build in non-production environments + skipIf: "{{eq .Environment \"production\"}}" + buildParams: {} +---- + +Template variables can be any standard Go template expression. Examples: + +[source,yaml] +---- +- skipIf: "{{.IsProduction}}" # Boolean variable +- skipIf: "{{eq .Environment \"ci\"}}" # Environment comparison +- skipIf: "{{gt .Replicas 1}}" # Numeric comparison +---- + +==== Step-Level Timeout + +Override the default timeout for individual steps: + +[source,yaml] +---- +- type: wait + description: Wait for slow deployment + timeout: 900 # seconds (overrides default) + waitParams: + condition: deployment-complete + resource: "deployment/{{.ComponentID}}" +---- + +=== Retry Logic + +Automatically retry steps on failure with configurable behavior: + +==== Basic Retry + +[source,yaml] +---- +- type: http + description: API call with retry + retry: + attempts: 5 # Retry up to 5 times + delay: "2s" # Wait 2 seconds between attempts + httpParams: + url: "http://{{.ComponentID}}.{{.ProjectID}}-dev.svc.cluster.local:8080/health" + expectedStatus: 200 +---- + +==== Smart Transient Error Retry + +Only retry on transient errors (timeouts, connection issues): + +[source,yaml] +---- +- type: wait + description: Wait with smart retry + retry: + attempts: 10 + delay: "1s" + onlyTransient: true # Skip retries for permanent errors + waitParams: + condition: http-accessible + url: "http://{{.ComponentID}}.{{.ProjectID}}-dev.svc.cluster.local:8080" + timeout: "5m" +---- + +*Transient Errors:* The framework automatically detects: + +* Connection timeouts +* Connection refused +* Temporary unavailability +* EOF and broken pipes +* I/O timeouts + +=== Test Reporting + +The test framework generates structured reports with execution metrics and can export to multiple formats. + +==== Automatic Report Generation + +Test reports are automatically generated and printed to console: + +[source,text] +---- +Test Report: be-java-springboot + Total Steps: 5 + Passed: 5 + Failed: 0 + Skipped: 0 + Success Rate: 100.00% + Total Duration: 2m30s + Avg Per Step: 30s +---- + +==== Export Reports to File + +Enable report export by setting an environment variable: + +[source,bash] +---- +EXPORT_TEST_REPORTS=true go test -v -run TestQuickstarter -timeout 30m \ + -args -quickstarter= -project= +---- + +This generates a `test-report-.json` file with detailed metrics: + +[source,json] +---- +{ + "startTime": "2024-01-21T10:30:00Z", + "endTime": "2024-01-21T10:32:30Z", + "totalDuration": 150000000000, + "quickstarterID": "be-java-springboot", + "steps": [ + { + "index": 0, + "type": "provision", + "description": "Provision quickstarter", + "startTime": "2024-01-21T10:30:00Z", + "endTime": "2024-01-21T10:30:30Z", + "duration": 30000000000, + "status": "passed", + "error": null, + "context": {} + } + ], + "summary": { + "totalSteps": 5, + "passedSteps": 5, + "failedSteps": 0, + "skippedSteps": 0, + "successRate": 100.0, + "averageDuration": 30000000000 + } +} +---- + +==== Report Contents + +Each report includes: + +* *Execution Timeline:* Start/end times and duration for each step +* *Step Status:* Passed, failed, or skipped +* *Error Details:* Full error messages for failed steps +* *Context Information:* Pod logs, events, and environment at time of failure +* *Aggregate Statistics:* Pass rate, timing averages, counts by status + +==== CI/CD Integration + +Reports can be processed by CI/CD systems for: + +* Trend analysis (run-to-run metrics) +* Performance regression detection +* Test flakiness tracking +* Automated failure notifications + +== Complete Example (steps.yml + run script) + +Example `testdata/steps.yml` using all step types with advanced features: + +[source,yaml] +---- +steps: + - type: provision + description: Provision quickstarter + beforeStep: "hooks/pre-provision.sh" + provisionParams: + quickstarter: be-python-flask + branch: master + verify: + jenkinsStages: golden/jenkins-provision-stages.json + + - type: upload + description: Add runtime config + uploadParams: + file: fixtures/app-config.json + filename: config/app-config.json + render: true + + - type: build + description: Build and deploy + retry: + attempts: 2 + delay: "5s" + buildParams: + verify: + jenkinsStages: golden/jenkins-build-stages.json + sonarScan: golden/sonar-scan.json + testResults: 1 + openShiftResources: + deployments: ["{{.ComponentID}}"] + services: ["{{.ComponentID}}", "{{.ComponentID}}-backend"] + + - type: wait + description: Wait for rollout + waitParams: + condition: deployment-complete + resource: "deployment/{{.ComponentID}}" + timeout: 10m + interval: 5s + + - type: expose-service + description: Resolve external/local URLs for tests + exposeServiceParams: + services: + - serviceName: "{{.ComponentID}}" + port: "8080" + - serviceName: "{{.ComponentID}}-backend" + port: "8080" + + - type: http + description: Healthcheck with retry and assertions + retry: + attempts: 10 + delay: 2s + onlyTransient: true + httpParams: + url: "http://{{.ComponentID}}.{{.ProjectID}}-dev.svc.cluster.local:8080/health" + expectedStatus: 200 + assertions: + - path: "status" + equals: "ok" + + - type: inspect + description: Verify logs and env + inspectParams: + resource: "deployment/{{.ComponentID}}" + checks: + logs: + notContains: ["Traceback", "panic:"] + env: + ODS_PROJECT: "{{.ProjectID}}" + + - type: run + description: End-to-end shell test + runParams: + file: functional/integration/e2e_test.sh + services: + api: "{{.ComponentID}}" + backend: "{{.ComponentID}}-backend" + + # Optional: diagnostic step (skipped by default in CI) + - type: inspect + description: Diagnostic pod inspection (optional) + skip: true + inspectParams: + resource: "deployment/{{.ComponentID}}" +---- + +Example `testdata/functional/integration/e2e_test.sh`: + +[source,bash] +---- +#!/usr/bin/env bash +set -euo pipefail + +: "${PROJECT_ID:?missing PROJECT_ID}" +: "${COMPONENT_ID:?missing COMPONENT_ID}" +: "${NAMESPACE:?missing NAMESPACE}" + +: "${API_SERVICE_URL:?missing API_SERVICE_URL}" +: "${BACKEND_SERVICE_URL:?missing BACKEND_SERVICE_URL}" + +echo "Project: $PROJECT_ID" +echo "Component: $COMPONENT_ID" +echo "Namespace: $NAMESPACE" +echo "API: $API_SERVICE_URL" +echo "Backend: $BACKEND_SERVICE_URL" + +curl -fsS "$API_SERVICE_URL/health" | jq -e '.status == "ok"' >/dev/null +curl -fsS "$API_SERVICE_URL/api/v1/info" | jq -e '.name != null' >/dev/null +curl -fsS "$BACKEND_SERVICE_URL/metrics" >/dev/null + +echo "OK" +---- + +Example `testdata/hooks/pre-provision.sh`: + +[source,bash] +---- +#!/usr/bin/env bash +set -euo pipefail + +echo "Pre-provisioning checks..." +# Verify cluster connectivity +oc whoami > /dev/null || exit 1 +# Any custom setup logic +echo "Pre-provisioning checks passed" +---- + +== Service URL Resolution + +Priority: + +. Route exists → use route URL (https/http). +. In-cluster (Jenkins) → use service DNS. +. Local → automatic `oc port-forward` on 8000–8009 with cleanup and reuse. + +== Migration Guidance + +* Replace hardcoded localhost URLs with templated service DNS in `steps.yml`. +* Add an `expose-service` step for every service you need to access from local runs. +* Remove manual port-forwarding from scripts. +* In `run` steps, declare services under `runParams.services` and consume `{ALIAS}_SERVICE_URL`. +* Validate with `./dev-test.sh `. + +== Developing Custom Step Types + +The quickstarter test framework uses a *registry pattern* that makes it easy to add new step types without modifying the core test execution logic. This section explains how to implement and register custom step types. + +=== Architecture Overview + +The framework consists of: + +* *StepHandler Interface*: Defines the contract all step types must implement +* *StepRegistry*: Maps step type names to their handler implementations +* *ExecutionParams*: Consolidates all context needed for step execution +* *Handler Implementations*: Individual step type logic (upload, build, http, etc.) + +=== Step 1: Define the Step Type Constant + +Add your new step type constant to `steps/types.go`: + +[source,go] +---- +const ( + StepTypeUpload = "upload" + StepTypeRun = "run" + // ... existing types ... + StepTypeMyCustom = "my-custom" // Add your new type here +) +---- + +=== Step 2: Add Step Parameters to TestStep + +In `steps/types.go`, add a parameters struct for your step if needed: + +[source,go] +---- +// MyCustomParams defines parameters for the my-custom step type +type MyCustomParams struct { + // Add your custom fields here + Target string `json:"target"` + Options []string `json:"options"` + RetryCount int `json:"retryCount"` +} +---- + +Then add a field to the `TestStep` struct: + +[source,go] +---- +type TestStep struct { + Type string `json:"type"` + Description string `json:"description"` + // ... existing params ... + MyCustomParams *MyCustomParams `json:"myCustomParams,omitempty"` +} +---- + +=== Step 3: Implement the Execution Logic + +Create a new file `steps/my_custom.go` with your step implementation: + +[source,go] +---- +package steps + +import ( + "fmt" + "testing" +) + +// ExecuteMyCustom handles the my-custom step type. +// This function contains the actual logic for your step. +func ExecuteMyCustom(t *testing.T, step TestStep, testdataPath string, + tmplData TemplateData, projectName string) { + + // Validate parameters + if step.MyCustomParams == nil { + t.Fatalf("Missing my-custom parameters") + } + + params := step.MyCustomParams + + // Implement your step logic here + fmt.Printf("Executing custom step with target: %s\n", params.Target) + + // Example: Run some operation + for _, option := range params.Options { + fmt.Printf("Processing option: %s\n", option) + // Your custom logic here + } + + // Use template data for dynamic values + renderedTarget := RenderTemplate(t, params.Target, tmplData) + fmt.Printf("Rendered target: %s\n", renderedTarget) + + // Fail the test if something goes wrong + if someCondition { + t.Fatalf("Custom step failed: %v", err) + } +} +---- + +=== Step 4: Create a Handler Adapter + +In `steps/registry.go`, add a handler struct that implements the `StepHandler` interface: + +[source,go] +---- +// MyCustomHandler implements the handler for my-custom steps. +type MyCustomHandler struct{} + +func (h *MyCustomHandler) Execute(t *testing.T, step *TestStep, params *ExecutionParams) error { + ExecuteMyCustom(t, *step, params.TestdataPath, params.TmplData, params.ProjectName) + return nil +} +---- + +*Note*: The handler adapter maps between the `StepHandler` interface (which receives `ExecutionParams`) and your specific execution function signature. + +=== Step 5: Register the Handler + +In `steps/registry.go`, add your handler to the `registerDefaultHandlers()` function: + +[source,go] +---- +func registerDefaultHandlers() { + defaultRegistry.Register(StepTypeUpload, &UploadHandler{}) + defaultRegistry.Register(StepTypeRun, &RunHandler{}) + // ... existing registrations ... + defaultRegistry.Register(StepTypeMyCustom, &MyCustomHandler{}) // Add this line +} +---- + +=== Step 6: Add Documentation + +Create `docs/steps/my-custom.md` documenting your new step type: + +[source,markdown] +---- +# my-custom Step + +## Purpose +Brief description of what this step does. + +## Parameters + +- `target` (string, required): Description of target parameter +- `options` (array, optional): Description of options +- `retryCount` (int, optional): Number of retry attempts + +## Example + +\```yaml +- type: my-custom + description: "Execute custom operation" + myCustomParams: + target: "{{.ComponentID}}" + options: + - "verbose" + - "debug" + retryCount: 3 +\``` + +## Common Use Cases +- Use case 1 +- Use case 2 +---- + +Update `docs/steps.md` to include your new step in the overview table. + +=== Step 7: Write Unit Tests + +Create `steps/my_custom_test.go` to test your handler: + +[source,go] +---- +package steps + +import ( + "testing" +) + +func TestMyCustomHandler(t *testing.T) { + registry := DefaultRegistry() + + handler, err := registry.Get(StepTypeMyCustom) + if err != nil { + t.Fatalf("Expected my-custom handler to be registered: %v", err) + } + + if handler == nil { + t.Fatal("Handler should not be nil") + } + + // Test execution (may need mocking for complex steps) + step := &TestStep{ + Type: StepTypeMyCustom, + MyCustomParams: &MyCustomParams{ + Target: "test-target", + Options: []string{"opt1"}, + }, + } + + params := &ExecutionParams{ + TestdataPath: "/tmp/testdata", + TmplData: TemplateData{}, + ProjectName: "test-project", + } + + // Test that handler executes without panic + // (Actual behavior testing may require more setup) +} +---- + +=== Step 8: Use Your New Step in Tests + +Add your step to any `testdata/steps.yml`: + +[source,yaml] +---- +componentID: my-component + +steps: + - type: provision + # ... provision step ... + + - type: my-custom + description: "Run my custom operation" + myCustomParams: + target: "{{.ComponentID}}-resource" + options: + - "enable-feature-x" + - "debug-mode" + retryCount: 2 +---- + +=== Best Practices for Custom Steps + +1. **Keep Steps Focused**: Each step should do one thing well +2. **Use Template Data**: Leverage `{{.Variable}}` syntax for dynamic values +3. **Fail Fast**: Use `t.Fatalf()` for unrecoverable errors +4. **Add Logging**: Use `fmt.Printf()` or the logger package for visibility +5. **Parameter Validation**: Always validate required parameters at the start +6. **Error Context**: Provide clear error messages with context +7. **Idempotency**: Consider making steps idempotent when possible +8. **Documentation**: Document all parameters and provide examples + +=== Example: Complete Custom Step + +Here's a complete example of a custom step that validates database connectivity: + +[source,go] +---- +// steps/database.go +package steps + +import ( + "fmt" + "testing" + "database/sql" + _ "github.com/lib/pq" +) + +type DatabaseParams struct { + ConnectionString string `json:"connectionString"` + Query string `json:"query"` + ExpectedRows int `json:"expectedRows"` +} + +func ExecuteDatabase(t *testing.T, step TestStep, testdataPath string, + tmplData TemplateData, projectName string) { + if step.DatabaseParams == nil { + t.Fatalf("Missing database parameters") + } + + params := step.DatabaseParams + connStr := RenderTemplate(t, params.ConnectionString, tmplData) + query := RenderTemplate(t, params.Query, tmplData) + + db, err := sql.Open("postgres", connStr) + if err != nil { + t.Fatalf("Failed to connect to database: %v", err) + } + defer db.Close() + + rows, err := db.Query(query) + if err != nil { + t.Fatalf("Query failed: %v", err) + } + defer rows.Close() + + count := 0 + for rows.Next() { + count++ + } + + if count != params.ExpectedRows { + t.Fatalf("Expected %d rows, got %d", params.ExpectedRows, count) + } + + fmt.Printf("✅ Database validation passed: %d rows\n", count) +} + +// steps/registry.go - add this handler +type DatabaseHandler struct{} + +func (h *DatabaseHandler) Execute(t *testing.T, step *TestStep, params *ExecutionParams) error { + ExecuteDatabase(t, *step, params.TestdataPath, params.TmplData, params.ProjectName) + return nil +} + +// In registerDefaultHandlers(): +// defaultRegistry.Register("database", &DatabaseHandler{}) +---- + +=== Advanced: Handler Parameters + +If your step needs additional context beyond `ExecutionParams`, you can: + +. *Add to ExecutionParams*: Extend the struct if the parameter is commonly needed +. *Use TestStep Fields*: Store step-specific data in your params struct +. *Access Global Config*: Use the `config` map in ExecutionParams + +=== Registry Pattern Benefits + +* ✅ *No Switch Statements*: Add steps without modifying test runner code +* ✅ *Plugin Architecture*: External packages can register custom steps +* ✅ *Testability*: Individual handlers can be unit tested in isolation +* ✅ *Type Safety*: Go compiler ensures all handlers implement the interface +* ✅ *Maintainability*: Step logic is cleanly separated and organized + +=== Troubleshooting Custom Steps + +*Handler not found*:: Ensure you've registered it in `registerDefaultHandlers()` +*Parameters nil*:: Check YAML structure and JSON tags match +*Template errors*:: Verify template syntax and that variables exist in `TemplateData` +*Import cycles*:: Keep step implementations in the `steps` package +*Test failures*:: Check parameter validation and error handling + +== Troubleshooting + +* Login: `oc whoami`. +* Port-forwards: `ps aux | grep "oc port-forward" | grep -v grep`; kill with `pkill -f "oc port-forward"`. +* Ports in use: `lsof -i :8000-8009`. +* Resources: `oc get svc -n -dev`, `oc get pods -n -dev`. +* Add/extend `wait` steps if endpoints are not ready. + +== Best Practices + +* Add `wait` before `http`/`run` to avoid races. +* Use retries for early endpoints. +* Keep scripts small; fail fast when expected `{ALIAS}_SERVICE_URL` is missing. +* Prefer templates for names/namespaces; avoid hardcoded hostnames. diff --git a/ds-jupyter-lab/testdata/golden/jenkins-build-stages.json b/ds-jupyter-lab/testdata/golden/jenkins-build-stages.json index 441b776ea..0caab02e1 100644 --- a/ds-jupyter-lab/testdata/golden/jenkins-build-stages.json +++ b/ds-jupyter-lab/testdata/golden/jenkins-build-stages.json @@ -3,10 +3,18 @@ "stage": "odsPipeline start", "status": "SUCCESS" }, + { + "stage": "SonarQube Analysis", + "status": "SUCCESS" + }, { "stage": "Build OpenShift Image", "status": "SUCCESS" }, + { + "stage": "Build OpenShift Image (jupyter-oauth)", + "status": "SUCCESS" + }, { "stage": "Deploy to OpenShift", "status": "SUCCESS" diff --git a/ds-jupyter-lab/testdata/steps.yml b/ds-jupyter-lab/testdata/steps.yml index 533a93731..5ca3cb5a7 100644 --- a/ds-jupyter-lab/testdata/steps.yml +++ b/ds-jupyter-lab/testdata/steps.yml @@ -16,7 +16,9 @@ steps: - "{{.ComponentID}}" deploymentConfigs: - "{{.ComponentID}}" - - "{{.ComponentID}}-auth-proxy" services: + - "{{.ComponentID}}-oauth" + routes: - "{{.ComponentID}}" - - "{{.ComponentID}}-auth-proxy" + runAttachments: + - sonarqube-report-{{.ProjectID}}-{{.ComponentID}}.pdf diff --git a/ds-rshiny/testdata/golden/jenkins-build-stages.json b/ds-rshiny/testdata/golden/jenkins-build-stages.json index 441b776ea..464541b7e 100644 --- a/ds-rshiny/testdata/golden/jenkins-build-stages.json +++ b/ds-rshiny/testdata/golden/jenkins-build-stages.json @@ -3,10 +3,18 @@ "stage": "odsPipeline start", "status": "SUCCESS" }, + { + "stage": "SonarQube Analysis", + "status": "SUCCESS" + }, { "stage": "Build OpenShift Image", "status": "SUCCESS" }, + { + "stage": "Build OpenShift Image (rshiny-oauth)", + "status": "SUCCESS" + }, { "stage": "Deploy to OpenShift", "status": "SUCCESS" diff --git a/ds-rshiny/testdata/steps.yml b/ds-rshiny/testdata/steps.yml index a46c5ff13..c2d7008ea 100644 --- a/ds-rshiny/testdata/steps.yml +++ b/ds-rshiny/testdata/steps.yml @@ -8,6 +8,8 @@ steps: buildParams: verify: jenkinsStages: golden/jenkins-build-stages.json + runAttachments: + - sonarqube-report-{{.ProjectID}}-{{.ComponentID}}.pdf openShiftResources: imageTags: - name: "{{.ComponentID}}" diff --git a/ds-streamlit/testdata/golden/sonar-scan.json b/ds-streamlit/testdata/golden/sonar-scan.json index 559ca033b..e0e03d862 100644 --- a/ds-streamlit/testdata/golden/sonar-scan.json +++ b/ds-streamlit/testdata/golden/sonar-scan.json @@ -3,8 +3,19 @@ "name": "{{.ProjectID}}-{{.ComponentID}}", "isFavorite": false, "visibility": "public", + "branch": "master", "extensions": [], "qualityProfiles": [ + { + "name": "Sonar way", + "language": "docker", + "deleted": false + }, + { + "name": "{{.SonarQualityProfile}}", + "language": "js", + "deleted": false + }, { "name": "{{.SonarQualityProfile}}", "language": "py", @@ -12,7 +23,7 @@ } ], "qualityGate": { - "name": "Sonar way", + "name": "{{.SonarQualityGate}}", "isDefault": true }, "breadcrumbs": [ diff --git a/ds-streamlit/testdata/steps.yml b/ds-streamlit/testdata/steps.yml index c5b100cd0..ff03c6b36 100644 --- a/ds-streamlit/testdata/steps.yml +++ b/ds-streamlit/testdata/steps.yml @@ -10,8 +10,7 @@ steps: jenkinsStages: golden/jenkins-build-stages.json sonarScan: golden/sonar-scan.json runAttachments: - - SCRR-{{.ProjectID}}-{{.ComponentID}}.docx - - SCRR-{{.ProjectID}}-{{.ComponentID}}.md + - sonarqube-report-{{.ProjectID}}-{{.ComponentID}}.pdf openShiftResources: imageTags: - name: "{{.ComponentID}}" diff --git a/e2e-cypress/testdata/golden/jenkins-build-stages.json b/e2e-cypress/testdata/golden/jenkins-build-stages.json index 43eeb9676..efd43c4f7 100644 --- a/e2e-cypress/testdata/golden/jenkins-build-stages.json +++ b/e2e-cypress/testdata/golden/jenkins-build-stages.json @@ -12,15 +12,23 @@ "status": "SUCCESS" }, { - "stage": "Integration Test", + "stage": "Functional Tests", "status": "SUCCESS" }, { "stage": "SonarQube Analysis", "status": "SUCCESS" }, + { + "stage": "Upload to Nexus", + "status": "SUCCESS" + }, + { + "stage": "Upload to Nexus", + "status": "SUCCESS" + }, { "stage": "odsPipeline finished", "status": "SUCCESS" } -] +] \ No newline at end of file diff --git a/e2e-cypress/testdata/golden/sonar-scan.json b/e2e-cypress/testdata/golden/sonar-scan.json index 4754f680c..471e7c2b7 100644 --- a/e2e-cypress/testdata/golden/sonar-scan.json +++ b/e2e-cypress/testdata/golden/sonar-scan.json @@ -3,13 +3,9 @@ "name": "{{.ProjectID}}-{{.ComponentID}}", "isFavorite": false, "visibility": "public", + "branch": "master", "extensions": [], "qualityProfiles": [ - { - "name": "{{.SonarQualityProfile}}", - "language": "js", - "deleted": false - }, { "name": "{{.SonarQualityProfile}}", "language": "ts", @@ -17,7 +13,7 @@ } ], "qualityGate": { - "name": "Sonar way", + "name": "{{.SonarQualityGate}}", "isDefault": true }, "breadcrumbs": [ diff --git a/e2e-cypress/testdata/steps.yml b/e2e-cypress/testdata/steps.yml index f971ef90d..c48662a7c 100644 --- a/e2e-cypress/testdata/steps.yml +++ b/e2e-cypress/testdata/steps.yml @@ -10,6 +10,6 @@ steps: jenkinsStages: golden/jenkins-build-stages.json sonarScan: golden/sonar-scan.json runAttachments: - - SCRR-{{.ProjectID}}-{{.ComponentID}}.docx - - SCRR-{{.ProjectID}}-{{.ComponentID}}.md + - sonarqube-report-{{.ProjectID}}-{{.ComponentID}}.pdf + - cypress-test-reports-pdf.zip testResults: 2 diff --git a/e2e-etl-python/testdata/golden/sonar-scan.json b/e2e-etl-python/testdata/golden/sonar-scan.json index 22413e7c9..89c0b0aba 100644 --- a/e2e-etl-python/testdata/golden/sonar-scan.json +++ b/e2e-etl-python/testdata/golden/sonar-scan.json @@ -2,6 +2,7 @@ "key": "{{.ProjectID}}-{{.ComponentID}}", "name": "{{.ProjectID}}-{{.ComponentID}}", "isFavorite": false, + "branch": "master", "visibility": "public", "extensions": [], "qualityProfiles": [ @@ -17,7 +18,7 @@ } ], "qualityGate": { - "name": "Sonar way", + "name": "{{.SonarQualityGate}}", "isDefault": true }, "breadcrumbs": [ diff --git a/e2e-etl-python/testdata/steps.yml b/e2e-etl-python/testdata/steps.yml index 293fcfcbc..81e5bbfa0 100644 --- a/e2e-etl-python/testdata/steps.yml +++ b/e2e-etl-python/testdata/steps.yml @@ -10,6 +10,5 @@ steps: jenkinsStages: golden/jenkins-build-stages.json sonarScan: golden/sonar-scan.json runAttachments: - - SCRR-{{.ProjectID}}-{{.ComponentID}}.docx - - SCRR-{{.ProjectID}}-{{.ComponentID}}.md + - sonarqube-report-{{.ProjectID}}-{{.ComponentID}}.pdf testResults: 2 diff --git a/e2e-spock-geb/testdata/golden/jenkins-build-stages.json b/e2e-spock-geb/testdata/golden/jenkins-build-stages.json index 8e4d80e98..86d41d398 100644 --- a/e2e-spock-geb/testdata/golden/jenkins-build-stages.json +++ b/e2e-spock-geb/testdata/golden/jenkins-build-stages.json @@ -4,7 +4,7 @@ "status": "SUCCESS" }, { - "stage": "Integration Test", + "stage": "Functional Test", "status": "SUCCESS" }, { diff --git a/e2e-spock-geb/testdata/golden/jenkins-provision-stages.json b/e2e-spock-geb/testdata/golden/jenkins-provision-stages.json index 9e9ae3cb8..78ccc1246 100644 --- a/e2e-spock-geb/testdata/golden/jenkins-provision-stages.json +++ b/e2e-spock-geb/testdata/golden/jenkins-provision-stages.json @@ -15,6 +15,10 @@ "stage": "Create Jenkinsfile", "status": "SUCCESS" }, + { + "stage": "Create Credentials", + "status": "SUCCESS" + }, { "stage": "Create sonar-project.properties", "status": "SUCCESS" diff --git a/e2e-spock-geb/testdata/golden/sonar-scan.json b/e2e-spock-geb/testdata/golden/sonar-scan.json index fc4abe7eb..3e2717d97 100644 --- a/e2e-spock-geb/testdata/golden/sonar-scan.json +++ b/e2e-spock-geb/testdata/golden/sonar-scan.json @@ -3,13 +3,9 @@ "name": "{{.ProjectID}}-{{.ComponentID}}", "isFavorite": false, "visibility": "public", + "branch": "master", "extensions": [], "qualityProfiles": [ - { - "name": "{{.SonarQualityProfile}}", - "language": "grvy", - "deleted": false - }, { "name": "{{.SonarQualityProfile}}", "language": "java", @@ -17,7 +13,7 @@ } ], "qualityGate": { - "name": "Sonar way", + "name": "{{.SonarQualityGate}}", "isDefault": true }, "breadcrumbs": [ diff --git a/e2e-spock-geb/testdata/steps.yml b/e2e-spock-geb/testdata/steps.yml index c43648648..8ef67adc6 100644 --- a/e2e-spock-geb/testdata/steps.yml +++ b/e2e-spock-geb/testdata/steps.yml @@ -10,6 +10,5 @@ steps: jenkinsStages: golden/jenkins-build-stages.json sonarScan: golden/sonar-scan.json runAttachments: - - SCRR-{{.ProjectID}}-{{.ComponentID}}.docx - - SCRR-{{.ProjectID}}-{{.ComponentID}}.md - testResults: 6 + - sonarqube-report-{{.ProjectID}}-{{.ComponentID}}.pdf + testResults: 1 diff --git a/fe-angular/testdata/fixtures/angular-cypress/Jenkinsfile b/fe-angular/testdata/fixtures/angular-cypress/Jenkinsfile new file mode 100644 index 000000000..68b00874a --- /dev/null +++ b/fe-angular/testdata/fixtures/angular-cypress/Jenkinsfile @@ -0,0 +1,134 @@ +@Library("ods-jenkins-shared-library@4.x") _ + +node { + dockerRegistry = env.DOCKER_REGISTRY + cypressRecordKey = env.CYPRESS_RECORD_KEY + agentImageTag = "4.x" +} + +odsComponentPipeline( + podContainers: [ + containerTemplate( + name: 'jnlp', + image: "${dockerRegistry}/ods/jenkins-agent-nodejs22:${agentImageTag}", + workingDir: '/tmp', + envVars: [ + envVar(key: 'CYPRESS_RECORD_KEY', value: cypressRecordKey) + ], + resourceRequestCpu: '100m', + resourceLimitCpu: '300m', + resourceRequestMemory: '1Gi', + resourceLimitMemory: '2Gi', + alwaysPullImage: true, + args: '${computer.jnlpmac} ${computer.name}' + ) + ], + branchToEnvironmentMapping: [ + 'master': 'dev', + ] +) { context -> + + def targetDirectory = "${context.projectId}/${context.componentId}/${context.gitBranch.replaceAll('/', '-')}/${context.buildNumber}" + + stageInstall(context) + stageTypeCheck(context) + stageTest(context) + odsComponentStageScanWithSonar(context) + + if (fileExists('cypress/screenshots.zip')) { + odsComponentStageUploadToNexus(context, + [ + distributionFile: 'cypress/screenshots.zip', + repository: 'leva-documentation', + repositoryType: 'raw', + targetDirectory: "${targetDirectory}" + ] + ) + } + if (fileExists('cypress/videos.zip')) { + odsComponentStageUploadToNexus(context, + [ + distributionFile: 'cypress/videos.zip', + repository: 'leva-documentation', + repositoryType: 'raw', + targetDirectory: "${targetDirectory}" + ] + ) + } + if (fileExists('cypress/cypress-test-reports-pdf.zip')) { + odsComponentStageUploadToNexus(context, + [ + distributionFile: 'cypress/cypress-test-reports-pdf.zip', + repository: 'leva-documentation', + repositoryType: 'raw', + targetDirectory: "${targetDirectory}" + ] + ) + } +} + + +def stageInstall(def context) { + stage('Install dependencies') { + sh 'npm ci' + } +} + +def stageTypeCheck(def context) { + stage('Check types') { + sh 'npx tsc --noEmit' + } +} + +def stageTest(def context) { + stage('Functional Tests') { + // Prefer an explicit BASE_URL passed from the test harness, fall back to the Angular service inside the cluster. + def baseUrl = env.BASE_URL ?: "http://fe-angular-test.${context.projectId}-dev.svc.cluster.local:8080" + + withEnv([ + "TAGVERSION=${context.tagversion}", + "NEXUS_HOST=${context.nexusHost}", + "OPENSHIFT_PROJECT=${context.targetProject}", + "OPENSHIFT_APP_DOMAIN=${context.getOpenshiftApplicationDomain()}", + "COMMIT_INFO_SHA=${context.gitCommit}", + "BUILD_NUMBER=${context.buildNumber}", + "BASE_URL=${baseUrl}", + ]) { + withCredentials([ + // usernamePassword(credentialsId: "${context.projectId}-cd-e2e-user", passwordVariable: 'CYPRESS_PASSWORD', usernameVariable: 'CYPRESS_USERNAME'), + // string(credentialsId: "${context.projectId}-cd-otp-secret", variable: 'OTP_SECRET') + ]) { + def status + if (context.environment == 'prod') { + status = sh(script: 'npm run e2e:prod', returnStatus: true) + sh 'npm run junit-installation-report' + junit(testResults:'build/test-results/*.xml') + stash(name: "installation-test-reports-junit-xml-${context.componentId}-${context.buildNumber}", includes: 'build/test-results/installation-junit.xml', allowEmpty: true) + } else { + status = sh(script: 'npm run e2e', returnStatus: true) + sh 'npm run combine:reports' + junit(testResults:'build/test-results/*.xml') + stash(name: "installation-test-reports-junit-xml-${context.componentId}-${context.buildNumber}", includes: 'build/test-results/installation-junit.xml', allowEmpty: true) + stash(name: "integration-test-reports-junit-xml-${context.componentId}-${context.buildNumber}", includes: 'build/test-results/integration-junit.xml', allowEmpty: true) + stash(name: "acceptance-test-reports-junit-xml-${context.componentId}-${context.buildNumber}", includes: 'build/test-results/acceptance-junit.xml', allowEmpty: true) + } + + sh 'npm run generate:pdf' + zip zipFile: 'cypress/cypress-test-reports-pdf.zip', archive: false, dir: 'build/test-results/mochawesome/pdf' + archiveArtifacts artifacts: 'cypress/cypress-test-reports-pdf.zip', fingerprint: true + + if (fileExists('cypress/videos')) { + zip zipFile: 'cypress/videos.zip', archive: false, dir: 'cypress/videos' + } + + if (fileExists('build/test-results/screenshots')) { + zip zipFile: 'cypress/screenshots.zip', archive: false, dir: 'build/test-results/screenshots' + } + + if (status != 0) { + unstable "Some tests have failed or encountered errors. Please check the logs for more details." + } + } + } + } +} diff --git a/fe-angular/testdata/fixtures/angular-cypress/tests/acceptance/angular-smoke.spec.cy.ts b/fe-angular/testdata/fixtures/angular-cypress/tests/acceptance/angular-smoke.spec.cy.ts new file mode 100644 index 000000000..a7f0873ea --- /dev/null +++ b/fe-angular/testdata/fixtures/angular-cypress/tests/acceptance/angular-smoke.spec.cy.ts @@ -0,0 +1,11 @@ +describe('Angular quickstarter smoke test', () => { + const rootPath = '/'; + + it('serves the landing page', () => { + cy.request(rootPath).its('status').should('eq', 200); + cy.visit(rootPath); + cy.get('body').should('exist'); + cy.get('app-root').should('exist'); + cy.title().should('not.be.empty'); + }); +}); diff --git a/fe-angular/testdata/fixtures/angular-cypress/tests/installation/angular-installation.spec.cy.ts b/fe-angular/testdata/fixtures/angular-cypress/tests/installation/angular-installation.spec.cy.ts new file mode 100644 index 000000000..20b61362b --- /dev/null +++ b/fe-angular/testdata/fixtures/angular-cypress/tests/installation/angular-installation.spec.cy.ts @@ -0,0 +1,65 @@ +/** + * Installation Tests for Angular Application + * + * These tests verify that the Angular application is properly installed, + * built, and deployed with all dependencies and configuration in place. + */ + +describe('Angular Application Installation Tests', () => { + beforeEach(() => { + // Visit the application + cy.visit('http://fe-angular-test.e2etsqs-dev.svc.cluster.local:8080/'); + }); + + it('Should load the application successfully', () => { + // Verify the page loads without errors + cy.get('body').should('be.visible'); + }); + + it('Should display the welcome header', () => { + // Verify the main heading is present + cy.contains('Hello, fe-angular-test').should('be.visible'); + }); + + it('Should display success message', () => { + // Verify congratulations message + cy.contains('Congratulations! Your app is running').should('be.visible'); + }); + + it('Should have proper document structure', () => { + // Verify basic HTML structure + cy.document().should('have.property', 'title'); + cy.title().should('include', 'FeAngular'); + }); + + it('Should load stylesheets without errors', () => { + // Verify CSS is loaded by checking computed styles + cy.get('body').should('have.css', 'font-family'); + }); + + it('Should render Angular root component', () => { + // Verify Angular app-root component is present + cy.get('app-root').should('exist'); + }); + + it('Should display emoji in success message', () => { + // Verify the celebration emoji is present + cy.contains('🎉').should('be.visible'); + }); + + it('Should have no console errors on load', () => { + // Check for console errors + cy.window().then((win) => { + cy.spy(win.console, 'error'); + }); + cy.get('body').should('be.visible'); + cy.window().its('console.error').should('not.have.been.called'); + }); + + it('Should have accessible document structure', () => { + // Verify basic accessibility structure + cy.get('html').should('have.attr', 'lang'); + cy.get('head').should('exist'); + cy.get('body').should('exist'); + }); +}); diff --git a/fe-angular/testdata/fixtures/angular-cypress/tests/integration/angular-integration.spec.cy.ts b/fe-angular/testdata/fixtures/angular-cypress/tests/integration/angular-integration.spec.cy.ts new file mode 100644 index 000000000..8f4747efb --- /dev/null +++ b/fe-angular/testdata/fixtures/angular-cypress/tests/integration/angular-integration.spec.cy.ts @@ -0,0 +1,133 @@ +/** + * Integration Tests for Angular Application + * + * These tests verify that the Angular application integrates correctly with + * its environment, handles user interactions, and maintains state properly. + */ + +describe('Angular Application Integration Tests', () => { + beforeEach(() => { + // Visit the application before each test + cy.visit('http://fe-angular-test.e2etsqs-dev.svc.cluster.local:8080/'); + }); + + describe('Page Layout and Navigation', () => { + it('Should have main content area visible', () => { + cy.get('main').should('be.visible'); + }); + + it('Should have heading section visible', () => { + cy.get('h1, h2, h3').should('exist').and('be.visible'); + }); + + it('Should render welcome message in DOM', () => { + cy.get('body').invoke('text').should('include', 'Hello'); + }); + }); + + describe('Component Rendering', () => { + it('Should render app component with content', () => { + cy.get('app-root').within(() => { + cy.contains('Hello').should('be.visible'); + }); + }); + + it('Should have toolbox section available', () => { + cy.contains('Explore the Docs').should('be.visible'); + }); + + it('Should display multiple resource links', () => { + const links = [ + 'Explore the Docs', + 'Learn with Tutorials', + 'CLI Docs' + ]; + + links.forEach(linkText => { + cy.contains(linkText).should('be.visible'); + }); + }); + }); + + describe('External Links', () => { + it('Should have Angular docs link', () => { + cy.contains('Explore the Docs').should('have.attr', 'href').and('match', /^https:\/\/angular\.dev\/?$/); + }); + + it('Should have Angular tutorials link', () => { + cy.contains('Learn with Tutorials').should('have.attr', 'href').and('include', 'angular.dev/tutorials'); + }); + + it('Should have CLI docs link', () => { + cy.contains('CLI Docs').should('have.attr', 'href').and('include', 'angular.dev/tools/cli'); + }); + }); + + describe('Application State', () => { + it('Should maintain content on page interaction', () => { + cy.contains('Hello, fe-angular-test').should('exist'); + cy.get('body').click(100, 100); + cy.contains('Hello, fe-angular-test').should('exist'); + }); + + it('Should preserve DOM structure after load', () => { + cy.get('app-root').should('have.length', 1); + cy.wait(500); + cy.get('app-root').should('have.length', 1); + }); + }); + + describe('Responsive Behavior', () => { + it('Should be visible on desktop viewport', () => { + cy.viewport(1920, 1080); + cy.get('body').should('be.visible'); + cy.contains('Hello, fe-angular-test').should('be.visible'); + }); + + it('Should render correctly with different viewport sizes', () => { + cy.viewport('iphone-x'); + cy.get('body').should('be.visible'); + cy.contains('Hello').should('be.visible'); + }); + }); + + describe('Performance and Load', () => { + it('Should load page within reasonable time', () => { + cy.get('app-root', { timeout: 10000 }).should('exist'); + }); + + it('Should have links with valid href attributes', () => { + cy.get('a[href]').should('have.length.greaterThan', 0); + cy.get('a[href]').each(($link) => { + cy.wrap($link).should('have.attr', 'href').and('not.be.empty'); + }); + }); + }); + + describe('Meta Information', () => { + it('Should have proper page title', () => { + cy.title().should('not.be.empty'); + }); + + it('Should have viewport meta tag', () => { + cy.get('meta[name="viewport"]').should('exist'); + }); + + it('Should have charset defined', () => { + cy.get('meta[charset]').should('exist'); + }); + }); + + describe('Error Handling', () => { + it('Should not show any 404 errors in network', () => { + // Verify the page loads successfully + cy.get('app-root').should('exist').and('be.visible'); + }); + + it('Should have CSS styles applied to elements', () => { + // Verify that CSS is loaded by checking computed styles on body + cy.get('body').should('have.css', 'margin'); + cy.get('body').should('have.css', 'padding'); + }); + }); +}); \ No newline at end of file diff --git a/fe-angular/testdata/golden/jenkins-build-cypress.json b/fe-angular/testdata/golden/jenkins-build-cypress.json new file mode 100644 index 000000000..44aef5929 --- /dev/null +++ b/fe-angular/testdata/golden/jenkins-build-cypress.json @@ -0,0 +1,30 @@ +[ + { + "stage": "odsPipeline start", + "status": "SUCCESS" + }, + { + "stage": "Install dependencies", + "status": "SUCCESS" + }, + { + "stage": "Check types", + "status": "SUCCESS" + }, + { + "stage": "Functional Tests", + "status": "SUCCESS" + }, + { + "stage": "SonarQube Analysis", + "status": "SUCCESS" + }, + { + "stage": "Upload to Nexus", + "status": "SUCCESS" + }, + { + "stage": "odsPipeline finished", + "status": "SUCCESS" + } +] \ No newline at end of file diff --git a/fe-angular/testdata/golden/sonar-scan-cypress.json b/fe-angular/testdata/golden/sonar-scan-cypress.json new file mode 100644 index 000000000..2b457dbea --- /dev/null +++ b/fe-angular/testdata/golden/sonar-scan-cypress.json @@ -0,0 +1,31 @@ +{ + "key": "{{.ProjectID}}-{{.ComponentID}}", + "name": "{{.ProjectID}}-{{.ComponentID}}", + "isFavorite": false, + "branch": "master", + "visibility": "public", + "extensions": [], + "qualityProfiles": [ + { + "name": "{{.SonarQualityProfile}}", + "language": "js", + "deleted": false + }, + { + "name": "{{.SonarQualityProfile}}", + "language": "ts", + "deleted": false + } + ], + "qualityGate": { + "name": "{{.SonarQualityGate}}", + "isDefault": true + }, + "breadcrumbs": [ + { + "key": "{{.ProjectID}}-{{.ComponentID}}", + "name": "{{.ProjectID}}-{{.ComponentID}}", + "qualifier": "TRK" + } + ] +} diff --git a/fe-angular/testdata/golden/sonar-scan.json b/fe-angular/testdata/golden/sonar-scan.json index f1e75ed46..7350a468c 100644 --- a/fe-angular/testdata/golden/sonar-scan.json +++ b/fe-angular/testdata/golden/sonar-scan.json @@ -2,17 +2,38 @@ "key": "{{.ProjectID}}-{{.ComponentID}}", "name": "{{.ProjectID}}-{{.ComponentID}}", "isFavorite": false, + "branch": "master", "visibility": "public", "extensions": [], "qualityProfiles": [ + { + "name": "Sonar way", + "language": "css", + "deleted": false + }, + { + "name": "Sonar way", + "language": "docker", + "deleted": false + }, + { + "name": "{{.SonarQualityProfile}}", + "language": "js", + "deleted": false + }, { "name": "{{.SonarQualityProfile}}", "language": "ts", "deleted": false + }, + { + "name": "Sonar way", + "language": "web", + "deleted": false } ], "qualityGate": { - "name": "Sonar way", + "name": "{{.SonarQualityGate}}", "isDefault": true }, "breadcrumbs": [ diff --git a/fe-angular/testdata/steps.yml b/fe-angular/testdata/steps.yml index c8d334c82..2a307e798 100644 --- a/fe-angular/testdata/steps.yml +++ b/fe-angular/testdata/steps.yml @@ -1,25 +1,123 @@ componentID: fe-angular-test steps: -- type: provision - provisionParams: - verify: - jenkinsStages: golden/jenkins-provision-stages.json -- type: build - buildParams: - verify: - jenkinsStages: golden/jenkins-build-stages.json - sonarScan: golden/sonar-scan.json - runAttachments: - - SCRR-{{.ProjectID}}-{{.ComponentID}}.docx - - SCRR-{{.ProjectID}}-{{.ComponentID}}.md - testResults: 3 - openShiftResources: - imageTags: - - name: "{{.ComponentID}}" - tag: latest - imageStreams: - - "{{.ComponentID}}" - deploymentConfigs: - - "{{.ComponentID}}" - services: - - "{{.ComponentID}}" + # Step 1: Provision the angular component + - description: "Provision fe-angular quickstarter" + type: provision + provisionParams: + verify: + jenkinsStages: golden/jenkins-provision-stages.json + + # Step 2: Build the component + - description: "Build the component" + type: build + buildParams: + verify: + jenkinsStages: golden/jenkins-build-stages.json + sonarScan: golden/sonar-scan.json + runAttachments: + - sonarqube-report-{{.ProjectID}}-{{.ComponentID}}.pdf + testResults: 3 + openShiftResources: + imageTags: + - name: "{{.ComponentID}}" + tag: latest + imageStreams: + - "{{.ComponentID}}" + deploymentConfigs: + - "{{.ComponentID}}" + services: + - "{{.ComponentID}}" + + # Step 3: Expose services + - description: "Expose service with defaults" + type: "expose-service" + exposeServiceParams: + services: + - serviceName: "{{.ComponentID}}" + + # Step 4: Test health endpoint + - description: "Verify health endpoint returns 200" + type: http + httpParams: + url: "http://{{.ComponentID}}.{{.ProjectID}}-dev.svc.cluster.local:8080/" + method: GET + expectedStatus: 200 + timeout: 30 + retry: + attempts: 10 + delay: "5s" + + - description: "Provision e2e-cypress test component" + type: provision + componentID: angular-cypress-test + provisionParams: + quickstarter: e2e-cypress + verify: + jenkinsStages: "../../e2e-cypress/testdata/golden/jenkins-provision-stages.json" + + - description: "Remove default test files from e2e-cypress" + type: bitbucket + componentID: angular-cypress-test + bitbucketParams: + action: delete-files + repository: "{{.ProjectID}}-angular-cypress-test" + paths: + - "tests/acceptance/" + - "tests/integration/" + - "tests/installation/" + commitMessage: "Remove default test files" + + - description: "Point Cypress pipeline to Angular service" + type: bitbucket + componentID: angular-cypress-test + bitbucketParams: + action: "upload-file" + file: "fixtures/angular-cypress/Jenkinsfile" + repository: "{{.ProjectID}}-angular-cypress-test" + filename: "Jenkinsfile" + render: true + + - description: "Add Cypress smoke test for Angular UI" + type: bitbucket + componentID: angular-cypress-test + bitbucketParams: + action: "upload-file" + file: "fixtures/angular-cypress/tests/acceptance/angular-smoke.spec.cy.ts" + repository: "{{.ProjectID}}-angular-cypress-test" + filename: "tests/acceptance/angular-smoke.spec.cy.ts" + render: true + + - description: "Add Cypress installation tests for Angular" + type: bitbucket + componentID: angular-cypress-test + bitbucketParams: + action: "upload-file" + file: "fixtures/angular-cypress/tests/installation/angular-installation.spec.cy.ts" + repository: "{{.ProjectID}}-angular-cypress-test" + filename: "tests/installation/angular-installation.spec.cy.ts" + render: true + + - description: "Add Cypress integration tests for Angular" + type: bitbucket + componentID: angular-cypress-test + bitbucketParams: + action: "upload-file" + file: "fixtures/angular-cypress/tests/integration/angular-integration.spec.cy.ts" + repository: "{{.ProjectID}}-angular-cypress-test" + filename: "tests/integration/angular-integration.spec.cy.ts" + render: true + + - description: "Run Cypress tests against Angular app" + type: build + componentID: angular-cypress-test + buildParams: + env: + - name: BASE_URL + value: "http://fe-angular-test.{{.ProjectID}}-dev.svc.cluster.local:8080" + verify: + jenkinsStages: "golden/jenkins-build-cypress.json" + sonarScan: "golden/sonar-scan-cypress.json" + runAttachments: + - sonarqube-report-{{.ProjectID}}-{{.ComponentID}}.pdf + - cypress/cypress-test-reports-pdf.zip + testResults: 1 \ No newline at end of file diff --git a/fe-ionic/testdata/golden/sonar-scan.json b/fe-ionic/testdata/golden/sonar-scan.json index f1e75ed46..07942e2ae 100644 --- a/fe-ionic/testdata/golden/sonar-scan.json +++ b/fe-ionic/testdata/golden/sonar-scan.json @@ -2,17 +2,39 @@ "key": "{{.ProjectID}}-{{.ComponentID}}", "name": "{{.ProjectID}}-{{.ComponentID}}", "isFavorite": false, + "branch": "master", "visibility": "public", "extensions": [], "qualityProfiles": [ + { + "name": "Sonar way", + "language": "css", + "deleted": false + }, + { + "name": "Sonar way", + "language": "docker", + "deleted": false + }, + { + "name": "{{.SonarQualityProfile}}", + "language": "js", + "deleted": false + }, { "name": "{{.SonarQualityProfile}}", "language": "ts", "deleted": false - } + }, + { + "name": "Sonar way", + "language": "web", + "deleted": false + } + ], "qualityGate": { - "name": "Sonar way", + "name": "{{.SonarQualityGate}}", "isDefault": true }, "breadcrumbs": [ diff --git a/fe-ionic/testdata/steps.yml b/fe-ionic/testdata/steps.yml index 100654077..b67c148d6 100644 --- a/fe-ionic/testdata/steps.yml +++ b/fe-ionic/testdata/steps.yml @@ -10,8 +10,7 @@ steps: jenkinsStages: golden/jenkins-build-stages.json sonarScan: golden/sonar-scan.json runAttachments: - - SCRR-{{.ProjectID}}-{{.ComponentID}}.docx - - SCRR-{{.ProjectID}}-{{.ComponentID}}.md + - sonarqube-report-{{.ProjectID}}-{{.ComponentID}}.pdf testResults: 2 openShiftResources: imageTags: diff --git a/ods-document-gen-svc/testdata/golden/sonar-scan.json b/ods-document-gen-svc/testdata/golden/sonar-scan.json index 013d7530c..80653c411 100644 --- a/ods-document-gen-svc/testdata/golden/sonar-scan.json +++ b/ods-document-gen-svc/testdata/golden/sonar-scan.json @@ -3,6 +3,7 @@ "name": "{{.ProjectID}}-{{.ComponentID}}", "isFavorite": false, "visibility": "public", + "branch": "master", "extensions": [], "qualityProfiles": [ { @@ -12,7 +13,7 @@ } ], "qualityGate": { - "name": "Sonar way", + "name": "{{.SonarQualityGate}}", "isDefault": true }, "breadcrumbs": [ diff --git a/ods-document-gen-svc/testdata/steps.yml b/ods-document-gen-svc/testdata/steps.yml index 973d46448..312329cef 100644 --- a/ods-document-gen-svc/testdata/steps.yml +++ b/ods-document-gen-svc/testdata/steps.yml @@ -11,8 +11,7 @@ steps: jenkinsStages: golden/jenkins-build-stages.json sonarScan: golden/sonar-scan.json runAttachments: - - SCRR-{{.ProjectID}}-{{.ComponentID}}.docx - - SCRR-{{.ProjectID}}-{{.ComponentID}}.md + - sonarqube-report-{{.ProjectID}}-{{.ComponentID}}.pdf testResults: 14 openShiftResources: namespace: "{{.ProjectID}}-cd" diff --git a/ods-provisioning-app/testdata/golden/sonar-scan.json b/ods-provisioning-app/testdata/golden/sonar-scan.json index e7ecc4ec3..3e2717d97 100644 --- a/ods-provisioning-app/testdata/golden/sonar-scan.json +++ b/ods-provisioning-app/testdata/golden/sonar-scan.json @@ -3,6 +3,7 @@ "name": "{{.ProjectID}}-{{.ComponentID}}", "isFavorite": false, "visibility": "public", + "branch": "master", "extensions": [], "qualityProfiles": [ { @@ -12,7 +13,7 @@ } ], "qualityGate": { - "name": "Sonar way", + "name": "{{.SonarQualityGate}}", "isDefault": true }, "breadcrumbs": [ diff --git a/ods-provisioning-app/testdata/steps.yml b/ods-provisioning-app/testdata/steps.yml index 08007e15e..9515b9782 100644 --- a/ods-provisioning-app/testdata/steps.yml +++ b/ods-provisioning-app/testdata/steps.yml @@ -11,8 +11,7 @@ steps: jenkinsStages: golden/jenkins-build-stages.json sonarScan: golden/sonar-scan.json runAttachments: - - SCRR-{{.ProjectID}}-{{.ComponentID}}.docx - - SCRR-{{.ProjectID}}-{{.ComponentID}}.md + - sonarqube-report-{{.ProjectID}}-{{.ComponentID}}.pdf testResults: 204 openShiftResources: namespace: "{{.ProjectID}}-cd" diff --git a/release-manager/testdata/steps.yml b/release-manager/testdata/steps.yml index 499f4bedb..92bfde483 100644 --- a/release-manager/testdata/steps.yml +++ b/release-manager/testdata/steps.yml @@ -1,4 +1,4 @@ -componentID: releasemanager +componentID: qs-test-releaese-manager steps: - type: provision description: 'provision RM repo' @@ -15,17 +15,18 @@ steps: - type: build description: 'run RM pipeline without any repos' buildParams: - pipeline: mro-pipeline + pipeline: "{{.ProjectID}}-mro-pipeline-master" verify: jenkinsStages: golden/jenkins-build-stages-after-provisioning.json - type: upload description: 'configure one component in RM repo' uploadParams: + render: true file: fixtures/metadata.yml - type: build description: 'RM defaults run (WIP)' buildParams: - pipeline: mro-pipeline + pipeline: "{{.ProjectID}}-mro-pipeline-master" verify: jenkinsStages: golden/jenkins-build-stages-with-added-repo.json runAttachments: @@ -36,7 +37,7 @@ steps: - type: build description: 'RM version=1 target=DEV run' buildParams: - pipeline: mro-pipeline + pipeline: "{{.ProjectID}}-mro-pipeline-master" env: - name: environment value: DEV @@ -65,7 +66,7 @@ steps: - type: build description: 'RM version=1 target=QA run' buildParams: - pipeline: mro-pipeline + pipeline: "{{.ProjectID}}-mro-pipeline-master" env: - name: environment value: QA From 5c91580e49923b876e210130be7e921a310993cf Mon Sep 17 00:00:00 2001 From: Jorge Romero Date: Fri, 13 Feb 2026 09:54:43 +0100 Subject: [PATCH 2/6] Add renovate bot QS #1145 (#1146) --- CHANGELOG.md | 1 + renovate/Dockerfile.template | 26 +++++++ renovate/Jenkinsfile | 32 ++++++++ renovate/Jenkinsfile.template | 37 +++++++++ renovate/README.md | 28 +++++++ renovate/files/.gitignore | 70 ++++++++++++++++++ renovate/files/README.md | 66 +++++++++++++++++ renovate/files/chart/.helmignore | 23 ++++++ renovate/files/chart/Chart.lock | 6 ++ renovate/files/chart/Chart.yaml | 24 ++++++ renovate/files/chart/templates/configmap.yaml | 36 +++++++++ renovate/files/chart/values.yaml | 53 +++++++++++++ .../files/chart/values.yaml.secret.example | 4 + renovate/files/import-certs.sh | 61 +++++++++++++++ renovate/files/metadata.yml | 6 ++ renovate/files/release-manager.yml | 2 + renovate/icon.png | Bin 0 -> 15381 bytes renovate/sonar-project.properties.template | 25 +++++++ renovate/testdata/fixtures/configmap.yaml | 33 +++++++++ renovate/testdata/fixtures/renovate-config.js | 33 +++++++++ .../testdata/golden/jenkins-build-stages.json | 26 +++++++ .../jenkins-provision-stages-python.json | 30 ++++++++ .../golden/jenkins-provision-stages.json | 30 ++++++++ renovate/testdata/golden/sonar-scan.json | 32 ++++++++ .../testdata/scripts/run-renovate-cronjob.sh | 24 ++++++ renovate/testdata/steps.yml | 63 ++++++++++++++++ 26 files changed, 771 insertions(+) create mode 100644 renovate/Dockerfile.template create mode 100644 renovate/Jenkinsfile create mode 100644 renovate/Jenkinsfile.template create mode 100644 renovate/README.md create mode 100644 renovate/files/.gitignore create mode 100644 renovate/files/README.md create mode 100644 renovate/files/chart/.helmignore create mode 100644 renovate/files/chart/Chart.lock create mode 100644 renovate/files/chart/Chart.yaml create mode 100644 renovate/files/chart/templates/configmap.yaml create mode 100644 renovate/files/chart/values.yaml create mode 100644 renovate/files/chart/values.yaml.secret.example create mode 100644 renovate/files/import-certs.sh create mode 100644 renovate/files/metadata.yml create mode 100644 renovate/files/release-manager.yml create mode 100644 renovate/icon.png create mode 100644 renovate/sonar-project.properties.template create mode 100644 renovate/testdata/fixtures/configmap.yaml create mode 100644 renovate/testdata/fixtures/renovate-config.js create mode 100644 renovate/testdata/golden/jenkins-build-stages.json create mode 100644 renovate/testdata/golden/jenkins-provision-stages-python.json create mode 100644 renovate/testdata/golden/jenkins-provision-stages.json create mode 100644 renovate/testdata/golden/sonar-scan.json create mode 100755 renovate/testdata/scripts/run-renovate-cronjob.sh create mode 100644 renovate/testdata/steps.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index b111a7210..79f814e8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## Unreleased +- Add renovate bot ([1146](https://github.com/opendevstack/ods-quickstarters/pull/1146)) - Update Quickstarter Tests to Support New Framework Test Capabilities ([#1144](https://github.com/opendevstack/ods-quickstarters/pull/1144)) ### Added diff --git a/renovate/Dockerfile.template b/renovate/Dockerfile.template new file mode 100644 index 000000000..35ec70cc3 --- /dev/null +++ b/renovate/Dockerfile.template @@ -0,0 +1,26 @@ +# Custom docker image with certificate import capability +# Documentation: https://github.com/renovatebot/renovate/blob/main/docs/usage/examples/self-hosting.md#renovate-docker-image + +FROM docker-group-@ods_namespace@.@app_domain@/renovate/renovate:latest + +# Updating certificates needs root privileges +USER root + +# Install script to fetch certificates from APP_DNS environment variable +# Usage: Set APP_DNS="host1:port1;host2:port2" to fetch and install certificates +COPY import-certs.sh /usr/local/bin/import-certs.sh +RUN chmod +x /usr/local/bin/import-certs.sh + +# Required for import-certs.sh and update-ca-certificates to work at runtime +RUN chmod -R 777 /usr/local/share/ca-certificates && \ + chmod 777 /etc/ssl/certs && \ + chmod 666 /etc/ca-certificates.conf + +# This is required because Node.js doesn't use the system CA store by default +ENV NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt + +# Switch back to non-root user +USER 12021 + +# Debug mode: set -x to show all commands in logs for troubleshooting +ENTRYPOINT [ "/bin/sh", "-c", "set -x; /usr/local/bin/import-certs.sh && exec /usr/local/sbin/renovate-entrypoint.sh" ] \ No newline at end of file diff --git a/renovate/Jenkinsfile b/renovate/Jenkinsfile new file mode 100644 index 000000000..f25d61819 --- /dev/null +++ b/renovate/Jenkinsfile @@ -0,0 +1,32 @@ +def odsNamespace = '' +def odsGitRef = '' +def odsImageTag = '' +def sharedLibraryRef = '' +def agentImageTag = '' + +node { + odsNamespace = env.ODS_NAMESPACE ?: 'ods' + odsGitRef = env.ODS_GIT_REF ?: 'master' + odsImageTag = env.ODS_IMAGE_TAG ?: 'latest' + sharedLibraryRef = env.SHARED_LIBRARY_REF ?: odsImageTag + agentImageTag = env.AGENT_IMAGE_TAG ?: odsImageTag +} + +library("ods-jenkins-shared-library@${sharedLibraryRef}") + +odsQuickstarterPipeline( + imageStreamTag: "${odsNamespace}/jenkins-agent-base:${agentImageTag}", +) { context -> + + odsQuickstarterStageCopyFiles(context) + + odsQuickstarterStageRenderJenkinsfile(context) + + odsQuickstarterStageRenderSonarProperties(context) + + odsQuickstarterStageRenderJenkinsfile( + context, + [source: 'Dockerfile.template', + target: 'Dockerfile'] + ) +} diff --git a/renovate/Jenkinsfile.template b/renovate/Jenkinsfile.template new file mode 100644 index 000000000..f30aadb43 --- /dev/null +++ b/renovate/Jenkinsfile.template @@ -0,0 +1,37 @@ +// See https://www.opendevstack.org/ods-documentation/ for usage and customization. + +/* groovylint-disable-next-line CompileStatic, NoDef, UnusedVariable, VariableName, VariableTypeRequired */ +@Library('ods-jenkins-shared-library@4.x') _ + +odsComponentPipeline( + imageStreamTag: 'ods/jenkins-agent-base:4.x', + branchToEnvironmentMapping: [ + 'master': 'cd', + ] +) { context -> + stagePrepareHelm() + stageBuild(context) + odsComponentStageScanWithSonar(context) + + odsComponentStageRolloutOpenShiftDeployment(context, [ + helmValues: [ + "renovate.image.repository": "${context.projectId}-cd/renovate", + "renovate.image.tag": "${context.shortGitCommit}" + ] + ]) +} + +void stagePrepareHelm() { + stage('Prepare Helm') { + sh 'helm dependency update chart' + } +} + +void stageBuild(def context) { + odsComponentFindOpenShiftImageOrElse(context, [resourceName: 'renovate']) { + odsComponentStageBuildOpenShiftImage(context, [ + dockerDir: '.', + resourceName: 'renovate', + ]) + } +} diff --git a/renovate/README.md b/renovate/README.md new file mode 100644 index 000000000..99dd2c590 --- /dev/null +++ b/renovate/README.md @@ -0,0 +1,28 @@ +# Renovate + +This is a quickstart template for Renovate, a tool for automating dependency updates in software projects. Lives in your cd-namespace. + +## Features + +- **Automated Dependency Updates**: Automatically keeps your dependencies up to date +- **Pull Request Integration**: Creates pull requests for each update, allowing for easy review and merging +- **Configurable**: Easily customize the behavior of Renovate to suit your project's needs +- **Support for Multiple Languages**: Works with a wide range of programming languages and package managers +- **Scheduling**: Configure when updates should be applied, such as daily, weekly, or monthly + +Check out the [Renovate documentation](https://docs.renovatebot.com) for more information on how to set up and configure Renovate for your project. + +## Getting Started + +1. **Install Renovate**: Use the Provision App to install Renovate into your project. +2. **Configure Renovate**: Modify the preconfigured `renovate.json` file to suit your project's needs. You can specify which dependencies to update, how often to check for updates, and more. +3. **Run Renovate**: Once configured, Renovate starts processing your dependencies on the next scheduled run. It will create pull requests for any updates it finds. + +## Further Resources + +### Documentation + +- [Renovate Documentation](https://docs.renovatebot.com) +- [Renovate Configuration Options](https://docs.renovatebot.com/configuration-options) +- [Renovate Presets](https://docs.renovatebot.com/config-presets) + diff --git a/renovate/files/.gitignore b/renovate/files/.gitignore new file mode 100644 index 000000000..ab41a27c5 --- /dev/null +++ b/renovate/files/.gitignore @@ -0,0 +1,70 @@ +# Created by https://www.toptal.com/developers/gitignore/api/macos,windows,helm,dotenv +# Edit at https://www.toptal.com/developers/gitignore?templates=macos,windows,helm,dotenv + +### dotenv ### +.env + +### Helm ### +# Chart dependencies +**/charts/*.tgz + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# End of https://www.toptal.com/developers/gitignore/api/macos,windows,helm,dotenv diff --git a/renovate/files/README.md b/renovate/files/README.md new file mode 100644 index 000000000..98a5c633c --- /dev/null +++ b/renovate/files/README.md @@ -0,0 +1,66 @@ +# Renovate + +This is a quickstart template for Renovate, a tool for automating dependency updates in software projects. + +## Features + +- **Automated Dependency Updates**: Automatically keeps your dependencies up to date +- **Pull Request Integration**: Creates pull requests for each update, allowing for easy review and merging +- **Configurable**: Easily customize the behavior of Renovate to suit your project's needs +- **Support for Multiple Languages**: Works with a wide range of programming languages and package managers +- **Scheduling**: Configure when updates should be applied, such as daily, weekly, or monthly +- **Lock File Maintenance**: Ensures that lock files are kept in sync with the latest dependencies + +Check out the [Renovate documentation](https://docs.renovatebot.com/) for more information on how to set up and configure Renovate for your project. + +## Getting Started + +1. Modify the `configmap.yaml` file to customize the Renovate configuration for your project +2. Supply the necessary credentials + 1. Configure your preferred way of specifying credentials (e.g. Helm Secrets or repository secrets with Transcrypt) + 2. (Recommended) For GitHub, create a personal access token to fetch changelogs of dependencies + 3. (Optional) For Nexus, create a token to access special packages + +## Recommended Configuration Options + +If `onboarding` is set to `true` in the config, Renovate will automatically create a pull request to add a `renovate.json` file to your repository. This file will contain the default configuration for Renovate. +Decide whether you want Renovate to scan all repositories it has access to or just specific ones. + +```js +// configmap.yaml +module.exports = { + // ... other configuration options + "repositories": ["PROJECTID/COMPONENTID"] +} +``` + +If you need Renovate to speak to a custom Nexus host, add the `NEXUS_HOST` environment variable to your configuration. Also, set the environment variable `NEXUS_TOKEN` similar to the GITHUB_TOKEN. + +```js +// configmap.yaml +module.exports = { + // ... other configuration options + "hostRules": [ + // ... other host rules + { + "matchHost": "nexus.digitale-hub.com", + "token": process.env.NEXUS_TOKEN + } + ] +} +``` + +## Further Resources + +### Documentation + +#### Renovate specific + +- [Renovate Documentation](https://docs.renovatebot.com) +- [Renovate Configuration Options](https://docs.renovatebot.com/configuration-options) +- [Renovate Presets](https://docs.renovatebot.com/config-presets) + +#### Secrets Management + +- [Helm Secrets Plugin](https://github.com/jkroepke/helm-secrets) +- [Transcrypt](https://github.com/elasticdog/transcrypt) diff --git a/renovate/files/chart/.helmignore b/renovate/files/chart/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /dev/null +++ b/renovate/files/chart/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/renovate/files/chart/Chart.lock b/renovate/files/chart/Chart.lock new file mode 100644 index 000000000..e05f854a6 --- /dev/null +++ b/renovate/files/chart/Chart.lock @@ -0,0 +1,6 @@ +dependencies: +- name: renovate + repository: https://docs.renovatebot.com/helm-charts + version: 45.71.2 +digest: sha256:c90afb616b7e68bea19917cb126b6e5b3e521c4e3cd72f28b3758a5219801509 +generated: "2026-01-12T08:38:13.079663+01:00" diff --git a/renovate/files/chart/Chart.yaml b/renovate/files/chart/Chart.yaml new file mode 100644 index 000000000..a8aec10ab --- /dev/null +++ b/renovate/files/chart/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: renovate +description: Renovate bot to run on Openshift +icon: https://docs.renovatebot.com/assets/images/logo.png + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 1.0.0 + +dependencies: + - name: renovate + version: 46.8.4 + repository: oci://ghcr.io/renovatebot/charts diff --git a/renovate/files/chart/templates/configmap.yaml b/renovate/files/chart/templates/configmap.yaml new file mode 100644 index 000000000..10d903b54 --- /dev/null +++ b/renovate/files/chart/templates/configmap.yaml @@ -0,0 +1,36 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: renovateconfigjs +data: + config.js: |- + module.exports = { + "autodiscover": false, + "branchPrefix": "renovate/", + "username": process.env.CD_USERNAME, + "password": process.env.CD_PASSWORD, + "gitAuthor": "Renovate Bot ", + "onboarding": true, + "platform": "bitbucket-server", + "endpoint": "", + "configMigration": true, + "extends": [ + "config:recommended", + "helpers:pinGitHubActionDigests", + ":pinDevDependencies", + ":disableDependencyDashboard", + ":semanticPrefixFixDepsChoreOthers", + ":ignoreModulesAndTests", + "group:monorepos", + "group:recommended", + "replacements:all", + "workarounds:all", + "mergeConfidence:all-badges" + ], + "hostRules": [ + { + "hostType": "github", + "token": process.env.GITHUB_TOKEN + } + ] + } diff --git a/renovate/files/chart/values.yaml b/renovate/files/chart/values.yaml new file mode 100644 index 000000000..886efb154 --- /dev/null +++ b/renovate/files/chart/values.yaml @@ -0,0 +1,53 @@ +renovate: + cronjob: + # -- Schedules the job to run using cron notation + schedule: "0 1,3 * * *" # At 01:00 and 03:00 every morning + # -- You can specify a time zone for a CronJob by setting timeZone to the name of a valid time zone. (starting with k8s 1.27) + timeZone: "Europe/Berlin" # see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones for valid names + # Delete pod after 3 days (in seconds) + ttlSecondsAfterFinished: 259200 + + image: + # -- Registry to pull image from + registry: image-registry.openshift-image-registry.svc:5000 + # -- Image name to pull + # repository: PROJECTID/renovate # will be set during deployment + # -- Renovate image tag to pull + # tag: latest # will be set during deployment + # -- "IfNotPresent" to pull the image if no image with the specified tag exists on the node, "Always" to always pull the image or "Never" to try and use pre-pulled images + pullPolicy: Always + + # -- Additional env. Helpful too if you want to use anything other than a `value` source. + envList: + - name: CD_USERNAME + valueFrom: + secretKeyRef: + name: cd-user-with-password + key: username + - name: CD_PASSWORD + valueFrom: + secretKeyRef: + name: cd-user-with-password + key: password + + # -- Additional volumes to the pod + extraVolumes: + - name: config-volume + configMap: + name: renovateconfigjs + + # -- Additional volumeMounts to the container + extraVolumeMounts: + - name: config-volume + mountPath: /usr/src/app + + # Double nested because renovate subchart is a dependency (called renovate) + renovate: + # -- Custom exiting global renovate config => loaded via configmap + existingConfigFile: "/usr/src/app/config.js" + + # -- Options related to persistence + persistence: + cache: + # -- Allow the cache to persist between runs + enabled: false diff --git a/renovate/files/chart/values.yaml.secret.example b/renovate/files/chart/values.yaml.secret.example new file mode 100644 index 000000000..7276561b2 --- /dev/null +++ b/renovate/files/chart/values.yaml.secret.example @@ -0,0 +1,4 @@ +renovate: + env: + # Used to pull the release changelog from GitHub + GITHUB_TOKEN: "" diff --git a/renovate/files/import-certs.sh b/renovate/files/import-certs.sh new file mode 100644 index 000000000..a0199ce02 --- /dev/null +++ b/renovate/files/import-certs.sh @@ -0,0 +1,61 @@ +#!/bin/bash +set -eu + +# Script to import SSL/TLS certificates from remote servers +# Usage: Set APP_DNS environment variable with format "host1:port1;host2:port2" +# Example: APP_DNS="example.com:443;internal.corp:8443" + +# If APP_DNS is not set, try to extract hosts from Renovate config +if [[ -z ${APP_DNS:=""} ]]; then + echo "APP_DNS not set, checking for Renovate configuration..." + + # Try to extract endpoint from config file if it exists + if [ -f /usr/src/app/config.js ]; then + ENDPOINT=$(grep -oP 'endpoint["\s:]+["'\'']\K[^"'\'']+' /usr/src/app/config.js 2>/dev/null || true) + if [ -n "$ENDPOINT" ]; then + # Extract hostname from URL + HOST=$(echo "$ENDPOINT" | sed -E 's#https?://([^/:]+).*#\1#') + if [ -n "$HOST" ]; then + echo "Found endpoint in config: $HOST" + APP_DNS="$HOST:443" + fi + fi + fi +fi + +if [[ ! -z ${APP_DNS:=""} ]]; then + echo "Setting up certificates from APP_DNS=${APP_DNS} ..." + + # Parse APP_DNS (semicolon-separated list) + arrIN=(${APP_DNS//;/ }) + for val in "${arrIN[@]}"; do + dnsPortTuple=(${val//:/ }) + DNS=${dnsPortTuple[0]} + PORT=${dnsPortTuple[1]:=443} + + echo "Importing certificate from DNS=$DNS PORT=$PORT" + cert_path="/usr/local/share/ca-certificates/${DNS}.crt" + + # Fetch certificate using openssl + openssl s_client -showcerts -host ${DNS} -port ${PORT} /dev/null | \ + sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > "${cert_path}" + + if [ -s "${cert_path}" ]; then + echo " ✓ Certificate saved to ${cert_path}" + else + echo " ✗ Failed to fetch certificate from ${DNS}:${PORT}" + rm -f "${cert_path}" + fi + done + + # Try to update system certificate trust store (may fail without root) + if update-ca-certificates 2>/dev/null; then + echo "System CA certificates updated" + else + echo "Could not update system CA store (no root access)" + fi + + echo "Done with certificate setup" +else + echo 'No certificates to import (APP_DNS not set and no config found)' +fi diff --git a/renovate/files/metadata.yml b/renovate/files/metadata.yml new file mode 100644 index 000000000..46262a1e5 --- /dev/null +++ b/renovate/files/metadata.yml @@ -0,0 +1,6 @@ +--- +name: Renovate +description: A tool for automating dependency updates in software projects. +supplier: https://www.opendevstack.org +version: 4.x +type: ods-service diff --git a/renovate/files/release-manager.yml b/renovate/files/release-manager.yml new file mode 100644 index 000000000..23d65c7ef --- /dev/null +++ b/renovate/files/release-manager.yml @@ -0,0 +1,2 @@ +--- +dependencies: [] diff --git a/renovate/icon.png b/renovate/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..2fdfe9aa25f3af01da649d692d619d290508402c GIT binary patch literal 15381 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 literal 0 HcmV?d00001 diff --git a/renovate/sonar-project.properties.template b/renovate/sonar-project.properties.template new file mode 100644 index 000000000..200795261 --- /dev/null +++ b/renovate/sonar-project.properties.template @@ -0,0 +1,25 @@ +# Project identification, see: https://docs.sonarsource.com/sonarqube-server/10.8/analyzing-source-code/analysis-parameters/#project-identification +## The project's unique key. Can include up to 400 characters. All letters, digits, dash, underscore, periods, and colons are accepted. (required) +sonar.projectKey=@project_id@-@component_id@ + +# Project Information, see: https://docs.sonarsource.com/sonarqube-server/10.8/analyzing-source-code/analysis-parameters/#project-information +## The name of the project that will be displayed on the SonarQube Server UI. (optional) +sonar.projectName=@project_id@-@component_id@ +## The project version. It should be set for branch analysis in case you use the new code definition based on the previous version. (optional, default short git commit hash) +#sonar.projectVersion=1.0.0 +## The project description. (optional) +sonar.projectDescription=A Renovate bot for automating dependency updates in software projects. + +# Analysis scope, see: https://docs.sonarsource.com/sonarqube-server/10.8/analyzing-source-code/analysis-parameters/#analysis-scope +## The initial analysis scope for main source code (non-test code) in the project. Comma-separated paths to directories with sources (required) +sonar.sources=. +## The initial analysis scope for test code in the project. Comma-separated paths to directories containing test source files (optional but recommended for performance reasons). +# sonar.tests= + +# Links displayed in the UI: https://docs.sonarsource.com/sonarqube-server/10.8/analyzing-source-code/analysis-parameters/#links-displayed-in-the-ui +## The URL of the continuous integration system used. (optional) +sonar.links.ci=${env.JOB_URL} + +# Other Parameters, see: https://docs.sonarsource.com/sonarqube-server/10.8/analyzing-source-code/analysis-parameters/#other-parameters +## Encoding of the source files (optional but recommended as default is ASCII) +sonar.sourceEncoding=UTF-8 diff --git a/renovate/testdata/fixtures/configmap.yaml b/renovate/testdata/fixtures/configmap.yaml new file mode 100644 index 000000000..b753cb000 --- /dev/null +++ b/renovate/testdata/fixtures/configmap.yaml @@ -0,0 +1,33 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: renovateconfigjs +data: + config.js: |- + module.exports = { + "repositories": ["{{.ProjectID}}/{{.ProjectID}}-python-test-renovate"], + "autodiscover": false, + "branchPrefix": "renovate/", + "username": process.env.CD_USERNAME, + "password": process.env.CD_PASSWORD, + "gitAuthor": "Renovate Bot ", + "onboarding": true, + "platform": "bitbucket-server", + "endpoint": "{{.BITBUCKET_URL}}", + "configMigration": true, + "dryRun": false, + "printConfig": true, + "extends": [ + "config:recommended", + "helpers:pinGitHubActionDigests", + ":pinDevDependencies", + ":disableDependencyDashboard", + ":semanticPrefixFixDepsChoreOthers", + ":ignoreModulesAndTests", + "group:monorepos", + "group:recommended", + "replacements:all", + "workarounds:all", + "mergeConfidence:all-badges" + ], + } diff --git a/renovate/testdata/fixtures/renovate-config.js b/renovate/testdata/fixtures/renovate-config.js new file mode 100644 index 000000000..32167d82b --- /dev/null +++ b/renovate/testdata/fixtures/renovate-config.js @@ -0,0 +1,33 @@ + module.exports = { + "repositories": ["{{.ProjectID}}/{{.ProjectID}}-python-test-renovate"], + "autodiscover": false, + "branchPrefix": "renovate/", + "username": process.env.CD_USERNAME, + "password": process.env.CD_PASSWORD, + "gitAuthor": "Renovate Bot ", + "onboarding": true, + "platform": "bitbucket-server", + "endpoint": "{{.BITBUCKET_URL}}", + "configMigration": true, + "dryRun": false, + "printConfig": true, + "extends": [ + "config:recommended", + "helpers:pinGitHubActionDigests", + ":pinDevDependencies", + ":disableDependencyDashboard", + ":semanticPrefixFixDepsChoreOthers", + ":ignoreModulesAndTests", + "group:monorepos", + "group:recommended", + "replacements:all", + "workarounds:all", + "mergeConfidence:all-badges" + ], + "hostRules": [ + { + "hostType": "github", + "token": process.env.GITHUB_TOKEN + } + ] + } diff --git a/renovate/testdata/golden/jenkins-build-stages.json b/renovate/testdata/golden/jenkins-build-stages.json new file mode 100644 index 000000000..f004f34d4 --- /dev/null +++ b/renovate/testdata/golden/jenkins-build-stages.json @@ -0,0 +1,26 @@ +[ + { + "stage": "odsPipeline start", + "status": "SUCCESS" + }, + { + "stage": "Prepare Helm", + "status": "SUCCESS" + }, + { + "stage": "Build OpenShift Image (renovate)", + "status": "SUCCESS" + }, + { + "stage": "SonarQube Analysis", + "status": "SUCCESS" + }, + { + "stage": "Deploy to OpenShift", + "status": "SUCCESS" + }, + { + "stage": "odsPipeline finished", + "status": "SUCCESS" + } +] diff --git a/renovate/testdata/golden/jenkins-provision-stages-python.json b/renovate/testdata/golden/jenkins-provision-stages-python.json new file mode 100644 index 000000000..fe9cad15b --- /dev/null +++ b/renovate/testdata/golden/jenkins-provision-stages-python.json @@ -0,0 +1,30 @@ +[ + { + "stage": "Checkout quickstarter", + "status": "SUCCESS" + }, + { + "stage": "Initialize output directory", + "status": "SUCCESS" + }, + { + "stage": "Copy files from quickstarter", + "status": "SUCCESS" + }, + { + "stage": "Create OpenShift resources", + "status": "SUCCESS" + }, + { + "stage": "Create Jenkinsfile", + "status": "SUCCESS" + }, + { + "stage": "Create sonar-project.properties", + "status": "SUCCESS" + }, + { + "stage": "Push to remote", + "status": "SUCCESS" + } +] diff --git a/renovate/testdata/golden/jenkins-provision-stages.json b/renovate/testdata/golden/jenkins-provision-stages.json new file mode 100644 index 000000000..c27806c1d --- /dev/null +++ b/renovate/testdata/golden/jenkins-provision-stages.json @@ -0,0 +1,30 @@ +[ + { + "stage": "Checkout quickstarter", + "status": "SUCCESS" + }, + { + "stage": "Initialize output directory", + "status": "SUCCESS" + }, + { + "stage": "Copy files from quickstarter", + "status": "SUCCESS" + }, + { + "stage": "Create Jenkinsfile", + "status": "SUCCESS" + }, + { + "stage": "Create sonar-project.properties", + "status": "SUCCESS" + }, + { + "stage": "Create Jenkinsfile", + "status": "SUCCESS" + }, + { + "stage": "Push to remote", + "status": "SUCCESS" + } +] diff --git a/renovate/testdata/golden/sonar-scan.json b/renovate/testdata/golden/sonar-scan.json new file mode 100644 index 000000000..a938ff20d --- /dev/null +++ b/renovate/testdata/golden/sonar-scan.json @@ -0,0 +1,32 @@ +{ + "key": "{{.ProjectID}}-renovate-qs", + "name": "{{.ProjectID}}-renovate-qs", + "description": "A Renovate bot for automating dependency updates in software projects.", + "isFavorite": false, + "branch": "master", + "visibility": "public", + "extensions": [], + "qualityProfiles": [ + { + "name": "Sonar way", + "language": "docker", + "deleted": false + }, + { + "name": "Sonar way", + "language": "yaml", + "deleted": false + } + ], + "qualityGate": { + "name": "ODS Default Quality Gate", + "isDefault": true + }, + "breadcrumbs": [ + { + "key": "{{.ProjectID}}-{{.ComponentID}}", + "name": "{{.ProjectID}}-{{.ComponentID}}", + "qualifier": "TRK" + } + ] +} diff --git a/renovate/testdata/scripts/run-renovate-cronjob.sh b/renovate/testdata/scripts/run-renovate-cronjob.sh new file mode 100755 index 000000000..ecb896e00 --- /dev/null +++ b/renovate/testdata/scripts/run-renovate-cronjob.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +set -euo pipefail + +# This script manually triggers the renovate-qs cron job by creating a one-off job +# derived from the cronjob definition. It relies on template-provided env vars. + +CRONJOB_NAME="renovate-qs" +MANUAL_JOB_NAME="${PROJECT_ID:-unknown}-renovate-qs-manual" +NAMESPACE="${NAMESPACE_CD:-${PROJECT_ID:-unknown}-cd}" + + +if [[ -z "${PROJECT_ID:-}" ]]; then + echo "PROJECT_ID environment variable not set; cannot determine namespace or job names." >&2 + exit 1 +fi + +echo "Ensuring previous job ${MANUAL_JOB_NAME} is removed (if present)" +oc delete job "${MANUAL_JOB_NAME}" -n "${NAMESPACE}" --ignore-not-found=true + +echo "Creating manual job ${MANUAL_JOB_NAME} from cronjob ${CRONJOB_NAME} in namespace ${NAMESPACE}" +oc create job --from=cronjob/${CRONJOB_NAME} "${MANUAL_JOB_NAME}" -n "${NAMESPACE}" + +# Show job status right after creation for easier troubleshooting +oc get job "${MANUAL_JOB_NAME}" -n "${NAMESPACE}" diff --git a/renovate/testdata/steps.yml b/renovate/testdata/steps.yml new file mode 100644 index 000000000..31c66ccf2 --- /dev/null +++ b/renovate/testdata/steps.yml @@ -0,0 +1,63 @@ +componentID: renovate-qs +steps: + - type: provision + provisionParams: + verify: + jenkinsStages: golden/jenkins-provision-stages.json + + - type: upload + description: "Upload configmap with configuration" + uploadParams: + file: fixtures/configmap.yaml + fileName: chart/templates/configmap.yaml + render: true + + - type: build + buildParams: + verify: + jenkinsStages: golden/jenkins-build-stages.json + sonarScan: golden/sonar-scan.json + runAttachments: + - "sonarqube-report-{{.ProjectID}}-renovate-qs.pdf" + openShiftResources: + namespace: "{{.ProjectID}}-cd" + imageStreams: + - renovate + cronJobs: + - renovate-qs + + - type: provision + description: "Provision an component to be tested" + componentID: python-test-renovate + provisionParams: + quickstarter: ods-quickstarters/be-python-flask + branch: 4.x + verify: + jenkinsStages: golden/jenkins-provision-stages-python.json + + - type: run + description: "Trigger renovate-qs cron job manually" + runParams: + file: scripts/run-renovate-cronjob.sh + + - type: wait + description: "Wait for renovate-qs manual job to finish" + waitParams: + condition: job-complete + resource: "job/{{.ProjectID}}-renovate-qs-manual" + namespace: "{{.ProjectID}}-cd" + timeout: "600s" + interval: "10s" + + - type: bitbucket + description: "Check if a pullrequest has been created" + bitbucketParams: + action: "get-pullrequest" + project: "{{.ProjectID}}" + repository: "{{.ProjectID}}-python-test-renovate" + pullRequestID: "1" + verify: + prChecks: + state: "OPEN" + description: "contains: To activate Renovate, merge this Pull Request" + From 0d8df641d1c0a445ac6d8e0d047ea5cc3b380c1d Mon Sep 17 00:00:00 2001 From: brais <26645694+BraisVQ@users.noreply.github.com> Date: Mon, 16 Feb 2026 10:07:05 +0100 Subject: [PATCH 3/6] Increase default memory resources for Docker-plain Quickstarter (#1147) --- CHANGELOG.md | 1 + docker-plain/files/chart/values.yaml | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79f814e8f..22ec272bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ### Changed ### Fixed +- Increase Docker-plain QS default memory resources ([#1147](https://github.com/opendevstack/ods-quickstarters/pull/1147)) ## [4.11.0] - 2025-12-03 diff --git a/docker-plain/files/chart/values.yaml b/docker-plain/files/chart/values.yaml index d5905eee3..59b7de980 100644 --- a/docker-plain/files/chart/values.yaml +++ b/docker-plain/files/chart/values.yaml @@ -105,11 +105,11 @@ securityContext: {} resources: limits: cpu: 100m - memory: 32Mi + memory: 64Mi ephemeral-storage: 1Mi requests: cpu: 10m - memory: 16Mi + memory: 32Mi ephemeral-storage: 1Mi ## Before enabling check the official ODS documentation about replicate support: https://www.opendevstack.org/ods-documentation/opendevstack/latest/jenkins-shared-library/orchestration-pipeline.html#_known_limitations From 011eb6bcbee605ef071c50ace94c3908e406fe04 Mon Sep 17 00:00:00 2001 From: roicarrera <120183761+roicarrera@users.noreply.github.com> Date: Mon, 23 Feb 2026 11:07:36 +0100 Subject: [PATCH 4/6] chore: add specific versioning to e2e-spock-geb and e2e-etl-python (#1148) --- CHANGELOG.md | 1 + e2e-etl-python/README.md | 2 ++ e2e-etl-python/files/pyproject.toml | 3 +++ e2e-spock-geb/README.md | 2 ++ e2e-spock-geb/files/build.gradle | 2 ++ 5 files changed, 10 insertions(+) create mode 100644 e2e-etl-python/files/pyproject.toml diff --git a/CHANGELOG.md b/CHANGELOG.md index 22ec272bb..7492c1cdb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ ### Added ### Changed +- Add specific quickstarter versioning to e2e-spock-geb and e2e-etl-python ([#1148](https://github.com/opendevstack/ods-quickstarters/pull/1148)) ### Fixed - Increase Docker-plain QS default memory resources ([#1147](https://github.com/opendevstack/ods-quickstarters/pull/1147)) diff --git a/e2e-etl-python/README.md b/e2e-etl-python/README.md index 0e8c848bb..2983b204d 100644 --- a/e2e-etl-python/README.md +++ b/e2e-etl-python/README.md @@ -1,5 +1,7 @@ # e2e-etl-python Quickstarter (e2e-etl-python) +This quickstarter tracks its own version in the files/pyproject.toml file. Make sure to update it accordingly. + Documentation is located in our [official documentation](https://www.opendevstack.org/ods-documentation/opendevstack/latest/getting-started/index.html) Please update documentation in the [antora page directory](https://github.com/opendevstack/ods-quickstarters/tree/master/docs/modules/quickstarters/pages) diff --git a/e2e-etl-python/files/pyproject.toml b/e2e-etl-python/files/pyproject.toml new file mode 100644 index 000000000..4799fefab --- /dev/null +++ b/e2e-etl-python/files/pyproject.toml @@ -0,0 +1,3 @@ +[project] +name = "e2e-etl-python" +version = "1.0.0" diff --git a/e2e-spock-geb/README.md b/e2e-spock-geb/README.md index c7249c0f2..e1883d488 100644 --- a/e2e-spock-geb/README.md +++ b/e2e-spock-geb/README.md @@ -1,5 +1,7 @@ # End-to-end tests with Spock, Geb and Unirest (e2e-spock-geb) +This quickstarter tracks its own version in the files/build.gradle file. Make sure to update it accordingly. + Documentation is located in our [official documentation](https://www.opendevstack.org/ods-documentation/opendevstack/3.x/quickstarters/e2e-spock-geb.html) Please update documentation in the [antora page directory](https://github.com/opendevstack/ods-quickstarters/tree/master/docs/modules/ROOT/pages) diff --git a/e2e-spock-geb/files/build.gradle b/e2e-spock-geb/files/build.gradle index 45a8d9670..c37008759 100644 --- a/e2e-spock-geb/files/build.gradle +++ b/e2e-spock-geb/files/build.gradle @@ -24,6 +24,8 @@ plugins { id 'com.adarshr.test-logger' version "2.0.0" } +version = '1.0.0' + java { toolchain { languageVersion.set(JavaLanguageVersion.of(17)) From ec34d4ea8397aba4781cee67f3b3c313f64f40d0 Mon Sep 17 00:00:00 2001 From: Angel MP Date: Fri, 27 Feb 2026 11:18:40 +0100 Subject: [PATCH 5/6] Chore/renovate evidences (#1149) * Add Cypress E2E tests and Jenkinsfile for Renovate Bot evidence collection * Update Cypress test environment variables and fix JSON output for Jenkins build * Add debug logging for environment variables in Renovate Bot tests * Refactor Renovate Bot tests to use template variables for Bitbucket credentials and remove debug logging * Update Renovate Bot tests to use string interpolation for Bitbucket credentials * Refactor Renovate Bot tests to use hardcoded credentials for Bitbucket authentication * Fix artifact archiving for Cypress test reports by copying to a new filename * Refactor Renovate Bot tests to use environment variables for Bitbucket credentials * Refactor Renovate Bot tests to use updated environment variable names for Bitbucket credentials * Update artifact archiving in Jenkinsfile to store Cypress test reports in an artifacts directory * Remove hardcoded Bitbucket credentials from steps.yml and update test report filename * Add Cypress video and screenshot archiving to Jenkins pipeline * Refactor Renovate Bot acceptance tests to use Bitbucket Web UI for verification and improve test descriptions * Improve PR existence check in Renovate Bot acceptance tests to assert visible links in main content * Refactor artifact paths in Jenkinsfile to use 'artifacts/' directory for Cypress test reports and screenshots * Refactor Renovate Bot tests and streamline evidence upload process in Jenkinsfile * Update Cypress test descriptions and file references in steps.yml for clarity * Improve visibility check for Renovate CronJob in OpenShift Console acceptance test * Add stage for packaging evidence in Jenkins pipeline and update acceptance test to use dynamic cron job details * Rename Cypress test reports zip file to 'cypress-evidence.zip' in steps.yml * Disable Cypress video recording and update evidence packaging in Jenkinsfile; adjust file paths for acceptance test JSON output * Updated CHANGELOG.md * Refactor Jenkinsfile to use environment variables for configuration and update shared library reference * Update BITBUCKET_URL environment variable to use template variable --------- Co-authored-by: EDPCommunity Automated Test Co-authored-by: Angel Martinez --- CHANGELOG.md | 1 + .../fixtures/renovate-cypress/Jenkinsfile | 123 ++++++++++++++++++ .../acceptance/renovate-acceptance.spec.cy.ts | 87 +++++++++++++ .../renovate-installation.spec.cy.ts | 108 +++++++++++++++ .../renovate-integration.spec.cy.ts | 86 ++++++++++++ .../golden/jenkins-build-cypress.json | 34 +++++ .../testdata/golden/sonar-scan-cypress.json | 31 +++++ renovate/testdata/steps.yml | 87 +++++++++++++ 8 files changed, 557 insertions(+) create mode 100644 renovate/testdata/fixtures/renovate-cypress/Jenkinsfile create mode 100644 renovate/testdata/fixtures/renovate-cypress/tests/acceptance/renovate-acceptance.spec.cy.ts create mode 100644 renovate/testdata/fixtures/renovate-cypress/tests/installation/renovate-installation.spec.cy.ts create mode 100644 renovate/testdata/fixtures/renovate-cypress/tests/integration/renovate-integration.spec.cy.ts create mode 100644 renovate/testdata/golden/jenkins-build-cypress.json create mode 100644 renovate/testdata/golden/sonar-scan-cypress.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 7492c1cdb..021278958 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## Unreleased +- Add renovate bot functional test evidences ([1146](https://github.com/opendevstack/ods-quickstarters/pull/1149)) - Add renovate bot ([1146](https://github.com/opendevstack/ods-quickstarters/pull/1146)) - Update Quickstarter Tests to Support New Framework Test Capabilities ([#1144](https://github.com/opendevstack/ods-quickstarters/pull/1144)) ### Added diff --git a/renovate/testdata/fixtures/renovate-cypress/Jenkinsfile b/renovate/testdata/fixtures/renovate-cypress/Jenkinsfile new file mode 100644 index 000000000..7956215d0 --- /dev/null +++ b/renovate/testdata/fixtures/renovate-cypress/Jenkinsfile @@ -0,0 +1,123 @@ +def odsNamespace = '' +def odsGitRef = '' +def odsImageTag = '' +def sharedLibraryRef = '' +def agentImageTag = '' + +node { + odsNamespace = env.ODS_NAMESPACE ?: 'ods' + odsGitRef = env.ODS_GIT_REF ?: 'master' + odsImageTag = env.ODS_IMAGE_TAG ?: 'latest' + sharedLibraryRef = env.SHARED_LIBRARY_REF ?: odsImageTag + agentImageTag = env.AGENT_IMAGE_TAG ?: odsImageTag + dockerRegistry = env.DOCKER_REGISTRY +} + +library("ods-jenkins-shared-library@${sharedLibraryRef}") + +odsComponentPipeline( + podContainers: [ + containerTemplate( + name: 'jnlp', + image: "${dockerRegistry}/${odsNamespace}/jenkins-agent-nodejs22:${agentImageTag}", + workingDir: '/tmp', + resourceRequestCpu: '100m', + resourceLimitCpu: '300m', + resourceRequestMemory: '1Gi', + resourceLimitMemory: '2Gi', + alwaysPullImage: true, + args: '${computer.jnlpmac} ${computer.name}' + ) + ], + branchToEnvironmentMapping: [ + 'master': 'dev', + ] +) { context -> + + def targetDirectory = "${context.projectId}/${context.componentId}/${context.gitBranch.replaceAll('/', '-')}/${context.buildNumber}" + + stageInstall(context) + stageTypeCheck(context) + stageTest(context) + odsComponentStageScanWithSonar(context) + stagePackageEvidences(context) + + odsComponentStageUploadToNexus(context, + [ + distributionFile: 'artifacts/cypress-evidence.zip', + repository: 'leva-documentation', + repositoryType: 'raw', + targetDirectory: "${targetDirectory}" + ] + ) +} + + +def stageInstall(def context) { + stage('Install dependencies') { + sh 'npm ci' + } +} + +def stageTypeCheck(def context) { + stage('Check types') { + sh 'npx tsc --noEmit' + } +} + +def stageTest(def context) { + stage('Functional Tests') { + def bitbucketBaseUrl = env.BITBUCKET_URL + + withEnv([ + "TAGVERSION=${context.tagversion}", + "NEXUS_HOST=${context.nexusHost}", + "OPENSHIFT_PROJECT=${context.targetProject}", + "OPENSHIFT_APP_DOMAIN=${context.getOpenshiftApplicationDomain()}", + "COMMIT_INFO_SHA=${context.gitCommit}", + "BUILD_NUMBER=${context.buildNumber}", + "CYPRESS_VIDEO=false", + "CYPRESS_BITBUCKET_BASE_URL=${bitbucketBaseUrl}", + "CYPRESS_PROJECT_ID=${context.projectId}", + "CYPRESS_OC_CONSOLE_CRONJOB_URL=https://console-openshift-console.${context.getOpenshiftApplicationDomain()}/k8s/ns/${context.targetProject}/cronjobs/renovate-qs/", + "OC_NAMESPACE=${context.targetProject}", + "CRONJOB_NAME=renovate-qs", + ]) { + withCredentials([ + usernamePassword(credentialsId: "${context.projectId}-cd-cd-user-with-password", passwordVariable: 'CYPRESS_BITBUCKET_PASSWORD', usernameVariable: 'CYPRESS_BITBUCKET_USERNAME') + ]) { + sh 'mkdir -p artifacts' + def status = sh(script: 'npm run e2e', returnStatus: true) + sh 'npm run combine:reports' + junit(testResults:'build/test-results/*.xml') + stash(name: "installation-test-reports-junit-xml-${context.componentId}-${context.buildNumber}", includes: 'build/test-results/installation-junit.xml', allowEmpty: true) + stash(name: "integration-test-reports-junit-xml-${context.componentId}-${context.buildNumber}", includes: 'build/test-results/integration-junit.xml', allowEmpty: true) + stash(name: "acceptance-test-reports-junit-xml-${context.componentId}-${context.buildNumber}", includes: 'build/test-results/acceptance-junit.xml', allowEmpty: true) + + sh 'npm run generate:pdf' + + if (status != 0) { + unstable "Some tests have failed or encountered errors. Please check the logs for more details." + } + } + } + } +} + +def stagePackageEvidences(def context) { + stage('Package Evidences') { + sh ''' + mkdir -p artifacts/evidence + if [ -d build/test-results/mochawesome/pdf ]; then + cp -r build/test-results/mochawesome/pdf artifacts/evidence/test-reports-pdf + fi + if [ -d build/test-results/screenshots ]; then + cp -r build/test-results/screenshots artifacts/evidence/screenshots + fi + find . -name "sonarqube-report-*.pdf" -exec cp {} artifacts/evidence/ \\; 2>/dev/null || true + cd artifacts && zip -r cypress-evidence.zip evidence + rm -f artifacts/sonarqube-report-*.pdf + ''' + archiveArtifacts artifacts: 'artifacts/cypress-evidence.zip', fingerprint: true + } +} diff --git a/renovate/testdata/fixtures/renovate-cypress/tests/acceptance/renovate-acceptance.spec.cy.ts b/renovate/testdata/fixtures/renovate-cypress/tests/acceptance/renovate-acceptance.spec.cy.ts new file mode 100644 index 000000000..f83e9f17c --- /dev/null +++ b/renovate/testdata/fixtures/renovate-cypress/tests/acceptance/renovate-acceptance.spec.cy.ts @@ -0,0 +1,87 @@ +/** + * Acceptance Tests for Renovate Bot + * + * Risk: The framework shall, upon execution of the Renovate Bot CronJob, + * automatically scan the consuming project's repositories for outdated + * dependencies and create corresponding Pull Requests in Bitbucket to update them. + * + * These tests navigate the Bitbucket Web UI and the OpenShift Console to verify: + * - The Renovate CronJob is deployed in the -cd namespace (OC Console) + * - A Pull Request was created by the Renovate Bot in the target repository + * - The PR contains the expected onboarding description + * - The PR state is OPEN + * - The PR metadata is correct (branch prefix, target branch, etc.) + * - The renovate.json configuration file is visible in the PR diff + * + * Each test captures a screenshot as visual evidence. + */ + +describe('Renovate Bot Acceptance Tests - Pull Request Creation Verification', () => { + const bitbucketBaseUrl = "{{.BITBUCKET_URL}}"; + const projectId = "{{.ProjectID}}"; + const username = Cypress.env("BITBUCKET_USERNAME"); + const password = Cypress.env("BITBUCKET_PASSWORD"); + const targetRepo = `${projectId}-python-test-renovate`; + const repoUrl = `${bitbucketBaseUrl}/projects/${projectId}/repos/${targetRepo}`; + + beforeEach(() => { + cy.session('bitbucket-login', () => { + cy.request({ + method: 'POST', + url: `${bitbucketBaseUrl}/login`, + form: true, + body: { + j_username: username, + j_password: password, + }, + followRedirect: true, + }); + }); + }); + + it('Should show the Renovate CronJob in the OpenShift Console', () => { + const ocNamespace = Cypress.env("OC_NAMESPACE") || `${projectId}-cd`; + const cronJobName = Cypress.env("CRONJOB_NAME") || "renovate-qs"; + cy.exec(`oc get cronjob ${cronJobName} -n ${ocNamespace} -o json`, { failOnNonZeroExit: false }).then(({ code, stdout, stderr }) => { + expect(code).to.eq(0); + const cronjob = JSON.parse(stdout); + expect(cronjob).to.have.property('metadata'); + expect(cronjob.metadata.name).to.eq(cronJobName); + cy.writeFile('build/test-results/screenshots/renovate-acceptance.spec.cy.ts/acceptance-01-oc-cronjob.json', JSON.stringify(cronjob, null, 2)); + }); + }); + + it('Should navigate to the Pull Requests page and see at least one PR', () => { + cy.visit(`${repoUrl}/pull-requests`); + cy.url().should('include', '/pull-requests'); + cy.get('#content', { timeout: 15000 }).should('be.visible'); + cy.get('#content a[href*="/pull-requests/"]:visible', { timeout: 15000 }) + .its('length') + .should('be.greaterThan', 0); + cy.screenshot('acceptance-02-pull-requests-exist'); + }); + + it('Should verify the Pull Request contains the Renovate activation message', () => { + cy.visit(`${repoUrl}/pull-requests/1/overview`); + cy.get('#content', { timeout: 15000 }).should('be.visible'); + cy.contains('To activate Renovate, merge this Pull Request', { timeout: 15000 }).should('be.visible'); + cy.screenshot('acceptance-03-pr-activate-renovate-message'); + }); + + it('Should navigate to the PR diff and show the renovate.json configuration file', () => { + cy.visit(`${repoUrl}/pull-requests/1/diff#renovate.json`); + cy.url().should('include', '/diff'); + cy.get('#content', { timeout: 30000 }).should('be.visible'); + cy.contains('renovate.json', { timeout: 30000 }).should('be.visible'); + cy.wait(2000); + cy.screenshot('acceptance-04-pr-diff-renovate-json'); + }); + + it('Should navigate to branches and verify a renovate/ branch exists', () => { + cy.visit(`${repoUrl}/branches`); + cy.get('#content', { timeout: 15000 }).should('be.visible'); + cy.contains('master', { timeout: 15000 }).should('be.visible'); + cy.contains('renovate/', { timeout: 15000 }).should('be.visible'); + cy.screenshot('acceptance-05-branches-with-renovate'); + }); +}); diff --git a/renovate/testdata/fixtures/renovate-cypress/tests/installation/renovate-installation.spec.cy.ts b/renovate/testdata/fixtures/renovate-cypress/tests/installation/renovate-installation.spec.cy.ts new file mode 100644 index 000000000..d03693a06 --- /dev/null +++ b/renovate/testdata/fixtures/renovate-cypress/tests/installation/renovate-installation.spec.cy.ts @@ -0,0 +1,108 @@ +/** + * Installation Tests for Renovate Bot + * + * Risk: The framework shall automatically create a dedicated Bitbucket repository + * within the consuming project upon provisioning of the Renovate Bot component, + * including the configuration files and predefined configuration settings. + * + * These tests verify through the Bitbucket API that: + * - The renovate-qs repository exists in the project + * - The repository contains the expected configuration files + * - The configmap with Renovate settings was applied + */ + +describe('Renovate Bot Installation Tests - Bitbucket Repository Verification', () => { + const bitbucketBaseUrl = "{{.BITBUCKET_URL}}"; + const projectId = "{{.ProjectID}}"; + const username = Cypress.env("BITBUCKET_USERNAME"); + const password = Cypress.env("BITBUCKET_PASSWORD"); + + const authHeader = `Basic ${btoa(`${username}:${password}`)}`; + const apiBase = `${bitbucketBaseUrl}/rest/api/1.0/projects/${projectId}`; + + + it('Should have the renovate-qs repository created in the project', () => { + const requestUrl = `${apiBase}/repos/${projectId}-renovate-qs`; + cy.request({ + method: 'GET', + url: requestUrl, + headers: { Authorization: authHeader }, + failOnStatusCode: false, + }).then((response) => { + expect(response.status).to.eq(200); + expect(response.body.slug).to.eq(`${projectId}-renovate-qs`.toLowerCase()); + cy.screenshot('installation-01-repository-exists'); + }); + }); + + it('Should have the python-test-renovate repository created for testing', () => { + const requestUrl = `${apiBase}/repos/${projectId}-python-test-renovate`; + cy.request({ + method: 'GET', + url: requestUrl, + headers: { Authorization: authHeader }, + failOnStatusCode: false, + }).then((response) => { + expect(response.status).to.eq(200); + expect(response.body.slug).to.eq(`${projectId}-python-test-renovate`.toLowerCase()); + cy.screenshot('installation-02-python-test-repo-exists'); + }); + }); + + it('Should have the Jenkinsfile in the renovate-qs repository', () => { + const requestUrl = `${apiBase}/repos/${projectId}-renovate-qs/browse/Jenkinsfile`; + cy.request({ + method: 'GET', + url: requestUrl, + headers: { Authorization: authHeader }, + failOnStatusCode: false, + }).then((response) => { + expect(response.status).to.eq(200); + const content = response.body.lines?.map((l: any) => l.text).join('\n') || ''; + expect(content).to.contain('odsComponentPipeline'); + cy.screenshot('installation-03-jenkinsfile-present'); + }); + }); + + it('Should have the sonar-project.properties in the renovate-qs repository', () => { + const requestUrl = `${apiBase}/repos/${projectId}-renovate-qs/browse/sonar-project.properties`; + cy.request({ + method: 'GET', + url: requestUrl, + headers: { Authorization: authHeader }, + failOnStatusCode: false, + }).then((response) => { + expect(response.status).to.eq(200); + cy.screenshot('installation-04-sonar-properties-present'); + }); + }); + + it('Should have the chart templates directory in the renovate-qs repository', () => { + const requestUrl = `${apiBase}/repos/${projectId}-renovate-qs/browse/chart/templates`; + cy.request({ + method: 'GET', + url: requestUrl, + headers: { Authorization: authHeader }, + failOnStatusCode: false, + }).then((response) => { + expect(response.status).to.eq(200); + cy.screenshot('installation-05-chart-templates-directory'); + }); + }); + + it('Should have the configmap.yaml with Renovate configuration', () => { + const requestUrl = `${apiBase}/repos/${projectId}-renovate-qs/browse/chart/templates/configmap.yaml`; + cy.request({ + method: 'GET', + url: requestUrl, + headers: { Authorization: authHeader }, + failOnStatusCode: false, + }).then((response) => { + expect(response.status).to.eq(200); + const content = response.body.lines?.map((l: any) => l.text).join('\n') || ''; + expect(content).to.contain('renovateconfigjs'); + expect(content).to.contain('repositories'); + cy.screenshot('installation-06-configmap-content'); + }); + }); +}); diff --git a/renovate/testdata/fixtures/renovate-cypress/tests/integration/renovate-integration.spec.cy.ts b/renovate/testdata/fixtures/renovate-cypress/tests/integration/renovate-integration.spec.cy.ts new file mode 100644 index 000000000..c8bcb9df6 --- /dev/null +++ b/renovate/testdata/fixtures/renovate-cypress/tests/integration/renovate-integration.spec.cy.ts @@ -0,0 +1,86 @@ +/** + * Integration Tests for Renovate Bot + * + * Risk: The framework shall automatically deploy a CronJob within the consuming + * project's -cd namespace upon provisioning of the Renovate Bot component. + * + * These tests verify through the Bitbucket API and OpenShift-exposed information + * that: + * - The CronJob was deployed in the correct namespace (-cd) + * - The Renovate Bot image stream exists + * - The CronJob triggered and completed successfully + * - The renovate-qs-manual job completed + */ + +describe('Renovate Bot Integration Tests - CronJob Deployment Verification', () => { + const bitbucketBaseUrl = "{{.BITBUCKET_URL}}"; + const projectId = "{{.ProjectID}}"; + const username = Cypress.env("BITBUCKET_USERNAME"); + const password = Cypress.env("BITBUCKET_PASSWORD"); + + const authHeader = `Basic ${btoa(`${username}:${password}`)}`; + const apiBase = `${bitbucketBaseUrl}/rest/api/1.0/projects/${projectId}`; + + before(() => { + }); + + it('Should confirm the renovate-qs repository is accessible in the -cd project context', () => { + // Verifies that the Bitbucket project (which maps to the OCP namespace) is properly set up + cy.request({ + method: 'GET', + url: `${apiBase}/repos/${projectId}-renovate-qs`, + headers: { Authorization: authHeader }, + }).then((response) => { + expect(response.status).to.eq(200); + expect(response.body.project.key).to.eq(projectId.toUpperCase()); + cy.screenshot('integration-01-project-context-valid'); + }); + }); + + it('Should verify configmap contains correct repository references for scanning', () => { + cy.request({ + method: 'GET', + url: `${apiBase}/repos/${projectId}-renovate-qs/browse/chart/templates/configmap.yaml`, + headers: { Authorization: authHeader }, + }).then((response) => { + expect(response.status).to.eq(200); + const content = response.body.lines?.map((l: any) => l.text).join('\n') || ''; + // Verify the configmap references the python-test-renovate repo for scanning + expect(content).to.contain('python-test-renovate'); + expect(content).to.contain('platform'); + expect(content).to.contain('bitbucket-server'); + cy.screenshot('integration-04-configmap-scanning-config'); + }); + }); + + it('Should verify the Renovate Bot configuration includes expected settings', () => { + cy.request({ + method: 'GET', + url: `${apiBase}/repos/${projectId}-renovate-qs/browse/chart/templates/configmap.yaml`, + headers: { Authorization: authHeader }, + }).then((response) => { + expect(response.status).to.eq(200); + const content = response.body.lines?.map((l: any) => l.text).join('\n') || ''; + // Verify the onboarding and autodiscover settings + expect(content).to.contain('onboarding'); + expect(content).to.contain('branchPrefix'); + expect(content).to.contain('renovate/'); + cy.screenshot('integration-05-renovate-settings'); + }); + }); + + it('Should verify repository list in the renovate-qs project', () => { + cy.request({ + method: 'GET', + url: `${apiBase}/repos?limit=25`, + headers: { Authorization: authHeader }, + }).then((response) => { + expect(response.status).to.eq(200); + const repoSlugs = response.body.values?.map((r: any) => r.slug) || []; + // Both renovate-qs and python-test-renovate should exist + expect(repoSlugs).to.include(`${projectId}-renovate-qs`.toLowerCase()); + expect(repoSlugs).to.include(`${projectId}-python-test-renovate`.toLowerCase()); + cy.screenshot('integration-06-all-repos-present'); + }); + }); +}); diff --git a/renovate/testdata/golden/jenkins-build-cypress.json b/renovate/testdata/golden/jenkins-build-cypress.json new file mode 100644 index 000000000..c1be11bf7 --- /dev/null +++ b/renovate/testdata/golden/jenkins-build-cypress.json @@ -0,0 +1,34 @@ +[ + { + "stage": "odsPipeline start", + "status": "SUCCESS" + }, + { + "stage": "Install dependencies", + "status": "SUCCESS" + }, + { + "stage": "Check types", + "status": "SUCCESS" + }, + { + "stage": "Functional Tests", + "status": "SUCCESS" + }, + { + "stage": "SonarQube Analysis", + "status": "SUCCESS" + }, + { + "stage": "Package Evidences", + "status": "SUCCESS" + }, + { + "stage": "Upload to Nexus", + "status": "SUCCESS" + }, + { + "stage": "odsPipeline finished", + "status": "SUCCESS" + } +] diff --git a/renovate/testdata/golden/sonar-scan-cypress.json b/renovate/testdata/golden/sonar-scan-cypress.json new file mode 100644 index 000000000..2b457dbea --- /dev/null +++ b/renovate/testdata/golden/sonar-scan-cypress.json @@ -0,0 +1,31 @@ +{ + "key": "{{.ProjectID}}-{{.ComponentID}}", + "name": "{{.ProjectID}}-{{.ComponentID}}", + "isFavorite": false, + "branch": "master", + "visibility": "public", + "extensions": [], + "qualityProfiles": [ + { + "name": "{{.SonarQualityProfile}}", + "language": "js", + "deleted": false + }, + { + "name": "{{.SonarQualityProfile}}", + "language": "ts", + "deleted": false + } + ], + "qualityGate": { + "name": "{{.SonarQualityGate}}", + "isDefault": true + }, + "breadcrumbs": [ + { + "key": "{{.ProjectID}}-{{.ComponentID}}", + "name": "{{.ProjectID}}-{{.ComponentID}}", + "qualifier": "TRK" + } + ] +} diff --git a/renovate/testdata/steps.yml b/renovate/testdata/steps.yml index 31c66ccf2..eb967add0 100644 --- a/renovate/testdata/steps.yml +++ b/renovate/testdata/steps.yml @@ -61,3 +61,90 @@ steps: state: "OPEN" description: "contains: To activate Renovate, merge this Pull Request" + # ───────────────────────────────────────────────────────────────────────────── + # Cypress E2E evidence collection steps + # These steps provision an e2e-cypress component and run acceptance / installation / + # integration tests against the Bitbucket UI / API to capture screenshots + # covering the following risks: + # - Bitbucket repository creation with configuration files + # - CronJob deployment in the -cd namespace + # - Pull Request creation by the Renovate Bot + # ───────────────────────────────────────────────────────────────────────────── + + - description: "Provision e2e-cypress test component for Renovate evidence" + type: provision + componentID: renovate-cypress-test + provisionParams: + quickstarter: e2e-cypress + verify: + jenkinsStages: "../../e2e-cypress/testdata/golden/jenkins-provision-stages.json" + + - description: "Remove default test files from e2e-cypress" + type: bitbucket + componentID: renovate-cypress-test + bitbucketParams: + action: delete-files + repository: "{{.ProjectID}}-renovate-cypress-test" + paths: + - "tests/acceptance/" + - "tests/integration/" + - "tests/installation/" + commitMessage: "Remove default test files" + + - description: "Point Cypress pipeline to Bitbucket for Renovate tests" + type: bitbucket + componentID: renovate-cypress-test + bitbucketParams: + action: "upload-file" + file: "fixtures/renovate-cypress/Jenkinsfile" + repository: "{{.ProjectID}}-renovate-cypress-test" + filename: "Jenkinsfile" + render: true + + - description: "Add Cypress acceptance tests - Renovate PR creation verification" + type: bitbucket + componentID: renovate-cypress-test + bitbucketParams: + action: "upload-file" + file: "fixtures/renovate-cypress/tests/acceptance/renovate-acceptance.spec.cy.ts" + repository: "{{.ProjectID}}-renovate-cypress-test" + filename: "tests/acceptance/renovate-acceptance.spec.cy.ts" + render: true + + - description: "Add Cypress installation tests - Bitbucket repository verification" + type: bitbucket + componentID: renovate-cypress-test + bitbucketParams: + action: "upload-file" + file: "fixtures/renovate-cypress/tests/installation/renovate-installation.spec.cy.ts" + repository: "{{.ProjectID}}-renovate-cypress-test" + filename: "tests/installation/renovate-installation.spec.cy.ts" + render: true + + - description: "Add Cypress integration tests - CronJob deployment verification" + type: bitbucket + componentID: renovate-cypress-test + bitbucketParams: + action: "upload-file" + file: "fixtures/renovate-cypress/tests/integration/renovate-integration.spec.cy.ts" + repository: "{{.ProjectID}}-renovate-cypress-test" + filename: "tests/integration/renovate-integration.spec.cy.ts" + render: true + + - description: "Run Cypress tests against Bitbucket to collect Renovate evidence" + type: build + componentID: renovate-cypress-test + buildParams: + env: + - name: BITBUCKET_URL + value: "{{.BITBUCKET_URL}}" + - name: PROJECT_ID + value: "{{.ProjectID}}" + verify: + jenkinsStages: "golden/jenkins-build-cypress.json" + sonarScan: "golden/sonar-scan-cypress.json" + runAttachments: + - sonarqube-report-{{.ProjectID}}-{{.ComponentID}}.pdf + - cypress-evidence.zip + testResults: 1 + From b7dd2268fc77b944aab91df88fcbf118d55e50af Mon Sep 17 00:00:00 2001 From: brais <26645694+BraisVQ@users.noreply.github.com> Date: Mon, 2 Mar 2026 14:13:43 +0100 Subject: [PATCH 6/6] Release 4.12.0 Updated changelog to include recent changes and fixes. --- CHANGELOG.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 021278958..88ee88b0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,16 @@ # Changelog ## Unreleased -- Add renovate bot functional test evidences ([1146](https://github.com/opendevstack/ods-quickstarters/pull/1149)) -- Add renovate bot ([1146](https://github.com/opendevstack/ods-quickstarters/pull/1146)) -- Update Quickstarter Tests to Support New Framework Test Capabilities ([#1144](https://github.com/opendevstack/ods-quickstarters/pull/1144)) ### Added +### Changed + +### Fixed + +## [4.12.0] - 2026-03-02 ### Changed - Add specific quickstarter versioning to e2e-spock-geb and e2e-etl-python ([#1148](https://github.com/opendevstack/ods-quickstarters/pull/1148)) +- Update Quickstarter Tests to Support New Framework Test Capabilities ([#1144](https://github.com/opendevstack/ods-quickstarters/pull/1144)) ### Fixed - Increase Docker-plain QS default memory resources ([#1147](https://github.com/opendevstack/ods-quickstarters/pull/1147))