@plune-ai/cli 0.25.0 → 0.27.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 +24 -2
- package/dist/cli.cjs +178 -40
- package/dist/reporter-core.cjs +34 -2
- package/dist/reporter-core.d.cts +2 -0
- package/dist/reporter-core.d.ts +2 -0
- package/dist/reporter-core.js +36 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -165,9 +165,20 @@ plune requirements import prd.md --sections Requirements --unit bullets --key-pr
|
|
|
165
165
|
|
|
166
166
|
`--sections` takes the heading or its number; `--unit bullets|subheadings` (default: sub-headings when the section has any); `--key-prefix` is text a bullet starts with that carries its key (`REQ-12: The system shall ...`; no pattern is accepted); `--priority-from subheading` makes the bullets under a sub-heading take its text as their priority.
|
|
167
167
|
|
|
168
|
-
**
|
|
168
|
+
**2c. Several tables in one run.** A PRD keeps its functional and non-functional requirements in different tables. Repeat `--table` and put each table's columns inside it, after the number and a colon:
|
|
169
169
|
|
|
170
|
-
|
|
170
|
+
```bash
|
|
171
|
+
plune requirements import prd.md \
|
|
172
|
+
--table "1:key=ID,text=Requirement,priority=Priority,fixed-category=functional" \
|
|
173
|
+
--table "2:key=ID,text=Requirement,fixed-category=non-functional" \
|
|
174
|
+
--map-priority "Must=Must-have"
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Inside a `--table` the names are `key`, `title` or `text` (one of the two), `description`, `priority`, `category`, `type`, `status`, `criteria`, plus `fixed-category=functional|non-functional|other` and `fixed-type=user_story|feature|epic` — one meaning for the whole table, used where a row has none of its own. Names are separated by commas, so a header that contains a comma cannot be named in this form. `--map-priority`, `--type` and `--map-category` are shared by all tables. A table number may be given once. The single-table form (`--table 0 --key-column ...`) keeps working, but the two forms are not mixed: `--key-column` and the other column flags are refused together with `--table "<n>:..."`. The plan shows `table #N "heading"` in front of each row.
|
|
178
|
+
|
|
179
|
+
**3. Read the plan, then write it.** Without `--yes` nothing is written. The report is the summary line (`create 3 · update 0 · unchanged 12 · skip 1`), the rows grouped by action with their line in the file, key, title and reason (a skipped row says why), the priority and category values seen and what they map to, and what was **not imported** (other tables, other sections, frontmatter keys). It ends with `dry run — nothing was written; add --yes to apply`. Add `--yes` to apply the same plan in one transaction. A row with a key matches the requirement of that key; one without matches by exact title; running the same file again changes nothing (every row is `unchanged`). A skipped row does not fail the command. Rows that are only placeholders (`[Behavior]`) are skipped with the reason `template placeholder`; a key that appears twice in the file is skipped with `first on line N, again on line M`. When every row is a placeholder the plan says `this file is an empty template` (`--json` has `"emptyTemplate": true`) and the exit code is `0`, like any plan whose rows are skipped: nothing is written, and nothing is an error.
|
|
180
|
+
|
|
181
|
+
`--json` prints the platform's answer instead. The project is the only one of the account, or `--project <id>` / `PLUNE_PROJECT` (a token only works in the project it was minted in). Exit `0` on success, `2` for a flag combination that cannot work (`--table` with `--sections`, `--title-column` with `--text-column`, a `--table "<n>:..."` spec with the single-table column flags), an unreadable or over-limit file, or a rejected token, `1` when the platform refuses (400, 403, 409 a key taken meanwhile, 413) or does not have the routes yet.
|
|
171
182
|
|
|
172
183
|
### A release gate: `--fail-on-failures`
|
|
173
184
|
|
|
@@ -381,6 +392,17 @@ The shard that is the Nth also prints `plune: run closed` just above that line,
|
|
|
381
392
|
and leaves the run open, as `PLUNE_SHARED_RUN` does: close it with `plune run finish`.
|
|
382
393
|
- With `--fail-on-failures`, the shard that closes the run gates on its summary; the others have no verdict yet.
|
|
383
394
|
|
|
395
|
+
### A time limit for the import
|
|
396
|
+
|
|
397
|
+
The whole `plune run import` stops after 600 seconds by default, so a platform that stops answering cannot hold a CI job until its own timeout kills it. Raise it for a very large report, or turn it off with `0`:
|
|
398
|
+
|
|
399
|
+
```bash
|
|
400
|
+
PLUNE_IMPORT_TIMEOUT=1200 plune run import ./playwright.json # seconds, 0 to 86400; 0 is no limit
|
|
401
|
+
plune run import ./playwright.json --timeout 1200 # the same, as a flag (it wins)
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
At the limit the command sends `terminate` for the run it opened (not for a run other jobs share), says in one line that the platform may have received part of the results, and exits 1. Import the report again afterwards.
|
|
405
|
+
|
|
384
406
|
### Many screenshots: upload concurrency and retries
|
|
385
407
|
|
|
386
408
|
Files go to Plune up to 100 to a request: `uploads-batch` announces them (with the SHA-256 of each, read as a stream,
|
package/dist/cli.cjs
CHANGED
|
@@ -2315,7 +2315,8 @@ function nodeTransport(url, init, idleMs = IDLE_MS) {
|
|
|
2315
2315
|
return new Promise((resolve10, reject) => {
|
|
2316
2316
|
const target = new URL(url);
|
|
2317
2317
|
const send = target.protocol === "https:" ? import_node_https.request : import_node_http.request;
|
|
2318
|
-
const
|
|
2318
|
+
const agent = POOLS[target.protocol === "https:" ? "https:" : "http:"];
|
|
2319
|
+
const req = send(target, { method: init.method, headers: init.headers, agent }, (res) => {
|
|
2319
2320
|
const chunks = [];
|
|
2320
2321
|
res.on("data", (chunk2) => chunks.push(chunk2));
|
|
2321
2322
|
res.on("error", reject);
|
|
@@ -2506,7 +2507,7 @@ function createClient(opts) {
|
|
|
2506
2507
|
}
|
|
2507
2508
|
};
|
|
2508
2509
|
}
|
|
2509
|
-
var import_node_fs2, import_node_stream, import_node_http, import_node_https, BACKOFF_BASE_MS, BACKOFF_CAP_MS, DEFAULT_RETRIES, RETRY_AFTER_CAP_MS, IDLE_MS, withoutLength, codeOfError, sleep;
|
|
2510
|
+
var import_node_fs2, import_node_stream, import_node_http, import_node_https, BACKOFF_BASE_MS, BACKOFF_CAP_MS, DEFAULT_RETRIES, RETRY_AFTER_CAP_MS, POOLS, IDLE_MS, withoutLength, codeOfError, sleep;
|
|
2510
2511
|
var init_client = __esm({
|
|
2511
2512
|
"src/reporter-core/client.ts"() {
|
|
2512
2513
|
"use strict";
|
|
@@ -2519,6 +2520,7 @@ var init_client = __esm({
|
|
|
2519
2520
|
BACKOFF_CAP_MS = 1e4;
|
|
2520
2521
|
DEFAULT_RETRIES = 2;
|
|
2521
2522
|
RETRY_AFTER_CAP_MS = 6e4;
|
|
2523
|
+
POOLS = { "http:": new import_node_http.Agent({ keepAlive: true }), "https:": new import_node_https.Agent({ keepAlive: true }) };
|
|
2522
2524
|
IDLE_MS = 3e5;
|
|
2523
2525
|
withoutLength = (headers) => Object.fromEntries(Object.entries(headers).filter(([name]) => name.toLowerCase() !== "content-length"));
|
|
2524
2526
|
codeOfError = (err) => {
|
|
@@ -2577,6 +2579,7 @@ function readEnv(env = process.env) {
|
|
|
2577
2579
|
const warnings = [];
|
|
2578
2580
|
const uploadConcurrency = bounded(env, "PLUNE_UPLOAD_CONCURRENCY", UPLOAD_CONCURRENCY, warnings);
|
|
2579
2581
|
const uploadRetries = bounded(env, "PLUNE_UPLOAD_RETRIES", UPLOAD_RETRIES, warnings);
|
|
2582
|
+
const importTimeout = bounded(env, "PLUNE_IMPORT_TIMEOUT", IMPORT_TIMEOUT, warnings);
|
|
2580
2583
|
const title = value(env, "PLUNE_RUN_TITLE");
|
|
2581
2584
|
const environment = value(env, "PLUNE_ENV");
|
|
2582
2585
|
const labels = list(env, "PLUNE_LABELS");
|
|
@@ -2608,6 +2611,7 @@ function readEnv(env = process.env) {
|
|
|
2608
2611
|
keepOpen: flag(env, "PLUNE_SHARED_RUN") || flag(env, "PLUNE_PROCEED") || shard !== void 0 || shardError !== void 0,
|
|
2609
2612
|
...shard !== void 0 ? { shard } : {},
|
|
2610
2613
|
...shardError !== void 0 ? { shardError } : {},
|
|
2614
|
+
...importTimeout !== void 0 ? { importTimeout } : {},
|
|
2611
2615
|
ignored: UNSUPPORTED_VARS.filter((name) => value(env, name) !== void 0),
|
|
2612
2616
|
warnings
|
|
2613
2617
|
};
|
|
@@ -2635,7 +2639,7 @@ function ciCommit(reported, env = process.env) {
|
|
|
2635
2639
|
...branch !== void 0 ? { branch } : {}
|
|
2636
2640
|
};
|
|
2637
2641
|
}
|
|
2638
|
-
var path6, UNSUPPORTED_VARS, SHARDS_MAX, UPLOAD_CONCURRENCY, UPLOAD_RETRIES, PROVIDERS;
|
|
2642
|
+
var path6, UNSUPPORTED_VARS, SHARDS_MAX, UPLOAD_CONCURRENCY, UPLOAD_RETRIES, IMPORT_TIMEOUT, PROVIDERS;
|
|
2639
2643
|
var init_env2 = __esm({
|
|
2640
2644
|
"src/reporter-core/env.ts"() {
|
|
2641
2645
|
"use strict";
|
|
@@ -2645,6 +2649,7 @@ var init_env2 = __esm({
|
|
|
2645
2649
|
SHARDS_MAX = 1e3;
|
|
2646
2650
|
UPLOAD_CONCURRENCY = { min: 1, max: 32, default: 16 };
|
|
2647
2651
|
UPLOAD_RETRIES = { min: 0, max: 20, default: 6 };
|
|
2652
|
+
IMPORT_TIMEOUT = { min: 0, max: 86400, default: 600 };
|
|
2648
2653
|
PROVIDERS = [
|
|
2649
2654
|
{ is: "GITHUB_ACTIONS", sha: "GITHUB_SHA", branch: ["GITHUB_HEAD_REF", "GITHUB_REF_NAME"] },
|
|
2650
2655
|
{ is: "GITLAB_CI", sha: "CI_COMMIT_SHA", branch: ["CI_COMMIT_REF_NAME"] },
|
|
@@ -3558,7 +3563,29 @@ async function startRun(passed, declared2) {
|
|
|
3558
3563
|
else await uploadBatched(jobs);
|
|
3559
3564
|
uploadMs += Date.now() - started;
|
|
3560
3565
|
}
|
|
3566
|
+
const sending = /* @__PURE__ */ new Set();
|
|
3567
|
+
let sendError;
|
|
3568
|
+
let answered = false;
|
|
3569
|
+
async function dispatch(work) {
|
|
3570
|
+
while (sending.size >= (answered ? SEND_CONCURRENCY : 1)) await Promise.race(sending);
|
|
3571
|
+
const task = work().catch((err) => {
|
|
3572
|
+
sendError ??= err;
|
|
3573
|
+
}).finally(() => sending.delete(task));
|
|
3574
|
+
sending.add(task);
|
|
3575
|
+
}
|
|
3576
|
+
async function settle() {
|
|
3577
|
+
while (sending.size > 0) await Promise.all([...sending]);
|
|
3578
|
+
if (sendError !== void 0) {
|
|
3579
|
+
const err = sendError;
|
|
3580
|
+
sendError = void 0;
|
|
3581
|
+
throw err;
|
|
3582
|
+
}
|
|
3583
|
+
}
|
|
3561
3584
|
async function flush() {
|
|
3585
|
+
await flushBuffer();
|
|
3586
|
+
await settle();
|
|
3587
|
+
}
|
|
3588
|
+
async function flushBuffer() {
|
|
3562
3589
|
if (buffer.length === 0) return;
|
|
3563
3590
|
const pending = buffer;
|
|
3564
3591
|
buffer = [];
|
|
@@ -3566,6 +3593,9 @@ async function startRun(passed, declared2) {
|
|
|
3566
3593
|
defer(pending);
|
|
3567
3594
|
return;
|
|
3568
3595
|
}
|
|
3596
|
+
await dispatch(() => deliver(pending));
|
|
3597
|
+
}
|
|
3598
|
+
async function deliver(pending) {
|
|
3569
3599
|
await resolveKeys(pending.flatMap((p) => p.keys));
|
|
3570
3600
|
const submissions = [];
|
|
3571
3601
|
const unanswered = [];
|
|
@@ -3606,6 +3636,7 @@ async function startRun(passed, declared2) {
|
|
|
3606
3636
|
for (const k of told) stats[k] += counts[k];
|
|
3607
3637
|
progress();
|
|
3608
3638
|
await upload(out.ok ? out.body?.items : void 0, filesByKey);
|
|
3639
|
+
answered = true;
|
|
3609
3640
|
continue;
|
|
3610
3641
|
}
|
|
3611
3642
|
if (out.ok) {
|
|
@@ -3680,7 +3711,7 @@ async function startRun(passed, declared2) {
|
|
|
3680
3711
|
},
|
|
3681
3712
|
async add(result) {
|
|
3682
3713
|
buffer.push({ ...result, keys: bound(result.keys) });
|
|
3683
|
-
if (buffer.length >= batchSize) await
|
|
3714
|
+
if (buffer.length >= batchSize) await flushBuffer();
|
|
3684
3715
|
},
|
|
3685
3716
|
flush,
|
|
3686
3717
|
async finish(reason) {
|
|
@@ -3745,7 +3776,7 @@ async function startRun(passed, declared2) {
|
|
|
3745
3776
|
}
|
|
3746
3777
|
};
|
|
3747
3778
|
}
|
|
3748
|
-
var import_node_crypto3, import_node_fs5, import_node_path3, import_node_util2, RESOLVE_CHUNK, BATCH_MAX, DISCOVER_MAX, BATCH_DEFAULT, EXPECTED_MAX, KEY_MAX, TITLE_MAX, COUNTED, SCREENSHOT_TYPES, SCREENSHOT_BYTES, SCREENSHOTS_PER_RESULT, TEXT_BYTES, TEXTS_PER_RESULT, UTF8, TYPE_BY_EXTENSION, METADATA_TYPE2, NAME_MAX, SKIPPED, BUCKET_FOUND, KINDS, codeOf2, BATCH_BYTES, ENVELOPE_BYTES;
|
|
3779
|
+
var import_node_crypto3, import_node_fs5, import_node_path3, import_node_util2, RESOLVE_CHUNK, BATCH_MAX, DISCOVER_MAX, BATCH_DEFAULT, SEND_CONCURRENCY, EXPECTED_MAX, KEY_MAX, TITLE_MAX, COUNTED, SCREENSHOT_TYPES, SCREENSHOT_BYTES, SCREENSHOTS_PER_RESULT, TEXT_BYTES, TEXTS_PER_RESULT, UTF8, TYPE_BY_EXTENSION, METADATA_TYPE2, NAME_MAX, SKIPPED, BUCKET_FOUND, KINDS, codeOf2, BATCH_BYTES, ENVELOPE_BYTES;
|
|
3749
3780
|
var init_session = __esm({
|
|
3750
3781
|
"src/reporter-core/session.ts"() {
|
|
3751
3782
|
"use strict";
|
|
@@ -3765,6 +3796,7 @@ var init_session = __esm({
|
|
|
3765
3796
|
BATCH_MAX = 500;
|
|
3766
3797
|
DISCOVER_MAX = 500;
|
|
3767
3798
|
BATCH_DEFAULT = 500;
|
|
3799
|
+
SEND_CONCURRENCY = 4;
|
|
3768
3800
|
EXPECTED_MAX = 1e4;
|
|
3769
3801
|
KEY_MAX = 1024;
|
|
3770
3802
|
TITLE_MAX = 300;
|
|
@@ -5336,8 +5368,24 @@ var init_importers = __esm({
|
|
|
5336
5368
|
// src/cli/commands/run-import.ts
|
|
5337
5369
|
var run_import_exports = {};
|
|
5338
5370
|
__export(run_import_exports, {
|
|
5371
|
+
ImportTimeoutError: () => ImportTimeoutError,
|
|
5339
5372
|
handleRunImport: () => handleRunImport
|
|
5340
5373
|
});
|
|
5374
|
+
async function timeoutMessage(limit, runId, ours, options) {
|
|
5375
|
+
const head = `plune: run import did not finish in ${limit} s (--timeout / PLUNE_IMPORT_TIMEOUT; 0 turns the limit off); the platform may have received part of the results`;
|
|
5376
|
+
if (runId === null) return `${head}; the run id is not known, so nothing was terminated - look for an open run with this key in Plune and terminate it there, then import again.`;
|
|
5377
|
+
if (!ours) return `${head}; run ${runId} is shared with other jobs and was left open - close it with "plune run finish ${runId}" once they are done, then import this report again.`;
|
|
5378
|
+
const wait = options.terminateWaitMs ?? 15e3;
|
|
5379
|
+
let timer;
|
|
5380
|
+
const gaveUp = new Promise((resolve10) => timer = setTimeout(() => resolve10("gaveup"), wait));
|
|
5381
|
+
const quiet = () => void 0;
|
|
5382
|
+
const closed = handleRunFinish(runId, { ...options, write: quiet, terminate: true, reason: `import timed out after ${limit} s` }).then(
|
|
5383
|
+
() => true,
|
|
5384
|
+
() => false
|
|
5385
|
+
);
|
|
5386
|
+
const done = await Promise.race([closed, gaveUp]).finally(() => clearTimeout(timer)) === true;
|
|
5387
|
+
return done ? `${head}; run ${runId} was terminated - import the report again.` : `${head}; run ${runId} could not be terminated from here - run "plune run finish ${runId} --terminate" (the platform also closes a silent run after 30 minutes), then import again.`;
|
|
5388
|
+
}
|
|
5341
5389
|
async function handleRunImport(options) {
|
|
5342
5390
|
const write = options.write ?? ((line) => void process.stdout.write(`${line}
|
|
5343
5391
|
`));
|
|
@@ -5364,31 +5412,47 @@ async function handleRunImport(options) {
|
|
|
5364
5412
|
const apiUrl = resolveApiUrl(options.apiUrl ?? env.config.apiUrl);
|
|
5365
5413
|
const token = options.token ?? env.config.token ?? loadToken() ?? "";
|
|
5366
5414
|
if (token === "") throw new NoTokenError();
|
|
5367
|
-
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
|
|
5372
|
-
|
|
5373
|
-
|
|
5374
|
-
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
|
|
5415
|
+
let session;
|
|
5416
|
+
const send = async () => {
|
|
5417
|
+
session = await startRun(
|
|
5418
|
+
{
|
|
5419
|
+
apiUrl,
|
|
5420
|
+
token,
|
|
5421
|
+
...options.fetchImpl !== void 0 ? { fetchImpl: options.fetchImpl } : {},
|
|
5422
|
+
...options.key !== void 0 ? { externalKey: options.key } : {},
|
|
5423
|
+
...options.create === true ? { offerDiscovered: true } : {},
|
|
5424
|
+
...options.uploadConcurrency !== void 0 ? { uploadConcurrency: options.uploadConcurrency } : {},
|
|
5425
|
+
kind: "automated",
|
|
5426
|
+
// The CI run the report names, never this machine's: whoever imports may be another job (#790).
|
|
5427
|
+
// The commit and branch likewise, where the report names a commit; a report that names none —
|
|
5428
|
+
// JUnit's never does — takes this job's own, which is where the tests ran when import follows
|
|
5429
|
+
// them in one job (plune-ai/plune#927).
|
|
5430
|
+
meta: { runner: format, ...ciUrl !== void 0 ? { ciUrl } : {}, ...ciCommit(ci) },
|
|
5431
|
+
log: write
|
|
5432
|
+
},
|
|
5433
|
+
// The file is the complete list of what ran, so the platform can resolve every test in one
|
|
5434
|
+
// request and derive what never ran from the same list (AC-09/AC-10) — the lazy path would be
|
|
5435
|
+
// a lookup per result.
|
|
5436
|
+
results.map((r) => r.keys)
|
|
5437
|
+
);
|
|
5438
|
+
for (const result2 of results) await session.add(result2);
|
|
5439
|
+
return env.keepOpen ? session.leaveOpen({ holdShardLines: true }) : session.finish();
|
|
5440
|
+
};
|
|
5441
|
+
const limit = options.timeoutSec ?? env.importTimeout ?? IMPORT_TIMEOUT.default;
|
|
5442
|
+
const sent = send();
|
|
5389
5443
|
let frozen;
|
|
5390
|
-
if (
|
|
5391
|
-
else
|
|
5444
|
+
if (limit === 0) frozen = await sent;
|
|
5445
|
+
else {
|
|
5446
|
+
sent.catch(() => void 0);
|
|
5447
|
+
let timer;
|
|
5448
|
+
const late = new Promise((resolve10) => timer = setTimeout(() => resolve10("late"), limit * 1e3));
|
|
5449
|
+
const first = await Promise.race([sent, late]).finally(() => clearTimeout(timer));
|
|
5450
|
+
if (first === "late") {
|
|
5451
|
+
throw new ImportTimeoutError(await timeoutMessage(limit, session?.runId ?? null, !env.keepOpen, { ...options, apiUrl, token }));
|
|
5452
|
+
}
|
|
5453
|
+
frozen = first;
|
|
5454
|
+
}
|
|
5455
|
+
if (session === void 0) throw new RunCommandError("The import did not start.");
|
|
5392
5456
|
const stats = session.stats;
|
|
5393
5457
|
const result = {
|
|
5394
5458
|
runId: session.runId,
|
|
@@ -5483,7 +5547,7 @@ function describe(result, apiUrl) {
|
|
|
5483
5547
|
if (result.runId !== null) lines.push(runUrl2(apiUrl, result.runId));
|
|
5484
5548
|
return lines;
|
|
5485
5549
|
}
|
|
5486
|
-
var fs12, NEXT;
|
|
5550
|
+
var fs12, ImportTimeoutError, NEXT;
|
|
5487
5551
|
var init_run_import = __esm({
|
|
5488
5552
|
"src/cli/commands/run-import.ts"() {
|
|
5489
5553
|
"use strict";
|
|
@@ -5497,6 +5561,8 @@ var init_run_import = __esm({
|
|
|
5497
5561
|
init_credentials();
|
|
5498
5562
|
init_gate();
|
|
5499
5563
|
init_env2();
|
|
5564
|
+
ImportTimeoutError = class extends RunCommandError {
|
|
5565
|
+
};
|
|
5500
5566
|
NEXT = {
|
|
5501
5567
|
queue: "the review queue is full. Approve or reject what is waiting, then import this report again to offer the rest.",
|
|
5502
5568
|
cases: "the project is at its test case limit. Delete cases you no longer need, or ask an operator to raise the limit, then import this report again to offer the rest.",
|
|
@@ -6291,6 +6357,7 @@ __export(requirements_import_exports, {
|
|
|
6291
6357
|
formatReport: () => formatReport2,
|
|
6292
6358
|
handleRequirementsImport: () => handleRequirementsImport,
|
|
6293
6359
|
parseMap: () => parseMap,
|
|
6360
|
+
parseTableSpec: () => parseTableSpec,
|
|
6294
6361
|
reportRequirementsImportFailure: () => reportRequirementsImportFailure
|
|
6295
6362
|
});
|
|
6296
6363
|
function parseMap(flag2, raw) {
|
|
@@ -6306,25 +6373,63 @@ function parseMap(flag2, raw) {
|
|
|
6306
6373
|
if (Object.keys(out).length === 0) usage(`${flag2} is empty. Write it like ${flag2} "Critical=Must-have,High=Should-have".`);
|
|
6307
6374
|
return out;
|
|
6308
6375
|
}
|
|
6376
|
+
function parseTableSpec(spec) {
|
|
6377
|
+
const at = spec.indexOf(":");
|
|
6378
|
+
const index = (at < 0 ? spec : spec.slice(0, at)).trim();
|
|
6379
|
+
if (!/^\d+$/.test(index)) usage(`--table "${spec}": "${index}" is not a table number. ${SPEC_HELP}`);
|
|
6380
|
+
const t = { table: Number(index) };
|
|
6381
|
+
for (const pair of (at < 0 ? "" : spec.slice(at + 1)).split(",")) {
|
|
6382
|
+
if (pair.trim() === "") continue;
|
|
6383
|
+
const eq = pair.indexOf("=");
|
|
6384
|
+
const name = (eq < 0 ? pair : pair.slice(0, eq)).trim().toLowerCase();
|
|
6385
|
+
const value2 = eq < 0 ? "" : pair.slice(eq + 1).trim();
|
|
6386
|
+
const field = TABLE_SPEC_KEYS[name];
|
|
6387
|
+
if (field === void 0) return usage(`--table "${spec}": "${name}" is not a column name. Use ${Object.keys(TABLE_SPEC_KEYS).join(", ")}.`);
|
|
6388
|
+
if (value2 === "") usage(`--table "${spec}": "${name}" has no value. ${SPEC_HELP}`);
|
|
6389
|
+
if (field in t) usage(`--table "${spec}": "${name}" is given twice.`);
|
|
6390
|
+
if (field === "fixedCategory") oneOf("fixed-category", value2, ["functional", "non-functional", "other"]);
|
|
6391
|
+
if (field === "fixedType") oneOf("fixed-type", value2, ["user_story", "feature", "epic"]);
|
|
6392
|
+
t[field] = value2;
|
|
6393
|
+
}
|
|
6394
|
+
if (t["titleColumn"] !== void 0 && t["textColumn"] !== void 0) usage(`--table "${spec}": give title= or text=, not both.`);
|
|
6395
|
+
if (t["titleColumn"] === void 0 && t["textColumn"] === void 0) usage(`--table "${spec}": each table needs title=<header> or text=<header>.`);
|
|
6396
|
+
return t;
|
|
6397
|
+
}
|
|
6309
6398
|
function buildImportBody(f) {
|
|
6399
|
+
const tables = f.table === void 0 ? [] : Array.isArray(f.table) ? f.table : [f.table];
|
|
6310
6400
|
const columnFlags = COLUMN_FLAGS.filter(([k]) => f[k] !== void 0);
|
|
6311
6401
|
const sectionFlags = SECTION_FLAGS.filter(([k]) => f[k] !== void 0);
|
|
6312
|
-
const anyMapping =
|
|
6402
|
+
const anyMapping = tables.length > 0 || f.sections !== void 0 || columnFlags.length > 0 || sectionFlags.length > 0 || f.mapPriority !== void 0 || f.mapCategory !== void 0 || f.type !== void 0;
|
|
6313
6403
|
if (!anyMapping) return null;
|
|
6314
|
-
if (
|
|
6315
|
-
if (
|
|
6404
|
+
if (tables.length > 0 && f.sections !== void 0) usage("Give --table or --sections, not both: one import reads one table or one section.");
|
|
6405
|
+
if (tables.length === 0 && f.sections === void 0) {
|
|
6316
6406
|
usage("The mapping flags need --table <n> (a table) or --sections <heading|n> (a section). Run without flags to see which the file has.");
|
|
6317
6407
|
}
|
|
6318
6408
|
const body = {};
|
|
6319
6409
|
if (f.mapPriority !== void 0) body["priorityMap"] = parseMap("--map-priority", f.mapPriority);
|
|
6320
6410
|
if (f.type !== void 0) body["defaultType"] = oneOf("--type", f.type, ["user_story", "feature", "epic"]);
|
|
6321
|
-
if (
|
|
6322
|
-
if (
|
|
6411
|
+
if (tables.length > 1 || tables.some((t) => t.includes(":"))) {
|
|
6412
|
+
if (sectionFlags.length > 0) usage(`${sectionFlags.map(([, n]) => n).join(", ")} ${sectionFlags.length > 1 ? "are" : "is"} for --sections, not --table.`);
|
|
6413
|
+
if (columnFlags.length > 0) {
|
|
6414
|
+
usage(`${columnFlags.map(([, n]) => n).join(", ")} cannot be used with --table "<n>:...": put the column inside each --table, e.g. --table "3:key=ID,text=Requirement".`);
|
|
6415
|
+
}
|
|
6416
|
+
const specs = tables.map(parseTableSpec);
|
|
6417
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6418
|
+
for (const t of specs) {
|
|
6419
|
+
if (seen.has(t["table"])) usage(`Table ${String(t["table"])} is given twice in --table.`);
|
|
6420
|
+
seen.add(t["table"]);
|
|
6421
|
+
}
|
|
6422
|
+
body["mode"] = "table";
|
|
6423
|
+
const categoryMap = f.mapCategory !== void 0 ? parseMap("--map-category", f.mapCategory) : void 0;
|
|
6424
|
+
body["tables"] = specs.map((t) => categoryMap === void 0 ? t : { ...t, categoryMap });
|
|
6425
|
+
} else if (tables.length === 1) {
|
|
6426
|
+
const only = tables[0];
|
|
6427
|
+
if (!/^\d+$/.test(only)) usage(`--table is the number of the table in the list (0, 1, ...), not "${only}".`);
|
|
6323
6428
|
if (sectionFlags.length > 0) usage(`${sectionFlags.map(([, n]) => n).join(", ")} ${sectionFlags.length > 1 ? "are" : "is"} for --sections, not --table.`);
|
|
6324
6429
|
if (f.titleColumn !== void 0 && f.textColumn !== void 0) usage("Give --title-column or --text-column, not both.");
|
|
6325
6430
|
if (f.titleColumn === void 0 && f.textColumn === void 0) usage("--table needs --title-column <header> or --text-column <header>.");
|
|
6326
6431
|
body["mode"] = "table";
|
|
6327
|
-
body["table"] = Number(
|
|
6432
|
+
body["table"] = Number(only);
|
|
6328
6433
|
for (const [k, , key] of columnFlags) body[key] = f[k];
|
|
6329
6434
|
if (f.mapCategory !== void 0) body["categoryMap"] = parseMap("--map-category", f.mapCategory);
|
|
6330
6435
|
} else {
|
|
@@ -6434,13 +6539,19 @@ function formatDiscover(file, r) {
|
|
|
6434
6539
|
"",
|
|
6435
6540
|
"Pick one and map it (a dry run, nothing is written):",
|
|
6436
6541
|
` plune requirements import ${file} --table <n> --key-column <header> --title-column <header>`,
|
|
6437
|
-
` plune requirements import ${file} --sections <heading|n> --unit bullets|subheadings
|
|
6542
|
+
` plune requirements import ${file} --sections <heading|n> --unit bullets|subheadings`,
|
|
6543
|
+
"Several tables in one run, each with its own columns:",
|
|
6544
|
+
` plune requirements import ${file} --table "<n>:key=<header>,text=<header>,fixed-category=functional" --table "<m>:key=<header>,text=<header>"`
|
|
6438
6545
|
);
|
|
6439
6546
|
return out.join("\n") + "\n";
|
|
6440
6547
|
}
|
|
6441
6548
|
function formatReport2(report) {
|
|
6442
6549
|
const s = report.summary;
|
|
6443
6550
|
const out = [`create ${s.create} \xB7 update ${s.update} \xB7 unchanged ${s.unchanged} \xB7 skip ${s.skip}`];
|
|
6551
|
+
if (report.emptyTemplate === true) {
|
|
6552
|
+
const said = report.message !== void 0 && !/empty template/i.test(report.message) ? ` ${flat(report.message)}` : "";
|
|
6553
|
+
out.push("", `this file is an empty template: every row is a placeholder, nothing to import.${said}`);
|
|
6554
|
+
}
|
|
6444
6555
|
for (const action of ["create", "update", "skip", "unchanged"]) {
|
|
6445
6556
|
const rows = report.items.filter((i) => i.action === action);
|
|
6446
6557
|
if (rows.length === 0) continue;
|
|
@@ -6448,7 +6559,8 @@ function formatReport2(report) {
|
|
|
6448
6559
|
for (const i of rows.slice(0, SHOWN)) {
|
|
6449
6560
|
const changes = i.changes !== void 0 && i.changes.length > 0 ? `; changes: ${i.changes.join(", ")}` : "";
|
|
6450
6561
|
const notes = i.notes !== void 0 && i.notes.length > 0 ? `; note: ${i.notes.join("; ")}` : "";
|
|
6451
|
-
|
|
6562
|
+
const from = i.table === void 0 ? "" : `table #${i.table}${i.tableHeading ? ` "${cut2(flat(i.tableHeading), 40)}"` : ""} `;
|
|
6563
|
+
out.push(` ${from}line ${i.line} ${i.key ?? "-"} ${cut2(flat(i.title), 80)} - ${flat(i.reason)}${changes}${notes}`);
|
|
6452
6564
|
}
|
|
6453
6565
|
if (rows.length > SHOWN) out.push(` ... and ${rows.length - SHOWN} more (--json lists every row)`);
|
|
6454
6566
|
}
|
|
@@ -6472,7 +6584,7 @@ function reportRequirementsImportFailure(err, write) {
|
|
|
6472
6584
|
write(err.message + "\n");
|
|
6473
6585
|
return err.exitCode;
|
|
6474
6586
|
}
|
|
6475
|
-
var import_node_fs7, import_node_path4, MAX_MARKDOWN_BYTES, RequirementsImportError, usage, COLUMN_FLAGS, SECTION_FLAGS, oneOf, SUPPORT, SHOWN, flat, cut2, plural;
|
|
6587
|
+
var import_node_fs7, import_node_path4, MAX_MARKDOWN_BYTES, RequirementsImportError, usage, COLUMN_FLAGS, SECTION_FLAGS, oneOf, TABLE_SPEC_KEYS, SPEC_HELP, SUPPORT, SHOWN, flat, cut2, plural;
|
|
6476
6588
|
var init_requirements_import = __esm({
|
|
6477
6589
|
"src/cli/commands/requirements-import.ts"() {
|
|
6478
6590
|
"use strict";
|
|
@@ -6511,6 +6623,20 @@ var init_requirements_import = __esm({
|
|
|
6511
6623
|
["priorityFrom", "--priority-from"]
|
|
6512
6624
|
];
|
|
6513
6625
|
oneOf = (flag2, value2, allowed) => allowed.includes(value2) ? value2 : usage(`${flag2} must be ${allowed.join(" or ")}, not "${value2}".`);
|
|
6626
|
+
TABLE_SPEC_KEYS = {
|
|
6627
|
+
key: "keyColumn",
|
|
6628
|
+
title: "titleColumn",
|
|
6629
|
+
text: "textColumn",
|
|
6630
|
+
description: "descriptionColumn",
|
|
6631
|
+
priority: "priorityColumn",
|
|
6632
|
+
category: "categoryColumn",
|
|
6633
|
+
type: "typeColumn",
|
|
6634
|
+
status: "statusColumn",
|
|
6635
|
+
criteria: "acceptanceCriteriaColumn",
|
|
6636
|
+
"fixed-category": "fixedCategory",
|
|
6637
|
+
"fixed-type": "fixedType"
|
|
6638
|
+
};
|
|
6639
|
+
SPEC_HELP = 'Write it like --table "3:key=ID,text=Requirement,fixed-category=functional".';
|
|
6514
6640
|
SUPPORT = "This platform does not support requirements import yet. Nothing was sent to a project; try a newer Plune platform.";
|
|
6515
6641
|
SHOWN = 50;
|
|
6516
6642
|
flat = (s) => s.replace(/\s+/g, " ").trim();
|
|
@@ -6773,6 +6899,13 @@ function createProgram() {
|
|
|
6773
6899
|
if (!/^\d+$/.test(v) || Number(v) < 1 || Number(v) > 32) throw new import_commander.InvalidArgumentError("a whole number from 1 to 32.");
|
|
6774
6900
|
return Number(v);
|
|
6775
6901
|
}
|
|
6902
|
+
).option(
|
|
6903
|
+
"--timeout <seconds>",
|
|
6904
|
+
"Give up on the whole import after this many seconds, 0 for no limit (default 600; PLUNE_IMPORT_TIMEOUT sets it too). On a timeout the run this import opened is terminated, and the exit code is 1",
|
|
6905
|
+
(v) => {
|
|
6906
|
+
if (!/^\d+$/.test(v) || Number(v) > 86400) throw new import_commander.InvalidArgumentError("a whole number of seconds from 0 to 86400.");
|
|
6907
|
+
return Number(v);
|
|
6908
|
+
}
|
|
6776
6909
|
).option("--fail-on-failures", FAIL_ON_FAILURES, false).action(
|
|
6777
6910
|
async (file, options, command) => {
|
|
6778
6911
|
const [
|
|
@@ -6799,6 +6932,7 @@ function createProgram() {
|
|
|
6799
6932
|
...options.key !== void 0 ? { key: options.key } : {},
|
|
6800
6933
|
...options.create ? { create: true } : {},
|
|
6801
6934
|
...options.uploadConcurrency !== void 0 ? { uploadConcurrency: options.uploadConcurrency } : {},
|
|
6935
|
+
...options.timeout !== void 0 ? { timeoutSec: options.timeout } : {},
|
|
6802
6936
|
...options.failOnFailures ? { failOnFailures: true } : {}
|
|
6803
6937
|
});
|
|
6804
6938
|
if (options.failOnMissingAttachments && imported.missingAttachments.length > 0) process.exitCode = 1;
|
|
@@ -7008,7 +7142,11 @@ function createProgram() {
|
|
|
7008
7142
|
const requirementsCommand = program.command("requirements").description("Requirements on the platform \u2014 import them from a Markdown file");
|
|
7009
7143
|
requirementsCommand.command("import").argument("<file>", "A Markdown file (UTF-8, at most 1 MiB)").description(
|
|
7010
7144
|
"Import requirements from a Markdown file. With no mapping flags it lists the tables and sections found; with them it is a dry run until --yes"
|
|
7011
|
-
).option(
|
|
7145
|
+
).option(
|
|
7146
|
+
"--table <n|spec>",
|
|
7147
|
+
'Read the table with this number (from the list); repeat as --table "<n>:key=ID,text=Requirement,fixed-category=functional" for several tables',
|
|
7148
|
+
(value2, previous = []) => [...previous, value2]
|
|
7149
|
+
).option("--key-column <header>", "Table: the column holding the requirement key").option("--title-column <header>", "Table: the column holding the title").option("--text-column <header>", "Table: one text column; the title is its first line (not with --title-column)").option("--description-column <header>", "Table: the description column").option("--priority-column <header>", "Table: the priority column").option("--category-column <header>", "Table: the category column").option("--type-column <header>", "Table: the type column").option("--status-column <header>", "Table: the status column (draft, active or done)").option("--criteria-column <header>", "Table: the acceptance criteria column").option("--map-priority <pairs>", 'Rename priority values, e.g. "Critical=Must-have,High=Should-have"').option("--map-category <pairs>", 'Table: rename category values, e.g. "Quality=non-functional"').option("--sections <heading|n>", "Read the section with this heading or number (from the list)").option("--unit <unit>", "Sections: bullets or subheadings (default: sub-headings if it has any)").option("--key-prefix <text>", "Sections: text a bullet or sub-heading starts with that carries its key").option("--priority-from <source>", "Sections: subheading \u2014 bullets take their sub-heading as priority").option("--type <type>", "What a row without a type becomes: user_story, feature or epic").option("--project <id>", "The project (default: PLUNE_PROJECT, else the only project of the account)").option("--yes", "Write it. Without it the import is a dry run", false).option("--json", "Print the platform\u2019s answer as JSON", false).action(async (file, options, command) => {
|
|
7012
7150
|
const verbose = command.optsWithGlobals().verbose === true;
|
|
7013
7151
|
const { handleRequirementsImport: handleRequirementsImport2, formatDiscover: formatDiscover2, formatReport: formatReport3, reportRequirementsImportFailure: reportRequirementsImportFailure2 } = await Promise.resolve().then(() => (init_requirements_import(), requirements_import_exports));
|
|
7014
7152
|
try {
|
package/dist/reporter-core.cjs
CHANGED
|
@@ -95,12 +95,14 @@ function parseRetryAfter(raw, now = Date.now()) {
|
|
|
95
95
|
if (!Number.isFinite(seconds)) return null;
|
|
96
96
|
return Math.min(Math.max(seconds, 0) * 1e3, RETRY_AFTER_CAP_MS);
|
|
97
97
|
}
|
|
98
|
+
var POOLS = { "http:": new import_node_http.Agent({ keepAlive: true }), "https:": new import_node_https.Agent({ keepAlive: true }) };
|
|
98
99
|
var IDLE_MS = 3e5;
|
|
99
100
|
function nodeTransport(url, init, idleMs = IDLE_MS) {
|
|
100
101
|
return new Promise((resolve2, reject) => {
|
|
101
102
|
const target = new URL(url);
|
|
102
103
|
const send = target.protocol === "https:" ? import_node_https.request : import_node_http.request;
|
|
103
|
-
const
|
|
104
|
+
const agent = POOLS[target.protocol === "https:" ? "https:" : "http:"];
|
|
105
|
+
const req = send(target, { method: init.method, headers: init.headers, agent }, (res) => {
|
|
104
106
|
const chunks = [];
|
|
105
107
|
res.on("data", (chunk2) => chunks.push(chunk2));
|
|
106
108
|
res.on("error", reject);
|
|
@@ -314,6 +316,7 @@ function shardOf(env) {
|
|
|
314
316
|
}
|
|
315
317
|
var UPLOAD_CONCURRENCY = { min: 1, max: 32, default: 16 };
|
|
316
318
|
var UPLOAD_RETRIES = { min: 0, max: 20, default: 6 };
|
|
319
|
+
var IMPORT_TIMEOUT = { min: 0, max: 86400, default: 600 };
|
|
317
320
|
function value(env, name) {
|
|
318
321
|
const raw = env[name]?.trim();
|
|
319
322
|
return raw === void 0 || raw === "" ? void 0 : raw;
|
|
@@ -351,6 +354,7 @@ function readEnv(env = process.env) {
|
|
|
351
354
|
const warnings = [];
|
|
352
355
|
const uploadConcurrency = bounded(env, "PLUNE_UPLOAD_CONCURRENCY", UPLOAD_CONCURRENCY, warnings);
|
|
353
356
|
const uploadRetries = bounded(env, "PLUNE_UPLOAD_RETRIES", UPLOAD_RETRIES, warnings);
|
|
357
|
+
const importTimeout = bounded(env, "PLUNE_IMPORT_TIMEOUT", IMPORT_TIMEOUT, warnings);
|
|
354
358
|
const title = value(env, "PLUNE_RUN_TITLE");
|
|
355
359
|
const environment = value(env, "PLUNE_ENV");
|
|
356
360
|
const labels = list(env, "PLUNE_LABELS");
|
|
@@ -382,6 +386,7 @@ function readEnv(env = process.env) {
|
|
|
382
386
|
keepOpen: flag(env, "PLUNE_SHARED_RUN") || flag(env, "PLUNE_PROCEED") || shard !== void 0 || shardError !== void 0,
|
|
383
387
|
...shard !== void 0 ? { shard } : {},
|
|
384
388
|
...shardError !== void 0 ? { shardError } : {},
|
|
389
|
+
...importTimeout !== void 0 ? { importTimeout } : {},
|
|
385
390
|
ignored: UNSUPPORTED_VARS.filter((name) => value(env, name) !== void 0),
|
|
386
391
|
warnings
|
|
387
392
|
};
|
|
@@ -591,6 +596,7 @@ var RESOLVE_CHUNK = 500;
|
|
|
591
596
|
var BATCH_MAX = 500;
|
|
592
597
|
var DISCOVER_MAX = 500;
|
|
593
598
|
var BATCH_DEFAULT = 500;
|
|
599
|
+
var SEND_CONCURRENCY = 4;
|
|
594
600
|
var EXPECTED_MAX = 1e4;
|
|
595
601
|
var KEY_MAX = 1024;
|
|
596
602
|
var TITLE_MAX = 300;
|
|
@@ -1344,7 +1350,29 @@ async function startRun(passed, declared) {
|
|
|
1344
1350
|
else await uploadBatched(jobs);
|
|
1345
1351
|
uploadMs += Date.now() - started;
|
|
1346
1352
|
}
|
|
1353
|
+
const sending = /* @__PURE__ */ new Set();
|
|
1354
|
+
let sendError;
|
|
1355
|
+
let answered = false;
|
|
1356
|
+
async function dispatch(work) {
|
|
1357
|
+
while (sending.size >= (answered ? SEND_CONCURRENCY : 1)) await Promise.race(sending);
|
|
1358
|
+
const task = work().catch((err) => {
|
|
1359
|
+
sendError ??= err;
|
|
1360
|
+
}).finally(() => sending.delete(task));
|
|
1361
|
+
sending.add(task);
|
|
1362
|
+
}
|
|
1363
|
+
async function settle() {
|
|
1364
|
+
while (sending.size > 0) await Promise.all([...sending]);
|
|
1365
|
+
if (sendError !== void 0) {
|
|
1366
|
+
const err = sendError;
|
|
1367
|
+
sendError = void 0;
|
|
1368
|
+
throw err;
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1347
1371
|
async function flush() {
|
|
1372
|
+
await flushBuffer();
|
|
1373
|
+
await settle();
|
|
1374
|
+
}
|
|
1375
|
+
async function flushBuffer() {
|
|
1348
1376
|
if (buffer.length === 0) return;
|
|
1349
1377
|
const pending = buffer;
|
|
1350
1378
|
buffer = [];
|
|
@@ -1352,6 +1380,9 @@ async function startRun(passed, declared) {
|
|
|
1352
1380
|
defer(pending);
|
|
1353
1381
|
return;
|
|
1354
1382
|
}
|
|
1383
|
+
await dispatch(() => deliver(pending));
|
|
1384
|
+
}
|
|
1385
|
+
async function deliver(pending) {
|
|
1355
1386
|
await resolveKeys(pending.flatMap((p) => p.keys));
|
|
1356
1387
|
const submissions = [];
|
|
1357
1388
|
const unanswered = [];
|
|
@@ -1392,6 +1423,7 @@ async function startRun(passed, declared) {
|
|
|
1392
1423
|
for (const k of told) stats[k] += counts[k];
|
|
1393
1424
|
progress();
|
|
1394
1425
|
await upload(out.ok ? out.body?.items : void 0, filesByKey);
|
|
1426
|
+
answered = true;
|
|
1395
1427
|
continue;
|
|
1396
1428
|
}
|
|
1397
1429
|
if (out.ok) {
|
|
@@ -1466,7 +1498,7 @@ async function startRun(passed, declared) {
|
|
|
1466
1498
|
},
|
|
1467
1499
|
async add(result) {
|
|
1468
1500
|
buffer.push({ ...result, keys: bound(result.keys) });
|
|
1469
|
-
if (buffer.length >= batchSize) await
|
|
1501
|
+
if (buffer.length >= batchSize) await flushBuffer();
|
|
1470
1502
|
},
|
|
1471
1503
|
flush,
|
|
1472
1504
|
async finish(reason) {
|
package/dist/reporter-core.d.cts
CHANGED
|
@@ -445,6 +445,8 @@ interface EnvSettings {
|
|
|
445
445
|
shard?: ShardOf;
|
|
446
446
|
/** `PLUNE_SHARD` was set to something that is not `k/N`. A configuration error: the caller refuses to start. */
|
|
447
447
|
shardError?: string;
|
|
448
|
+
/** `PLUNE_IMPORT_TIMEOUT` in seconds, when set and usable. */
|
|
449
|
+
importTimeout?: number;
|
|
448
450
|
/** Variables that were set and have nowhere to land. The caller says so once. */
|
|
449
451
|
ignored: string[];
|
|
450
452
|
/** Variables that were set to something unusable and fell back to their default. The caller says so once. */
|
package/dist/reporter-core.d.ts
CHANGED
|
@@ -445,6 +445,8 @@ interface EnvSettings {
|
|
|
445
445
|
shard?: ShardOf;
|
|
446
446
|
/** `PLUNE_SHARD` was set to something that is not `k/N`. A configuration error: the caller refuses to start. */
|
|
447
447
|
shardError?: string;
|
|
448
|
+
/** `PLUNE_IMPORT_TIMEOUT` in seconds, when set and usable. */
|
|
449
|
+
importTimeout?: number;
|
|
448
450
|
/** Variables that were set and have nowhere to land. The caller says so once. */
|
|
449
451
|
ignored: string[];
|
|
450
452
|
/** Variables that were set to something unusable and fell back to their default. The caller says so once. */
|
package/dist/reporter-core.js
CHANGED
|
@@ -33,8 +33,8 @@ function loadToken() {
|
|
|
33
33
|
// src/reporter-core/client.ts
|
|
34
34
|
import { createReadStream, openAsBlob } from "fs";
|
|
35
35
|
import { pipeline } from "stream";
|
|
36
|
-
import { request as httpRequest } from "http";
|
|
37
|
-
import { request as httpsRequest } from "https";
|
|
36
|
+
import { Agent as HttpAgent, request as httpRequest } from "http";
|
|
37
|
+
import { Agent as HttpsAgent, request as httpsRequest } from "https";
|
|
38
38
|
var BACKOFF_BASE_MS = 300;
|
|
39
39
|
var BACKOFF_CAP_MS = 1e4;
|
|
40
40
|
var DEFAULT_RETRIES = 2;
|
|
@@ -46,12 +46,14 @@ function parseRetryAfter(raw, now = Date.now()) {
|
|
|
46
46
|
if (!Number.isFinite(seconds)) return null;
|
|
47
47
|
return Math.min(Math.max(seconds, 0) * 1e3, RETRY_AFTER_CAP_MS);
|
|
48
48
|
}
|
|
49
|
+
var POOLS = { "http:": new HttpAgent({ keepAlive: true }), "https:": new HttpsAgent({ keepAlive: true }) };
|
|
49
50
|
var IDLE_MS = 3e5;
|
|
50
51
|
function nodeTransport(url, init, idleMs = IDLE_MS) {
|
|
51
52
|
return new Promise((resolve2, reject) => {
|
|
52
53
|
const target = new URL(url);
|
|
53
54
|
const send = target.protocol === "https:" ? httpsRequest : httpRequest;
|
|
54
|
-
const
|
|
55
|
+
const agent = POOLS[target.protocol === "https:" ? "https:" : "http:"];
|
|
56
|
+
const req = send(target, { method: init.method, headers: init.headers, agent }, (res) => {
|
|
55
57
|
const chunks = [];
|
|
56
58
|
res.on("data", (chunk2) => chunks.push(chunk2));
|
|
57
59
|
res.on("error", reject);
|
|
@@ -265,6 +267,7 @@ function shardOf(env) {
|
|
|
265
267
|
}
|
|
266
268
|
var UPLOAD_CONCURRENCY = { min: 1, max: 32, default: 16 };
|
|
267
269
|
var UPLOAD_RETRIES = { min: 0, max: 20, default: 6 };
|
|
270
|
+
var IMPORT_TIMEOUT = { min: 0, max: 86400, default: 600 };
|
|
268
271
|
function value(env, name) {
|
|
269
272
|
const raw = env[name]?.trim();
|
|
270
273
|
return raw === void 0 || raw === "" ? void 0 : raw;
|
|
@@ -302,6 +305,7 @@ function readEnv(env = process.env) {
|
|
|
302
305
|
const warnings = [];
|
|
303
306
|
const uploadConcurrency = bounded(env, "PLUNE_UPLOAD_CONCURRENCY", UPLOAD_CONCURRENCY, warnings);
|
|
304
307
|
const uploadRetries = bounded(env, "PLUNE_UPLOAD_RETRIES", UPLOAD_RETRIES, warnings);
|
|
308
|
+
const importTimeout = bounded(env, "PLUNE_IMPORT_TIMEOUT", IMPORT_TIMEOUT, warnings);
|
|
305
309
|
const title = value(env, "PLUNE_RUN_TITLE");
|
|
306
310
|
const environment = value(env, "PLUNE_ENV");
|
|
307
311
|
const labels = list(env, "PLUNE_LABELS");
|
|
@@ -333,6 +337,7 @@ function readEnv(env = process.env) {
|
|
|
333
337
|
keepOpen: flag(env, "PLUNE_SHARED_RUN") || flag(env, "PLUNE_PROCEED") || shard !== void 0 || shardError !== void 0,
|
|
334
338
|
...shard !== void 0 ? { shard } : {},
|
|
335
339
|
...shardError !== void 0 ? { shardError } : {},
|
|
340
|
+
...importTimeout !== void 0 ? { importTimeout } : {},
|
|
336
341
|
ignored: UNSUPPORTED_VARS.filter((name) => value(env, name) !== void 0),
|
|
337
342
|
warnings
|
|
338
343
|
};
|
|
@@ -542,6 +547,7 @@ var RESOLVE_CHUNK = 500;
|
|
|
542
547
|
var BATCH_MAX = 500;
|
|
543
548
|
var DISCOVER_MAX = 500;
|
|
544
549
|
var BATCH_DEFAULT = 500;
|
|
550
|
+
var SEND_CONCURRENCY = 4;
|
|
545
551
|
var EXPECTED_MAX = 1e4;
|
|
546
552
|
var KEY_MAX = 1024;
|
|
547
553
|
var TITLE_MAX = 300;
|
|
@@ -1295,7 +1301,29 @@ async function startRun(passed, declared) {
|
|
|
1295
1301
|
else await uploadBatched(jobs);
|
|
1296
1302
|
uploadMs += Date.now() - started;
|
|
1297
1303
|
}
|
|
1304
|
+
const sending = /* @__PURE__ */ new Set();
|
|
1305
|
+
let sendError;
|
|
1306
|
+
let answered = false;
|
|
1307
|
+
async function dispatch(work) {
|
|
1308
|
+
while (sending.size >= (answered ? SEND_CONCURRENCY : 1)) await Promise.race(sending);
|
|
1309
|
+
const task = work().catch((err) => {
|
|
1310
|
+
sendError ??= err;
|
|
1311
|
+
}).finally(() => sending.delete(task));
|
|
1312
|
+
sending.add(task);
|
|
1313
|
+
}
|
|
1314
|
+
async function settle() {
|
|
1315
|
+
while (sending.size > 0) await Promise.all([...sending]);
|
|
1316
|
+
if (sendError !== void 0) {
|
|
1317
|
+
const err = sendError;
|
|
1318
|
+
sendError = void 0;
|
|
1319
|
+
throw err;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1298
1322
|
async function flush() {
|
|
1323
|
+
await flushBuffer();
|
|
1324
|
+
await settle();
|
|
1325
|
+
}
|
|
1326
|
+
async function flushBuffer() {
|
|
1299
1327
|
if (buffer.length === 0) return;
|
|
1300
1328
|
const pending = buffer;
|
|
1301
1329
|
buffer = [];
|
|
@@ -1303,6 +1331,9 @@ async function startRun(passed, declared) {
|
|
|
1303
1331
|
defer(pending);
|
|
1304
1332
|
return;
|
|
1305
1333
|
}
|
|
1334
|
+
await dispatch(() => deliver(pending));
|
|
1335
|
+
}
|
|
1336
|
+
async function deliver(pending) {
|
|
1306
1337
|
await resolveKeys(pending.flatMap((p) => p.keys));
|
|
1307
1338
|
const submissions = [];
|
|
1308
1339
|
const unanswered = [];
|
|
@@ -1343,6 +1374,7 @@ async function startRun(passed, declared) {
|
|
|
1343
1374
|
for (const k of told) stats[k] += counts[k];
|
|
1344
1375
|
progress();
|
|
1345
1376
|
await upload(out.ok ? out.body?.items : void 0, filesByKey);
|
|
1377
|
+
answered = true;
|
|
1346
1378
|
continue;
|
|
1347
1379
|
}
|
|
1348
1380
|
if (out.ok) {
|
|
@@ -1417,7 +1449,7 @@ async function startRun(passed, declared) {
|
|
|
1417
1449
|
},
|
|
1418
1450
|
async add(result) {
|
|
1419
1451
|
buffer.push({ ...result, keys: bound(result.keys) });
|
|
1420
|
-
if (buffer.length >= batchSize) await
|
|
1452
|
+
if (buffer.length >= batchSize) await flushBuffer();
|
|
1421
1453
|
},
|
|
1422
1454
|
flush,
|
|
1423
1455
|
async finish(reason) {
|
package/package.json
CHANGED