From 13bd696433b60e6bfbb19e1df16fcb059283ea08 Mon Sep 17 00:00:00 2001 From: Aaron van Meerten Date: Tue, 21 Apr 2026 09:33:05 +0200 Subject: [PATCH] feat(selenium-grid,jenkins): release job for selenium grid upgrade --- .../release-selenium-grid-node/Jenkinsfile | 118 ++++++++++++++++++ jenkins/jobs/release-selenium-grid-node.yaml | 68 ++++++++++ scripts/test-selenium-grid-browsers.sh | 109 ++++++++++++++++ 3 files changed, 295 insertions(+) create mode 100644 jenkins/groovy/release-selenium-grid-node/Jenkinsfile create mode 100644 jenkins/jobs/release-selenium-grid-node.yaml create mode 100755 scripts/test-selenium-grid-browsers.sh diff --git a/jenkins/groovy/release-selenium-grid-node/Jenkinsfile b/jenkins/groovy/release-selenium-grid-node/Jenkinsfile new file mode 100644 index 000000000..37309f2e9 --- /dev/null +++ b/jenkins/groovy/release-selenium-grid-node/Jenkinsfile @@ -0,0 +1,118 @@ +def utils +def imageTag + +pipeline { + agent any + options { + ansiColor('xterm') + timestamps() + buildDiscarder(logRotator(numToKeepStr: '10', artifactNumToKeepStr: '10')) + } + stages { + stage('Prepare/Checkout') { + steps { + script { + def rootDir = pwd() + utils = load "${rootDir}/jenkins/groovy/Utils.groovy" + utils.SetupRepos(env.VIDEO_INFRA_BRANCH) + utils.SetupOCI() + } + } + } + stage('Resolve Tag') { + steps { + script { + if (env.TAG?.trim()) { + imageTag = env.TAG.trim() + } else { + dir('infra-provisioning') { + imageTag = sh( + returnStdout: true, + script: 'git rev-parse --short HEAD' + ).trim() + } + } + echo "Using image tag: ${imageTag}" + } + } + } + stage('Build Docker Image') { + when { + expression { env.SKIP_BUILD != 'true' } + } + steps { + script { + build job: 'build-docker-image-selenium-grid-node', parameters: [ + [$class: 'StringParameterValue', name: 'VIDEO_INFRA_BRANCH', value: env.VIDEO_INFRA_BRANCH], + [$class: 'StringParameterValue', name: 'TAG', value: imageTag] + ] + } + } + } + stage('Deploy to Test Grid') { + steps { + script { + echo "Deploying to test grid: ${env.TEST_GRID_NAME} with image tag: ${imageTag}" + dir('infra-provisioning') { + withCredentials([ + string(credentialsId: 'jenkins-aws-secret', variable: 'AWS_SECRET_ACCESS_KEY'), + string(credentialsId: 'jenkins-aws-id', variable: 'AWS_ACCESS_KEY_ID') + ]) { + sh """#!/bin/bash + export SELENIUM_GRID_IMAGE_VERSION="${imageTag}" + GRID=${env.TEST_GRID_NAME} scripts/deploy-nomad-selenium-grid-hub.sh + GRID=${env.TEST_GRID_NAME} scripts/deploy-nomad-selenium-grid-node.sh + RET=\$? + if [[ \$RET -eq 2 ]]; then + echo "Complete but failed to place all allocations (expected)" + exit 0 + else + exit \$RET + fi + """ + } + } + } + } + } + stage('Smoke Test') { + steps { + script { + echo "Running browser smoke test against test grid: ${env.TEST_GRID_NAME}" + dir('infra-provisioning') { + sh """#!/bin/bash + export GRID="${env.TEST_GRID_NAME}" + scripts/test-selenium-grid-browsers.sh + """ + } + } + } + } + stage('Deploy to Production Grid') { + steps { + script { + echo "Deploying to production grid: ${env.PROD_GRID_NAME} with image tag: ${imageTag}" + dir('infra-provisioning') { + withCredentials([ + string(credentialsId: 'jenkins-aws-secret', variable: 'AWS_SECRET_ACCESS_KEY'), + string(credentialsId: 'jenkins-aws-id', variable: 'AWS_ACCESS_KEY_ID') + ]) { + sh """#!/bin/bash + export SELENIUM_GRID_IMAGE_VERSION="${imageTag}" + GRID=${env.PROD_GRID_NAME} scripts/deploy-nomad-selenium-grid-hub.sh + GRID=${env.PROD_GRID_NAME} scripts/deploy-nomad-selenium-grid-node.sh + RET=\$? + if [[ \$RET -eq 2 ]]; then + echo "Complete but failed to place all allocations (expected)" + exit 0 + else + exit \$RET + fi + """ + } + } + } + } + } + } +} diff --git a/jenkins/jobs/release-selenium-grid-node.yaml b/jenkins/jobs/release-selenium-grid-node.yaml new file mode 100644 index 000000000..fd7854839 --- /dev/null +++ b/jenkins/jobs/release-selenium-grid-node.yaml @@ -0,0 +1,68 @@ +- job: + name: release-selenium-grid-node + display-name: release selenium grid node + concurrent: true + parameters: + - string: + name: VIDEO_INFRA_BRANCH + default: main + description: "Controls checkout branch for infra repos, defaults to 'main'." + trim: true + - string: + name: TAG + description: "Docker image tag; defaults to git short hash if not set." + trim: true + - string: + name: ENVIRONMENT + default: torture-test + description: "Environment to deploy in, defaults to 'torture-test'." + trim: true + - string: + name: ORACLE_REGION + default: us-phoenix-1 + description: "Oracle Region to deploy in" + trim: true + - string: + name: TEST_GRID_NAME + default: aaron + description: "Grid name for initial test deployment" + trim: true + - string: + name: PROD_GRID_NAME + default: validate + description: "Grid name for production deployment after smoke test passes" + trim: true + - bool: + name: SKIP_BUILD + description: "Check to skip the Docker image build (use when deploying an existing tag)" + default: false + - string: + name: SELENIUM_GRID_HUB_VERSION + description: "Selenium Grid Hub version (e.g., '4.41'). If empty, latest is resolved from Docker Hub." + trim: true + - string: + name: INFRA_CONFIGURATION_REPO + default: git@github.com:jitsi/infra-configuration.git + description: "Repo for configuration code (ansible etc), defaults to 'git@github.com:jitsi/infra-configuration.git'." + trim: true + - string: + name: INFRA_CUSTOMIZATIONS_REPO + default: git@github.com:jitsi/infra-customizations.git + description: "Repo with customized configurations, defaults to 'git@github.com:jitsi/infra-customizations.git'." + trim: true + + project-type: pipeline + sandbox: true + pipeline-scm: + scm: + - git: + url: git@github.com:jitsi/infra-provisioning.git + credentials-id: "video-infra" + branches: + - "origin/${{VIDEO_INFRA_BRANCH}}" + browser: githubweb + browser-url: https://github.com/jitsi/infra-provisioning + submodule: + recursive: true + script-path: jenkins/groovy/release-selenium-grid-node/Jenkinsfile + lightweight-checkout: true diff --git a/scripts/test-selenium-grid-browsers.sh b/scripts/test-selenium-grid-browsers.sh new file mode 100755 index 000000000..201119f7a --- /dev/null +++ b/scripts/test-selenium-grid-browsers.sh @@ -0,0 +1,109 @@ +#!/bin/bash +# +# Smoke test for a selenium grid: creates Chrome and Firefox sessions +# via the W3C WebDriver API, then cleans them up. +# +# Required env vars (or pass GRID_URL directly): +# GRID_URL - full WebDriver hub URL (e.g. https://...:4444/wd/hub) +# OR all of: ENVIRONMENT, ORACLE_REGION, GRID +# +# Optional: +# RETRY_ATTEMPTS - number of retries waiting for grid readiness (default: 10) +# RETRY_DELAY - seconds between retries (default: 30) + +set -euo pipefail + +LOCAL_PATH=$(dirname "${BASH_SOURCE[0]}") + +# Source environment configs if needed to resolve DNS zone +[ -e "$LOCAL_PATH/../clouds/all.sh" ] && . "$LOCAL_PATH/../clouds/all.sh" + +# Build GRID_URL from components if not provided directly +if [ -z "${GRID_URL:-}" ]; then + if [ -z "${ENVIRONMENT:-}" ] || [ -z "${ORACLE_REGION:-}" ] || [ -z "${GRID:-}" ]; then + echo "ERROR: Either GRID_URL or (ENVIRONMENT, ORACLE_REGION, GRID) must be set" + exit 2 + fi + DNS_ZONE="${TOP_LEVEL_DNS_ZONE_NAME:-jitsi.net}" + GRID_URL="https://${ENVIRONMENT}-${ORACLE_REGION}-${GRID}-grid.${DNS_ZONE}/wd/hub" +fi + +echo "## Selenium Grid smoke test against: $GRID_URL" + +RETRY_ATTEMPTS="${RETRY_ATTEMPTS:-10}" +RETRY_DELAY="${RETRY_DELAY:-30}" + +# Wait for grid to become ready +echo "## Waiting for grid to become ready..." +for i in $(seq 1 "$RETRY_ATTEMPTS"); do + STATUS=$(curl -sf --connect-timeout 10 --max-time 30 "${GRID_URL}/status" 2>/dev/null || true) + if [ -n "$STATUS" ]; then + READY=$(echo "$STATUS" | jq -r '.value.ready // false' 2>/dev/null || echo "false") + if [ "$READY" = "true" ]; then + echo "## Grid is ready (attempt $i/$RETRY_ATTEMPTS)" + break + fi + fi + if [ "$i" -eq "$RETRY_ATTEMPTS" ]; then + echo "ERROR: Grid not ready after $RETRY_ATTEMPTS attempts" + exit 1 + fi + echo "## Grid not ready, retrying in ${RETRY_DELAY}s (attempt $i/$RETRY_ATTEMPTS)..." + sleep "$RETRY_DELAY" +done + +SUCCESS=0 + +# Test a browser session: create, verify, delete +# Usage: test_browser +test_browser() { + local BROWSER="$1" + local CAPS="$2" + + echo "## Testing $BROWSER session..." + + RESPONSE=$(curl -sf --connect-timeout 30 --max-time 120 \ + -X POST "${GRID_URL}/session" \ + -H "Content-Type: application/json" \ + -d "$CAPS" 2>&1) || { + echo "ERROR: Failed to create $BROWSER session" + echo "$RESPONSE" + return 1 + } + + SESSION_ID=$(echo "$RESPONSE" | jq -r '.value.sessionId // empty' 2>/dev/null) + if [ -z "$SESSION_ID" ]; then + echo "ERROR: No session ID returned for $BROWSER" + echo "$RESPONSE" + return 1 + fi + + echo "## $BROWSER session created: $SESSION_ID" + + # Clean up session + curl -sf --connect-timeout 10 --max-time 30 \ + -X DELETE "${GRID_URL}/session/${SESSION_ID}" > /dev/null 2>&1 || true + + echo "## $BROWSER session deleted successfully" + return 0 +} + +# Test Chrome +CHROME_CAPS='{"capabilities":{"alwaysMatch":{"browserName":"chrome","goog:chromeOptions":{"args":["--headless","--no-sandbox","--disable-dev-shm-usage"]}}}}' +if ! test_browser "Chrome" "$CHROME_CAPS"; then + SUCCESS=1 +fi + +# Test Firefox +FIREFOX_CAPS='{"capabilities":{"alwaysMatch":{"browserName":"firefox","moz:firefoxOptions":{"args":["-headless"]}}}}' +if ! test_browser "Firefox" "$FIREFOX_CAPS"; then + SUCCESS=1 +fi + +if [ $SUCCESS -eq 0 ]; then + echo "## Smoke test PASSED: Chrome and Firefox sessions verified" +else + echo "## Smoke test FAILED" +fi + +exit $SUCCESS