donobu
Version:
Create browser automations with an LLM agent and replay them as Playwright scripts.
156 lines • 6.72 kB
JavaScript
;
/**
* @fileoverview Donobu Slack Reporter for Playwright.
*
* Writes a Slack Block Kit payload to disk and, when configured, POSTs it
* directly to a Slack Incoming Webhook. During auto-heal the POST is deferred
* to the orchestrator so the user sees exactly one message per run reflecting
* the final (merged, possibly healed) outcome — not an initial failure
* message followed by a healed one.
*
* @usage
* The reporter takes no Slack-related options. All Slack configuration is
* driven by environment variables (see below) — typically populated from CI
* secrets and contextual run metadata. Wire it up in `playwright.config.ts`
* with no arguments:
*
* ```ts
* reporter: [
* ['donobu/reporter/slack'],
* ],
* ```
*
* Optionally set `outputFile` to control the on-disk payload path:
*
* ```ts
* reporter: [
* ['donobu/reporter/slack', { outputFile: 'test-results/slack-payload.json' }],
* ],
* ```
*
* @env Configuration
*
* **`DONOBU_SLACK_WEBHOOK_URL`** *(required for posting; secret)* — the Slack
* Incoming Webhook URL to POST the payload to. When unset, the reporter still
* writes the payload file but performs no network call (the file is then
* useful as a CI artifact or for piping to `curl` manually). This is treated
* as a secret and is read only from the environment — never as a reporter
* option — so it cannot accidentally end up checked into a config file.
*
* **`DONOBU_REPORT_URL`** *(optional; not secret)* — a URL to embed in the
* Slack message that links back to the full report (e.g. the GitHub Actions
* run URL, or a published HTML report). When unset, the message simply omits
* the link section. Read from the environment for symmetry with the webhook
* URL: both come from CI context, and a single configuration story keeps the
* mental model simple. To override per-suite, set the env var differently
* before invoking `donobu test`.
*
* **`DONOBU_AUTO_HEAL_ACTIVE`** / **`DONOBU_AUTO_HEAL_ORCHESTRATED`**
* *(set internally; not user-facing)* — coordination flags the donobu CLI
* sets so the reporter knows when to defer Slack posting to the orchestrator.
* Users should never set these themselves.
*
* @CI A typical GitHub Actions wiring:
*
* ```yaml
* env:
* DONOBU_SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
* DONOBU_REPORT_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
* run: npx donobu test --auto-heal
* ```
*
* @auto-heal Per-run delivery semantics
*
* Every Donobu run produces exactly one Slack POST when a webhook is
* configured, regardless of whether auto-heal triggers:
* - **No auto-heal configured:** the reporter posts directly during `onEnd`.
* - **Auto-heal configured, tests pass:** the reporter defers; the
* orchestrator posts the initial payload after the early-return.
* - **Auto-heal configured, no eligible plans:** same — orchestrator posts
* the initial payload via the fallback path.
* - **Auto-heal configured, heal runs and merges:** the reporter defers in
* both the initial run and the heal rerun; the orchestrator posts the
* merged payload after re-rendering.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.postSlackPayload = postSlackPayload;
const fs_1 = require("fs");
const path_1 = require("path");
const envVars_1 = require("../envVars");
const Logger_1 = require("../utils/Logger");
const buildReport_1 = require("./buildReport");
const renderSlack_1 = require("./renderSlack");
const stateFile_1 = require("./stateFile");
/**
* POST a Slack Block Kit payload to an Incoming Webhook URL. Logs failures but
* never throws — Slack delivery is a nice-to-have, not a test-run blocker.
*/
async function postSlackPayload(webhookUrl, payload) {
try {
const response = await fetch(webhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!response.ok) {
const body = await response.text().catch(() => '<no body>');
Logger_1.appLogger.warn(`Donobu Slack POST returned ${response.status}: ${body}`);
}
}
catch (error) {
Logger_1.appLogger.warn('Donobu Slack POST failed.', error);
}
}
class DonobuSlackReporter {
constructor(options = {}) {
this.resultsByTest = new Map();
this.options = options;
}
onBegin(config, _suite) {
this.rootDir = config.rootDir;
}
onTestEnd(test, result) {
const existing = this.resultsByTest.get(test);
if (existing) {
existing.push(result);
}
else {
this.resultsByTest.set(test, [result]);
}
}
async onEnd(_result) {
const outputFile = (0, path_1.resolve)(this.options.outputFile ?? 'test-results/slack-payload.json');
const outputDir = (0, path_1.dirname)(outputFile);
const autoHealActive = envVars_1.env.data.DONOBU_AUTO_HEAL_ACTIVE === '1';
const autoHealOrchestrated = envVars_1.env.data.DONOBU_AUTO_HEAL_ORCHESTRATED === '1';
const reportUrl = envVars_1.env.data.DONOBU_REPORT_URL;
const report = (0, buildReport_1.buildDonobuReport)(this.resultsByTest, this.rootDir);
(0, stateFile_1.mergeStateFileEntry)(envVars_1.env.data.PLAYWRIGHT_JSON_OUTPUT_DIR, report, {
slack: { outputFile },
});
// Payload file is always written — even during a heal rerun — so the
// auto-heal orchestrator and any CI artifact upload can see the final state.
const payload = (0, renderSlack_1.renderSlack)(report, { reportUrl });
(0, fs_1.mkdirSync)(outputDir, { recursive: true });
(0, fs_1.writeFileSync)(outputFile, JSON.stringify(payload, null, 2), 'utf8');
Logger_1.appLogger.info(`Donobu Slack payload written to ${outputFile}`);
// Defer Slack POST to the orchestrator when either:
// - this is the auto-heal rerun (orchestrator will re-render and post the
// merged result itself), OR
// - the outer `donobu test` invocation enabled auto-heal (orchestrator will
// decide whether to post the pre-heal or merged payload once it knows
// whether auto-heal actually triggered).
if (autoHealActive || autoHealOrchestrated) {
return;
}
const webhookUrl = envVars_1.env.data.DONOBU_SLACK_WEBHOOK_URL;
if (webhookUrl) {
await postSlackPayload(webhookUrl, payload);
}
}
printsToStdio() {
return false;
}
}
exports.default = DonobuSlackReporter;
//# sourceMappingURL=slack.js.map