From 97515487e57368254b2b1e2b0e8aa85ce4c97347 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Fri, 17 Jul 2026 14:24:02 +0300 Subject: [PATCH] Improve E2E launch diagnostics and failure alerts --- .github/scripts/cleanup-nightly-e2e.sh | 1 + .github/scripts/run-nightly-e2e.sh | 7 +++ .github/scripts/summarize-maestro.mjs | 78 ++++++++++++++++++++++++-- __e2e__/setupApp.yml | 2 +- 4 files changed, 82 insertions(+), 6 deletions(-) diff --git a/.github/scripts/cleanup-nightly-e2e.sh b/.github/scripts/cleanup-nightly-e2e.sh index d15123403d..c37974de92 100755 --- a/.github/scripts/cleanup-nightly-e2e.sh +++ b/.github/scripts/cleanup-nightly-e2e.sh @@ -27,6 +27,7 @@ stop_pid_file() { stop_process_tree "$pid" } +stop_pid_file "$artifact_dir/logcat.pid" stop_pid_file "$artifact_dir/metro.pid" stop_pid_file "$artifact_dir/mock-server.pid" stop_pid_file "$artifact_dir/emulator.pid" diff --git a/.github/scripts/run-nightly-e2e.sh b/.github/scripts/run-nightly-e2e.sh index fedab57e93..e2f7afee4e 100755 --- a/.github/scripts/run-nightly-e2e.sh +++ b/.github/scripts/run-nightly-e2e.sh @@ -60,6 +60,7 @@ stop_pid_file() { # shellcheck disable=SC2329 # Invoked by the EXIT/INT/TERM trap below. cleanup() { set +e + stop_pid_file "$artifact_dir/logcat.pid" stop_pid_file "$artifact_dir/metro.pid" stop_pid_file "$artifact_dir/mock-server.pid" @@ -81,6 +82,12 @@ cleanup() { trap cleanup EXIT INT TERM +if [[ "$platform" == "android" ]]; then + adb -s "$device_id" logcat -c + adb -s "$device_id" logcat -v threadtime >"$artifact_dir/logcat.log" 2>&1 & + printf '%s\n' "$!" >"$artifact_dir/logcat.pid" +fi + phase "Starting PostgreSQL, Redis, and mock server" if [[ "$platform" == "ios" ]]; then brew install postgresql@14 2>&1 | tee "$artifact_dir/native-dependencies.log" diff --git a/.github/scripts/summarize-maestro.mjs b/.github/scripts/summarize-maestro.mjs index b7ccb4aa19..c022cfd523 100644 --- a/.github/scripts/summarize-maestro.mjs +++ b/.github/scripts/summarize-maestro.mjs @@ -75,6 +75,18 @@ export function parseJUnit(xml) { return failures } +export function parseMaestroCli(log) { + const failures = [] + const failurePattern = /^\[Failed\]\s+(.+?)\s+\([^)]*\)\s+\((.+)\)\s*$/gm + for (const match of log.matchAll(failurePattern)) { + failures.push({ + name: concise(match[1], 120), + message: concise(match[2]), + }) + } + return failures +} + function walk(root) { if (!root || !fs.existsSync(root)) return [] const entries = fs.readdirSync(root, {withFileTypes: true}) @@ -90,12 +102,20 @@ function readPhase(root) { } function platformResult({name, status, root, artifactUrl}) { - const reports = walk(root).filter(file => - /(?:report|junit).*\.xml$/i.test(file), - ) - const failures = reports.flatMap(report => + const files = walk(root) + const reports = files.filter(file => /(?:report|junit).*\.xml$/i.test(file)) + const junitFailures = reports.flatMap(report => parseJUnit(fs.readFileSync(report, 'utf8')), ) + const maestroLogs = files.filter( + file => path.basename(file) === 'maestro-cli.log', + ) + const cliFailures = maestroLogs.flatMap(log => + parseMaestroCli(fs.readFileSync(log, 'utf8')), + ) + // A cancelled or timed-out Maestro run may never flush JUnit. Its CLI log is + // streamed continuously, so use those failure lines when JUnit has no detail. + const failures = junitFailures.length > 0 ? junitFailures : cliFailures const failed = status !== 'success' || failures.length > 0 return { name, @@ -119,6 +139,36 @@ function slackEscape(value) { .replaceAll('>', '>') } +function platformBlock(platform) { + const lines = [ + `${statusEmoji(platform.status)} *${platform.name}* — job status: \`${platform.status}\``, + ] + if (platform.failures.length > 0) { + for (const failure of platform.failures.slice(0, 8)) { + lines.push( + `• *${slackEscape(failure.name)}:* ${slackEscape(failure.message)}`, + ) + } + if (platform.failures.length > 8) { + lines.push(`• …and ${platform.failures.length - 8} more failed flows`) + } + } else if (platform.failed && !platform.hasJUnit) { + lines.push( + `• *Setup phase:* ${slackEscape(platform.phase || 'No phase metadata was captured')}`, + ) + } else if (platform.failed) { + lines.push( + `• Job failed after JUnit was written; latest phase: ${slackEscape(platform.phase || 'unknown')}`, + ) + } + if (platform.artifactUrl) { + lines.push( + `• <${platform.artifactUrl}|Open ${platform.name} logs and artifacts>`, + ) + } + return lines.join('\n').slice(0, 3000) +} + export function buildSummary({ iosStatus, androidStatus, @@ -183,10 +233,28 @@ export function buildSummary({ } const text = lines.join('\n').trim() + const blocks = [ + { + type: 'header', + text: {type: 'plain_text', text: 'Nightly Maestro E2E failed'}, + }, + { + type: 'section', + text: { + type: 'mrkdwn', + text: `*Commit:* <${commitUrl}|\`${shortSha}\`>\n*Workflow run:* <${runUrl}|open run>`, + }, + }, + {type: 'divider'}, + ...platforms.flatMap((platform, index) => [ + {type: 'section', text: {type: 'mrkdwn', text: platformBlock(platform)}}, + ...(index < platforms.length - 1 ? [{type: 'divider'}] : []), + ]), + ] return { notify, platforms, - payload: {text}, + payload: {text, blocks}, } } diff --git a/__e2e__/setupApp.yml b/__e2e__/setupApp.yml index 01ba6703fc..ede257ca38 100644 --- a/__e2e__/setupApp.yml +++ b/__e2e__/setupApp.yml @@ -19,7 +19,7 @@ appId: xyz.blueskyweb.app when: platform: Android commands: - - openLink: "exp+bluesky://expo-development-client/?url=http%3A%2F%2Flocalhost%3A8081" + - openLink: "exp+bluesky://expo-development-client/?url=http%3A%2F%2F10.0.2.2%3A8081" - runFlow: label: "Dismiss Expo dev menu" when: