Improve E2E launch diagnostics and failure alerts
This commit is contained in:
@@ -27,6 +27,7 @@ stop_pid_file() {
|
|||||||
stop_process_tree "$pid"
|
stop_process_tree "$pid"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stop_pid_file "$artifact_dir/logcat.pid"
|
||||||
stop_pid_file "$artifact_dir/metro.pid"
|
stop_pid_file "$artifact_dir/metro.pid"
|
||||||
stop_pid_file "$artifact_dir/mock-server.pid"
|
stop_pid_file "$artifact_dir/mock-server.pid"
|
||||||
stop_pid_file "$artifact_dir/emulator.pid"
|
stop_pid_file "$artifact_dir/emulator.pid"
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ stop_pid_file() {
|
|||||||
# shellcheck disable=SC2329 # Invoked by the EXIT/INT/TERM trap below.
|
# shellcheck disable=SC2329 # Invoked by the EXIT/INT/TERM trap below.
|
||||||
cleanup() {
|
cleanup() {
|
||||||
set +e
|
set +e
|
||||||
|
stop_pid_file "$artifact_dir/logcat.pid"
|
||||||
stop_pid_file "$artifact_dir/metro.pid"
|
stop_pid_file "$artifact_dir/metro.pid"
|
||||||
stop_pid_file "$artifact_dir/mock-server.pid"
|
stop_pid_file "$artifact_dir/mock-server.pid"
|
||||||
|
|
||||||
@@ -81,6 +82,12 @@ cleanup() {
|
|||||||
|
|
||||||
trap cleanup EXIT INT TERM
|
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"
|
phase "Starting PostgreSQL, Redis, and mock server"
|
||||||
if [[ "$platform" == "ios" ]]; then
|
if [[ "$platform" == "ios" ]]; then
|
||||||
brew install postgresql@14 2>&1 | tee "$artifact_dir/native-dependencies.log"
|
brew install postgresql@14 2>&1 | tee "$artifact_dir/native-dependencies.log"
|
||||||
|
|||||||
@@ -75,6 +75,18 @@ export function parseJUnit(xml) {
|
|||||||
return failures
|
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) {
|
function walk(root) {
|
||||||
if (!root || !fs.existsSync(root)) return []
|
if (!root || !fs.existsSync(root)) return []
|
||||||
const entries = fs.readdirSync(root, {withFileTypes: true})
|
const entries = fs.readdirSync(root, {withFileTypes: true})
|
||||||
@@ -90,12 +102,20 @@ function readPhase(root) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function platformResult({name, status, root, artifactUrl}) {
|
function platformResult({name, status, root, artifactUrl}) {
|
||||||
const reports = walk(root).filter(file =>
|
const files = walk(root)
|
||||||
/(?:report|junit).*\.xml$/i.test(file),
|
const reports = files.filter(file => /(?:report|junit).*\.xml$/i.test(file))
|
||||||
)
|
const junitFailures = reports.flatMap(report =>
|
||||||
const failures = reports.flatMap(report =>
|
|
||||||
parseJUnit(fs.readFileSync(report, 'utf8')),
|
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
|
const failed = status !== 'success' || failures.length > 0
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
@@ -119,6 +139,36 @@ function slackEscape(value) {
|
|||||||
.replaceAll('>', '>')
|
.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({
|
export function buildSummary({
|
||||||
iosStatus,
|
iosStatus,
|
||||||
androidStatus,
|
androidStatus,
|
||||||
@@ -183,10 +233,28 @@ export function buildSummary({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const text = lines.join('\n').trim()
|
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 {
|
return {
|
||||||
notify,
|
notify,
|
||||||
platforms,
|
platforms,
|
||||||
payload: {text},
|
payload: {text, blocks},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ appId: xyz.blueskyweb.app
|
|||||||
when:
|
when:
|
||||||
platform: Android
|
platform: Android
|
||||||
commands:
|
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:
|
- runFlow:
|
||||||
label: "Dismiss Expo dev menu"
|
label: "Dismiss Expo dev menu"
|
||||||
when:
|
when:
|
||||||
|
|||||||
Reference in New Issue
Block a user