@devasign/verify 1.0.0 → 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.
Files changed (3) hide show
  1. package/README.md +2 -0
  2. package/dist/cli.js +156 -35
  3. package/package.json +3 -3
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.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 child = spawn(opts.cmd, opts.args, { cwd: opts.cwd, env, stdio: ["ignore", "pipe", "pipe"] });
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
- child.kill("SIGKILL");
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
- "const projects = Array.isArray(b.projects) && b.projects.length",
8033
- " ? [b.projects.find((p: any) => /chrom/i.test(String(p?.name || ''))) ?? b.projects[0]]",
8034
- " : undefined;",
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 statuses = attempts.map((a) => a.status);
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 deadline = Date.now() + timeoutMs;
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 (Date.now() > deadline) return res;
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
- function summaryTable(results, plan) {
8308
- const byId = new Map(plan.criteria.map((c) => [c.id, c]));
8309
- const rows = results.map((r) => `| ${r.criterionIds.map((id) => `${id}. ${byId.get(id)?.text ?? ""}`).join("<br>")} | ${r.status} | ${r.level} ${r.origin} \`${r.test}\` | ${r.attempts.length} |`);
8310
- return ["| Criterion | Test outcome | Test | Attempts |", "|---|---|---|---|", ...rows].join("\n");
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
- log.info(`outcome: ${Object.entries(counts).map(([k, v]) => `${v} ${k}`).join(", ") || "no tests"}${doctor ? ` \xB7 setup needs attention: ${doctor.code}` : ""}`);
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
- appendFileSync(process.env.GITHUB_STEP_SUMMARY, `## DevAsign verification
8501
+ appendFileSync2(process.env.GITHUB_STEP_SUMMARY, `## DevAsign verification
8390
8502
 
8391
8503
  ${doctor ? `> Setup needs attention: ${doctor.message}
8392
8504
 
8393
- ` : ""}${summaryTable(finalResults, plan)}
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 (opts.failOn === "verdict" && api) {
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
- if (fails.length) {
8407
- log.error(`${fails.length} criteria failed verification: ${fails.map((f) => f.criterionId).join(", ")}`);
8408
- return 1;
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|verdict Fail the job on a failed criterion (default 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 600)
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 failOn = values["fail-on"] === "verdict" ? "verdict" : "never";
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: (Number(values["resolve-timeout"]) || 600) * 1e3,
8499
- testTimeoutMs: (Number(values["test-timeout"]) || 600) * 1e3,
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,
@@ -8509,9 +8632,7 @@ async function main(argv = process.argv.slice(2)) {
8509
8632
  return 0;
8510
8633
  }
8511
8634
  }
8512
- if (import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith("/dist/cli.js") || process.argv[1]?.endsWith("/src/cli.ts")) {
8513
- main().then((code) => process.exit(code));
8514
- }
8635
+ main().then((code) => process.exit(code));
8515
8636
  export {
8516
8637
  main
8517
8638
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devasign/verify",
3
- "version": "1.0.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",
@@ -15,7 +15,7 @@
15
15
  "node": ">=20"
16
16
  },
17
17
  "scripts": {
18
- "build": "esbuild src/cli.ts --bundle --platform=node --target=node20 --format=esm --outfile=dist/cli.js --external:tsx --external:@playwright/test --banner:js=\"#!/usr/bin/env node\nimport { createRequire as __cr } from 'node:module'; const require = __cr(import.meta.url);\"",
18
+ "build": "esbuild src/cli.ts --bundle --platform=node --target=node20 --format=esm --outfile=dist/cli.js --external:tsx --external:@playwright/test --banner:js=\"#!/usr/bin/env node\nimport { createRequire as __cr } from 'node:module'; const require = __cr(import.meta.url);\" && chmod +x dist/cli.js",
19
19
  "typecheck": "tsc -p tsconfig.json --noEmit",
20
20
  "test": "node --import tsx/esm --test 'src/**/*.test.ts'",
21
21
  "test:e2e": "node --import tsx/esm --test test/e2e.test.ts",
@@ -23,7 +23,7 @@
23
23
  "prepublishOnly": "npm run typecheck && npm test && npm run build"
24
24
  },
25
25
  "dependencies": {
26
- "@playwright/test": "^1.55.0",
26
+ "@playwright/test": "1.55.0",
27
27
  "tsx": "^4.22.4",
28
28
  "yaml": "^2.9.0"
29
29
  },