@devasign/verify 1.0.1 → 1.2.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 +2 -0
- package/dist/cli.js +155 -32
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -16,3 +16,5 @@ Guarantees:
|
|
|
16
16
|
- Setup problems (no start command, missing secret names, wrong runtime) are uploaded as a structured diagnosis, and the process exits 0.
|
|
17
17
|
|
|
18
18
|
Local run against a dev backend: mint a token with `backend/scripts/verify-dev-token.ts`, then `devasign-verify run --api-url http://localhost:8787 --token <jwt> --pr <n> --sha <head-sha>`. Offline: `--plan-file plan.json --results-out results.json`.
|
|
19
|
+
|
|
20
|
+
Exit code: 0 unless `--fail-on verdict` (a criterion failed) or `--fail-on unverifiable` (a criterion failed or could not be verified) is set; unset, the repository's DevAsign setting applies. An unknown `--fail-on` value exits 2. Criteria the plan could not cover are printed as warnings with their reason and fix link, and the Actions step summary lists every criterion. On Actions the CLI also writes `run-id`, `outcome` and `browsers` to `GITHUB_OUTPUT`.
|
package/dist/cli.js
CHANGED
|
@@ -7577,7 +7577,17 @@ function diagnosePlaywrightOutput(output) {
|
|
|
7577
7577
|
}
|
|
7578
7578
|
|
|
7579
7579
|
// src/log.ts
|
|
7580
|
+
import { appendFileSync } from "node:fs";
|
|
7580
7581
|
var onActions = () => process.env.GITHUB_ACTIONS === "true";
|
|
7582
|
+
function setOutput(name, value) {
|
|
7583
|
+
const file = process.env.GITHUB_OUTPUT;
|
|
7584
|
+
if (!file) return;
|
|
7585
|
+
try {
|
|
7586
|
+
appendFileSync(file, `${name}=${value.replace(/\r?\n/g, " ")}
|
|
7587
|
+
`);
|
|
7588
|
+
} catch {
|
|
7589
|
+
}
|
|
7590
|
+
}
|
|
7581
7591
|
var log = {
|
|
7582
7592
|
info(msg) {
|
|
7583
7593
|
console.log(`devasign: ${msg}`);
|
|
@@ -7627,11 +7637,11 @@ function staticTokenSource(token) {
|
|
|
7627
7637
|
}
|
|
7628
7638
|
|
|
7629
7639
|
// src/run.ts
|
|
7630
|
-
import { appendFileSync, existsSync as existsSync5, writeFileSync as writeFileSync3 } from "node:fs";
|
|
7640
|
+
import { appendFileSync as appendFileSync2, existsSync as existsSync5, writeFileSync as writeFileSync3 } from "node:fs";
|
|
7631
7641
|
import path7 from "node:path";
|
|
7632
7642
|
|
|
7633
7643
|
// src/types.ts
|
|
7634
|
-
var CLI_VERSION = "1.0
|
|
7644
|
+
var CLI_VERSION = "1.2.0";
|
|
7635
7645
|
|
|
7636
7646
|
// src/api.ts
|
|
7637
7647
|
var ApiError = class extends Error {
|
|
@@ -7889,6 +7899,7 @@ import { spawn } from "node:child_process";
|
|
|
7889
7899
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
7890
7900
|
import path3 from "node:path";
|
|
7891
7901
|
var MAX_CAPTURE = 5 * 1024 * 1024;
|
|
7902
|
+
var DRAIN_MS = 2e3;
|
|
7892
7903
|
async function runCommand(opts) {
|
|
7893
7904
|
const started = Date.now();
|
|
7894
7905
|
return new Promise((resolve) => {
|
|
@@ -7897,12 +7908,28 @@ async function runCommand(opts) {
|
|
|
7897
7908
|
let output = "";
|
|
7898
7909
|
let timedOut = false;
|
|
7899
7910
|
let settled = false;
|
|
7911
|
+
let drain;
|
|
7912
|
+
let exited = null;
|
|
7913
|
+
const armDrain = () => {
|
|
7914
|
+
if (!exited) return;
|
|
7915
|
+
clearTimeout(drain);
|
|
7916
|
+
drain = setTimeout(() => finish({ ...exited, timedOut }), DRAIN_MS);
|
|
7917
|
+
};
|
|
7900
7918
|
const env = { ...process.env, ...opts.env || {}, CI: "true", FORCE_COLOR: "0" };
|
|
7901
7919
|
for (const k of Object.keys(env)) if (k.startsWith("NODE_TEST_")) delete env[k];
|
|
7902
|
-
const
|
|
7920
|
+
const group = process.platform !== "win32";
|
|
7921
|
+
const child = spawn(opts.cmd, opts.args, { cwd: opts.cwd, env, stdio: ["ignore", "pipe", "pipe"], detached: group });
|
|
7922
|
+
const killAll = () => {
|
|
7923
|
+
try {
|
|
7924
|
+
if (group && child.pid) process.kill(-child.pid, "SIGKILL");
|
|
7925
|
+
else child.kill("SIGKILL");
|
|
7926
|
+
} catch {
|
|
7927
|
+
child.kill("SIGKILL");
|
|
7928
|
+
}
|
|
7929
|
+
};
|
|
7903
7930
|
const timer = setTimeout(() => {
|
|
7904
7931
|
timedOut = true;
|
|
7905
|
-
|
|
7932
|
+
killAll();
|
|
7906
7933
|
}, opts.timeoutMs);
|
|
7907
7934
|
const take = (chunk, which) => {
|
|
7908
7935
|
const s = chunk.toString("utf8");
|
|
@@ -7912,6 +7939,7 @@ async function runCommand(opts) {
|
|
|
7912
7939
|
if (opts.onLine) {
|
|
7913
7940
|
for (const line of s.split("\n")) if (line) opts.onLine(line);
|
|
7914
7941
|
}
|
|
7942
|
+
armDrain();
|
|
7915
7943
|
};
|
|
7916
7944
|
child.stdout?.on("data", (c) => take(c, "out"));
|
|
7917
7945
|
child.stderr?.on("data", (c) => take(c, "err"));
|
|
@@ -7919,6 +7947,7 @@ async function runCommand(opts) {
|
|
|
7919
7947
|
if (settled) return;
|
|
7920
7948
|
settled = true;
|
|
7921
7949
|
clearTimeout(timer);
|
|
7950
|
+
clearTimeout(drain);
|
|
7922
7951
|
const full = { ...result, stdout, stderr, output, durationMs: Date.now() - started };
|
|
7923
7952
|
if (opts.logFile) {
|
|
7924
7953
|
try {
|
|
@@ -7935,6 +7964,10 @@ ${output}
|
|
|
7935
7964
|
resolve(full);
|
|
7936
7965
|
};
|
|
7937
7966
|
child.on("error", (err) => finish({ code: null, signal: null, timedOut, spawnError: err.message }));
|
|
7967
|
+
child.on("exit", (code, signal) => {
|
|
7968
|
+
exited = { code, signal };
|
|
7969
|
+
armDrain();
|
|
7970
|
+
});
|
|
7938
7971
|
child.on("close", (code, signal) => finish({ code, signal, timedOut }));
|
|
7939
7972
|
});
|
|
7940
7973
|
}
|
|
@@ -8009,7 +8042,8 @@ async function runFileTests(args) {
|
|
|
8009
8042
|
|
|
8010
8043
|
// src/runners/playwright.ts
|
|
8011
8044
|
import { createRequire as createRequire2 } from "node:module";
|
|
8012
|
-
import { existsSync as existsSync3, readFileSync as readFileSync4, statSync as statSync3 } from "node:fs";
|
|
8045
|
+
import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync3 } from "node:fs";
|
|
8046
|
+
import { homedir } from "node:os";
|
|
8013
8047
|
import path5 from "node:path";
|
|
8014
8048
|
var require3 = createRequire2(import.meta.url);
|
|
8015
8049
|
function playwrightCli(root) {
|
|
@@ -8029,9 +8063,17 @@ function generatePlaywrightConfig(a) {
|
|
|
8029
8063
|
baseImport,
|
|
8030
8064
|
"const b: any = (__base as any)?.default ?? __base ?? {};",
|
|
8031
8065
|
"// One browser project only: recordings once, and no browsers we did not install.",
|
|
8032
|
-
"
|
|
8033
|
-
"
|
|
8034
|
-
"
|
|
8066
|
+
"// Project fields win over config fields in Playwright, so the customer's own",
|
|
8067
|
+
"// testDir/retries/recording settings are stripped and ours are re-applied here;",
|
|
8068
|
+
"// dependencies/teardown would reference projects we did not keep.",
|
|
8069
|
+
"const __pick = (ps: any[]) => ps.find((p: any) => /chrom/i.test(String(p?.name || ''))) ?? ps[0];",
|
|
8070
|
+
"const __clean = (p: any) => {",
|
|
8071
|
+
" const q: any = { ...p };",
|
|
8072
|
+
" for (const k of ['dependencies', 'teardown', 'testDir', 'testMatch', 'testIgnore', 'retries', 'outputDir', 'grep', 'grepInvert', 'snapshotDir']) delete q[k];",
|
|
8073
|
+
" q.use = { ...(q.use || {}), video: 'on', trace: 'on', screenshot: 'on' };",
|
|
8074
|
+
" return q;",
|
|
8075
|
+
"};",
|
|
8076
|
+
"const projects = Array.isArray(b.projects) && b.projects.length ? [__clean(__pick(b.projects))] : undefined;",
|
|
8035
8077
|
"export default defineConfig({",
|
|
8036
8078
|
" ...b,",
|
|
8037
8079
|
` testDir: ${j(a.testDir)},`,
|
|
@@ -8071,6 +8113,20 @@ function stripAnsi(s) {
|
|
|
8071
8113
|
}
|
|
8072
8114
|
var KIND_BY_ATTACHMENT = { video: "video", trace: "trace", screenshot: "screenshot" };
|
|
8073
8115
|
var TYPE_BY_KIND = { video: "video/webm", trace: "application/zip", screenshot: "image/png" };
|
|
8116
|
+
function testOutcome(retries) {
|
|
8117
|
+
if (!retries.length) return "error";
|
|
8118
|
+
const last = retries[retries.length - 1];
|
|
8119
|
+
if (last === "pass") return retries.every((s) => s === "pass") ? "pass" : "flaky";
|
|
8120
|
+
return retries.includes("fail") ? "fail" : "error";
|
|
8121
|
+
}
|
|
8122
|
+
function fileStatus(perTest) {
|
|
8123
|
+
const outcomes = perTest.map(testOutcome);
|
|
8124
|
+
if (!outcomes.length) return "error";
|
|
8125
|
+
if (outcomes.includes("fail")) return "fail";
|
|
8126
|
+
if (outcomes.includes("flaky")) return "flaky";
|
|
8127
|
+
if (outcomes.includes("error")) return "error";
|
|
8128
|
+
return "pass";
|
|
8129
|
+
}
|
|
8074
8130
|
function mapReport(report, tests, ws, artifacts, overallOutput) {
|
|
8075
8131
|
const specs = flattenSpecs(report);
|
|
8076
8132
|
const results = [];
|
|
@@ -8079,8 +8135,11 @@ function mapReport(report, tests, ws, artifacts, overallOutput) {
|
|
|
8079
8135
|
const attempts = [];
|
|
8080
8136
|
let n = 0;
|
|
8081
8137
|
let anyResult = false;
|
|
8138
|
+
const perTest = [];
|
|
8082
8139
|
for (const spec of mine) {
|
|
8083
8140
|
for (const pt of spec.tests) {
|
|
8141
|
+
const own = [];
|
|
8142
|
+
perTest.push(own);
|
|
8084
8143
|
for (const r of [...pt.results || []].sort((x, y) => (x.retry ?? 0) - (y.retry ?? 0))) {
|
|
8085
8144
|
anyResult = true;
|
|
8086
8145
|
n += 1;
|
|
@@ -8106,6 +8165,7 @@ function mapReport(report, tests, ws, artifacts, overallOutput) {
|
|
|
8106
8165
|
artifacts.push({ clientRef: posterRef, kind: "poster", path: screenshotPath, displayPath: ws.relative(screenshotPath), contentType: "image/png", testId: t.id, criterionIds: t.criterionIds, attempt: n, posterFor: videoRef });
|
|
8107
8166
|
refs.push(posterRef);
|
|
8108
8167
|
}
|
|
8168
|
+
own.push(status2);
|
|
8109
8169
|
attempts.push({ n, status: status2, durationMs: Math.round(r.duration ?? 0), error: status2 === "pass" ? void 0 : stripAnsi(message || r.status).split("\n").slice(0, 3).join(" ").slice(0, 500), artifactIds: refs });
|
|
8110
8170
|
}
|
|
8111
8171
|
}
|
|
@@ -8113,10 +8173,9 @@ function mapReport(report, tests, ws, artifacts, overallOutput) {
|
|
|
8113
8173
|
if (!anyResult) {
|
|
8114
8174
|
const err = report.errors?.[0]?.message || overallOutput.split("\n").find((l) => /Error|error/.test(l)) || "no result for this test";
|
|
8115
8175
|
attempts.push({ n: 1, status: "error", durationMs: 0, error: err.slice(0, 500), artifactIds: [] });
|
|
8176
|
+
perTest.push(["error"]);
|
|
8116
8177
|
}
|
|
8117
|
-
const
|
|
8118
|
-
const passes = statuses.filter((s) => s === "pass").length;
|
|
8119
|
-
const status = passes === statuses.length ? "pass" : passes > 0 ? "flaky" : statuses.includes("fail") ? "fail" : "error";
|
|
8178
|
+
const status = fileStatus(perTest.filter((t2) => t2.length));
|
|
8120
8179
|
results.push({
|
|
8121
8180
|
id: `r-${t.id}`,
|
|
8122
8181
|
testId: t.id,
|
|
@@ -8134,7 +8193,24 @@ function mapReport(report, tests, ws, artifacts, overallOutput) {
|
|
|
8134
8193
|
}
|
|
8135
8194
|
return results;
|
|
8136
8195
|
}
|
|
8196
|
+
function playwrightBrowsersRoot(env = process.env, home = homedir()) {
|
|
8197
|
+
if (env.PLAYWRIGHT_BROWSERS_PATH) return env.PLAYWRIGHT_BROWSERS_PATH;
|
|
8198
|
+
if (process.platform === "darwin") return path5.join(home, "Library", "Caches", "ms-playwright");
|
|
8199
|
+
if (process.platform === "win32") return path5.join(env.LOCALAPPDATA || home, "ms-playwright");
|
|
8200
|
+
return path5.join(home, ".cache", "ms-playwright");
|
|
8201
|
+
}
|
|
8202
|
+
function hasChromium(root, read = readdirSync2) {
|
|
8203
|
+
try {
|
|
8204
|
+
return read(root).some((d) => /^chromium(_headless_shell)?-\d+$/.test(d));
|
|
8205
|
+
} catch {
|
|
8206
|
+
return false;
|
|
8207
|
+
}
|
|
8208
|
+
}
|
|
8137
8209
|
async function ensureBrowsers(root, ws) {
|
|
8210
|
+
if (hasChromium(playwrightBrowsersRoot())) {
|
|
8211
|
+
log.info("Chromium already installed; skipping download");
|
|
8212
|
+
return { ok: true, log: "cached" };
|
|
8213
|
+
}
|
|
8138
8214
|
const cli = playwrightCli(root);
|
|
8139
8215
|
const args = [...cli.args, "install", ...process.platform === "linux" ? ["--with-deps"] : [], "chromium"];
|
|
8140
8216
|
log.info("installing Chromium for Playwright");
|
|
@@ -8235,9 +8311,16 @@ var Workspace = class {
|
|
|
8235
8311
|
|
|
8236
8312
|
// src/run.ts
|
|
8237
8313
|
var sleep3 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
8314
|
+
var MIN_SERVER_DEADLINE_MS = 3e4;
|
|
8315
|
+
function serverDeadline(current, startedAt, hintMs) {
|
|
8316
|
+
if (typeof hintMs !== "number" || !Number.isFinite(hintMs) || hintMs <= 0) return current;
|
|
8317
|
+
return Math.min(current, startedAt + Math.max(hintMs, MIN_SERVER_DEADLINE_MS));
|
|
8318
|
+
}
|
|
8238
8319
|
async function resolvePlan(api, ctx, setup, timeoutMs) {
|
|
8239
|
-
const
|
|
8320
|
+
const startedAt = Date.now();
|
|
8321
|
+
let deadline = startedAt + timeoutMs;
|
|
8240
8322
|
let polls = 0;
|
|
8323
|
+
let finalPoll = false;
|
|
8241
8324
|
for (; ; ) {
|
|
8242
8325
|
const res = await api.resolve({
|
|
8243
8326
|
sha: ctx.sha,
|
|
@@ -8246,14 +8329,19 @@ async function resolvePlan(api, ctx, setup, timeoutMs) {
|
|
|
8246
8329
|
attempt: ctx.runAttempt,
|
|
8247
8330
|
setup: polls === 0 ? setup : void 0,
|
|
8248
8331
|
actions: { runId: ctx.runId, jobUrl: ctx.jobUrl, runnerOs: ctx.runnerOs },
|
|
8249
|
-
cliVersion: CLI_VERSION
|
|
8332
|
+
cliVersion: CLI_VERSION,
|
|
8333
|
+
// Tells the server this job is leaving, so a plan landing later re-dispatches CI
|
|
8334
|
+
// instead of stranding the run until it times out.
|
|
8335
|
+
...finalPoll ? { giveUp: true } : {}
|
|
8250
8336
|
});
|
|
8251
8337
|
polls += 1;
|
|
8252
8338
|
if (res.status !== "pending") return res;
|
|
8253
|
-
if (
|
|
8339
|
+
if (finalPoll) return res;
|
|
8340
|
+
deadline = serverDeadline(deadline, startedAt, res.giveUpAfterMs);
|
|
8254
8341
|
const wait = Math.min(Math.max(res.retryAfterMs || 5e3, 2e3), 3e4);
|
|
8255
8342
|
if (polls === 1 || polls % 6 === 0) log.info(`waiting for DevAsign to plan the tests${res.runId ? ` (run ${res.runId})` : ""}\u2026`);
|
|
8256
8343
|
await sleep3(wait);
|
|
8344
|
+
if (Date.now() >= deadline) finalPoll = true;
|
|
8257
8345
|
}
|
|
8258
8346
|
}
|
|
8259
8347
|
async function executePlan(plan, ws, opts) {
|
|
@@ -8284,6 +8372,7 @@ async function executePlan(plan, ws, opts) {
|
|
|
8284
8372
|
if (plan.playwright?.installBrowsers || !repoHasPlaywright(ws.root)) {
|
|
8285
8373
|
const inst = await ensureBrowsers(ws.root, ws);
|
|
8286
8374
|
if (!inst.ok) log.warn("Chromium install reported a failure; continuing \u2014 the run will tell us if the browser is missing");
|
|
8375
|
+
else setOutput("browsers", "true");
|
|
8287
8376
|
}
|
|
8288
8377
|
const generated = pw.filter((t) => t.origin === "generated");
|
|
8289
8378
|
const existing = pw.filter((t) => t.origin === "existing");
|
|
@@ -8304,10 +8393,27 @@ async function executePlan(plan, ws, opts) {
|
|
|
8304
8393
|
}
|
|
8305
8394
|
return { results, artifacts, doctor };
|
|
8306
8395
|
}
|
|
8307
|
-
|
|
8308
|
-
|
|
8309
|
-
const
|
|
8310
|
-
|
|
8396
|
+
var cell = (s) => s.replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
|
|
8397
|
+
function summaryTable(plan, results) {
|
|
8398
|
+
const unverifiable = new Map((plan.unverifiable ?? []).map((u) => [u.criterionId, u]));
|
|
8399
|
+
const rows = plan.criteria.map((c) => {
|
|
8400
|
+
const mine = results.filter((r) => r.criterionIds.includes(c.id));
|
|
8401
|
+
const u = unverifiable.get(c.id);
|
|
8402
|
+
if (mine.length) {
|
|
8403
|
+
const tests = mine.map((r) => `${r.level} ${r.origin} \`${cell(r.test)}\``).join("<br>");
|
|
8404
|
+
const outcome = mine.map((r) => `${r.status} (${r.attempts.length} attempt${r.attempts.length === 1 ? "" : "s"})`).join("<br>");
|
|
8405
|
+
return `| ${c.id}. ${cell(c.text)} | ${tests} | ${outcome} | |`;
|
|
8406
|
+
}
|
|
8407
|
+
const note = u ? `${cell(u.reason)}${u.fixUrl ? ` \u2014 [configure app start](${u.fixUrl})` : ""}` : "no test planned";
|
|
8408
|
+
return `| ${c.id}. ${cell(c.text)} | \u2014 | unverifiable | ${note} |`;
|
|
8409
|
+
});
|
|
8410
|
+
return ["| Criterion | Test | Outcome | Notes |", "|---|---|---|---|", ...rows].join("\n");
|
|
8411
|
+
}
|
|
8412
|
+
function announceUnverifiable(plan) {
|
|
8413
|
+
for (const u of plan.unverifiable ?? []) {
|
|
8414
|
+
log.warn(`criterion ${u.criterionId} is unverifiable: ${u.reason}${u.fixUrl ? ` \u2014 fix: ${u.fixUrl}` : ""}`);
|
|
8415
|
+
}
|
|
8416
|
+
if (plan.tests.length === 0) log.warn("no tests could be planned for this PR; every criterion will be reported as unverifiable");
|
|
8311
8417
|
}
|
|
8312
8418
|
async function run(opts) {
|
|
8313
8419
|
const started = Date.now();
|
|
@@ -8351,7 +8457,10 @@ async function run(opts) {
|
|
|
8351
8457
|
plan = resolved.plan;
|
|
8352
8458
|
runId = resolved.runId;
|
|
8353
8459
|
log.info(`plan ${plan.planId}: ${plan.tests.length} test(s) for ${plan.criteria.length} criteria (run ${runId})`);
|
|
8460
|
+
setOutput("run-id", runId);
|
|
8354
8461
|
}
|
|
8462
|
+
announceUnverifiable(plan);
|
|
8463
|
+
const failOn = opts.failOn ?? plan.failOn ?? "never";
|
|
8355
8464
|
try {
|
|
8356
8465
|
const { results, artifacts, doctor } = await executePlan(plan, ws, { yml, testTimeoutMs: opts.testTimeoutMs, setup });
|
|
8357
8466
|
let finalResults = results;
|
|
@@ -8383,31 +8492,33 @@ async function run(opts) {
|
|
|
8383
8492
|
log.info("results uploaded \u2014 DevAsign is judging; the PR check run and comment will update");
|
|
8384
8493
|
}
|
|
8385
8494
|
const counts = finalResults.reduce((m, r) => (m[r.status] = (m[r.status] || 0) + 1, m), {});
|
|
8386
|
-
|
|
8495
|
+
const unplanned = plan.unverifiable?.length ?? 0;
|
|
8496
|
+
const outcome = `${Object.entries(counts).map(([k, v]) => `${v} ${k}`).join(", ") || "no tests ran"}${unplanned ? `, ${unplanned} unverifiable by plan` : ""}`;
|
|
8497
|
+
log.info(`outcome: ${outcome}${doctor ? ` \xB7 setup needs attention: ${doctor.code}` : ""}`);
|
|
8498
|
+
setOutput("outcome", outcome);
|
|
8387
8499
|
if (process.env.GITHUB_STEP_SUMMARY) {
|
|
8388
8500
|
try {
|
|
8389
|
-
|
|
8501
|
+
appendFileSync2(process.env.GITHUB_STEP_SUMMARY, `## DevAsign verification
|
|
8390
8502
|
|
|
8391
8503
|
${doctor ? `> Setup needs attention: ${doctor.message}
|
|
8392
8504
|
|
|
8393
|
-
` : ""}${summaryTable(
|
|
8505
|
+
` : ""}${summaryTable(plan, finalResults)}
|
|
8394
8506
|
|
|
8395
8507
|
Verdicts are judged by DevAsign and posted on the PR (check run "DevAsign \xB7 Verify").
|
|
8396
8508
|
`);
|
|
8397
8509
|
} catch {
|
|
8398
8510
|
}
|
|
8399
8511
|
}
|
|
8400
|
-
if (
|
|
8512
|
+
if (failOn !== "never" && api) {
|
|
8401
8513
|
const deadline = Date.now() + 10 * 6e4;
|
|
8402
8514
|
while (Date.now() < deadline) {
|
|
8403
8515
|
const view = await api.getRun(runId);
|
|
8404
8516
|
if (view.terminal) {
|
|
8405
8517
|
const fails = view.run.verdicts.filter((v) => v.verdict === "fail");
|
|
8406
|
-
|
|
8407
|
-
|
|
8408
|
-
|
|
8409
|
-
|
|
8410
|
-
return 0;
|
|
8518
|
+
const unverified = failOn === "unverifiable" ? view.run.verdicts.filter((v) => v.verdict === "unverifiable") : [];
|
|
8519
|
+
for (const v of unverified) log.error(`criterion ${v.criterionId} could not be verified: ${v.reason}${v.fixUrl ? ` \u2014 fix: ${v.fixUrl}` : ""}`);
|
|
8520
|
+
if (fails.length) log.error(`${fails.length} criteria failed verification: ${fails.map((f) => f.criterionId).join(", ")}`);
|
|
8521
|
+
return fails.length || unverified.length ? 1 : 0;
|
|
8411
8522
|
}
|
|
8412
8523
|
await sleep3(5e3);
|
|
8413
8524
|
}
|
|
@@ -8431,17 +8542,24 @@ Usage: devasign-verify [run|detect|doctor] [options]
|
|
|
8431
8542
|
|
|
8432
8543
|
Options:
|
|
8433
8544
|
--api-url <url> DevAsign API origin (env DEVASIGN_API_URL)
|
|
8434
|
-
--fail-on never
|
|
8545
|
+
--fail-on <mode> never (default): always exit 0; verdict: fail the job on a
|
|
8546
|
+
failed criterion; unverifiable: also fail when a criterion
|
|
8547
|
+
could not be verified. Unset: the repo's DevAsign setting
|
|
8435
8548
|
--audience <aud> OIDC audience (default devasign)
|
|
8436
8549
|
--token <jwt> Use this token instead of the Actions OIDC token (local runs)
|
|
8437
8550
|
--pr <n> --sha <sha> Override the PR number / head sha (local runs)
|
|
8438
|
-
--resolve-timeout <s> Max seconds to wait for a plan (default
|
|
8551
|
+
--resolve-timeout <s> Max seconds to wait for a plan (default 180; DevAsign
|
|
8552
|
+
may shorten it and re-run this workflow when ready)
|
|
8439
8553
|
--test-timeout <s> Per test-file timeout in seconds (default 600)
|
|
8440
8554
|
--plan-file <path> Offline: run this plan JSON with no API
|
|
8441
8555
|
--results-out <path> Write the results JSON to this path
|
|
8442
8556
|
--keep Keep .devasign/tests and artifacts after the run
|
|
8443
8557
|
--cwd <dir> Repository checkout (default cwd)
|
|
8444
8558
|
`;
|
|
8559
|
+
var seconds = (raw, fallback) => {
|
|
8560
|
+
const n = Number(raw);
|
|
8561
|
+
return raw !== void 0 && Number.isFinite(n) && n >= 0 ? n : fallback;
|
|
8562
|
+
};
|
|
8445
8563
|
async function main(argv = process.argv.slice(2)) {
|
|
8446
8564
|
const { values, positionals } = parseArgs({
|
|
8447
8565
|
args: argv,
|
|
@@ -8487,7 +8605,12 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
8487
8605
|
log.error("--api-url (or DEVASIGN_API_URL) is required");
|
|
8488
8606
|
return 2;
|
|
8489
8607
|
}
|
|
8490
|
-
const
|
|
8608
|
+
const failOnValue = values["fail-on"];
|
|
8609
|
+
if (failOnValue !== void 0 && !["never", "verdict", "unverifiable"].includes(failOnValue)) {
|
|
8610
|
+
log.error(`--fail-on must be never, verdict or unverifiable (got "${failOnValue}")`);
|
|
8611
|
+
return 2;
|
|
8612
|
+
}
|
|
8613
|
+
const failOn = failOnValue;
|
|
8491
8614
|
const tokenValue = values.token || process.env.DEVASIGN_TOKEN;
|
|
8492
8615
|
const token = tokenValue ? staticTokenSource(tokenValue) : actionsTokenSource(values.audience || process.env.DEVASIGN_OIDC_AUDIENCE || "devasign");
|
|
8493
8616
|
try {
|
|
@@ -8495,8 +8618,8 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
8495
8618
|
apiUrl,
|
|
8496
8619
|
token,
|
|
8497
8620
|
failOn,
|
|
8498
|
-
resolveTimeoutMs: (
|
|
8499
|
-
testTimeoutMs: (
|
|
8621
|
+
resolveTimeoutMs: seconds(values["resolve-timeout"], 180) * 1e3,
|
|
8622
|
+
testTimeoutMs: seconds(values["test-timeout"], 600) * 1e3,
|
|
8500
8623
|
keep: !!values.keep,
|
|
8501
8624
|
cwd,
|
|
8502
8625
|
pr: values.pr ? Number(values.pr) : void 0,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devasign/verify",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Runs DevAsign's generated acceptance tests inside your CI and reports per-criterion evidence.",
|
|
5
5
|
"license": "GPL-3.0-or-later",
|
|
6
6
|
"type": "module",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"prepublishOnly": "npm run typecheck && npm test && npm run build"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@playwright/test": "
|
|
26
|
+
"@playwright/test": "1.55.0",
|
|
27
27
|
"tsx": "^4.22.4",
|
|
28
28
|
"yaml": "^2.9.0"
|
|
29
29
|
},
|