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..88ee88b0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,20 @@ # Changelog ## Unreleased - ### 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)) + ## [4.11.0] - 2025-12-03 ### Added 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/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 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/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-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/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)) 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 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 000000000..2fdfe9aa2 Binary files /dev/null and b/renovate/icon.png differ 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/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/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-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/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..eb967add0 --- /dev/null +++ b/renovate/testdata/steps.yml @@ -0,0 +1,150 @@ +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" + + # ───────────────────────────────────────────────────────────────────────────── + # 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 +