@akshatmittal/invoker 0.1.0 → 0.3.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 +145 -13
- package/dist/github.d.mts +22 -0
- package/dist/github.mjs +353 -0
- package/dist/index.d.mts +6 -12
- package/dist/index.mjs +79 -62
- package/dist/slack.d.mts +10 -0
- package/dist/slack.mjs +482 -0
- package/package.json +22 -1
package/dist/slack.mjs
ADDED
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { WebAPIRateLimitedError, WebClient } from "@slack/web-api";
|
|
3
|
+
import { setTimeout } from "node:timers/promises";
|
|
4
|
+
//#region src/slack/report.ts
|
|
5
|
+
const stringSchema = z.string();
|
|
6
|
+
const jsonObjectSchema = z.record(stringSchema, z.json());
|
|
7
|
+
const invokerMetaSchema = z.strictObject({
|
|
8
|
+
schema: z.literal(1),
|
|
9
|
+
matrix: jsonObjectSchema,
|
|
10
|
+
metadata: jsonObjectSchema.optional(),
|
|
11
|
+
output: z.json().optional()
|
|
12
|
+
});
|
|
13
|
+
const testMetaSchema = z.object({ invoker: invokerMetaSchema.optional() });
|
|
14
|
+
const errorMessageSchema = z.object({ message: stringSchema });
|
|
15
|
+
const errorStackSchema = z.object({ stack: stringSchema });
|
|
16
|
+
const TABLE_ROW_LIMIT = 100;
|
|
17
|
+
const TABLE_CHARACTER_LIMIT = 1e4;
|
|
18
|
+
const SECTION_CHARACTER_LIMIT = 3e3;
|
|
19
|
+
const NAME_CHARACTER_LIMIT = 200;
|
|
20
|
+
const METADATA_CHARACTER_LIMIT = 1800;
|
|
21
|
+
function collectWorkflowReports(modules) {
|
|
22
|
+
const workflows = /* @__PURE__ */ new Map();
|
|
23
|
+
for (const module of modules) for (const testCase of module.children.allTests()) {
|
|
24
|
+
const meta = testMetaSchema.safeParse(testCase.meta());
|
|
25
|
+
const invoker = meta.success ? meta.data.invoker : void 0;
|
|
26
|
+
const taskSuite = testCase.parent;
|
|
27
|
+
const workflowSuite = taskSuite.type === "suite" ? taskSuite.parent : void 0;
|
|
28
|
+
if (!invoker || taskSuite.type !== "suite" || workflowSuite?.type !== "suite") continue;
|
|
29
|
+
let workflow = workflows.get(workflowSuite);
|
|
30
|
+
if (!workflow) {
|
|
31
|
+
workflow = {
|
|
32
|
+
name: workflowSuite.name,
|
|
33
|
+
module,
|
|
34
|
+
suite: workflowSuite,
|
|
35
|
+
metadata: invoker.metadata,
|
|
36
|
+
tasks: /* @__PURE__ */ new Map()
|
|
37
|
+
};
|
|
38
|
+
workflows.set(workflowSuite, workflow);
|
|
39
|
+
}
|
|
40
|
+
let task = workflow.tasks.get(taskSuite);
|
|
41
|
+
if (!task) {
|
|
42
|
+
task = {
|
|
43
|
+
name: taskSuite.name,
|
|
44
|
+
suite: taskSuite,
|
|
45
|
+
total: 0,
|
|
46
|
+
passed: 0,
|
|
47
|
+
retried: 0,
|
|
48
|
+
failed: 0,
|
|
49
|
+
skipped: 0,
|
|
50
|
+
incomplete: 0,
|
|
51
|
+
failures: [],
|
|
52
|
+
retries: []
|
|
53
|
+
};
|
|
54
|
+
workflow.tasks.set(taskSuite, task);
|
|
55
|
+
}
|
|
56
|
+
task.total += 1;
|
|
57
|
+
const result = testCase.result();
|
|
58
|
+
const diagnostic = testCase.diagnostic();
|
|
59
|
+
task[result.state === "pending" ? "incomplete" : result.state] += 1;
|
|
60
|
+
const retryCount = diagnostic?.retryCount ?? 0;
|
|
61
|
+
if (retryCount > 0) {
|
|
62
|
+
task.retried += 1;
|
|
63
|
+
if (result.state === "passed") task.retries.push({
|
|
64
|
+
task: task.name,
|
|
65
|
+
caseName: testCase.name,
|
|
66
|
+
matrix: invoker.matrix,
|
|
67
|
+
count: retryCount,
|
|
68
|
+
messages: (result.errors ?? []).map(errorMessage)
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
if (diagnostic) {
|
|
72
|
+
task.startedAt = Math.min(task.startedAt ?? diagnostic.startTime, diagnostic.startTime);
|
|
73
|
+
task.endedAt = Math.max(task.endedAt ?? 0, diagnostic.startTime + diagnostic.duration);
|
|
74
|
+
}
|
|
75
|
+
if (result.state === "failed") task.failures.push({
|
|
76
|
+
task: task.name,
|
|
77
|
+
caseName: testCase.name,
|
|
78
|
+
matrix: invoker.matrix,
|
|
79
|
+
messages: result.errors.map(errorMessage)
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return [...workflows.values()].map((workflow) => {
|
|
83
|
+
const failures = [];
|
|
84
|
+
const retries = [];
|
|
85
|
+
addErrors(failures, workflow.suite.errors());
|
|
86
|
+
addErrors(failures, workflow.module.errors());
|
|
87
|
+
const collectedTasks = [...workflow.tasks.values()];
|
|
88
|
+
for (const task of collectedTasks) {
|
|
89
|
+
failures.push(...task.failures);
|
|
90
|
+
retries.push(...task.retries);
|
|
91
|
+
addErrors(failures, task.suite.errors(), task.name);
|
|
92
|
+
}
|
|
93
|
+
const { startedAt, endedAt } = timeSpan(collectedTasks);
|
|
94
|
+
return {
|
|
95
|
+
name: workflow.name,
|
|
96
|
+
metadata: workflow.metadata,
|
|
97
|
+
tasks: collectedTasks.map(({ suite: _suite, failures: _failures, retries: _retries, startedAt, endedAt, ...task }) => ({
|
|
98
|
+
...task,
|
|
99
|
+
duration: startedAt === void 0 || endedAt === void 0 ? 0 : endedAt - startedAt
|
|
100
|
+
})),
|
|
101
|
+
failures: deduplicateFailures(failures),
|
|
102
|
+
retries,
|
|
103
|
+
startedAt,
|
|
104
|
+
endedAt
|
|
105
|
+
};
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
function summaryMessage(reports, runUrl) {
|
|
109
|
+
const timestamp = Math.floor(Date.now() / 1e3);
|
|
110
|
+
const { startedAt, endedAt } = timeSpan(reports);
|
|
111
|
+
const footer = [
|
|
112
|
+
`Elapsed: ${formatDuration(startedAt === void 0 || endedAt === void 0 ? 0 : endedAt - startedAt)}`,
|
|
113
|
+
`<!date^${timestamp}^{date_short_pretty} at {time}|${(/* @__PURE__ */ new Date(timestamp * 1e3)).toISOString()}>`,
|
|
114
|
+
...runUrl ? [`<${escapeSlackControl(runUrl)}|View run>`] : []
|
|
115
|
+
].join(" • ");
|
|
116
|
+
return {
|
|
117
|
+
text: "Invoker Report",
|
|
118
|
+
attachments: [...reports.flatMap(workflowAttachments), { blocks: [{
|
|
119
|
+
type: "context",
|
|
120
|
+
elements: [{
|
|
121
|
+
type: "mrkdwn",
|
|
122
|
+
text: footer
|
|
123
|
+
}]
|
|
124
|
+
}] }]
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function workflowAttachments(report) {
|
|
128
|
+
const tables = taskTables(report.tasks);
|
|
129
|
+
return tables.map((rows, index) => workflowAttachment(report, rows, index, tables.length));
|
|
130
|
+
}
|
|
131
|
+
function workflowAttachment(report, rows, index, pages) {
|
|
132
|
+
const totals = taskTotals(report.tasks);
|
|
133
|
+
const status = workflowStatus(report);
|
|
134
|
+
const metadata = metadataText(report.metadata);
|
|
135
|
+
const page = pages > 1 ? ` (${index + 1}/${pages})` : "";
|
|
136
|
+
const headline = `${status.emoji} *${escapeSlack(truncate(report.name, NAME_CHARACTER_LIMIT))} — ${totals.passed}/${totals.total} passed${page}*`;
|
|
137
|
+
return {
|
|
138
|
+
color: status.color,
|
|
139
|
+
blocks: [
|
|
140
|
+
{
|
|
141
|
+
type: "section",
|
|
142
|
+
text: {
|
|
143
|
+
type: "mrkdwn",
|
|
144
|
+
text: headline
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
...metadata ? [{
|
|
148
|
+
type: "context",
|
|
149
|
+
elements: [{
|
|
150
|
+
type: "mrkdwn",
|
|
151
|
+
text: metadata
|
|
152
|
+
}]
|
|
153
|
+
}] : [],
|
|
154
|
+
{
|
|
155
|
+
type: "table",
|
|
156
|
+
column_settings: [
|
|
157
|
+
{ is_wrapped: true },
|
|
158
|
+
{ align: "right" },
|
|
159
|
+
{ align: "right" },
|
|
160
|
+
{ align: "right" },
|
|
161
|
+
{ align: "right" },
|
|
162
|
+
{ align: "right" }
|
|
163
|
+
],
|
|
164
|
+
rows
|
|
165
|
+
}
|
|
166
|
+
]
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function failureMessages(report) {
|
|
170
|
+
return [...Map.groupBy(report.failures, (failure) => failure.task)].flatMap(([task, failures]) => {
|
|
171
|
+
const title = escapeSlack(truncate(task ?? "Workflow errors", NAME_CHARACTER_LIMIT));
|
|
172
|
+
const workflow = `*Workflow:* ${escapeSlack(truncate(report.name, NAME_CHARACTER_LIMIT))}`;
|
|
173
|
+
const metadata = metadataText(report.metadata);
|
|
174
|
+
const context = metadata ? `${workflow} • ${metadata}` : workflow;
|
|
175
|
+
return chunk(failures.map((failure) => {
|
|
176
|
+
return `${failure.caseName ? `*${escapeSlack(failure.caseName)}*` : task ? "*Task error*" : "*Workflow error*"}${failure.matrix ? `\nMatrix: ${escapeSlack(JSON.stringify(failure.matrix))}` : ""}${failure.messages.map((message) => `\n• ${escapeSlack(message)}`).join("")}`;
|
|
177
|
+
}), 3e3).map((details, index, messages) => {
|
|
178
|
+
const part = messages.length > 1 ? ` (${index + 1}/${messages.length})` : "";
|
|
179
|
+
return {
|
|
180
|
+
text: `${truncate(report.name, NAME_CHARACTER_LIMIT)} › ${truncate(task ?? "Workflow", NAME_CHARACTER_LIMIT)} — ${failures.length} failed${part}`,
|
|
181
|
+
attachments: [{
|
|
182
|
+
color: "danger",
|
|
183
|
+
blocks: [
|
|
184
|
+
{
|
|
185
|
+
type: "section",
|
|
186
|
+
text: {
|
|
187
|
+
type: "mrkdwn",
|
|
188
|
+
text: `🔴 *${title} — ${failures.length} failed${part}*`
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
type: "context",
|
|
193
|
+
elements: [{
|
|
194
|
+
type: "mrkdwn",
|
|
195
|
+
text: context
|
|
196
|
+
}]
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
type: "section",
|
|
200
|
+
text: {
|
|
201
|
+
type: "mrkdwn",
|
|
202
|
+
text: details
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
]
|
|
206
|
+
}]
|
|
207
|
+
};
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
function retryMessages(report) {
|
|
212
|
+
return [...Map.groupBy(report.retries, (retry) => retry.task)].flatMap(([task, retries]) => {
|
|
213
|
+
const title = escapeSlack(truncate(task, NAME_CHARACTER_LIMIT));
|
|
214
|
+
const workflow = `*Workflow:* ${escapeSlack(truncate(report.name, NAME_CHARACTER_LIMIT))}`;
|
|
215
|
+
const metadata = metadataText(report.metadata);
|
|
216
|
+
const context = metadata ? `${workflow} • ${metadata}` : workflow;
|
|
217
|
+
return chunk(retries.map((retry) => {
|
|
218
|
+
const matrix = `\nMatrix: ${escapeSlack(JSON.stringify(retry.matrix))}`;
|
|
219
|
+
const messages = retry.messages.map((message) => `\n• ${escapeSlack(message)}`).join("");
|
|
220
|
+
return `*${escapeSlack(retry.caseName)}*\nRetries: ${retry.count}${matrix}${messages}`;
|
|
221
|
+
}), SECTION_CHARACTER_LIMIT).map((details, index, messages) => {
|
|
222
|
+
const part = messages.length > 1 ? ` (${index + 1}/${messages.length})` : "";
|
|
223
|
+
return {
|
|
224
|
+
text: `${truncate(report.name, NAME_CHARACTER_LIMIT)} › ${truncate(task, NAME_CHARACTER_LIMIT)} — ${retries.length} retried${part}`,
|
|
225
|
+
attachments: [{
|
|
226
|
+
color: "warning",
|
|
227
|
+
blocks: [
|
|
228
|
+
{
|
|
229
|
+
type: "section",
|
|
230
|
+
text: {
|
|
231
|
+
type: "mrkdwn",
|
|
232
|
+
text: `🟡 *${title} — ${retries.length} retried${part}*`
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
type: "context",
|
|
237
|
+
elements: [{
|
|
238
|
+
type: "mrkdwn",
|
|
239
|
+
text: context
|
|
240
|
+
}]
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
type: "section",
|
|
244
|
+
text: {
|
|
245
|
+
type: "mrkdwn",
|
|
246
|
+
text: details
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
]
|
|
250
|
+
}]
|
|
251
|
+
};
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
function unhandledErrorMessages(errors) {
|
|
256
|
+
const messages = [...new Set(errors.map(errorMessage))];
|
|
257
|
+
return chunk(messages.map((message) => `• ${escapeSlack(message)}`), SECTION_CHARACTER_LIMIT).map((details, index, chunks) => {
|
|
258
|
+
const part = chunks.length > 1 ? ` (${index + 1}/${chunks.length})` : "";
|
|
259
|
+
return {
|
|
260
|
+
text: `Invoker run — ${messages.length} unhandled error${messages.length === 1 ? "" : "s"}${part}`,
|
|
261
|
+
attachments: [{
|
|
262
|
+
color: "danger",
|
|
263
|
+
blocks: [{
|
|
264
|
+
type: "section",
|
|
265
|
+
text: {
|
|
266
|
+
type: "mrkdwn",
|
|
267
|
+
text: `🔴 *Run errors${part}*`
|
|
268
|
+
}
|
|
269
|
+
}, {
|
|
270
|
+
type: "section",
|
|
271
|
+
text: {
|
|
272
|
+
type: "mrkdwn",
|
|
273
|
+
text: details
|
|
274
|
+
}
|
|
275
|
+
}]
|
|
276
|
+
}]
|
|
277
|
+
};
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
function addErrors(failures, errors, task) {
|
|
281
|
+
const messages = errors.map(errorMessage);
|
|
282
|
+
if (messages.length > 0) failures.push({
|
|
283
|
+
task,
|
|
284
|
+
messages
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
function errorMessage(cause) {
|
|
288
|
+
const message = errorMessageSchema.safeParse(cause);
|
|
289
|
+
if (message.success) return message.data.message;
|
|
290
|
+
const stack = errorStackSchema.safeParse(cause);
|
|
291
|
+
if (stack.success) return stack.data.stack.split("\n", 1)[0];
|
|
292
|
+
return String(cause);
|
|
293
|
+
}
|
|
294
|
+
function deduplicateFailures(failures) {
|
|
295
|
+
const seen = /* @__PURE__ */ new Set();
|
|
296
|
+
return failures.flatMap((failure) => {
|
|
297
|
+
const messages = failure.messages.filter((message) => {
|
|
298
|
+
const key = JSON.stringify([
|
|
299
|
+
failure.task,
|
|
300
|
+
failure.caseName,
|
|
301
|
+
failure.matrix,
|
|
302
|
+
message
|
|
303
|
+
]);
|
|
304
|
+
if (seen.has(key)) return false;
|
|
305
|
+
seen.add(key);
|
|
306
|
+
return true;
|
|
307
|
+
});
|
|
308
|
+
return messages.length > 0 ? [{
|
|
309
|
+
...failure,
|
|
310
|
+
messages
|
|
311
|
+
}] : [];
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
function escapeSlack(value) {
|
|
315
|
+
return escapeSlackControl(value).replaceAll("*", "∗").replaceAll("_", "_").replaceAll("~", "∼").replaceAll("`", "ˋ");
|
|
316
|
+
}
|
|
317
|
+
function escapeSlackControl(value) {
|
|
318
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
319
|
+
}
|
|
320
|
+
function taskTotals(tasks) {
|
|
321
|
+
return tasks.reduce((totals, task) => ({
|
|
322
|
+
total: totals.total + task.total,
|
|
323
|
+
passed: totals.passed + task.passed
|
|
324
|
+
}), {
|
|
325
|
+
total: 0,
|
|
326
|
+
passed: 0
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
function workflowStatus(report) {
|
|
330
|
+
if (report.failures.length > 0 || report.tasks.some((task) => task.failed > 0)) return {
|
|
331
|
+
emoji: "🔴",
|
|
332
|
+
color: "danger"
|
|
333
|
+
};
|
|
334
|
+
if (report.tasks.some((task) => task.retried > 0 || task.skipped > 0 || task.incomplete > 0)) return {
|
|
335
|
+
emoji: "🟡",
|
|
336
|
+
color: "warning"
|
|
337
|
+
};
|
|
338
|
+
return {
|
|
339
|
+
emoji: "🟢",
|
|
340
|
+
color: "good"
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
function metadataText(metadata) {
|
|
344
|
+
if (!metadata || Object.keys(metadata).length === 0) return void 0;
|
|
345
|
+
return truncate(Object.entries(metadata).map(([key, value]) => {
|
|
346
|
+
const string = stringSchema.safeParse(value);
|
|
347
|
+
return `*${escapeSlack(key)}:* ${escapeSlack(string.success ? string.data : JSON.stringify(value))}`;
|
|
348
|
+
}).join(" • "), METADATA_CHARACTER_LIMIT);
|
|
349
|
+
}
|
|
350
|
+
function rawCell(text) {
|
|
351
|
+
return {
|
|
352
|
+
type: "raw_text",
|
|
353
|
+
text
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
function taskTables(tasks) {
|
|
357
|
+
const header = [
|
|
358
|
+
"Task",
|
|
359
|
+
"Passed",
|
|
360
|
+
"Retries",
|
|
361
|
+
"Failed",
|
|
362
|
+
"Skipped",
|
|
363
|
+
"Time"
|
|
364
|
+
].map(rawCell);
|
|
365
|
+
const headerCharacters = rowCharacters(header);
|
|
366
|
+
const tables = [];
|
|
367
|
+
let rows = [header];
|
|
368
|
+
let characters = headerCharacters;
|
|
369
|
+
for (const task of tasks) {
|
|
370
|
+
const values = [
|
|
371
|
+
String(task.passed),
|
|
372
|
+
String(task.retried),
|
|
373
|
+
String(task.failed),
|
|
374
|
+
String(task.skipped),
|
|
375
|
+
formatDuration(task.duration)
|
|
376
|
+
];
|
|
377
|
+
const taskNameLimit = TABLE_CHARACTER_LIMIT - headerCharacters - values.reduce((total, value) => total + value.length, 0);
|
|
378
|
+
const row = [truncate(task.name, taskNameLimit), ...values].map(rawCell);
|
|
379
|
+
const rowLength = rowCharacters(row);
|
|
380
|
+
if (rows.length === TABLE_ROW_LIMIT || characters + rowLength > TABLE_CHARACTER_LIMIT) {
|
|
381
|
+
tables.push(rows);
|
|
382
|
+
rows = [header];
|
|
383
|
+
characters = headerCharacters;
|
|
384
|
+
}
|
|
385
|
+
rows.push(row);
|
|
386
|
+
characters += rowLength;
|
|
387
|
+
}
|
|
388
|
+
tables.push(rows);
|
|
389
|
+
return tables;
|
|
390
|
+
}
|
|
391
|
+
function rowCharacters(row) {
|
|
392
|
+
return row.reduce((total, cell) => total + cell.text.length, 0);
|
|
393
|
+
}
|
|
394
|
+
function formatDuration(milliseconds) {
|
|
395
|
+
if (milliseconds < 1e3) return `${Math.round(milliseconds)}ms`;
|
|
396
|
+
return `${(milliseconds / 1e3).toFixed(1)}s`;
|
|
397
|
+
}
|
|
398
|
+
function chunk(entries, limit) {
|
|
399
|
+
const chunks = [];
|
|
400
|
+
for (const entry of entries) {
|
|
401
|
+
const value = entry.length > limit ? `${entry.slice(0, limit - 1)}…` : entry;
|
|
402
|
+
const previous = chunks.at(-1);
|
|
403
|
+
if (previous && previous.length + value.length + 2 <= limit) chunks[chunks.length - 1] = `${previous}\n\n${value}`;
|
|
404
|
+
else chunks.push(value);
|
|
405
|
+
}
|
|
406
|
+
return chunks;
|
|
407
|
+
}
|
|
408
|
+
function truncate(value, limit) {
|
|
409
|
+
return value.length > limit ? `${value.slice(0, limit - 1)}…` : value;
|
|
410
|
+
}
|
|
411
|
+
function timeSpan(values) {
|
|
412
|
+
let startedAt;
|
|
413
|
+
let endedAt;
|
|
414
|
+
for (const value of values) {
|
|
415
|
+
if (value.startedAt !== void 0) startedAt = Math.min(startedAt ?? value.startedAt, value.startedAt);
|
|
416
|
+
if (value.endedAt !== void 0) endedAt = Math.max(endedAt ?? value.endedAt, value.endedAt);
|
|
417
|
+
}
|
|
418
|
+
return {
|
|
419
|
+
startedAt,
|
|
420
|
+
endedAt
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
//#endregion
|
|
424
|
+
//#region src/slack/reporter.ts
|
|
425
|
+
function slackReporter(options) {
|
|
426
|
+
const client = new WebClient(options.token, {
|
|
427
|
+
rejectRateLimitedCalls: true,
|
|
428
|
+
retryConfig: { retries: 0 },
|
|
429
|
+
timeout: 1e4
|
|
430
|
+
});
|
|
431
|
+
const postMessage = async (arguments_) => {
|
|
432
|
+
try {
|
|
433
|
+
return await client.chat.postMessage(arguments_);
|
|
434
|
+
} catch (error) {
|
|
435
|
+
if (!(error instanceof WebAPIRateLimitedError)) throw error;
|
|
436
|
+
await setTimeout(error.retryAfter * 1e3);
|
|
437
|
+
return client.chat.postMessage(arguments_);
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
return { async onTestRunEnd(modules, unhandledErrors) {
|
|
441
|
+
const reports = collectWorkflowReports(modules);
|
|
442
|
+
if (reports.length === 0) return;
|
|
443
|
+
const parentMessage = summaryMessage(reports, options.runUrl);
|
|
444
|
+
let parentTimestamp;
|
|
445
|
+
try {
|
|
446
|
+
const parentArguments = {
|
|
447
|
+
channel: options.channel,
|
|
448
|
+
...parentMessage,
|
|
449
|
+
mrkdwn: false,
|
|
450
|
+
unfurl_links: false
|
|
451
|
+
};
|
|
452
|
+
parentTimestamp = (await postMessage(parentArguments)).ts;
|
|
453
|
+
} catch {
|
|
454
|
+
console.warn("[invoker] Could not post the Slack report.");
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
const replies = [
|
|
458
|
+
...reports.flatMap(failureMessages),
|
|
459
|
+
...reports.flatMap(retryMessages),
|
|
460
|
+
...unhandledErrorMessages(unhandledErrors)
|
|
461
|
+
];
|
|
462
|
+
if (replies.length > 0 && !parentTimestamp) {
|
|
463
|
+
console.warn("[invoker] Slack did not return a timestamp for the report thread.");
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
let failedMessages = 0;
|
|
467
|
+
for (const reply of replies) try {
|
|
468
|
+
await postMessage({
|
|
469
|
+
channel: options.channel,
|
|
470
|
+
...reply,
|
|
471
|
+
thread_ts: parentTimestamp,
|
|
472
|
+
mrkdwn: false,
|
|
473
|
+
unfurl_links: false
|
|
474
|
+
});
|
|
475
|
+
} catch {
|
|
476
|
+
failedMessages += 1;
|
|
477
|
+
}
|
|
478
|
+
if (failedMessages > 0) console.warn(`[invoker] Could not post ${failedMessages} Slack report message${failedMessages === 1 ? "" : "s"}.`);
|
|
479
|
+
} };
|
|
480
|
+
}
|
|
481
|
+
//#endregion
|
|
482
|
+
export { slackReporter };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akshatmittal/invoker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Typed matrix-driven regression workflows for Vitest.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"matrix",
|
|
@@ -27,11 +27,27 @@
|
|
|
27
27
|
".": {
|
|
28
28
|
"types": "./dist/index.d.mts",
|
|
29
29
|
"import": "./dist/index.mjs"
|
|
30
|
+
},
|
|
31
|
+
"./github": {
|
|
32
|
+
"types": "./dist/github.d.mts",
|
|
33
|
+
"import": "./dist/github.mjs"
|
|
34
|
+
},
|
|
35
|
+
"./slack": {
|
|
36
|
+
"types": "./dist/slack.d.mts",
|
|
37
|
+
"import": "./dist/slack.mjs"
|
|
30
38
|
}
|
|
31
39
|
},
|
|
32
40
|
"publishConfig": {
|
|
33
41
|
"access": "public"
|
|
34
42
|
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@octokit/auth-app": "^8.3.0",
|
|
45
|
+
"@octokit/request": "^10.0.13",
|
|
46
|
+
"@slack/web-api": "^8.0.0",
|
|
47
|
+
"croner": "^10.0.1",
|
|
48
|
+
"evlog": "^2.26.0",
|
|
49
|
+
"zod": "^4.4.3"
|
|
50
|
+
},
|
|
35
51
|
"devDependencies": {
|
|
36
52
|
"@types/node": "^24.13.3",
|
|
37
53
|
"tsdown": "^0.22.14",
|
|
@@ -42,6 +58,11 @@
|
|
|
42
58
|
"peerDependencies": {
|
|
43
59
|
"vitest": "^4.1.10"
|
|
44
60
|
},
|
|
61
|
+
"peerDependenciesMeta": {
|
|
62
|
+
"vitest": {
|
|
63
|
+
"optional": true
|
|
64
|
+
}
|
|
65
|
+
},
|
|
45
66
|
"engines": {
|
|
46
67
|
"node": "^24.18.1"
|
|
47
68
|
},
|