executable-stories-formatters 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -1
- package/dist/adapters.d.cts +1 -1
- package/dist/adapters.d.ts +1 -1
- package/dist/cli.js +810 -6
- package/dist/cli.js.map +1 -1
- package/dist/{index-DyeUWfYK.d.cts → index-C4QO-SVT.d.cts} +33 -8
- package/dist/{index-DyeUWfYK.d.ts → index-C4QO-SVT.d.ts} +33 -8
- package/dist/index.cjs +900 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +403 -5
- package/dist/index.d.ts +403 -5
- package/dist/index.js +881 -17
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/schemas/README.md +11 -2
package/dist/cli.js
CHANGED
|
@@ -2764,6 +2764,21 @@ body {
|
|
|
2764
2764
|
}
|
|
2765
2765
|
}
|
|
2766
2766
|
|
|
2767
|
+
/* ============================================================================
|
|
2768
|
+
History metric badges
|
|
2769
|
+
============================================================================ */
|
|
2770
|
+
.badge { display: inline-block; padding: 2px 6px; border-radius: 4px; font-size: 0.75em; font-weight: 600; margin-left: 4px; vertical-align: middle; }
|
|
2771
|
+
.badge-grade { color: #fff; }
|
|
2772
|
+
.badge-grade-A { background: var(--success); }
|
|
2773
|
+
.badge-grade-B { background: #2196F3; }
|
|
2774
|
+
.badge-grade-C { background: #FF9800; }
|
|
2775
|
+
.badge-grade-D { background: #f44336; }
|
|
2776
|
+
.badge-grade-F { background: #9E0000; }
|
|
2777
|
+
.badge-flaky { background: #FF9800; color: #fff; }
|
|
2778
|
+
.badge-perf { font-size: 0.7em; }
|
|
2779
|
+
.badge-perf-improving { color: var(--success); }
|
|
2780
|
+
.badge-perf-regressing { color: var(--error); }
|
|
2781
|
+
|
|
2767
2782
|
`;
|
|
2768
2783
|
|
|
2769
2784
|
// src/formatters/html/renderers/status.ts
|
|
@@ -2797,7 +2812,22 @@ function renderMetaInfo(args, deps) {
|
|
|
2797
2812
|
items.push(`<dt>Git:</dt><dd>${deps.escapeHtml(shortSha)}</dd>`);
|
|
2798
2813
|
}
|
|
2799
2814
|
if (args.ciName) {
|
|
2800
|
-
|
|
2815
|
+
if (args.ciUrl && args.ciBuildNumber) {
|
|
2816
|
+
items.push(
|
|
2817
|
+
`<dt>CI:</dt><dd>${deps.escapeHtml(args.ciName)} <a href="${deps.escapeHtml(args.ciUrl)}">#${deps.escapeHtml(args.ciBuildNumber)}</a></dd>`
|
|
2818
|
+
);
|
|
2819
|
+
} else {
|
|
2820
|
+
items.push(`<dt>CI:</dt><dd>${deps.escapeHtml(args.ciName)}</dd>`);
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
if (args.ciBranch) {
|
|
2824
|
+
items.push(`<dt>Branch:</dt><dd>${deps.escapeHtml(args.ciBranch)}</dd>`);
|
|
2825
|
+
}
|
|
2826
|
+
if (args.ciCommitSha) {
|
|
2827
|
+
const shortSha = args.ciCommitSha.length > 7 ? args.ciCommitSha.slice(0, 7) : args.ciCommitSha;
|
|
2828
|
+
items.push(
|
|
2829
|
+
`<dt>Commit:</dt><dd title="${deps.escapeHtml(args.ciCommitSha)}">${deps.escapeHtml(shortSha)}</dd>`
|
|
2830
|
+
);
|
|
2801
2831
|
}
|
|
2802
2832
|
return `<dl class="meta-info">${items.join("")}</dl>`;
|
|
2803
2833
|
}
|
|
@@ -3048,6 +3078,9 @@ function highlightStepParams(text, deps) {
|
|
|
3048
3078
|
return result;
|
|
3049
3079
|
}
|
|
3050
3080
|
|
|
3081
|
+
// src/history/sample-policy.ts
|
|
3082
|
+
var MIN_METRIC_SAMPLES = 5;
|
|
3083
|
+
|
|
3051
3084
|
// src/formatters/html/renderers/scenario.ts
|
|
3052
3085
|
function renderScenario(args, deps) {
|
|
3053
3086
|
const { tc } = args;
|
|
@@ -3068,6 +3101,19 @@ function renderScenario(args, deps) {
|
|
|
3068
3101
|
traceBadge = `<span class="tag trace-tag" title="${deps.escapeHtml(otelMeta.traceId)}">${deps.escapeHtml(shortId)}\u2026</span>`;
|
|
3069
3102
|
}
|
|
3070
3103
|
}
|
|
3104
|
+
let metricBadges = "";
|
|
3105
|
+
const { metrics } = args;
|
|
3106
|
+
if (metrics && metrics.sampleSize >= MIN_METRIC_SAMPLES) {
|
|
3107
|
+
const grade = metrics.stabilityGrade;
|
|
3108
|
+
metricBadges += `<span class="badge badge-grade badge-grade-${grade}" title="Pass rate: ${(metrics.passRate * 100).toFixed(0)}% (${metrics.sampleSize} runs)">${grade}</span>`;
|
|
3109
|
+
if (metrics.flakinessLevel !== "stable") {
|
|
3110
|
+
metricBadges += `<span class="badge badge-flaky">${metrics.flakinessLevel}</span>`;
|
|
3111
|
+
}
|
|
3112
|
+
if (metrics.performanceTrend !== "stable") {
|
|
3113
|
+
const arrow = metrics.performanceTrend === "improving" ? "\u2191" : "\u2193";
|
|
3114
|
+
metricBadges += `<span class="badge badge-perf badge-perf-${metrics.performanceTrend}">${arrow} ${metrics.performanceTrend}</span>`;
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
3071
3117
|
const storyDocs = deps.renderDocs(tc.story.docs, "story-docs");
|
|
3072
3118
|
const steps = deps.renderSteps(
|
|
3073
3119
|
{ steps: tc.story.steps, stepResults: tc.stepResults },
|
|
@@ -3102,7 +3148,7 @@ function renderScenario(args, deps) {
|
|
|
3102
3148
|
<span class="status-icon ${statusClass}">${statusIcon}</span>
|
|
3103
3149
|
<span class="scenario-name">${deps.escapeHtml(tc.story.scenario)}</span>
|
|
3104
3150
|
</div>
|
|
3105
|
-
<div class="scenario-meta">${tags}${traceBadge}</div>
|
|
3151
|
+
<div class="scenario-meta">${tags}${traceBadge}${metricBadges}</div>
|
|
3106
3152
|
</div>
|
|
3107
3153
|
<span class="scenario-duration">${duration}</span>
|
|
3108
3154
|
</div>
|
|
@@ -3314,7 +3360,12 @@ function renderFeature(args, deps) {
|
|
|
3314
3360
|
const featureName = suitePaths.length > 0 && suitePaths[0].length > 0 ? suitePaths[0][0] : file.split("/").pop()?.replace(/\.[^.]+$/, "") ?? file;
|
|
3315
3361
|
const collapsedClass = deps.startCollapsed ? " collapsed" : "";
|
|
3316
3362
|
const ariaExpanded = !deps.startCollapsed;
|
|
3317
|
-
const scenarios = testCases.map(
|
|
3363
|
+
const scenarios = testCases.map(
|
|
3364
|
+
(tc) => deps.renderScenario(
|
|
3365
|
+
{ tc, metrics: args.metricsMap?.get(tc.id) },
|
|
3366
|
+
deps.scenarioDeps
|
|
3367
|
+
)
|
|
3368
|
+
).join("\n");
|
|
3318
3369
|
return `
|
|
3319
3370
|
<div class="feature${collapsedClass}">
|
|
3320
3371
|
<div class="feature-header" role="button" tabindex="0" aria-expanded="${ariaExpanded}">
|
|
@@ -3359,7 +3410,11 @@ function buildBody(args, deps) {
|
|
|
3359
3410
|
durationMs: run.durationMs,
|
|
3360
3411
|
packageVersion: run.packageVersion,
|
|
3361
3412
|
gitSha: run.gitSha,
|
|
3362
|
-
ciName: run.ci?.name
|
|
3413
|
+
ciName: run.ci?.name,
|
|
3414
|
+
ciBranch: run.ci?.branch,
|
|
3415
|
+
ciUrl: run.ci?.url,
|
|
3416
|
+
ciCommitSha: run.ci?.commitSha,
|
|
3417
|
+
ciBuildNumber: run.ci?.buildNumber
|
|
3363
3418
|
},
|
|
3364
3419
|
deps.metaDeps
|
|
3365
3420
|
)
|
|
@@ -3388,7 +3443,10 @@ function buildBody(args, deps) {
|
|
|
3388
3443
|
const byFile = groupBy(run.testCases, (tc) => tc.sourceFile);
|
|
3389
3444
|
for (const [file, testCases] of byFile) {
|
|
3390
3445
|
parts.push(
|
|
3391
|
-
deps.renderFeature(
|
|
3446
|
+
deps.renderFeature(
|
|
3447
|
+
{ file, testCases, metricsMap: args.metricsMap },
|
|
3448
|
+
deps.featureDeps
|
|
3449
|
+
)
|
|
3392
3450
|
);
|
|
3393
3451
|
}
|
|
3394
3452
|
return parts.join("\n");
|
|
@@ -5326,6 +5384,574 @@ function pickleStepArgumentToDocs(ps) {
|
|
|
5326
5384
|
return docs;
|
|
5327
5385
|
}
|
|
5328
5386
|
|
|
5387
|
+
// src/notifiers/ansi-strip.ts
|
|
5388
|
+
function stripAnsi(text) {
|
|
5389
|
+
return text.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, "");
|
|
5390
|
+
}
|
|
5391
|
+
|
|
5392
|
+
// src/notifiers/slack.ts
|
|
5393
|
+
function truncate(text, maxLen) {
|
|
5394
|
+
if (text.length <= maxLen) return text;
|
|
5395
|
+
return text.slice(0, maxLen - 3) + "...";
|
|
5396
|
+
}
|
|
5397
|
+
function formatDuration2(ms) {
|
|
5398
|
+
const seconds = ms / 1e3;
|
|
5399
|
+
if (seconds < 60) return `${seconds.toFixed(1)}s`;
|
|
5400
|
+
const minutes = Math.floor(seconds / 60);
|
|
5401
|
+
const remainingSeconds = seconds % 60;
|
|
5402
|
+
return `${minutes}m ${remainingSeconds.toFixed(0)}s`;
|
|
5403
|
+
}
|
|
5404
|
+
function buildSlackPayload(summary, maxFailedTests) {
|
|
5405
|
+
const allPassed = summary.failed === 0;
|
|
5406
|
+
const emoji = allPassed ? ":white_check_mark:" : ":x:";
|
|
5407
|
+
const statusText = allPassed ? "Passed" : "Failed";
|
|
5408
|
+
const blocks = [];
|
|
5409
|
+
blocks.push({
|
|
5410
|
+
type: "header",
|
|
5411
|
+
text: {
|
|
5412
|
+
type: "plain_text",
|
|
5413
|
+
text: `${emoji} Test Results: ${summary.passed} passed, ${summary.failed} failed`,
|
|
5414
|
+
emoji: true
|
|
5415
|
+
}
|
|
5416
|
+
});
|
|
5417
|
+
blocks.push({
|
|
5418
|
+
type: "section",
|
|
5419
|
+
fields: [
|
|
5420
|
+
{ type: "mrkdwn", text: `*Total:* ${summary.total}` },
|
|
5421
|
+
{ type: "mrkdwn", text: `*Passed:* ${summary.passed}` },
|
|
5422
|
+
{ type: "mrkdwn", text: `*Failed:* ${summary.failed}` },
|
|
5423
|
+
{ type: "mrkdwn", text: `*Skipped:* ${summary.skipped}` },
|
|
5424
|
+
{ type: "mrkdwn", text: `*Duration:* ${formatDuration2(summary.durationMs)}` },
|
|
5425
|
+
{ type: "mrkdwn", text: `*Status:* ${statusText}` }
|
|
5426
|
+
]
|
|
5427
|
+
});
|
|
5428
|
+
if (summary.failedTests.length > 0) {
|
|
5429
|
+
const displayedTests = summary.failedTests.slice(0, maxFailedTests);
|
|
5430
|
+
const lines = displayedTests.map((t) => {
|
|
5431
|
+
const name = t.name;
|
|
5432
|
+
if (t.error) {
|
|
5433
|
+
const cleanError = truncate(stripAnsi(t.error), 500);
|
|
5434
|
+
return `*${name}*
|
|
5435
|
+
\`\`\`${cleanError}\`\`\``;
|
|
5436
|
+
}
|
|
5437
|
+
return `*${name}*`;
|
|
5438
|
+
});
|
|
5439
|
+
let text = lines.join("\n\n");
|
|
5440
|
+
if (summary.failedTests.length > maxFailedTests) {
|
|
5441
|
+
text += `
|
|
5442
|
+
|
|
5443
|
+
_...and ${summary.failedTests.length - maxFailedTests} more_`;
|
|
5444
|
+
}
|
|
5445
|
+
blocks.push({
|
|
5446
|
+
type: "section",
|
|
5447
|
+
text: {
|
|
5448
|
+
type: "mrkdwn",
|
|
5449
|
+
text
|
|
5450
|
+
}
|
|
5451
|
+
});
|
|
5452
|
+
}
|
|
5453
|
+
if (summary.ci) {
|
|
5454
|
+
const elements = [];
|
|
5455
|
+
if (summary.ci.displayName) {
|
|
5456
|
+
elements.push({ type: "mrkdwn", text: `*CI:* ${summary.ci.displayName}` });
|
|
5457
|
+
}
|
|
5458
|
+
if (summary.ci.branch) {
|
|
5459
|
+
elements.push({ type: "mrkdwn", text: `*Branch:* ${summary.ci.branch}` });
|
|
5460
|
+
}
|
|
5461
|
+
if (summary.ci.commitSha) {
|
|
5462
|
+
elements.push({ type: "mrkdwn", text: `*Commit:* ${summary.ci.commitSha.slice(0, 7)}` });
|
|
5463
|
+
}
|
|
5464
|
+
if (summary.ci.buildNumber) {
|
|
5465
|
+
elements.push({ type: "mrkdwn", text: `*Build:* #${summary.ci.buildNumber}` });
|
|
5466
|
+
}
|
|
5467
|
+
if (elements.length > 0) {
|
|
5468
|
+
blocks.push({
|
|
5469
|
+
type: "context",
|
|
5470
|
+
elements
|
|
5471
|
+
});
|
|
5472
|
+
}
|
|
5473
|
+
}
|
|
5474
|
+
if (summary.reportUrl) {
|
|
5475
|
+
blocks.push({
|
|
5476
|
+
type: "actions",
|
|
5477
|
+
elements: [
|
|
5478
|
+
{
|
|
5479
|
+
type: "button",
|
|
5480
|
+
text: {
|
|
5481
|
+
type: "plain_text",
|
|
5482
|
+
text: "View Report",
|
|
5483
|
+
emoji: true
|
|
5484
|
+
},
|
|
5485
|
+
url: summary.reportUrl,
|
|
5486
|
+
action_id: "view_report"
|
|
5487
|
+
}
|
|
5488
|
+
]
|
|
5489
|
+
});
|
|
5490
|
+
}
|
|
5491
|
+
return { blocks };
|
|
5492
|
+
}
|
|
5493
|
+
async function sendSlackNotification(args, deps) {
|
|
5494
|
+
const { summary, webhookUrl, maxFailedTests = 5 } = args;
|
|
5495
|
+
const { fetch, logger } = deps;
|
|
5496
|
+
const payload = buildSlackPayload(summary, maxFailedTests);
|
|
5497
|
+
try {
|
|
5498
|
+
const response = await fetch(webhookUrl, {
|
|
5499
|
+
method: "POST",
|
|
5500
|
+
headers: { "Content-Type": "application/json" },
|
|
5501
|
+
body: JSON.stringify(payload)
|
|
5502
|
+
});
|
|
5503
|
+
if (!response.ok) {
|
|
5504
|
+
const requestId = response.headers.get("x-request-id") ?? void 0;
|
|
5505
|
+
let bodyText = "";
|
|
5506
|
+
try {
|
|
5507
|
+
bodyText = await response.text();
|
|
5508
|
+
} catch {
|
|
5509
|
+
}
|
|
5510
|
+
const truncatedBody = truncate(bodyText, 200);
|
|
5511
|
+
const idPart = requestId ? ` x-request-id=${requestId}` : "";
|
|
5512
|
+
const errorMsg = `Slack notifier failed: HTTP ${response.status}${idPart} ${truncatedBody}`;
|
|
5513
|
+
logger.warn(errorMsg);
|
|
5514
|
+
return { ok: false, error: errorMsg };
|
|
5515
|
+
}
|
|
5516
|
+
return { ok: true };
|
|
5517
|
+
} catch (err) {
|
|
5518
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5519
|
+
const errorMsg = `Slack notifier failed: ${msg}`;
|
|
5520
|
+
logger.warn(errorMsg);
|
|
5521
|
+
return { ok: false, error: errorMsg };
|
|
5522
|
+
}
|
|
5523
|
+
}
|
|
5524
|
+
|
|
5525
|
+
// src/notifiers/teams.ts
|
|
5526
|
+
function truncate2(text, maxLen) {
|
|
5527
|
+
if (text.length <= maxLen) return text;
|
|
5528
|
+
return text.slice(0, maxLen - 3) + "...";
|
|
5529
|
+
}
|
|
5530
|
+
function formatDuration3(ms) {
|
|
5531
|
+
const seconds = ms / 1e3;
|
|
5532
|
+
if (seconds < 60) return `${seconds.toFixed(1)}s`;
|
|
5533
|
+
const minutes = Math.floor(seconds / 60);
|
|
5534
|
+
const remainingSeconds = seconds % 60;
|
|
5535
|
+
return `${minutes}m ${remainingSeconds.toFixed(0)}s`;
|
|
5536
|
+
}
|
|
5537
|
+
function buildTeamsPayload(summary, maxFailedTests) {
|
|
5538
|
+
const allPassed = summary.failed === 0;
|
|
5539
|
+
const statusEmoji = allPassed ? "\u2705" : "\u274C";
|
|
5540
|
+
const statusColor = allPassed ? "good" : "attention";
|
|
5541
|
+
const bodyItems = [];
|
|
5542
|
+
bodyItems.push({
|
|
5543
|
+
type: "TextBlock",
|
|
5544
|
+
size: "Large",
|
|
5545
|
+
weight: "Bolder",
|
|
5546
|
+
text: `${statusEmoji} Test Results`,
|
|
5547
|
+
color: statusColor
|
|
5548
|
+
});
|
|
5549
|
+
bodyItems.push({
|
|
5550
|
+
type: "FactSet",
|
|
5551
|
+
facts: [
|
|
5552
|
+
{ title: "Total", value: String(summary.total) },
|
|
5553
|
+
{ title: "Passed", value: String(summary.passed) },
|
|
5554
|
+
{ title: "Failed", value: String(summary.failed) },
|
|
5555
|
+
{ title: "Skipped", value: String(summary.skipped) },
|
|
5556
|
+
{ title: "Duration", value: formatDuration3(summary.durationMs) }
|
|
5557
|
+
]
|
|
5558
|
+
});
|
|
5559
|
+
if (summary.failedTests.length > 0) {
|
|
5560
|
+
const displayedTests = summary.failedTests.slice(0, maxFailedTests);
|
|
5561
|
+
const failedItems = [
|
|
5562
|
+
{
|
|
5563
|
+
type: "TextBlock",
|
|
5564
|
+
text: "Failed Tests",
|
|
5565
|
+
weight: "Bolder",
|
|
5566
|
+
spacing: "Medium"
|
|
5567
|
+
}
|
|
5568
|
+
];
|
|
5569
|
+
for (const t of displayedTests) {
|
|
5570
|
+
failedItems.push({
|
|
5571
|
+
type: "TextBlock",
|
|
5572
|
+
text: `**${t.name}**`,
|
|
5573
|
+
wrap: true
|
|
5574
|
+
});
|
|
5575
|
+
if (t.error) {
|
|
5576
|
+
const cleanError = truncate2(stripAnsi(t.error), 500);
|
|
5577
|
+
failedItems.push({
|
|
5578
|
+
type: "TextBlock",
|
|
5579
|
+
text: cleanError,
|
|
5580
|
+
wrap: true,
|
|
5581
|
+
fontType: "Monospace",
|
|
5582
|
+
size: "Small",
|
|
5583
|
+
color: "Attention"
|
|
5584
|
+
});
|
|
5585
|
+
}
|
|
5586
|
+
}
|
|
5587
|
+
if (summary.failedTests.length > maxFailedTests) {
|
|
5588
|
+
failedItems.push({
|
|
5589
|
+
type: "TextBlock",
|
|
5590
|
+
text: `...and ${summary.failedTests.length - maxFailedTests} more`,
|
|
5591
|
+
isSubtle: true,
|
|
5592
|
+
spacing: "Small"
|
|
5593
|
+
});
|
|
5594
|
+
}
|
|
5595
|
+
bodyItems.push({
|
|
5596
|
+
type: "Container",
|
|
5597
|
+
items: failedItems
|
|
5598
|
+
});
|
|
5599
|
+
}
|
|
5600
|
+
if (summary.ci) {
|
|
5601
|
+
const ciFacts = [];
|
|
5602
|
+
if (summary.ci.displayName) {
|
|
5603
|
+
ciFacts.push({ title: "CI", value: summary.ci.displayName });
|
|
5604
|
+
}
|
|
5605
|
+
if (summary.ci.branch) {
|
|
5606
|
+
ciFacts.push({ title: "Branch", value: summary.ci.branch });
|
|
5607
|
+
}
|
|
5608
|
+
if (summary.ci.commitSha) {
|
|
5609
|
+
ciFacts.push({ title: "Commit", value: summary.ci.commitSha.slice(0, 7) });
|
|
5610
|
+
}
|
|
5611
|
+
if (summary.ci.buildNumber) {
|
|
5612
|
+
ciFacts.push({ title: "Build", value: `#${summary.ci.buildNumber}` });
|
|
5613
|
+
}
|
|
5614
|
+
if (ciFacts.length > 0) {
|
|
5615
|
+
bodyItems.push({
|
|
5616
|
+
type: "FactSet",
|
|
5617
|
+
facts: ciFacts,
|
|
5618
|
+
separator: true
|
|
5619
|
+
});
|
|
5620
|
+
}
|
|
5621
|
+
}
|
|
5622
|
+
const card = {
|
|
5623
|
+
type: "AdaptiveCard",
|
|
5624
|
+
$schema: "http://adaptivecards.io/schemas/adaptive-card.json",
|
|
5625
|
+
version: "1.4",
|
|
5626
|
+
body: bodyItems
|
|
5627
|
+
};
|
|
5628
|
+
if (summary.reportUrl) {
|
|
5629
|
+
card.actions = [
|
|
5630
|
+
{
|
|
5631
|
+
type: "Action.OpenUrl",
|
|
5632
|
+
title: "View Report",
|
|
5633
|
+
url: summary.reportUrl
|
|
5634
|
+
}
|
|
5635
|
+
];
|
|
5636
|
+
}
|
|
5637
|
+
return {
|
|
5638
|
+
type: "message",
|
|
5639
|
+
attachments: [
|
|
5640
|
+
{
|
|
5641
|
+
contentType: "application/vnd.microsoft.card.adaptive",
|
|
5642
|
+
content: card
|
|
5643
|
+
}
|
|
5644
|
+
]
|
|
5645
|
+
};
|
|
5646
|
+
}
|
|
5647
|
+
async function sendTeamsNotification(args, deps) {
|
|
5648
|
+
const { summary, webhookUrl, maxFailedTests = 5 } = args;
|
|
5649
|
+
const { fetch, logger } = deps;
|
|
5650
|
+
const payload = buildTeamsPayload(summary, maxFailedTests);
|
|
5651
|
+
try {
|
|
5652
|
+
const response = await fetch(webhookUrl, {
|
|
5653
|
+
method: "POST",
|
|
5654
|
+
headers: { "Content-Type": "application/json" },
|
|
5655
|
+
body: JSON.stringify(payload)
|
|
5656
|
+
});
|
|
5657
|
+
if (!response.ok) {
|
|
5658
|
+
const requestId = response.headers.get("x-request-id") ?? void 0;
|
|
5659
|
+
let bodyText = "";
|
|
5660
|
+
try {
|
|
5661
|
+
bodyText = await response.text();
|
|
5662
|
+
} catch {
|
|
5663
|
+
}
|
|
5664
|
+
const truncatedBody = truncate2(bodyText, 200);
|
|
5665
|
+
const idPart = requestId ? ` x-request-id=${requestId}` : "";
|
|
5666
|
+
const errorMsg = `Teams notifier failed: HTTP ${response.status}${idPart} ${truncatedBody}`;
|
|
5667
|
+
logger.warn(errorMsg);
|
|
5668
|
+
return { ok: false, error: errorMsg };
|
|
5669
|
+
}
|
|
5670
|
+
return { ok: true };
|
|
5671
|
+
} catch (err) {
|
|
5672
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5673
|
+
const errorMsg = `Teams notifier failed: ${msg}`;
|
|
5674
|
+
logger.warn(errorMsg);
|
|
5675
|
+
return { ok: false, error: errorMsg };
|
|
5676
|
+
}
|
|
5677
|
+
}
|
|
5678
|
+
|
|
5679
|
+
// src/notifiers/hmac.ts
|
|
5680
|
+
import { createHmac } from "crypto";
|
|
5681
|
+
function signBody(args) {
|
|
5682
|
+
let input;
|
|
5683
|
+
let timestamp;
|
|
5684
|
+
if (args.includeTimestamp) {
|
|
5685
|
+
timestamp = args.timestamp ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
5686
|
+
input = `${timestamp}.${args.body}`;
|
|
5687
|
+
} else {
|
|
5688
|
+
input = args.body;
|
|
5689
|
+
}
|
|
5690
|
+
const hex = createHmac("sha256", args.secret).update(input, "utf8").digest("hex");
|
|
5691
|
+
return {
|
|
5692
|
+
signature: `sha256=${hex}`,
|
|
5693
|
+
timestamp
|
|
5694
|
+
};
|
|
5695
|
+
}
|
|
5696
|
+
|
|
5697
|
+
// src/notifiers/webhook.ts
|
|
5698
|
+
async function sendWebhookNotification(args, deps) {
|
|
5699
|
+
const { summary, options } = args;
|
|
5700
|
+
const { fetch, logger } = deps;
|
|
5701
|
+
const payload = {
|
|
5702
|
+
schemaVersion: 1,
|
|
5703
|
+
event: "test_run_finished",
|
|
5704
|
+
summary
|
|
5705
|
+
};
|
|
5706
|
+
const body = JSON.stringify(payload);
|
|
5707
|
+
const headers = { "Content-Type": "application/json" };
|
|
5708
|
+
if (options.headers) {
|
|
5709
|
+
for (const [key, value] of Object.entries(options.headers)) {
|
|
5710
|
+
headers[key] = value;
|
|
5711
|
+
}
|
|
5712
|
+
}
|
|
5713
|
+
if (options.signer) {
|
|
5714
|
+
const { secret, header, includeTimestamp, timestampHeader } = options.signer;
|
|
5715
|
+
const result = signBody({ body, secret, includeTimestamp });
|
|
5716
|
+
headers[header] = result.signature;
|
|
5717
|
+
if (result.timestamp) {
|
|
5718
|
+
headers[timestampHeader ?? "X-Timestamp"] = result.timestamp;
|
|
5719
|
+
}
|
|
5720
|
+
}
|
|
5721
|
+
try {
|
|
5722
|
+
const response = await fetch(options.url, {
|
|
5723
|
+
method: options.method ?? "POST",
|
|
5724
|
+
headers,
|
|
5725
|
+
body
|
|
5726
|
+
});
|
|
5727
|
+
if (!response.ok) {
|
|
5728
|
+
const requestId = response.headers.get("x-request-id") ?? void 0;
|
|
5729
|
+
let snippet = "";
|
|
5730
|
+
try {
|
|
5731
|
+
snippet = (await response.text()).slice(0, 200);
|
|
5732
|
+
} catch {
|
|
5733
|
+
}
|
|
5734
|
+
const idPart = requestId ? ` x-request-id=${requestId}` : "";
|
|
5735
|
+
const errorMsg = `webhook: HTTP ${response.status}${idPart} ${snippet}`;
|
|
5736
|
+
logger.warn(errorMsg);
|
|
5737
|
+
return { ok: false, error: errorMsg };
|
|
5738
|
+
}
|
|
5739
|
+
return { ok: true };
|
|
5740
|
+
} catch (err) {
|
|
5741
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5742
|
+
const errorMsg = `webhook: ${msg}`;
|
|
5743
|
+
logger.warn(errorMsg);
|
|
5744
|
+
return { ok: false, error: errorMsg };
|
|
5745
|
+
}
|
|
5746
|
+
}
|
|
5747
|
+
|
|
5748
|
+
// src/notifiers/index.ts
|
|
5749
|
+
function buildSummary(run, reportUrl, toCIInfo2) {
|
|
5750
|
+
let passed = 0;
|
|
5751
|
+
let failed = 0;
|
|
5752
|
+
let skipped = 0;
|
|
5753
|
+
const failedTests = [];
|
|
5754
|
+
for (const tc of run.testCases) {
|
|
5755
|
+
switch (tc.status) {
|
|
5756
|
+
case "passed":
|
|
5757
|
+
passed++;
|
|
5758
|
+
break;
|
|
5759
|
+
case "failed":
|
|
5760
|
+
failed++;
|
|
5761
|
+
failedTests.push({
|
|
5762
|
+
testId: tc.id,
|
|
5763
|
+
name: tc.story.scenario,
|
|
5764
|
+
error: tc.errorMessage
|
|
5765
|
+
});
|
|
5766
|
+
break;
|
|
5767
|
+
case "skipped":
|
|
5768
|
+
case "pending":
|
|
5769
|
+
skipped++;
|
|
5770
|
+
break;
|
|
5771
|
+
}
|
|
5772
|
+
}
|
|
5773
|
+
let ci;
|
|
5774
|
+
if (run.ci) {
|
|
5775
|
+
ci = toCIInfo2(run.ci);
|
|
5776
|
+
}
|
|
5777
|
+
return {
|
|
5778
|
+
total: run.testCases.length,
|
|
5779
|
+
passed,
|
|
5780
|
+
failed,
|
|
5781
|
+
skipped,
|
|
5782
|
+
durationMs: run.durationMs,
|
|
5783
|
+
failedTests,
|
|
5784
|
+
ci,
|
|
5785
|
+
reportUrl
|
|
5786
|
+
};
|
|
5787
|
+
}
|
|
5788
|
+
function shouldNotify(condition, failedCount) {
|
|
5789
|
+
if (condition === "never") return false;
|
|
5790
|
+
if (condition === "on-failure" && failedCount === 0) return false;
|
|
5791
|
+
return true;
|
|
5792
|
+
}
|
|
5793
|
+
async function sendNotifications(args, deps) {
|
|
5794
|
+
const { run, notification } = args;
|
|
5795
|
+
const { logger, toCIInfo: toCIInfo2 } = deps;
|
|
5796
|
+
const env = deps.env ?? process.env;
|
|
5797
|
+
if (!deps.fetch) {
|
|
5798
|
+
logger.warn("notifications: skipped (fetch unavailable)");
|
|
5799
|
+
return;
|
|
5800
|
+
}
|
|
5801
|
+
const fetch = deps.fetch;
|
|
5802
|
+
const slackWebhookUrl = notification?.slackWebhookUrl ?? env.SLACK_WEBHOOK_URL;
|
|
5803
|
+
const teamsWebhookUrl = notification?.teamsWebhookUrl ?? env.TEAMS_WEBHOOK_URL;
|
|
5804
|
+
const globalCondition = notification?.condition ?? "on-failure";
|
|
5805
|
+
const reportUrl = notification?.reportUrl;
|
|
5806
|
+
const maxFailedTests = notification?.maxFailedTests ?? 5;
|
|
5807
|
+
const webhooks = notification?.webhooks ?? [];
|
|
5808
|
+
if (!slackWebhookUrl && !teamsWebhookUrl && webhooks.length === 0) {
|
|
5809
|
+
return;
|
|
5810
|
+
}
|
|
5811
|
+
const summary = buildSummary(run, reportUrl, toCIInfo2);
|
|
5812
|
+
const promises = [];
|
|
5813
|
+
if (slackWebhookUrl && shouldNotify(globalCondition, summary.failed)) {
|
|
5814
|
+
promises.push(
|
|
5815
|
+
sendSlackNotification(
|
|
5816
|
+
{ summary, webhookUrl: slackWebhookUrl, maxFailedTests },
|
|
5817
|
+
{ fetch, logger }
|
|
5818
|
+
).then(() => void 0)
|
|
5819
|
+
);
|
|
5820
|
+
}
|
|
5821
|
+
if (teamsWebhookUrl && shouldNotify(globalCondition, summary.failed)) {
|
|
5822
|
+
promises.push(
|
|
5823
|
+
sendTeamsNotification(
|
|
5824
|
+
{ summary, webhookUrl: teamsWebhookUrl, maxFailedTests },
|
|
5825
|
+
{ fetch, logger }
|
|
5826
|
+
).then(() => void 0)
|
|
5827
|
+
);
|
|
5828
|
+
}
|
|
5829
|
+
for (const webhook of webhooks) {
|
|
5830
|
+
const effectiveCondition = webhook.condition ?? globalCondition;
|
|
5831
|
+
if (!shouldNotify(effectiveCondition, summary.failed)) continue;
|
|
5832
|
+
promises.push(
|
|
5833
|
+
sendWebhookNotification(
|
|
5834
|
+
{ summary, options: webhook, maxFailedTests },
|
|
5835
|
+
{ fetch, logger }
|
|
5836
|
+
).then(() => void 0)
|
|
5837
|
+
);
|
|
5838
|
+
}
|
|
5839
|
+
await Promise.allSettled(promises);
|
|
5840
|
+
}
|
|
5841
|
+
|
|
5842
|
+
// src/types/ci.ts
|
|
5843
|
+
var DISPLAY_NAMES = {
|
|
5844
|
+
github: "GitHub Actions",
|
|
5845
|
+
gitlab: "GitLab CI",
|
|
5846
|
+
circleci: "CircleCI",
|
|
5847
|
+
jenkins: "Jenkins",
|
|
5848
|
+
azure: "Azure DevOps",
|
|
5849
|
+
buildkite: "Buildkite",
|
|
5850
|
+
travis: "Travis CI",
|
|
5851
|
+
unknown: "CI"
|
|
5852
|
+
};
|
|
5853
|
+
var NAME_TO_PROVIDER = {
|
|
5854
|
+
github: "github",
|
|
5855
|
+
gitlab: "gitlab",
|
|
5856
|
+
circleci: "circleci",
|
|
5857
|
+
jenkins: "jenkins",
|
|
5858
|
+
azure: "azure",
|
|
5859
|
+
buildkite: "buildkite",
|
|
5860
|
+
travis: "travis",
|
|
5861
|
+
ci: "unknown"
|
|
5862
|
+
};
|
|
5863
|
+
function toCIInfo(raw) {
|
|
5864
|
+
if (!raw) return void 0;
|
|
5865
|
+
const provider = raw.provider ?? NAME_TO_PROVIDER[raw.name] ?? "unknown";
|
|
5866
|
+
return {
|
|
5867
|
+
provider,
|
|
5868
|
+
displayName: DISPLAY_NAMES[provider],
|
|
5869
|
+
url: raw.url,
|
|
5870
|
+
buildNumber: raw.buildNumber,
|
|
5871
|
+
branch: raw.branch,
|
|
5872
|
+
commitSha: raw.commitSha,
|
|
5873
|
+
prNumber: raw.prNumber
|
|
5874
|
+
};
|
|
5875
|
+
}
|
|
5876
|
+
|
|
5877
|
+
// src/history/history-store.ts
|
|
5878
|
+
function emptyStore() {
|
|
5879
|
+
return { version: 1, maxRuns: 10, tests: {}, lastUpdated: 0 };
|
|
5880
|
+
}
|
|
5881
|
+
function loadHistory(args, deps) {
|
|
5882
|
+
const content = deps.readFile(args.filePath);
|
|
5883
|
+
if (content === void 0) {
|
|
5884
|
+
return emptyStore();
|
|
5885
|
+
}
|
|
5886
|
+
let parsed;
|
|
5887
|
+
try {
|
|
5888
|
+
parsed = JSON.parse(content);
|
|
5889
|
+
} catch {
|
|
5890
|
+
deps.logger.warn(`Failed to parse history file: ${args.filePath}`);
|
|
5891
|
+
return emptyStore();
|
|
5892
|
+
}
|
|
5893
|
+
if (typeof parsed !== "object" || parsed === null || parsed.version !== 1) {
|
|
5894
|
+
deps.logger.warn(
|
|
5895
|
+
`Unknown history version in ${args.filePath}, expected version 1`
|
|
5896
|
+
);
|
|
5897
|
+
return emptyStore();
|
|
5898
|
+
}
|
|
5899
|
+
const obj = parsed;
|
|
5900
|
+
if (typeof obj.tests !== "object" || obj.tests === null || Array.isArray(obj.tests)) {
|
|
5901
|
+
deps.logger.warn(
|
|
5902
|
+
`Malformed history store in ${args.filePath}: tests must be a non-null object`
|
|
5903
|
+
);
|
|
5904
|
+
return emptyStore();
|
|
5905
|
+
}
|
|
5906
|
+
return parsed;
|
|
5907
|
+
}
|
|
5908
|
+
function saveHistory(args, deps) {
|
|
5909
|
+
deps.writeFile(args.filePath, JSON.stringify(args.store, null, 2));
|
|
5910
|
+
}
|
|
5911
|
+
function updateHistory(args) {
|
|
5912
|
+
const { store, run, maxRuns } = args;
|
|
5913
|
+
const newTests = { ...store.tests };
|
|
5914
|
+
for (const tc of run.testCases) {
|
|
5915
|
+
const entry = {
|
|
5916
|
+
runId: run.runId,
|
|
5917
|
+
timestamp: run.startedAtMs,
|
|
5918
|
+
status: tc.status,
|
|
5919
|
+
durationMs: tc.durationMs,
|
|
5920
|
+
ci: run.ci ? {
|
|
5921
|
+
provider: void 0,
|
|
5922
|
+
branch: run.ci.branch,
|
|
5923
|
+
commitSha: run.ci.commitSha
|
|
5924
|
+
} : void 0
|
|
5925
|
+
};
|
|
5926
|
+
const existing = newTests[tc.id];
|
|
5927
|
+
if (existing) {
|
|
5928
|
+
const updatedEntries = [...existing.entries, entry];
|
|
5929
|
+
const trimmed = updatedEntries.length > maxRuns ? updatedEntries.slice(updatedEntries.length - maxRuns) : updatedEntries;
|
|
5930
|
+
newTests[tc.id] = {
|
|
5931
|
+
...existing,
|
|
5932
|
+
testName: tc.story.scenario,
|
|
5933
|
+
sourceFile: tc.sourceFile,
|
|
5934
|
+
sourceLine: tc.sourceLine,
|
|
5935
|
+
entries: trimmed
|
|
5936
|
+
};
|
|
5937
|
+
} else {
|
|
5938
|
+
newTests[tc.id] = {
|
|
5939
|
+
testId: tc.id,
|
|
5940
|
+
testName: tc.story.scenario,
|
|
5941
|
+
sourceFile: tc.sourceFile,
|
|
5942
|
+
sourceLine: tc.sourceLine,
|
|
5943
|
+
entries: [entry]
|
|
5944
|
+
};
|
|
5945
|
+
}
|
|
5946
|
+
}
|
|
5947
|
+
return {
|
|
5948
|
+
version: 1,
|
|
5949
|
+
maxRuns,
|
|
5950
|
+
tests: newTests,
|
|
5951
|
+
lastUpdated: Date.now()
|
|
5952
|
+
};
|
|
5953
|
+
}
|
|
5954
|
+
|
|
5329
5955
|
// src/index.ts
|
|
5330
5956
|
var FORMAT_EXTENSIONS = {
|
|
5331
5957
|
markdown: ".md",
|
|
@@ -5697,6 +6323,26 @@ OPTIONS
|
|
|
5697
6323
|
--emit-canonical <path> Write canonical JSON to given path
|
|
5698
6324
|
--help Show this help message
|
|
5699
6325
|
|
|
6326
|
+
NOTIFICATIONS
|
|
6327
|
+
--slack-webhook <url> Slack incoming webhook URL (fallback: SLACK_WEBHOOK_URL env var)
|
|
6328
|
+
--teams-webhook <url> Teams incoming webhook URL (fallback: TEAMS_WEBHOOK_URL env var)
|
|
6329
|
+
--notify <condition> When to send: always, on-failure, never (default: on-failure)
|
|
6330
|
+
--report-url <url> URL to link in notification messages
|
|
6331
|
+
--max-failed-tests <n> Max failed tests to show in notifications (default: 5)
|
|
6332
|
+
|
|
6333
|
+
GENERIC WEBHOOK
|
|
6334
|
+
--webhook-url <url> Generic webhook URL (repeatable for multiple endpoints)
|
|
6335
|
+
--webhook-header <Key: Value> Custom request header (repeatable)
|
|
6336
|
+
--webhook-method <POST|PUT> HTTP method (default: POST)
|
|
6337
|
+
--webhook-hmac-secret <s> HMAC-SHA256 signing secret
|
|
6338
|
+
--webhook-hmac-header <name> Signature header name (default: X-Signature)
|
|
6339
|
+
--webhook-hmac-timestamp Include timestamp in HMAC signing
|
|
6340
|
+
Note: all --webhook-url entries share the same method/headers/signing options.
|
|
6341
|
+
|
|
6342
|
+
HISTORY
|
|
6343
|
+
--history-file <path> Path to JSON history file (enables tracking)
|
|
6344
|
+
--max-history-runs <n> Max runs to keep in history per test (default: 10)
|
|
6345
|
+
|
|
5700
6346
|
EXIT CODES
|
|
5701
6347
|
0 Success
|
|
5702
6348
|
1 Schema validation failure
|
|
@@ -5733,6 +6379,19 @@ function parseCliArgs(argv) {
|
|
|
5733
6379
|
stdin: { type: "boolean", default: false },
|
|
5734
6380
|
"json-summary": { type: "boolean", default: false },
|
|
5735
6381
|
"emit-canonical": { type: "string" },
|
|
6382
|
+
"slack-webhook": { type: "string" },
|
|
6383
|
+
"teams-webhook": { type: "string" },
|
|
6384
|
+
notify: { type: "string", default: "on-failure" },
|
|
6385
|
+
"report-url": { type: "string" },
|
|
6386
|
+
"max-failed-tests": { type: "string" },
|
|
6387
|
+
"history-file": { type: "string" },
|
|
6388
|
+
"max-history-runs": { type: "string" },
|
|
6389
|
+
"webhook-url": { type: "string", multiple: true },
|
|
6390
|
+
"webhook-header": { type: "string", multiple: true },
|
|
6391
|
+
"webhook-method": { type: "string" },
|
|
6392
|
+
"webhook-hmac-secret": { type: "string" },
|
|
6393
|
+
"webhook-hmac-header": { type: "string" },
|
|
6394
|
+
"webhook-hmac-timestamp": { type: "boolean", default: false },
|
|
5736
6395
|
help: { type: "boolean", default: false }
|
|
5737
6396
|
},
|
|
5738
6397
|
allowPositionals: true,
|
|
@@ -5764,6 +6423,53 @@ function parseCliArgs(argv) {
|
|
|
5764
6423
|
}
|
|
5765
6424
|
const noSynthesize = values["no-synthesize-stories"];
|
|
5766
6425
|
const parseGlobs = (v) => v ? v.split(",").map((s) => s.trim()).filter(Boolean) : [];
|
|
6426
|
+
const notifyValue = values.notify;
|
|
6427
|
+
const validNotifyConditions = /* @__PURE__ */ new Set(["always", "on-failure", "never"]);
|
|
6428
|
+
if (!validNotifyConditions.has(notifyValue)) {
|
|
6429
|
+
console.error(`Error: --notify must be "always", "on-failure", or "never", got "${notifyValue}".`);
|
|
6430
|
+
process.exit(EXIT_USAGE);
|
|
6431
|
+
}
|
|
6432
|
+
const maxFailedTestsStr = values["max-failed-tests"];
|
|
6433
|
+
const maxFailedTests = maxFailedTestsStr ? parseInt(maxFailedTestsStr, 10) : 5;
|
|
6434
|
+
if (maxFailedTestsStr && (isNaN(maxFailedTests) || maxFailedTests < 0)) {
|
|
6435
|
+
console.error(`Error: --max-failed-tests must be a non-negative integer, got "${maxFailedTestsStr}".`);
|
|
6436
|
+
process.exit(EXIT_USAGE);
|
|
6437
|
+
}
|
|
6438
|
+
const slackWebhook = values["slack-webhook"];
|
|
6439
|
+
const teamsWebhook = values["teams-webhook"];
|
|
6440
|
+
const webhookUrls = values["webhook-url"] ?? [];
|
|
6441
|
+
const webhookHeaders = {};
|
|
6442
|
+
const rawHeaders = values["webhook-header"] ?? [];
|
|
6443
|
+
for (const h of rawHeaders) {
|
|
6444
|
+
const colonIdx = h.indexOf(":");
|
|
6445
|
+
if (colonIdx <= 0) {
|
|
6446
|
+
console.error(`Warning: ignoring invalid --webhook-header "${h}" (expected "Key: Value")`);
|
|
6447
|
+
continue;
|
|
6448
|
+
}
|
|
6449
|
+
const key = h.slice(0, colonIdx).trim();
|
|
6450
|
+
const value = h.slice(colonIdx + 1).trim();
|
|
6451
|
+
if (!key) {
|
|
6452
|
+
console.error(`Warning: ignoring --webhook-header with empty key`);
|
|
6453
|
+
continue;
|
|
6454
|
+
}
|
|
6455
|
+
webhookHeaders[key] = value;
|
|
6456
|
+
}
|
|
6457
|
+
const webhookMethodRaw = values["webhook-method"];
|
|
6458
|
+
let webhookMethod = "POST";
|
|
6459
|
+
if (webhookMethodRaw) {
|
|
6460
|
+
const upper = webhookMethodRaw.toUpperCase();
|
|
6461
|
+
if (upper !== "POST" && upper !== "PUT") {
|
|
6462
|
+
console.error(`Error: --webhook-method must be "POST" or "PUT", got "${webhookMethodRaw}".`);
|
|
6463
|
+
process.exit(EXIT_USAGE);
|
|
6464
|
+
}
|
|
6465
|
+
webhookMethod = upper;
|
|
6466
|
+
}
|
|
6467
|
+
const maxHistoryRunsStr = values["max-history-runs"];
|
|
6468
|
+
const maxHistoryRuns = maxHistoryRunsStr ? parseInt(maxHistoryRunsStr, 10) : 10;
|
|
6469
|
+
if (maxHistoryRunsStr && (isNaN(maxHistoryRuns) || maxHistoryRuns < 1)) {
|
|
6470
|
+
console.error(`Error: --max-history-runs must be a positive integer, got "${maxHistoryRunsStr}".`);
|
|
6471
|
+
process.exit(EXIT_USAGE);
|
|
6472
|
+
}
|
|
5767
6473
|
return {
|
|
5768
6474
|
subcommand,
|
|
5769
6475
|
inputFile,
|
|
@@ -5780,7 +6486,20 @@ function parseCliArgs(argv) {
|
|
|
5780
6486
|
htmlNoMermaid: values["html-no-mermaid"],
|
|
5781
6487
|
htmlNoMarkdown: values["html-no-markdown"],
|
|
5782
6488
|
jsonSummary: values["json-summary"],
|
|
5783
|
-
emitCanonical: values["emit-canonical"]
|
|
6489
|
+
emitCanonical: values["emit-canonical"],
|
|
6490
|
+
slackWebhook,
|
|
6491
|
+
teamsWebhook,
|
|
6492
|
+
notify: notifyValue,
|
|
6493
|
+
reportUrl: values["report-url"],
|
|
6494
|
+
maxFailedTests,
|
|
6495
|
+
historyFile: values["history-file"],
|
|
6496
|
+
maxHistoryRuns,
|
|
6497
|
+
webhookUrls,
|
|
6498
|
+
webhookHeaders,
|
|
6499
|
+
webhookMethod,
|
|
6500
|
+
webhookHmacSecret: values["webhook-hmac-secret"],
|
|
6501
|
+
webhookHmacHeader: values["webhook-hmac-header"] ?? "X-Signature",
|
|
6502
|
+
webhookHmacTimestamp: values["webhook-hmac-timestamp"]
|
|
5784
6503
|
};
|
|
5785
6504
|
}
|
|
5786
6505
|
async function readInput(args) {
|
|
@@ -5864,6 +6583,8 @@ async function main() {
|
|
|
5864
6583
|
}
|
|
5865
6584
|
try {
|
|
5866
6585
|
const result = await generateReports(run, args);
|
|
6586
|
+
await dispatchNotifications(run, args);
|
|
6587
|
+
runHistoryPipeline(run, args);
|
|
5867
6588
|
printResult(result, args, startMs);
|
|
5868
6589
|
process.exit(EXIT_SUCCESS);
|
|
5869
6590
|
} catch (err) {
|
|
@@ -5920,6 +6641,8 @@ ${msg}`);
|
|
|
5920
6641
|
}
|
|
5921
6642
|
try {
|
|
5922
6643
|
const result = await generateReports(run, args);
|
|
6644
|
+
await dispatchNotifications(run, args);
|
|
6645
|
+
runHistoryPipeline(run, args);
|
|
5923
6646
|
printResult(result, args, startMs);
|
|
5924
6647
|
process.exit(EXIT_SUCCESS);
|
|
5925
6648
|
} catch (err) {
|
|
@@ -5975,6 +6698,8 @@ ${msg}`);
|
|
|
5975
6698
|
}
|
|
5976
6699
|
try {
|
|
5977
6700
|
const result = await generateReports(canonical, args, droppedMissingStory);
|
|
6701
|
+
await dispatchNotifications(canonical, args);
|
|
6702
|
+
runHistoryPipeline(canonical, args);
|
|
5978
6703
|
printResult(result, args, startMs, droppedMissingStory);
|
|
5979
6704
|
process.exit(EXIT_SUCCESS);
|
|
5980
6705
|
} catch (err) {
|
|
@@ -5983,6 +6708,85 @@ ${msg}`);
|
|
|
5983
6708
|
process.exit(EXIT_GENERATION);
|
|
5984
6709
|
}
|
|
5985
6710
|
}
|
|
6711
|
+
async function dispatchNotifications(run, args) {
|
|
6712
|
+
const webhooks = args.webhookUrls.map((url) => {
|
|
6713
|
+
const opts = { url };
|
|
6714
|
+
if (Object.keys(args.webhookHeaders).length > 0) {
|
|
6715
|
+
opts.headers = { ...args.webhookHeaders };
|
|
6716
|
+
}
|
|
6717
|
+
if (args.webhookMethod !== "POST") {
|
|
6718
|
+
opts.method = args.webhookMethod;
|
|
6719
|
+
}
|
|
6720
|
+
if (args.webhookHmacSecret) {
|
|
6721
|
+
const signer = {
|
|
6722
|
+
type: "hmac-sha256",
|
|
6723
|
+
secret: args.webhookHmacSecret,
|
|
6724
|
+
header: args.webhookHmacHeader
|
|
6725
|
+
};
|
|
6726
|
+
if (args.webhookHmacTimestamp) {
|
|
6727
|
+
signer.includeTimestamp = true;
|
|
6728
|
+
}
|
|
6729
|
+
opts.signer = signer;
|
|
6730
|
+
}
|
|
6731
|
+
return opts;
|
|
6732
|
+
});
|
|
6733
|
+
await sendNotifications(
|
|
6734
|
+
{
|
|
6735
|
+
run,
|
|
6736
|
+
notification: {
|
|
6737
|
+
slackWebhookUrl: args.slackWebhook,
|
|
6738
|
+
teamsWebhookUrl: args.teamsWebhook,
|
|
6739
|
+
condition: args.notify,
|
|
6740
|
+
reportUrl: args.reportUrl,
|
|
6741
|
+
maxFailedTests: args.maxFailedTests,
|
|
6742
|
+
webhooks: webhooks.length > 0 ? webhooks : void 0
|
|
6743
|
+
}
|
|
6744
|
+
},
|
|
6745
|
+
{
|
|
6746
|
+
fetch: globalThis.fetch,
|
|
6747
|
+
logger: console,
|
|
6748
|
+
toCIInfo
|
|
6749
|
+
}
|
|
6750
|
+
);
|
|
6751
|
+
}
|
|
6752
|
+
function runHistoryPipeline(run, args) {
|
|
6753
|
+
if (!args.historyFile) return;
|
|
6754
|
+
const historyPath = path3.resolve(args.historyFile);
|
|
6755
|
+
const store = loadHistory(
|
|
6756
|
+
{ filePath: historyPath },
|
|
6757
|
+
{
|
|
6758
|
+
readFile: (p) => {
|
|
6759
|
+
try {
|
|
6760
|
+
return fs3.readFileSync(p, "utf8");
|
|
6761
|
+
} catch {
|
|
6762
|
+
return void 0;
|
|
6763
|
+
}
|
|
6764
|
+
},
|
|
6765
|
+
logger: console
|
|
6766
|
+
}
|
|
6767
|
+
);
|
|
6768
|
+
const updated = updateHistory({
|
|
6769
|
+
store,
|
|
6770
|
+
run,
|
|
6771
|
+
maxRuns: args.maxHistoryRuns
|
|
6772
|
+
});
|
|
6773
|
+
const dir = path3.dirname(historyPath);
|
|
6774
|
+
fs3.mkdirSync(dir, { recursive: true });
|
|
6775
|
+
saveHistory(
|
|
6776
|
+
{ filePath: historyPath, store: updated },
|
|
6777
|
+
{ writeFile: (p, content) => fs3.writeFileSync(p, content, "utf8") }
|
|
6778
|
+
);
|
|
6779
|
+
let metricsCount = 0;
|
|
6780
|
+
for (const testId of Object.keys(updated.tests)) {
|
|
6781
|
+
const history = updated.tests[testId];
|
|
6782
|
+
if (history.entries.length >= 3) {
|
|
6783
|
+
metricsCount++;
|
|
6784
|
+
}
|
|
6785
|
+
}
|
|
6786
|
+
if (metricsCount > 0) {
|
|
6787
|
+
console.error(`History updated: ${historyPath} (${Object.keys(updated.tests).length} tests tracked)`);
|
|
6788
|
+
}
|
|
6789
|
+
}
|
|
5986
6790
|
async function generateReports(run, args, _droppedMissingStory = 0) {
|
|
5987
6791
|
const generator = new ReportGenerator({
|
|
5988
6792
|
include: args.include,
|