From d841c5421f0ef52c639198eb66b40775eb43fab9 Mon Sep 17 00:00:00 2001 From: Vincent Ahrend Date: Thu, 11 Sep 2025 16:11:41 +0200 Subject: [PATCH 1/6] feat: configure websocket URL for React Native app --- react-native/App.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/react-native/App.tsx b/react-native/App.tsx index 2d15f2ffc..44bbe5043 100644 --- a/react-native/App.tsx +++ b/react-native/App.tsx @@ -22,6 +22,7 @@ import { DITTO_APP_ID, DITTO_PLAYGROUND_TOKEN, DITTO_AUTH_URL, + DITTO_WEBSOCKET_URL, } from '@env'; import Fab from './components/Fab'; @@ -132,6 +133,11 @@ const App = () => { ditto.current = await Ditto.open(config); + // Configure websocket URL for transport + ditto.current.updateTransportConfig((transportConfig) => { + transportConfig.connect.websocketURLs = [DITTO_WEBSOCKET_URL]; + }); + if (connectConfig.mode === 'server') { await ditto.current.auth.setExpirationHandler(async (dittoInstance, timeUntilExpiration) => { console.log('Authentication expiring soon, time until expiration:', timeUntilExpiration); From b16a088c704b62e41eae08c9e45860f973c77c2b Mon Sep 17 00:00:00 2001 From: Vincent Ahrend Date: Thu, 11 Sep 2025 16:14:26 +0200 Subject: [PATCH 2/6] feat: add optional websocket URL input to BrowserStack workflows - Add workflow_dispatch input for custom websocket URL to: - android-kotlin-ci.yml - swift-ci.yml - javascript-web-browserstack.yml - android-cpp-browserstack.yml - Default to existing secrets when input not provided - Enables testing new websocket server versions via manual workflow dispatch Note: React Native app was also updated to use DITTO_WEBSOCKET_URL environment variable (was previously defined but not used) --- .github/workflows/android-cpp-browserstack.yml | 7 ++++++- .github/workflows/android-kotlin-ci.yml | 9 +++++++-- .github/workflows/javascript-web-browserstack.yml | 7 ++++++- .github/workflows/swift-ci.yml | 11 ++++++++--- 4 files changed, 27 insertions(+), 7 deletions(-) diff --git a/.github/workflows/android-cpp-browserstack.yml b/.github/workflows/android-cpp-browserstack.yml index dc98833d1..1bf960539 100644 --- a/.github/workflows/android-cpp-browserstack.yml +++ b/.github/workflows/android-cpp-browserstack.yml @@ -21,6 +21,11 @@ on: - 'android-cpp/**' - '.github/workflows/android-cpp-browserstack.yml' workflow_dispatch: # Allow manual trigger + inputs: + websocket_url: + description: 'Custom Ditto websocket URL (optional, defaults to secret)' + required: false + type: string concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -52,7 +57,7 @@ jobs: echo "DITTO_APP_ID=${{ secrets.DITTO_APP_ID }}" > .env echo "DITTO_PLAYGROUND_TOKEN=${{ secrets.DITTO_PLAYGROUND_TOKEN }}" >> .env echo "DITTO_AUTH_URL=${{ secrets.DITTO_AUTH_URL }}" >> .env - echo "DITTO_WEBSOCKET_URL=${{ secrets.DITTO_WEBSOCKET_URL }}" >> .env + echo "DITTO_WEBSOCKET_URL=${{ github.event.inputs.websocket_url || secrets.DITTO_WEBSOCKET_URL }}" >> .env - name: Cache Gradle dependencies uses: actions/cache@v4 diff --git a/.github/workflows/android-kotlin-ci.yml b/.github/workflows/android-kotlin-ci.yml index 7d9b033c4..eef69c598 100644 --- a/.github/workflows/android-kotlin-ci.yml +++ b/.github/workflows/android-kotlin-ci.yml @@ -12,6 +12,11 @@ on: - 'android-kotlin/**' - '.github/workflows/android-kotlin-ci.yml' workflow_dispatch: + inputs: + websocket_url: + description: 'Custom Ditto websocket URL (optional, defaults to secret)' + required: false + type: string concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -109,7 +114,7 @@ jobs: DITTO_APP_ID: ${{ secrets.DITTO_APP_ID }} DITTO_PLAYGROUND_TOKEN: ${{ secrets.DITTO_PLAYGROUND_TOKEN }} DITTO_AUTH_URL: ${{ secrets.DITTO_AUTH_URL }} - DITTO_WEBSOCKET_URL: ${{ secrets.DITTO_WEBSOCKET_URL }} + DITTO_WEBSOCKET_URL: ${{ github.event.inputs.websocket_url || secrets.DITTO_WEBSOCKET_URL }} TEST_DOCUMENT_TITLE: ${{ steps.test_doc.outputs.test_doc_title }} run: ./gradlew assembleDebug assembleDebugAndroidTest @@ -119,7 +124,7 @@ jobs: DITTO_APP_ID: ${{ secrets.DITTO_APP_ID }} DITTO_PLAYGROUND_TOKEN: ${{ secrets.DITTO_PLAYGROUND_TOKEN }} DITTO_AUTH_URL: ${{ secrets.DITTO_AUTH_URL }} - DITTO_WEBSOCKET_URL: ${{ secrets.DITTO_WEBSOCKET_URL }} + DITTO_WEBSOCKET_URL: ${{ github.event.inputs.websocket_url || secrets.DITTO_WEBSOCKET_URL }} run: ./gradlew test - name: Upload APK artifacts diff --git a/.github/workflows/javascript-web-browserstack.yml b/.github/workflows/javascript-web-browserstack.yml index fc77be8cd..6045198b5 100644 --- a/.github/workflows/javascript-web-browserstack.yml +++ b/.github/workflows/javascript-web-browserstack.yml @@ -16,6 +16,11 @@ on: - 'javascript-web/**' - '.github/workflows/javascript-web-browserstack.yml' workflow_dispatch: + inputs: + websocket_url: + description: 'Custom Ditto websocket URL (optional, defaults to secret)' + required: false + type: string concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -42,7 +47,7 @@ jobs: echo "DITTO_APP_ID=${{ secrets.DITTO_APP_ID }}" > .env echo "DITTO_PLAYGROUND_TOKEN=${{ secrets.DITTO_PLAYGROUND_TOKEN }}" >> .env echo "DITTO_AUTH_URL=${{ secrets.DITTO_AUTH_URL }}" >> .env - echo "DITTO_WEBSOCKET_URL=${{ secrets.DITTO_WEBSOCKET_URL }}" >> .env + echo "DITTO_WEBSOCKET_URL=${{ github.event.inputs.websocket_url || secrets.DITTO_WEBSOCKET_URL }}" >> .env - name: Insert test document into Ditto Cloud run: | diff --git a/.github/workflows/swift-ci.yml b/.github/workflows/swift-ci.yml index 6eee7de6a..9550c5dd4 100644 --- a/.github/workflows/swift-ci.yml +++ b/.github/workflows/swift-ci.yml @@ -8,6 +8,11 @@ on: push: branches: [main, 'sdk-*'] workflow_dispatch: + inputs: + websocket_url: + description: 'Custom Ditto websocket URL (optional, defaults to secret)' + required: false + type: string concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -73,7 +78,7 @@ jobs: echo "DITTO_APP_ID=${{ secrets.DITTO_APP_ID }}" > .env echo "DITTO_PLAYGROUND_TOKEN=${{ secrets.DITTO_PLAYGROUND_TOKEN }}" >> .env echo "DITTO_AUTH_URL=${{ secrets.DITTO_AUTH_URL }}" >> .env - echo "DITTO_WEBSOCKET_URL=${{ secrets.DITTO_WEBSOCKET_URL }}" >> .env + echo "DITTO_WEBSOCKET_URL=${{ github.event.inputs.websocket_url || secrets.DITTO_WEBSOCKET_URL }}" >> .env - name: Generate Env.swift (production) working-directory: swift @@ -125,7 +130,7 @@ jobs: echo "DITTO_APP_ID=${{ secrets.DITTO_APP_ID }}" > .env echo "DITTO_PLAYGROUND_TOKEN=${{ secrets.DITTO_PLAYGROUND_TOKEN }}" >> .env echo "DITTO_AUTH_URL=${{ secrets.DITTO_AUTH_URL }}" >> .env - echo "DITTO_WEBSOCKET_URL=${{ secrets.DITTO_WEBSOCKET_URL }}" >> .env + echo "DITTO_WEBSOCKET_URL=${{ github.event.inputs.websocket_url || secrets.DITTO_WEBSOCKET_URL }}" >> .env - name: Generate Env.swift (production) working-directory: swift @@ -231,7 +236,7 @@ jobs: echo "DITTO_APP_ID=${{ secrets.DITTO_APP_ID }}" > .env echo "DITTO_PLAYGROUND_TOKEN=${{ secrets.DITTO_PLAYGROUND_TOKEN }}" >> .env echo "DITTO_AUTH_URL=${{ secrets.DITTO_AUTH_URL }}" >> .env - echo "DITTO_WEBSOCKET_URL=${{ secrets.DITTO_WEBSOCKET_URL }}" >> .env + echo "DITTO_WEBSOCKET_URL=${{ github.event.inputs.websocket_url || secrets.DITTO_WEBSOCKET_URL }}" >> .env - name: Generate Env.swift (production for XCUITest build) working-directory: swift From 97f8bfdede774158bd26e1327e64f58586a81098 Mon Sep 17 00:00:00 2001 From: Vincent Ahrend Date: Thu, 11 Sep 2025 16:48:51 +0200 Subject: [PATCH 3/6] Add dispatch script POC --- scripts/cloud-smoke-test.js | 280 ++++++++++++++++++++++++++++++++++++ 1 file changed, 280 insertions(+) create mode 100755 scripts/cloud-smoke-test.js diff --git a/scripts/cloud-smoke-test.js b/scripts/cloud-smoke-test.js new file mode 100755 index 000000000..74e7931e1 --- /dev/null +++ b/scripts/cloud-smoke-test.js @@ -0,0 +1,280 @@ +#!/usr/bin/env node + +/** + * Cloud Smoke Test Script + * + * Dispatches all BrowserStack workflows with a custom websocket URL + * and waits for completion, reporting results. + * + * Usage: node scripts/cloud-smoke-test.js + */ + +const { execSync, spawn } = require('child_process'); +const path = require('path'); + +// ANSI color codes for better output +const colors = { + reset: '\x1b[0m', + bright: '\x1b[1m', + red: '\x1b[31m', + green: '\x1b[32m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + magenta: '\x1b[35m', + cyan: '\x1b[36m' +}; + +const workflows = [ + 'android-kotlin-ci.yml', + 'swift-ci.yml', + 'javascript-web-browserstack.yml', + 'android-cpp-browserstack.yml' +]; + +function log(message, color = colors.reset) { + console.log(`${color}${message}${colors.reset}`); +} + +function execCommand(command, options = {}) { + try { + return execSync(command, { + encoding: 'utf8', + stdio: options.silent ? 'pipe' : 'inherit', + ...options + }).trim(); + } catch (error) { + if (!options.allowFailure) { + log(`❌ Command failed: ${command}`, colors.red); + log(error.message, colors.red); + process.exit(1); + } + return null; + } +} + +function validateWebsocketUrl(url) { + try { + const parsed = new URL(url); + if (!['ws:', 'wss:'].includes(parsed.protocol)) { + throw new Error('URL must use ws:// or wss:// protocol'); + } + return true; + } catch (error) { + log(`❌ Invalid websocket URL: ${error.message}`, colors.red); + return false; + } +} + +function getCurrentBranch() { + return execCommand('git branch --show-current', { silent: true }); +} + +function dispatchWorkflow(workflow, websocketUrl, branch) { + log(`📤 Dispatching workflow: ${workflow}`, colors.blue); + + const command = `gh workflow run "${workflow}" --ref "${branch}" -f websocket_url="${websocketUrl}"`; + execCommand(command, { silent: true }); + + // Small delay to ensure workflow appears in listing + execCommand('sleep 2', { silent: true }); +} + +function getLatestWorkflowRun(workflow) { + const command = `gh run list --workflow="${workflow}" --limit=1 --json databaseId,status,conclusion,headBranch,createdAt`; + const output = execCommand(command, { silent: true }); + const runs = JSON.parse(output); + return runs.length > 0 ? runs[0] : null; +} + +function getWorkflowRunDetails(runId) { + const command = `gh run view ${runId} --json status,conclusion,url,workflowName,jobs`; + const output = execCommand(command, { silent: true }); + return JSON.parse(output); +} + +function waitForWorkflowCompletion(runIds, maxWaitMinutes = 45) { + log(`⏳ Waiting for ${runIds.length} workflows to complete (max ${maxWaitMinutes} minutes)...`, colors.yellow); + + const startTime = Date.now(); + const maxWaitMs = maxWaitMinutes * 60 * 1000; + const checkIntervalMs = 30 * 1000; // Check every 30 seconds + + let completed = new Set(); + let lastStatus = new Map(); + + while (completed.size < runIds.length && (Date.now() - startTime) < maxWaitMs) { + for (const runId of runIds) { + if (completed.has(runId)) continue; + + const details = getWorkflowRunDetails(runId); + const status = `${details.status}${details.conclusion ? `:${details.conclusion}` : ''}`; + + // Only log status changes + if (lastStatus.get(runId) !== status) { + log(` ${details.workflowName}: ${status}`, colors.cyan); + lastStatus.set(runId, status); + } + + if (details.status === 'completed') { + completed.add(runId); + } + } + + if (completed.size < runIds.length) { + // Sleep for check interval + execCommand(`sleep ${checkIntervalMs / 1000}`, { silent: true }); + } + } + + const elapsedMinutes = Math.round((Date.now() - startTime) / 60000); + + if (completed.size === runIds.length) { + log(`✅ All workflows completed after ${elapsedMinutes} minutes`, colors.green); + return true; + } else { + log(`⏰ Timeout after ${elapsedMinutes} minutes. ${completed.size}/${runIds.length} workflows completed`, colors.yellow); + return false; + } +} + +function main() { + const args = process.argv.slice(2); + + if (args.length !== 1) { + log('Usage: node scripts/cloud-smoke-test.js ', colors.red); + log('', colors.reset); + log('Example:', colors.yellow); + log(' node scripts/cloud-smoke-test.js wss://cloud.ditto.live/ws', colors.yellow); + process.exit(1); + } + + const websocketUrl = args[0]; + + log('🚀 Ditto Cloud Smoke Test', colors.bright); + log('========================', colors.bright); + log(''); + + // Validate websocket URL + if (!validateWebsocketUrl(websocketUrl)) { + process.exit(1); + } + + log(`📋 Websocket URL: ${websocketUrl}`, colors.magenta); + + // Check if gh CLI is available + try { + execCommand('gh --version', { silent: true }); + } catch { + log('❌ GitHub CLI (gh) is required but not found', colors.red); + log('Install it from: https://github.com/cli/cli#installation', colors.yellow); + process.exit(1); + } + + // Get current branch + const branch = getCurrentBranch(); + log(`🌿 Using branch: ${branch}`, colors.magenta); + log(''); + + // Dispatch all workflows + log('📤 Dispatching workflows...', colors.blue); + const dispatchedRuns = new Map(); + let dispatchFailures = 0; + + for (const workflow of workflows) { + // Get the run count before dispatch to identify our run + const beforeRuns = execCommand(`gh run list --workflow="${workflow}" --limit=1 --json databaseId`, { silent: true }); + const beforeCount = JSON.parse(beforeRuns).length; + + dispatchWorkflow(workflow, websocketUrl, branch); + + // Find the new run + let attempts = 0; + let newRun = null; + + while (attempts < 10 && !newRun) { + execCommand('sleep 2', { silent: true }); + const afterRuns = execCommand(`gh run list --workflow="${workflow}" --limit=5 --json databaseId,status,headBranch,createdAt`, { silent: true }); + const runs = JSON.parse(afterRuns); + + // Find the newest run for our branch + newRun = runs.find(run => + run.headBranch === branch && + new Date(run.createdAt) > new Date(Date.now() - 2 * 60 * 1000) // Within last 2 minutes + ); + + attempts++; + } + + if (newRun) { + dispatchedRuns.set(workflow, newRun.databaseId); + log(` ✅ ${workflow} → Run #${newRun.databaseId}`, colors.green); + } else { + log(` ❌ Failed to find dispatched run for ${workflow}`, colors.red); + dispatchFailures++; + } + } + + if (dispatchedRuns.size === 0) { + log('❌ No workflows were successfully dispatched', colors.red); + process.exit(1); + } + + log(''); + + // Wait for completion + const runIds = Array.from(dispatchedRuns.values()); + const allCompleted = waitForWorkflowCompletion(runIds); + + log(''); + log('📊 Final Results:', colors.bright); + log('================', colors.bright); + + let hasFailures = dispatchFailures > 0; + + for (const [workflow, runId] of dispatchedRuns.entries()) { + const details = getWorkflowRunDetails(runId); + const success = details.conclusion === 'success'; + const icon = success ? '✅' : '❌'; + const color = success ? colors.green : colors.red; + + if (!success) hasFailures = true; + + log(`${icon} ${workflow}: ${details.conclusion || details.status}`, color); + log(` URL: ${details.url}`, colors.cyan); + + if (!success && details.jobs) { + // Show failed jobs + const failedJobs = details.jobs.filter(job => job.conclusion === 'failure'); + if (failedJobs.length > 0) { + log(` Failed jobs:`, colors.red); + failedJobs.forEach(job => { + log(` - ${job.name}`, colors.red); + }); + } + } + log(''); + } + + if (!allCompleted) { + log('⚠️ Some workflows may still be running. Check the URLs above for latest status.', colors.yellow); + hasFailures = true; + } + + if (hasFailures) { + log('❌ Some tests failed. Check the workflow runs for details.', colors.red); + process.exit(1); + } else { + log('🎉 All smoke tests passed!', colors.green); + process.exit(0); + } +} + +if (require.main === module) { + try { + main(); + } catch (error) { + log(`💥 Unexpected error: ${error.message}`, colors.red); + console.error(error.stack); + process.exit(1); + } +} \ No newline at end of file From 85de3de1799fae0748c52eb611ed27f098069155 Mon Sep 17 00:00:00 2001 From: Vincent Ahrend Date: Thu, 11 Sep 2025 17:09:16 +0200 Subject: [PATCH 4/6] Add support for all config options --- .../workflows/android-cpp-browserstack.yml | 18 +- .github/workflows/android-kotlin-ci.yml | 24 ++- .../workflows/javascript-web-browserstack.yml | 18 +- .github/workflows/swift-ci.yml | 30 ++- scripts/cloud-smoke-test.js | 176 +++++++++++++++--- 5 files changed, 217 insertions(+), 49 deletions(-) diff --git a/.github/workflows/android-cpp-browserstack.yml b/.github/workflows/android-cpp-browserstack.yml index 1bf960539..931f9cd7a 100644 --- a/.github/workflows/android-cpp-browserstack.yml +++ b/.github/workflows/android-cpp-browserstack.yml @@ -26,6 +26,18 @@ on: description: 'Custom Ditto websocket URL (optional, defaults to secret)' required: false type: string + app_id: + description: 'Custom Ditto app ID (optional, defaults to secret)' + required: false + type: string + playground_token: + description: 'Custom Ditto playground token (optional, defaults to secret)' + required: false + type: string + auth_url: + description: 'Custom Ditto auth URL (optional, defaults to secret)' + required: false + type: string concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -54,9 +66,9 @@ jobs: - name: Create .env file run: | - echo "DITTO_APP_ID=${{ secrets.DITTO_APP_ID }}" > .env - echo "DITTO_PLAYGROUND_TOKEN=${{ secrets.DITTO_PLAYGROUND_TOKEN }}" >> .env - echo "DITTO_AUTH_URL=${{ secrets.DITTO_AUTH_URL }}" >> .env + echo "DITTO_APP_ID=${{ github.event.inputs.app_id || secrets.DITTO_APP_ID }}" > .env + echo "DITTO_PLAYGROUND_TOKEN=${{ github.event.inputs.playground_token || secrets.DITTO_PLAYGROUND_TOKEN }}" >> .env + echo "DITTO_AUTH_URL=${{ github.event.inputs.auth_url || secrets.DITTO_AUTH_URL }}" >> .env echo "DITTO_WEBSOCKET_URL=${{ github.event.inputs.websocket_url || secrets.DITTO_WEBSOCKET_URL }}" >> .env - name: Cache Gradle dependencies diff --git a/.github/workflows/android-kotlin-ci.yml b/.github/workflows/android-kotlin-ci.yml index eef69c598..3fb82c327 100644 --- a/.github/workflows/android-kotlin-ci.yml +++ b/.github/workflows/android-kotlin-ci.yml @@ -17,6 +17,18 @@ on: description: 'Custom Ditto websocket URL (optional, defaults to secret)' required: false type: string + app_id: + description: 'Custom Ditto app ID (optional, defaults to secret)' + required: false + type: string + playground_token: + description: 'Custom Ditto playground token (optional, defaults to secret)' + required: false + type: string + auth_url: + description: 'Custom Ditto auth URL (optional, defaults to secret)' + required: false + type: string concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -111,9 +123,9 @@ jobs: - name: Build APKs working-directory: android-kotlin/QuickStartTasks env: - DITTO_APP_ID: ${{ secrets.DITTO_APP_ID }} - DITTO_PLAYGROUND_TOKEN: ${{ secrets.DITTO_PLAYGROUND_TOKEN }} - DITTO_AUTH_URL: ${{ secrets.DITTO_AUTH_URL }} + DITTO_APP_ID: ${{ github.event.inputs.app_id || secrets.DITTO_APP_ID }} + DITTO_PLAYGROUND_TOKEN: ${{ github.event.inputs.playground_token || secrets.DITTO_PLAYGROUND_TOKEN }} + DITTO_AUTH_URL: ${{ github.event.inputs.auth_url || secrets.DITTO_AUTH_URL }} DITTO_WEBSOCKET_URL: ${{ github.event.inputs.websocket_url || secrets.DITTO_WEBSOCKET_URL }} TEST_DOCUMENT_TITLE: ${{ steps.test_doc.outputs.test_doc_title }} run: ./gradlew assembleDebug assembleDebugAndroidTest @@ -121,9 +133,9 @@ jobs: - name: Run unit tests working-directory: android-kotlin/QuickStartTasks env: - DITTO_APP_ID: ${{ secrets.DITTO_APP_ID }} - DITTO_PLAYGROUND_TOKEN: ${{ secrets.DITTO_PLAYGROUND_TOKEN }} - DITTO_AUTH_URL: ${{ secrets.DITTO_AUTH_URL }} + DITTO_APP_ID: ${{ github.event.inputs.app_id || secrets.DITTO_APP_ID }} + DITTO_PLAYGROUND_TOKEN: ${{ github.event.inputs.playground_token || secrets.DITTO_PLAYGROUND_TOKEN }} + DITTO_AUTH_URL: ${{ github.event.inputs.auth_url || secrets.DITTO_AUTH_URL }} DITTO_WEBSOCKET_URL: ${{ github.event.inputs.websocket_url || secrets.DITTO_WEBSOCKET_URL }} run: ./gradlew test diff --git a/.github/workflows/javascript-web-browserstack.yml b/.github/workflows/javascript-web-browserstack.yml index 6045198b5..b3c11b828 100644 --- a/.github/workflows/javascript-web-browserstack.yml +++ b/.github/workflows/javascript-web-browserstack.yml @@ -21,6 +21,18 @@ on: description: 'Custom Ditto websocket URL (optional, defaults to secret)' required: false type: string + app_id: + description: 'Custom Ditto app ID (optional, defaults to secret)' + required: false + type: string + playground_token: + description: 'Custom Ditto playground token (optional, defaults to secret)' + required: false + type: string + auth_url: + description: 'Custom Ditto auth URL (optional, defaults to secret)' + required: false + type: string concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -44,9 +56,9 @@ jobs: - name: Create .env file run: | - echo "DITTO_APP_ID=${{ secrets.DITTO_APP_ID }}" > .env - echo "DITTO_PLAYGROUND_TOKEN=${{ secrets.DITTO_PLAYGROUND_TOKEN }}" >> .env - echo "DITTO_AUTH_URL=${{ secrets.DITTO_AUTH_URL }}" >> .env + echo "DITTO_APP_ID=${{ github.event.inputs.app_id || secrets.DITTO_APP_ID }}" > .env + echo "DITTO_PLAYGROUND_TOKEN=${{ github.event.inputs.playground_token || secrets.DITTO_PLAYGROUND_TOKEN }}" >> .env + echo "DITTO_AUTH_URL=${{ github.event.inputs.auth_url || secrets.DITTO_AUTH_URL }}" >> .env echo "DITTO_WEBSOCKET_URL=${{ github.event.inputs.websocket_url || secrets.DITTO_WEBSOCKET_URL }}" >> .env - name: Insert test document into Ditto Cloud diff --git a/.github/workflows/swift-ci.yml b/.github/workflows/swift-ci.yml index 9550c5dd4..724c72fb5 100644 --- a/.github/workflows/swift-ci.yml +++ b/.github/workflows/swift-ci.yml @@ -13,6 +13,18 @@ on: description: 'Custom Ditto websocket URL (optional, defaults to secret)' required: false type: string + app_id: + description: 'Custom Ditto app ID (optional, defaults to secret)' + required: false + type: string + playground_token: + description: 'Custom Ditto playground token (optional, defaults to secret)' + required: false + type: string + auth_url: + description: 'Custom Ditto auth URL (optional, defaults to secret)' + required: false + type: string concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -75,9 +87,9 @@ jobs: - name: Create .env file (production credentials) run: | - echo "DITTO_APP_ID=${{ secrets.DITTO_APP_ID }}" > .env - echo "DITTO_PLAYGROUND_TOKEN=${{ secrets.DITTO_PLAYGROUND_TOKEN }}" >> .env - echo "DITTO_AUTH_URL=${{ secrets.DITTO_AUTH_URL }}" >> .env + echo "DITTO_APP_ID=${{ github.event.inputs.app_id || secrets.DITTO_APP_ID }}" > .env + echo "DITTO_PLAYGROUND_TOKEN=${{ github.event.inputs.playground_token || secrets.DITTO_PLAYGROUND_TOKEN }}" >> .env + echo "DITTO_AUTH_URL=${{ github.event.inputs.auth_url || secrets.DITTO_AUTH_URL }}" >> .env echo "DITTO_WEBSOCKET_URL=${{ github.event.inputs.websocket_url || secrets.DITTO_WEBSOCKET_URL }}" >> .env - name: Generate Env.swift (production) @@ -127,9 +139,9 @@ jobs: - name: Create .env file (production credentials) run: | - echo "DITTO_APP_ID=${{ secrets.DITTO_APP_ID }}" > .env - echo "DITTO_PLAYGROUND_TOKEN=${{ secrets.DITTO_PLAYGROUND_TOKEN }}" >> .env - echo "DITTO_AUTH_URL=${{ secrets.DITTO_AUTH_URL }}" >> .env + echo "DITTO_APP_ID=${{ github.event.inputs.app_id || secrets.DITTO_APP_ID }}" > .env + echo "DITTO_PLAYGROUND_TOKEN=${{ github.event.inputs.playground_token || secrets.DITTO_PLAYGROUND_TOKEN }}" >> .env + echo "DITTO_AUTH_URL=${{ github.event.inputs.auth_url || secrets.DITTO_AUTH_URL }}" >> .env echo "DITTO_WEBSOCKET_URL=${{ github.event.inputs.websocket_url || secrets.DITTO_WEBSOCKET_URL }}" >> .env - name: Generate Env.swift (production) @@ -233,9 +245,9 @@ jobs: - name: Create .env file (production credentials for BrowserStack API) run: | - echo "DITTO_APP_ID=${{ secrets.DITTO_APP_ID }}" > .env - echo "DITTO_PLAYGROUND_TOKEN=${{ secrets.DITTO_PLAYGROUND_TOKEN }}" >> .env - echo "DITTO_AUTH_URL=${{ secrets.DITTO_AUTH_URL }}" >> .env + echo "DITTO_APP_ID=${{ github.event.inputs.app_id || secrets.DITTO_APP_ID }}" > .env + echo "DITTO_PLAYGROUND_TOKEN=${{ github.event.inputs.playground_token || secrets.DITTO_PLAYGROUND_TOKEN }}" >> .env + echo "DITTO_AUTH_URL=${{ github.event.inputs.auth_url || secrets.DITTO_AUTH_URL }}" >> .env echo "DITTO_WEBSOCKET_URL=${{ github.event.inputs.websocket_url || secrets.DITTO_WEBSOCKET_URL }}" >> .env - name: Generate Env.swift (production for XCUITest build) diff --git a/scripts/cloud-smoke-test.js b/scripts/cloud-smoke-test.js index 74e7931e1..605734f68 100755 --- a/scripts/cloud-smoke-test.js +++ b/scripts/cloud-smoke-test.js @@ -3,10 +3,23 @@ /** * Cloud Smoke Test Script * - * Dispatches all BrowserStack workflows with a custom websocket URL + * Dispatches all BrowserStack workflows with custom Ditto configuration * and waits for completion, reporting results. * - * Usage: node scripts/cloud-smoke-test.js + * Usage: node scripts/cloud-smoke-test.js [options] + * + * Options: + * --websocket-url Custom websocket URL (optional, defaults to env var) + * --app-id Custom app ID (optional, defaults to env var) + * --playground-token Custom playground token (optional, defaults to env var) + * --auth-url Custom auth URL (optional, defaults to env var) + * --help Show this help message + * + * Environment variables (used as defaults): + * DITTO_WEBSOCKET_URL Default websocket URL + * DITTO_APP_ID Default app ID + * DITTO_PLAYGROUND_TOKEN Default playground token + * DITTO_AUTH_URL Default auth URL */ const { execSync, spawn } = require('child_process'); @@ -52,27 +65,134 @@ function execCommand(command, options = {}) { } } -function validateWebsocketUrl(url) { - try { - const parsed = new URL(url); - if (!['ws:', 'wss:'].includes(parsed.protocol)) { - throw new Error('URL must use ws:// or wss:// protocol'); +function parseArguments() { + const args = process.argv.slice(2); + const config = { + websocketUrl: process.env.DITTO_WEBSOCKET_URL, + appId: process.env.DITTO_APP_ID, + playgroundToken: process.env.DITTO_PLAYGROUND_TOKEN, + authUrl: process.env.DITTO_AUTH_URL + }; + + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + + if (arg === '--help' || arg === '-h') { + showHelp(); + process.exit(0); + } else if (arg === '--websocket-url') { + config.websocketUrl = args[++i]; + } else if (arg === '--app-id') { + config.appId = args[++i]; + } else if (arg === '--playground-token') { + config.playgroundToken = args[++i]; + } else if (arg === '--auth-url') { + config.authUrl = args[++i]; + } else { + log(`❌ Unknown argument: ${arg}`, colors.red); + log('Use --help for usage information', colors.yellow); + process.exit(1); } - return true; - } catch (error) { - log(`❌ Invalid websocket URL: ${error.message}`, colors.red); - return false; } + + return config; +} + +function showHelp() { + console.log(` +🚭 Ditto Cloud Smoke Test + +Dispatches all BrowserStack workflows with custom Ditto configuration +and waits for completion, reporting results. + +Usage: node scripts/cloud-smoke-test.js [options] + +Options: + --websocket-url Custom websocket URL (optional, defaults to env var) + --app-id Custom app ID (optional, defaults to env var) + --playground-token Custom playground token (optional, defaults to env var) + --auth-url Custom auth URL (optional, defaults to env var) + --help, -h Show this help message + +Environment variables (used as defaults): + DITTO_WEBSOCKET_URL Default websocket URL + DITTO_APP_ID Default app ID + DITTO_PLAYGROUND_TOKEN Default playground token + DITTO_AUTH_URL Default auth URL + +Examples: + # Test with all defaults from environment variables + node scripts/cloud-smoke-test.js + + # Test with custom websocket URL only + node scripts/cloud-smoke-test.js --websocket-url wss://test.example.com/ws + + # Test with custom websocket URL and app ID + node scripts/cloud-smoke-test.js --websocket-url wss://test.example.com/ws --app-id my-app-id + + # Test with all custom values + node scripts/cloud-smoke-test.js \\ + --websocket-url wss://test.example.com/ws \\ + --app-id my-app-id \\ + --playground-token my-token \\ + --auth-url https://auth.example.com +`); +} + +function validateConfig(config) { + // Validate websocket URL format if provided + if (config.websocketUrl) { + try { + const parsed = new URL(config.websocketUrl); + if (!['ws:', 'wss:'].includes(parsed.protocol)) { + throw new Error('URL must use ws:// or wss:// protocol'); + } + } catch (error) { + log(`❌ Invalid websocket URL: ${error.message}`, colors.red); + return false; + } + } + + // Warn about missing values - all are optional now + if (!config.websocketUrl) { + log('⚠️ No websocket URL specified (using workflow default)', colors.yellow); + } + if (!config.appId) { + log('⚠️ No app ID specified (using workflow default)', colors.yellow); + } + if (!config.playgroundToken) { + log('⚠️ No playground token specified (using workflow default)', colors.yellow); + } + if (!config.authUrl) { + log('⚠️ No auth URL specified (using workflow default)', colors.yellow); + } + + return true; } function getCurrentBranch() { return execCommand('git branch --show-current', { silent: true }); } -function dispatchWorkflow(workflow, websocketUrl, branch) { +function dispatchWorkflow(workflow, config, branch) { log(`📤 Dispatching workflow: ${workflow}`, colors.blue); - const command = `gh workflow run "${workflow}" --ref "${branch}" -f websocket_url="${websocketUrl}"`; + let command = `gh workflow run "${workflow}" --ref "${branch}"`; + + // Add optional parameters if provided + if (config.websocketUrl) { + command += ` -f websocket_url="${config.websocketUrl}"`; + } + if (config.appId) { + command += ` -f app_id="${config.appId}"`; + } + if (config.playgroundToken) { + command += ` -f playground_token="${config.playgroundToken}"`; + } + if (config.authUrl) { + command += ` -f auth_url="${config.authUrl}"`; + } + execCommand(command, { silent: true }); // Small delay to ensure workflow appears in listing @@ -138,28 +258,28 @@ function waitForWorkflowCompletion(runIds, maxWaitMinutes = 45) { } function main() { - const args = process.argv.slice(2); - - if (args.length !== 1) { - log('Usage: node scripts/cloud-smoke-test.js ', colors.red); - log('', colors.reset); - log('Example:', colors.yellow); - log(' node scripts/cloud-smoke-test.js wss://cloud.ditto.live/ws', colors.yellow); - process.exit(1); - } - - const websocketUrl = args[0]; + const config = parseArguments(); log('🚀 Ditto Cloud Smoke Test', colors.bright); log('========================', colors.bright); log(''); - // Validate websocket URL - if (!validateWebsocketUrl(websocketUrl)) { + // Validate configuration + if (!validateConfig(config)) { process.exit(1); } - log(`📋 Websocket URL: ${websocketUrl}`, colors.magenta); + log('📋 Configuration:', colors.magenta); + log(` Websocket URL: ${config.websocketUrl}`, colors.magenta); + if (config.appId) { + log(` App ID: ${config.appId}`, colors.magenta); + } + if (config.playgroundToken) { + log(` Playground Token: ${config.playgroundToken.substring(0, 8)}...`, colors.magenta); + } + if (config.authUrl) { + log(` Auth URL: ${config.authUrl}`, colors.magenta); + } // Check if gh CLI is available try { @@ -185,7 +305,7 @@ function main() { const beforeRuns = execCommand(`gh run list --workflow="${workflow}" --limit=1 --json databaseId`, { silent: true }); const beforeCount = JSON.parse(beforeRuns).length; - dispatchWorkflow(workflow, websocketUrl, branch); + dispatchWorkflow(workflow, config, branch); // Find the new run let attempts = 0; From 00827c5e49eb40b583b696da5653e80ca61d115e Mon Sep 17 00:00:00 2001 From: Vincent Ahrend Date: Thu, 11 Sep 2025 17:14:09 +0200 Subject: [PATCH 5/6] Format code --- scripts/cloud-smoke-test.js | 260 +++++++++++++++++++++--------------- 1 file changed, 149 insertions(+), 111 deletions(-) diff --git a/scripts/cloud-smoke-test.js b/scripts/cloud-smoke-test.js index 605734f68..211c7efd4 100755 --- a/scripts/cloud-smoke-test.js +++ b/scripts/cloud-smoke-test.js @@ -2,19 +2,19 @@ /** * Cloud Smoke Test Script - * + * * Dispatches all BrowserStack workflows with custom Ditto configuration * and waits for completion, reporting results. - * + * * Usage: node scripts/cloud-smoke-test.js [options] - * + * * Options: * --websocket-url Custom websocket URL (optional, defaults to env var) * --app-id Custom app ID (optional, defaults to env var) * --playground-token Custom playground token (optional, defaults to env var) * --auth-url Custom auth URL (optional, defaults to env var) * --help Show this help message - * + * * Environment variables (used as defaults): * DITTO_WEBSOCKET_URL Default websocket URL * DITTO_APP_ID Default app ID @@ -22,26 +22,26 @@ * DITTO_AUTH_URL Default auth URL */ -const { execSync, spawn } = require('child_process'); -const path = require('path'); +const { execSync, spawn } = require("child_process"); +const path = require("path"); // ANSI color codes for better output const colors = { - reset: '\x1b[0m', - bright: '\x1b[1m', - red: '\x1b[31m', - green: '\x1b[32m', - yellow: '\x1b[33m', - blue: '\x1b[34m', - magenta: '\x1b[35m', - cyan: '\x1b[36m' + reset: "\x1b[0m", + bright: "\x1b[1m", + red: "\x1b[31m", + green: "\x1b[32m", + yellow: "\x1b[33m", + blue: "\x1b[34m", + magenta: "\x1b[35m", + cyan: "\x1b[36m", }; const workflows = [ - 'android-kotlin-ci.yml', - 'swift-ci.yml', - 'javascript-web-browserstack.yml', - 'android-cpp-browserstack.yml' + "android-kotlin-ci.yml", + "swift-ci.yml", + "javascript-web-browserstack.yml", + "android-cpp-browserstack.yml", ]; function log(message, color = colors.reset) { @@ -50,10 +50,10 @@ function log(message, color = colors.reset) { function execCommand(command, options = {}) { try { - return execSync(command, { - encoding: 'utf8', - stdio: options.silent ? 'pipe' : 'inherit', - ...options + return execSync(command, { + encoding: "utf8", + stdio: options.silent ? "pipe" : "inherit", + ...options, }).trim(); } catch (error) { if (!options.allowFailure) { @@ -71,26 +71,26 @@ function parseArguments() { websocketUrl: process.env.DITTO_WEBSOCKET_URL, appId: process.env.DITTO_APP_ID, playgroundToken: process.env.DITTO_PLAYGROUND_TOKEN, - authUrl: process.env.DITTO_AUTH_URL + authUrl: process.env.DITTO_AUTH_URL, }; for (let i = 0; i < args.length; i++) { const arg = args[i]; - - if (arg === '--help' || arg === '-h') { + + if (arg === "--help" || arg === "-h") { showHelp(); process.exit(0); - } else if (arg === '--websocket-url') { + } else if (arg === "--websocket-url") { config.websocketUrl = args[++i]; - } else if (arg === '--app-id') { + } else if (arg === "--app-id") { config.appId = args[++i]; - } else if (arg === '--playground-token') { + } else if (arg === "--playground-token") { config.playgroundToken = args[++i]; - } else if (arg === '--auth-url') { + } else if (arg === "--auth-url") { config.authUrl = args[++i]; } else { log(`❌ Unknown argument: ${arg}`, colors.red); - log('Use --help for usage information', colors.yellow); + log("Use --help for usage information", colors.yellow); process.exit(1); } } @@ -144,8 +144,8 @@ function validateConfig(config) { if (config.websocketUrl) { try { const parsed = new URL(config.websocketUrl); - if (!['ws:', 'wss:'].includes(parsed.protocol)) { - throw new Error('URL must use ws:// or wss:// protocol'); + if (!["ws:", "wss:"].includes(parsed.protocol)) { + throw new Error("URL must use ws:// or wss:// protocol"); } } catch (error) { log(`❌ Invalid websocket URL: ${error.message}`, colors.red); @@ -155,30 +155,36 @@ function validateConfig(config) { // Warn about missing values - all are optional now if (!config.websocketUrl) { - log('⚠️ No websocket URL specified (using workflow default)', colors.yellow); + log( + "⚠️ No websocket URL specified (using workflow default)", + colors.yellow + ); } if (!config.appId) { - log('⚠️ No app ID specified (using workflow default)', colors.yellow); + log("⚠️ No app ID specified (using workflow default)", colors.yellow); } if (!config.playgroundToken) { - log('⚠️ No playground token specified (using workflow default)', colors.yellow); + log( + "⚠️ No playground token specified (using workflow default)", + colors.yellow + ); } if (!config.authUrl) { - log('⚠️ No auth URL specified (using workflow default)', colors.yellow); + log("⚠️ No auth URL specified (using workflow default)", colors.yellow); } return true; } function getCurrentBranch() { - return execCommand('git branch --show-current', { silent: true }); + return execCommand("git branch --show-current", { silent: true }); } function dispatchWorkflow(workflow, config, branch) { log(`📤 Dispatching workflow: ${workflow}`, colors.blue); - + let command = `gh workflow run "${workflow}" --ref "${branch}"`; - + // Add optional parameters if provided if (config.websocketUrl) { command += ` -f websocket_url="${config.websocketUrl}"`; @@ -192,11 +198,11 @@ function dispatchWorkflow(workflow, config, branch) { if (config.authUrl) { command += ` -f auth_url="${config.authUrl}"`; } - + execCommand(command, { silent: true }); - + // Small delay to ensure workflow appears in listing - execCommand('sleep 2', { silent: true }); + execCommand("sleep 2", { silent: true }); } function getLatestWorkflowRun(workflow) { @@ -213,118 +219,142 @@ function getWorkflowRunDetails(runId) { } function waitForWorkflowCompletion(runIds, maxWaitMinutes = 45) { - log(`⏳ Waiting for ${runIds.length} workflows to complete (max ${maxWaitMinutes} minutes)...`, colors.yellow); - + log( + `⏳ Waiting for ${runIds.length} workflows to complete (max ${maxWaitMinutes} minutes)...`, + colors.yellow + ); + const startTime = Date.now(); const maxWaitMs = maxWaitMinutes * 60 * 1000; const checkIntervalMs = 30 * 1000; // Check every 30 seconds - + let completed = new Set(); let lastStatus = new Map(); - - while (completed.size < runIds.length && (Date.now() - startTime) < maxWaitMs) { + + while (completed.size < runIds.length && Date.now() - startTime < maxWaitMs) { for (const runId of runIds) { if (completed.has(runId)) continue; - + const details = getWorkflowRunDetails(runId); - const status = `${details.status}${details.conclusion ? `:${details.conclusion}` : ''}`; - + const status = `${details.status}${ + details.conclusion ? `:${details.conclusion}` : "" + }`; + // Only log status changes if (lastStatus.get(runId) !== status) { log(` ${details.workflowName}: ${status}`, colors.cyan); lastStatus.set(runId, status); } - - if (details.status === 'completed') { + + if (details.status === "completed") { completed.add(runId); } } - + if (completed.size < runIds.length) { // Sleep for check interval execCommand(`sleep ${checkIntervalMs / 1000}`, { silent: true }); } } - + const elapsedMinutes = Math.round((Date.now() - startTime) / 60000); - + if (completed.size === runIds.length) { - log(`✅ All workflows completed after ${elapsedMinutes} minutes`, colors.green); + log( + `✅ All workflows completed after ${elapsedMinutes} minutes`, + colors.green + ); return true; } else { - log(`⏰ Timeout after ${elapsedMinutes} minutes. ${completed.size}/${runIds.length} workflows completed`, colors.yellow); + log( + `⏰ Timeout after ${elapsedMinutes} minutes. ${completed.size}/${runIds.length} workflows completed`, + colors.yellow + ); return false; } } function main() { const config = parseArguments(); - - log('🚀 Ditto Cloud Smoke Test', colors.bright); - log('========================', colors.bright); - log(''); - + + log("🚭 Ditto Cloud Smoke Test", colors.bright); + log("========================", colors.bright); + log(""); + // Validate configuration if (!validateConfig(config)) { process.exit(1); } - - log('📋 Configuration:', colors.magenta); + + log("📋 Configuration:", colors.magenta); log(` Websocket URL: ${config.websocketUrl}`, colors.magenta); if (config.appId) { log(` App ID: ${config.appId}`, colors.magenta); } if (config.playgroundToken) { - log(` Playground Token: ${config.playgroundToken.substring(0, 8)}...`, colors.magenta); + log( + ` Playground Token: ${config.playgroundToken.substring(0, 8)}...`, + colors.magenta + ); } if (config.authUrl) { log(` Auth URL: ${config.authUrl}`, colors.magenta); } - + // Check if gh CLI is available try { - execCommand('gh --version', { silent: true }); + execCommand("gh --version", { silent: true }); } catch { - log('❌ GitHub CLI (gh) is required but not found', colors.red); - log('Install it from: https://github.com/cli/cli#installation', colors.yellow); + log("❌ GitHub CLI (gh) is required but not found", colors.red); + log( + "Install it from: https://github.com/cli/cli#installation", + colors.yellow + ); process.exit(1); } - + // Get current branch const branch = getCurrentBranch(); log(`🌿 Using branch: ${branch}`, colors.magenta); - log(''); - + log(""); + // Dispatch all workflows - log('📤 Dispatching workflows...', colors.blue); + log("📤 Dispatching workflows...", colors.blue); const dispatchedRuns = new Map(); let dispatchFailures = 0; - + for (const workflow of workflows) { // Get the run count before dispatch to identify our run - const beforeRuns = execCommand(`gh run list --workflow="${workflow}" --limit=1 --json databaseId`, { silent: true }); + const beforeRuns = execCommand( + `gh run list --workflow="${workflow}" --limit=1 --json databaseId`, + { silent: true } + ); const beforeCount = JSON.parse(beforeRuns).length; - + dispatchWorkflow(workflow, config, branch); - + // Find the new run let attempts = 0; let newRun = null; - + while (attempts < 10 && !newRun) { - execCommand('sleep 2', { silent: true }); - const afterRuns = execCommand(`gh run list --workflow="${workflow}" --limit=5 --json databaseId,status,headBranch,createdAt`, { silent: true }); + execCommand("sleep 2", { silent: true }); + const afterRuns = execCommand( + `gh run list --workflow="${workflow}" --limit=5 --json databaseId,status,headBranch,createdAt`, + { silent: true } + ); const runs = JSON.parse(afterRuns); - + // Find the newest run for our branch - newRun = runs.find(run => - run.headBranch === branch && - new Date(run.createdAt) > new Date(Date.now() - 2 * 60 * 1000) // Within last 2 minutes + newRun = runs.find( + (run) => + run.headBranch === branch && + new Date(run.createdAt) > new Date(Date.now() - 2 * 60 * 1000) // Within last 2 minutes ); - + attempts++; } - + if (newRun) { dispatchedRuns.set(workflow, newRun.databaseId); log(` ✅ ${workflow} → Run #${newRun.databaseId}`, colors.green); @@ -333,58 +363,66 @@ function main() { dispatchFailures++; } } - + if (dispatchedRuns.size === 0) { - log('❌ No workflows were successfully dispatched', colors.red); + log("❌ No workflows were successfully dispatched", colors.red); process.exit(1); } - - log(''); - + + log(""); + // Wait for completion const runIds = Array.from(dispatchedRuns.values()); const allCompleted = waitForWorkflowCompletion(runIds); - - log(''); - log('📊 Final Results:', colors.bright); - log('================', colors.bright); - + + log(""); + log("📊 Final Results:", colors.bright); + log("================", colors.bright); + let hasFailures = dispatchFailures > 0; - + for (const [workflow, runId] of dispatchedRuns.entries()) { const details = getWorkflowRunDetails(runId); - const success = details.conclusion === 'success'; - const icon = success ? '✅' : '❌'; + const success = details.conclusion === "success"; + const icon = success ? "✅" : "❌"; const color = success ? colors.green : colors.red; - + if (!success) hasFailures = true; - + log(`${icon} ${workflow}: ${details.conclusion || details.status}`, color); log(` URL: ${details.url}`, colors.cyan); - + if (!success && details.jobs) { // Show failed jobs - const failedJobs = details.jobs.filter(job => job.conclusion === 'failure'); + const failedJobs = details.jobs.filter( + (job) => job.conclusion === "failure" + ); if (failedJobs.length > 0) { log(` Failed jobs:`, colors.red); - failedJobs.forEach(job => { + failedJobs.forEach((job) => { log(` - ${job.name}`, colors.red); }); } } - log(''); + log(""); } - + if (!allCompleted) { - log('⚠️ Some workflows may still be running. Check the URLs above for latest status.', colors.yellow); + log( + "⚠️ Some workflows may still be running. Check the URLs above for latest status.", + colors.yellow + ); hasFailures = true; } - + if (hasFailures) { - log('❌ Some tests failed. Check the workflow runs for details.', colors.red); + log( + "❌ Some tests failed. Check the workflow runs for details.", + colors.red + ); process.exit(1); } else { - log('🎉 All smoke tests passed!', colors.green); + log("🎉 All smoke tests passed!", colors.green); process.exit(0); } } @@ -397,4 +435,4 @@ if (require.main === module) { console.error(error.stack); process.exit(1); } -} \ No newline at end of file +} From 74d4897b0fcef596056d950d1692f45e1eb01c5e Mon Sep 17 00:00:00 2001 From: Vincent Ahrend Date: Wed, 17 Sep 2025 16:58:20 +0200 Subject: [PATCH 6/6] Show run URL in output --- scripts/cloud-smoke-test.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/scripts/cloud-smoke-test.js b/scripts/cloud-smoke-test.js index 211c7efd4..b24448036 100755 --- a/scripts/cloud-smoke-test.js +++ b/scripts/cloud-smoke-test.js @@ -323,6 +323,11 @@ function main() { const dispatchedRuns = new Map(); let dispatchFailures = 0; + // Get repo info once for URL generation + const repoInfo = JSON.parse( + execCommand("gh repo view --json owner,name", { silent: true }) + ); + for (const workflow of workflows) { // Get the run count before dispatch to identify our run const beforeRuns = execCommand( @@ -357,7 +362,8 @@ function main() { if (newRun) { dispatchedRuns.set(workflow, newRun.databaseId); - log(` ✅ ${workflow} → Run #${newRun.databaseId}`, colors.green); + const runUrl = `https://github.com/${repoInfo.owner.login}/${repoInfo.name}/actions/runs/${newRun.databaseId}`; + log(` ✅ ${workflow} → ${runUrl}`, colors.green); } else { log(` ❌ Failed to find dispatched run for ${workflow}`, colors.red); dispatchFailures++;