feat(e2e): send agent x cloud matrix email on completion (#2297)

After every e2e run, send an HTML matrix report to KEY_REQUEST_EMAIL
via Resend showing pass/fail/skip per agent x cloud combination.

- e2e.sh: add send_matrix_email() — builds result table from LOG_DIR
  result files, writes temp TS, calls bun run to POST to Resend API.
  Called just before exit so LOG_DIR is still available.
- qa.sh (e2e mode): load RESEND_API_KEY + KEY_REQUEST_EMAIL from
  /etc/spawn-key-server-auth.env before launching Claude so the creds
  are inherited by the e2e.sh subprocess.

Both changes are no-ops when credentials are absent (silent skip).

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
A 2026-03-07 14:07:55 -08:00 committed by GitHub
parent 1991ffcb15
commit 099ad8940e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 164 additions and 0 deletions

View file

@ -202,6 +202,25 @@ if [[ "${RUN_MODE}" == "fixtures" ]] || [[ "${RUN_MODE}" == "quality" ]] || [[ "
fi
fi
# --- Load email credentials for matrix report (e2e mode) ---
if [[ "${RUN_MODE}" == "e2e" ]]; then
if [[ -f /etc/spawn-key-server-auth.env ]]; then
while IFS='=' read -r _ekey _eval || [[ -n "${_ekey}" ]]; do
_ekey="${_ekey#"${_ekey%%[! ]*}"}"
_ekey="${_ekey%"${_ekey##*[! ]}"}"
[[ -z "${_ekey}" || "${_ekey}" == \#* ]] && continue
case "${_ekey}" in
RESEND_API_KEY|KEY_REQUEST_EMAIL)
export "${_ekey}=${_eval}"
;;
esac
done < /etc/spawn-key-server-auth.env
log "Email credentials loaded for matrix report"
else
log "No /etc/spawn-key-server-auth.env found — matrix email will be skipped"
fi
fi
# Launch Claude Code with mode-specific prompt
# Enable agent teams (required for team-based workflows)
export CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1

View file

@ -361,6 +361,148 @@ run_agents_for_cloud() {
return 0
}
# ---------------------------------------------------------------------------
# send_matrix_email LOG_DIR CLOUDS AGENTS TOTAL_PASS TOTAL_FAIL DURATION_STR
#
# Sends an agent x cloud matrix report via Resend.
# Requires: RESEND_API_KEY, KEY_REQUEST_EMAIL env vars (silently skips if absent).
# ---------------------------------------------------------------------------
send_matrix_email() {
local log_dir="$1"
local clouds="$2"
local agents="$3"
local total_pass="$4"
local total_fail="$5"
local duration_str="$6"
local resend_key="${RESEND_API_KEY:-}"
local to_email="${KEY_REQUEST_EMAIL:-}"
if [ -z "${resend_key}" ] || [ -z "${to_email}" ]; then
log_info "Matrix email skipped (RESEND_API_KEY or KEY_REQUEST_EMAIL not set)"
return 0
fi
# Build results string: "cloud:agent:result,..." for bun to process
local results=""
for cloud in ${clouds}; do
for agent in ${agents}; do
local result="skip"
local result_file="${log_dir}/${cloud}-${agent}.result"
if [ -f "${result_file}" ]; then
result=$(cat "${result_file}")
fi
if [ -n "${results}" ]; then results="${results},"; fi
results="${results}${cloud}:${agent}:${result}"
done
done
local ts_file
ts_file=$(mktemp /tmp/e2e-email-XXXXXX.ts)
cat > "${ts_file}" << 'TS_EOF'
const results = (process.env._E2E_RESULTS ?? "").split(",").filter(Boolean);
const clouds = (process.env._E2E_CLOUDS ?? "").split(" ").filter(Boolean);
const agents = (process.env._E2E_AGENTS ?? "").split(" ").filter(Boolean);
const totalPass = process.env._E2E_TOTAL_PASS ?? "0";
const totalFail = process.env._E2E_TOTAL_FAIL ?? "0";
const duration = process.env._E2E_DURATION ?? "?";
const toEmail = process.env.KEY_REQUEST_EMAIL ?? "";
const resendKey = process.env.RESEND_API_KEY ?? "";
const timestamp = new Date().toUTCString();
// Build lookup map: "cloud:agent" -> result
const resultMap: Record<string, string> = {};
for (const entry of results) {
const parts = entry.split(":");
resultMap[`${parts[0]}:${parts[1]}`] = parts[2] ?? "skip";
}
// Cell styles per result
const cellStyle = (result: string): string => {
if (result === "pass") return "background:#22c55e;color:#fff;font-weight:bold;padding:4px 10px;border-radius:4px;";
if (result === "fail") return "background:#ef4444;color:#fff;font-weight:bold;padding:4px 10px;border-radius:4px;";
return "background:#e2e8f0;color:#94a3b8;padding:4px 10px;border-radius:4px;";
};
const headerCells = clouds
.map(c => `<th style="padding:8px 14px;background:#1e293b;color:#fff;text-transform:uppercase;font-size:11px;letter-spacing:.05em;">${c}</th>`)
.join("");
const bodyRows = agents
.map(agent => {
const cells = clouds
.map(cloud => {
const r = resultMap[`${cloud}:${agent}`] ?? "skip";
return `<td style="padding:6px 14px;text-align:center;"><span style="${cellStyle(r)}">${r.toUpperCase()}</span></td>`;
})
.join("");
return `<tr><td style="padding:6px 14px;font-weight:600;white-space:nowrap;color:#1e293b;">${agent}</td>${cells}</tr>`;
})
.join("");
const status = totalFail === "0" ? "✅ All Passed" : `${totalFail} Failed`;
const html = `<!DOCTYPE html>
<html><body style="font-family:system-ui,-apple-system,sans-serif;max-width:860px;margin:0 auto;padding:24px;color:#1e293b;">
<h2 style="margin:0 0 4px;">${status} — Spawn E2E Matrix</h2>
<p style="margin:0 0 20px;color:#64748b;font-size:14px;">Completed ${timestamp}</p>
<table style="border-collapse:collapse;width:100%;">
<thead>
<tr>
<th style="padding:8px 14px;background:#1e293b;color:#fff;text-align:left;font-size:11px;text-transform:uppercase;letter-spacing:.05em;">Agent</th>
${headerCells}
</tr>
</thead>
<tbody>
${bodyRows}
</tbody>
</table>
<p style="margin-top:18px;color:#64748b;font-size:13px;">
<strong style="color:#1e293b;">Total:</strong> ${totalPass} passed, ${totalFail} failed
&nbsp;·&nbsp;
<strong style="color:#1e293b;">Duration:</strong> ${duration}
</p>
</body></html>`;
const subject = totalFail === "0"
? `✅ E2E Matrix: ${totalPass} passed · ${duration}`
: `❌ E2E Matrix: ${totalFail} failed, ${totalPass} passed · ${duration}`;
const res = await fetch("https://api.resend.com/emails", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${resendKey}`,
},
body: JSON.stringify({
from: "Spawn QA <onboarding@resend.dev>",
to: [toEmail],
subject,
html,
}),
});
if (!res.ok) {
const body = await res.text();
console.error(`Resend API error ${res.status}: ${body}`);
process.exit(1);
}
console.log(`Matrix email sent to ${toEmail}`);
TS_EOF
log_info "Sending matrix email to ${to_email}..."
_E2E_RESULTS="${results}" \
_E2E_CLOUDS="${clouds}" \
_E2E_AGENTS="${agents}" \
_E2E_TOTAL_PASS="${total_pass}" \
_E2E_TOTAL_FAIL="${total_fail}" \
_E2E_DURATION="${duration_str}" \
bun run "${ts_file}" 2>&1 || log_warn "Failed to send matrix email"
rm -f "${ts_file}" 2>/dev/null || true
}
# ---------------------------------------------------------------------------
# Final cleanup trap
# ---------------------------------------------------------------------------
@ -508,6 +650,9 @@ if [ "${total_fail}" -gt 0 ]; then
fi
printf "\n Duration: %s\n" "${DURATION_STR}"
# Send matrix email report
send_matrix_email "${LOG_DIR}" "${CLOUDS}" "${AGENTS_TO_TEST}" "${total_pass}" "${total_fail}" "${DURATION_STR}"
# Exit with failure if any agent on any cloud failed
if [ "${total_fail}" -gt 0 ]; then
exit 1