qualty 0.1.4 → 0.1.5
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/bin/qualty.js +188 -38
- package/package.json +1 -1
package/bin/qualty.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import { appendFileSync } from "node:fs";
|
|
3
4
|
import { spawn } from "node:child_process";
|
|
4
5
|
import process from "node:process";
|
|
5
6
|
|
|
@@ -174,18 +175,143 @@ function sleep(ms) {
|
|
|
174
175
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
175
176
|
}
|
|
176
177
|
|
|
178
|
+
function isGithubActions() {
|
|
179
|
+
return process.env.GITHUB_ACTIONS === "true";
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Safe one-line text for a GitHub-flavored Markdown table cell. */
|
|
183
|
+
function mdTableCell(text, maxLen = 200) {
|
|
184
|
+
return String(text ?? "")
|
|
185
|
+
.replace(/\r\n/g, "\n")
|
|
186
|
+
.replace(/\n/g, " ")
|
|
187
|
+
.replace(/\\/g, "\\\\")
|
|
188
|
+
.replace(/\|/g, "\\|")
|
|
189
|
+
.trim()
|
|
190
|
+
.slice(0, maxLen) || "—";
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function githubWorkflowRunUrl() {
|
|
194
|
+
const server = process.env.GITHUB_SERVER_URL || "https://github.com";
|
|
195
|
+
const repo = process.env.GITHUB_REPOSITORY;
|
|
196
|
+
const runId = process.env.GITHUB_RUN_ID;
|
|
197
|
+
if (!repo || !runId) return "";
|
|
198
|
+
return `${server.replace(/\/$/, "")}/${repo}/actions/runs/${runId}`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function executionOutcome(status) {
|
|
202
|
+
const st = status?.status;
|
|
203
|
+
const failed = Number(status?.failed_tests ?? 0);
|
|
204
|
+
if (st === "completed" && failed === 0) return { ok: true, label: "Pass" };
|
|
205
|
+
if (st === "completed" && failed > 0) return { ok: false, label: "Fail" };
|
|
206
|
+
if (st === "cancelled") return { ok: false, label: "Cancelled" };
|
|
207
|
+
if (st === "failed") return { ok: false, label: "Failed" };
|
|
208
|
+
return { ok: false, label: mdTableCell(st || "?", 24) };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Rich Markdown for the job "Summary" tab (tables, links). Step logs stay plain text.
|
|
213
|
+
* https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary
|
|
214
|
+
*/
|
|
215
|
+
function appendGithubJobSummaryMarkdown(markdown) {
|
|
216
|
+
const path = process.env.GITHUB_STEP_SUMMARY;
|
|
217
|
+
if (!path || !markdown) return;
|
|
218
|
+
try {
|
|
219
|
+
appendFileSync(path, `${markdown}\n`, "utf8");
|
|
220
|
+
} catch {
|
|
221
|
+
// best effort — never fail the job for summary I/O
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function writeQualtyGithubJobSummary({ executionJobIds, finalStatuses, passed, failed }) {
|
|
226
|
+
if (!isGithubActions()) return;
|
|
227
|
+
|
|
228
|
+
const total = executionJobIds.length;
|
|
229
|
+
const lines = [];
|
|
230
|
+
lines.push("## Qualty");
|
|
231
|
+
lines.push("");
|
|
232
|
+
lines.push(
|
|
233
|
+
`**${passed} passed**, **${failed} failed** · ${total} run${total === 1 ? "" : "s"}.`
|
|
234
|
+
);
|
|
235
|
+
lines.push("");
|
|
236
|
+
lines.push("| Test | Execution | Status | Failed | Result | Qualty |");
|
|
237
|
+
lines.push("| --- | --- | --- | ---: | --- | --- |");
|
|
238
|
+
|
|
239
|
+
for (const executionId of executionJobIds) {
|
|
240
|
+
const status = finalStatuses[executionId] || {};
|
|
241
|
+
const title = mdTableCell(status.episode_name || "—", 72);
|
|
242
|
+
const idCell = `\`${mdTableCell(executionId, 80)}\``;
|
|
243
|
+
const apiStatus = mdTableCell(status.status ?? "—", 20);
|
|
244
|
+
const failedN = status.failed_tests != null ? String(status.failed_tests) : "—";
|
|
245
|
+
const outcome = executionOutcome(status);
|
|
246
|
+
const resultCell = outcome.ok ? `✅ **${outcome.label}**` : `❌ **${outcome.label}**`;
|
|
247
|
+
const url = String(status.url || "").trim();
|
|
248
|
+
const linkCell = url ? `[Open run](${url})` : "—";
|
|
249
|
+
lines.push(
|
|
250
|
+
`| ${title} | ${idCell} | ${apiStatus} | ${failedN} | ${resultCell} | ${linkCell} |`
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const comboRows = [];
|
|
255
|
+
for (const executionId of executionJobIds) {
|
|
256
|
+
const status = finalStatuses[executionId] || {};
|
|
257
|
+
const combos = Array.isArray(status.combinations) ? status.combinations : [];
|
|
258
|
+
const shortTitle = mdTableCell(
|
|
259
|
+
status.episode_name || (executionId ? String(executionId).slice(0, 8) : "—"),
|
|
260
|
+
40
|
|
261
|
+
);
|
|
262
|
+
for (const c of combos) {
|
|
263
|
+
const device = mdTableCell(c.device ?? "?", 32);
|
|
264
|
+
const comboSt = mdTableCell(
|
|
265
|
+
c.status ?? (c.success === true ? "passed" : c.success === false ? "failed" : "?"),
|
|
266
|
+
16
|
|
267
|
+
);
|
|
268
|
+
comboRows.push(`| \`${mdTableCell(executionId, 36)}\` | ${shortTitle} | ${device} | ${comboSt} |`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (comboRows.length > 0) {
|
|
273
|
+
lines.push("");
|
|
274
|
+
lines.push("<details>");
|
|
275
|
+
lines.push("<summary><strong>Per device / combination</strong></summary>");
|
|
276
|
+
lines.push("");
|
|
277
|
+
lines.push("| Execution | Test | Device | Result |");
|
|
278
|
+
lines.push("| --- | --- | --- | --- |");
|
|
279
|
+
lines.push(...comboRows);
|
|
280
|
+
lines.push("");
|
|
281
|
+
lines.push("</details>");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const runUrl = githubWorkflowRunUrl();
|
|
285
|
+
if (runUrl) {
|
|
286
|
+
lines.push("");
|
|
287
|
+
lines.push(`[This workflow run on GitHub](${runUrl})`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
appendGithubJobSummaryMarkdown(lines.join("\n"));
|
|
291
|
+
}
|
|
292
|
+
|
|
177
293
|
function truncateForCiLog(text, maxLen) {
|
|
178
294
|
const s = String(text ?? "");
|
|
179
295
|
if (s.length <= maxLen) return s;
|
|
180
|
-
|
|
296
|
+
const tail = isGithubActions()
|
|
297
|
+
? `… (truncated, ${s.length - maxLen} more chars)`
|
|
298
|
+
: `[qualty] … (truncated, ${s.length - maxLen} more chars)`;
|
|
299
|
+
return `${s.slice(0, maxLen)}\n${tail}`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** One log line: `[qualty]` prefix only outside GitHub Actions (inside ::group::, plain text reads better). */
|
|
303
|
+
function viewOut(line) {
|
|
304
|
+
// eslint-disable-next-line no-console
|
|
305
|
+
console.log(isGithubActions() ? line : `[qualty] ${line}`);
|
|
181
306
|
}
|
|
182
307
|
|
|
183
|
-
function logPrefixedLines(
|
|
308
|
+
function logPrefixedLines(ghaIndent, nonGhaPrefix, text) {
|
|
184
309
|
const body = String(text ?? "").trimEnd();
|
|
185
310
|
if (!body) return;
|
|
186
|
-
|
|
311
|
+
const prefix = isGithubActions() ? ghaIndent : nonGhaPrefix;
|
|
312
|
+
for (const ln of body.split("\n")) {
|
|
187
313
|
// eslint-disable-next-line no-console
|
|
188
|
-
console.log(`${prefix}${
|
|
314
|
+
console.log(`${prefix}${ln}`);
|
|
189
315
|
}
|
|
190
316
|
}
|
|
191
317
|
|
|
@@ -194,40 +320,51 @@ function logPrefixedLines(prefix, text) {
|
|
|
194
320
|
* Data comes from GET status payload fields on each combination (steps_json, explanation, agent_output).
|
|
195
321
|
*/
|
|
196
322
|
function printQualtyViewLogsReport(executionId, status) {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if (
|
|
323
|
+
const gha = isGithubActions();
|
|
324
|
+
const title = `${status.episode_name || "run"} (${executionId})`;
|
|
325
|
+
if (gha) {
|
|
326
|
+
// Collapsible section in GitHub Actions (no rich tables — stdout is plain text).
|
|
327
|
+
// eslint-disable-next-line no-console
|
|
328
|
+
console.log(`::group::Qualty · ${title}`);
|
|
329
|
+
} else {
|
|
200
330
|
// eslint-disable-next-line no-console
|
|
201
|
-
console.log(
|
|
331
|
+
console.log(`\n[qualty] ━━━ View logs: ${executionId} (${status.episode_name || "run"}) ━━━`);
|
|
202
332
|
}
|
|
333
|
+
if (status.url) viewOut(`URL: ${status.url}`);
|
|
203
334
|
if (status.error) {
|
|
204
|
-
|
|
205
|
-
|
|
335
|
+
viewOut("Run error:");
|
|
336
|
+
logPrefixedLines(" ", "[qualty] ", truncateForCiLog(status.error, 4000));
|
|
206
337
|
}
|
|
207
338
|
if (status.expected_behavior) {
|
|
208
|
-
|
|
209
|
-
|
|
339
|
+
viewOut("Expected behavior:");
|
|
340
|
+
logPrefixedLines(" ", "[qualty] ", truncateForCiLog(status.expected_behavior, 6000));
|
|
210
341
|
}
|
|
211
342
|
const combos = Array.isArray(status.combinations) ? status.combinations : [];
|
|
212
343
|
if (combos.length === 0) {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
344
|
+
viewOut("(No per-device breakdown in API response yet — open this run in the Qualty dashboard for full logs.)");
|
|
345
|
+
if (gha) {
|
|
346
|
+
// eslint-disable-next-line no-console
|
|
347
|
+
console.log("::endgroup::");
|
|
348
|
+
} else {
|
|
349
|
+
// eslint-disable-next-line no-console
|
|
350
|
+
console.log(`[qualty] ━━━ End view logs: ${executionId} ━━━\n`);
|
|
351
|
+
}
|
|
219
352
|
return;
|
|
220
353
|
}
|
|
221
354
|
for (let i = 0; i < combos.length; i += 1) {
|
|
222
355
|
const c = combos[i];
|
|
223
356
|
const device = c.device ?? "?";
|
|
224
357
|
const comboStatus = c.status ?? (c.success === true ? "passed" : c.success === false ? "failed" : "?");
|
|
225
|
-
|
|
226
|
-
|
|
358
|
+
if (gha) {
|
|
359
|
+
// eslint-disable-next-line no-console
|
|
360
|
+
console.log(`::group::${device} · ${comboStatus} (${i + 1}/${combos.length})`);
|
|
361
|
+
} else {
|
|
362
|
+
// eslint-disable-next-line no-console
|
|
363
|
+
console.log(`\n[qualty] --- Combination ${i + 1}/${combos.length} (${device} · ${comboStatus}) ---`);
|
|
364
|
+
}
|
|
227
365
|
const steps = Array.isArray(c.steps_json) ? c.steps_json : [];
|
|
228
366
|
if (steps.length > 0) {
|
|
229
|
-
|
|
230
|
-
console.log(`[qualty] Steps (${steps.length}):`);
|
|
367
|
+
viewOut(`Steps (${steps.length}):`);
|
|
231
368
|
for (let j = 0; j < steps.length; j += 1) {
|
|
232
369
|
const s = steps[j];
|
|
233
370
|
const label =
|
|
@@ -235,34 +372,38 @@ function printQualtyViewLogsReport(executionId, status) {
|
|
|
235
372
|
(s.description && String(s.description).trim().slice(0, 100)) ||
|
|
236
373
|
`Step ${j + 1}`;
|
|
237
374
|
const st = s.status != null ? s.status : "?";
|
|
238
|
-
|
|
239
|
-
console.log(`[qualty] ${j + 1}. [${st}] ${label}`);
|
|
375
|
+
viewOut(` ${j + 1}. [${st}] ${label}`);
|
|
240
376
|
if (s.description && String(s.description).trim() && String(s.description) !== String(s.name)) {
|
|
241
|
-
logPrefixedLines("[qualty] ", truncateForCiLog(s.description, 4000));
|
|
377
|
+
logPrefixedLines(" ", "[qualty] ", truncateForCiLog(s.description, 4000));
|
|
242
378
|
}
|
|
243
379
|
if (s.action) {
|
|
244
|
-
|
|
245
|
-
console.log(`[qualty] action: ${truncateForCiLog(s.action, 2000)}`);
|
|
380
|
+
logPrefixedLines(" ", "[qualty] ", `action: ${truncateForCiLog(s.action, 2000)}`);
|
|
246
381
|
}
|
|
247
382
|
}
|
|
248
383
|
} else if (c.total_steps) {
|
|
249
|
-
|
|
250
|
-
console.log(`[qualty] (Step list not available yet; ${c.total_steps} step(s) reported.)`);
|
|
384
|
+
viewOut(`(Step list not available yet; ${c.total_steps} step(s) reported.)`);
|
|
251
385
|
}
|
|
252
386
|
if (c.explanation) {
|
|
253
|
-
|
|
254
|
-
|
|
387
|
+
viewOut("Explanation:");
|
|
388
|
+
logPrefixedLines(" ", "[qualty] ", truncateForCiLog(c.explanation, 12000));
|
|
255
389
|
}
|
|
256
390
|
const evaluator = c.agent_output ?? c.gpt_output;
|
|
257
391
|
if (evaluator) {
|
|
392
|
+
viewOut("Final evaluator output:");
|
|
393
|
+
logPrefixedLines(" ", "[qualty] ", truncateForCiLog(evaluator, 16000));
|
|
394
|
+
}
|
|
395
|
+
if (gha) {
|
|
258
396
|
// eslint-disable-next-line no-console
|
|
259
|
-
console.log(
|
|
260
|
-
`[qualty] Final evaluator output:\n[qualty] ${truncateForCiLog(evaluator, 16000).split("\n").join("\n[qualty] ")}`
|
|
261
|
-
);
|
|
397
|
+
console.log("::endgroup::");
|
|
262
398
|
}
|
|
263
399
|
}
|
|
264
|
-
|
|
265
|
-
|
|
400
|
+
if (gha) {
|
|
401
|
+
// eslint-disable-next-line no-console
|
|
402
|
+
console.log("::endgroup::");
|
|
403
|
+
} else {
|
|
404
|
+
// eslint-disable-next-line no-console
|
|
405
|
+
console.log(`\n[qualty] ━━━ End view logs: ${executionId} ━━━\n`);
|
|
406
|
+
}
|
|
266
407
|
}
|
|
267
408
|
|
|
268
409
|
async function runCi(args) {
|
|
@@ -377,9 +518,18 @@ async function runCi(args) {
|
|
|
377
518
|
);
|
|
378
519
|
}
|
|
379
520
|
|
|
521
|
+
writeQualtyGithubJobSummary({ executionJobIds, finalStatuses, passed, failed });
|
|
522
|
+
|
|
380
523
|
if (!noViewLogs) {
|
|
381
|
-
|
|
382
|
-
|
|
524
|
+
if (isGithubActions()) {
|
|
525
|
+
// eslint-disable-next-line no-console
|
|
526
|
+
console.log(
|
|
527
|
+
"[qualty] Open the job Summary tab for a results table; expand the Qualty groups below for full step logs."
|
|
528
|
+
);
|
|
529
|
+
} else {
|
|
530
|
+
// eslint-disable-next-line no-console
|
|
531
|
+
console.log(`[qualty] Detailed run output (same fields as dashboard "View logs"):`);
|
|
532
|
+
}
|
|
383
533
|
for (const executionId of executionJobIds) {
|
|
384
534
|
printQualtyViewLogsReport(executionId, finalStatuses[executionId] || {});
|
|
385
535
|
}
|