@plune-ai/cli 0.30.1 → 0.31.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 +33 -2
- package/dist/cli.cjs +207 -123
- package/dist/index.cjs +113 -75
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +113 -75
- package/dist/reporter-core.cjs +31 -4
- package/dist/reporter-core.d.cts +13 -0
- package/dist/reporter-core.d.ts +13 -0
- package/dist/reporter-core.js +31 -4
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -375,7 +375,7 @@ function normalizeProviderError(err, secret, envVar) {
|
|
|
375
375
|
}
|
|
376
376
|
|
|
377
377
|
// src/providers/retry.ts
|
|
378
|
-
var defaultSleep = (ms) => new Promise((
|
|
378
|
+
var defaultSleep = (ms) => new Promise((resolve6) => setTimeout(resolve6, ms));
|
|
379
379
|
function retryAfterMs(err) {
|
|
380
380
|
if (typeof err !== "object" || err === null || !("headers" in err)) return void 0;
|
|
381
381
|
const raw = readHeader(err.headers, "retry-after");
|
|
@@ -1855,6 +1855,78 @@ function gateExit(verdict, runnerCode) {
|
|
|
1855
1855
|
return n("failed") + n("broken") > 0 || notRun > 0 ? { code: 1, line: `plune: gate failed \u2014 ${said}` } : { code: 0, line: `plune: gate passed \u2014 ${said}` };
|
|
1856
1856
|
}
|
|
1857
1857
|
|
|
1858
|
+
// src/cli/commands/job-reporter.ts
|
|
1859
|
+
var JOB_TIMEOUT_MS = 5e3;
|
|
1860
|
+
var JOB_SUBJECT_NAME_MAX = 200;
|
|
1861
|
+
var JOB_REASON_MAX = 2e3;
|
|
1862
|
+
var JobReporter = class {
|
|
1863
|
+
constructor(doFetch, base, token, kind) {
|
|
1864
|
+
this.doFetch = doFetch;
|
|
1865
|
+
this.base = base;
|
|
1866
|
+
this.token = token;
|
|
1867
|
+
this.kind = kind;
|
|
1868
|
+
}
|
|
1869
|
+
doFetch;
|
|
1870
|
+
base;
|
|
1871
|
+
token;
|
|
1872
|
+
kind;
|
|
1873
|
+
id;
|
|
1874
|
+
/** The upload's rows, the job's `total`; `null` while there are none to count. */
|
|
1875
|
+
size = null;
|
|
1876
|
+
/** The first report the platform did not take, for the CLI to say once. */
|
|
1877
|
+
problem;
|
|
1878
|
+
async send(method, route, body) {
|
|
1879
|
+
try {
|
|
1880
|
+
const res = await this.doFetch(`${this.base}/v1/jobs${route}`, {
|
|
1881
|
+
method,
|
|
1882
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${this.token}` },
|
|
1883
|
+
body: JSON.stringify(body),
|
|
1884
|
+
signal: AbortSignal.timeout(JOB_TIMEOUT_MS)
|
|
1885
|
+
});
|
|
1886
|
+
const json = await res.json().catch(() => void 0);
|
|
1887
|
+
if (res.ok) return json ?? {};
|
|
1888
|
+
if (route === "" && (res.status === 404 || res.status === 405)) return void 0;
|
|
1889
|
+
this.problem ??= `HTTP ${res.status}${typeof json?.error === "string" ? ` \u2014 ${json.error}` : ""}`;
|
|
1890
|
+
} catch (err) {
|
|
1891
|
+
this.problem ??= err instanceof Error ? err.message : String(err);
|
|
1892
|
+
}
|
|
1893
|
+
return void 0;
|
|
1894
|
+
}
|
|
1895
|
+
async open(subjectName, rows) {
|
|
1896
|
+
this.size = Number.isInteger(rows) && rows > 0 ? rows : null;
|
|
1897
|
+
const name = subjectName.trim().slice(0, JOB_SUBJECT_NAME_MAX);
|
|
1898
|
+
const body = await this.send("POST", "", {
|
|
1899
|
+
kind: this.kind,
|
|
1900
|
+
...name !== "" ? { subjectName: name } : {},
|
|
1901
|
+
...this.size !== null ? { progress: { done: 0, total: this.size } } : {}
|
|
1902
|
+
});
|
|
1903
|
+
if (typeof body?.id === "string") this.id = body.id;
|
|
1904
|
+
}
|
|
1905
|
+
/**
|
|
1906
|
+
* The upload is in: every row is on the platform. `subjectId` is what the job works on, learnt from the upload's
|
|
1907
|
+
* answer (the run an ingest landed in): the platform takes it while the job names none (#1279).
|
|
1908
|
+
*/
|
|
1909
|
+
async uploaded(at) {
|
|
1910
|
+
if (this.id === void 0) return;
|
|
1911
|
+
await this.send("PATCH", `/${encodeURIComponent(this.id)}/progress`, {
|
|
1912
|
+
done: this.size,
|
|
1913
|
+
total: this.size,
|
|
1914
|
+
...at.cursor !== void 0 ? { cursor: at.cursor } : {},
|
|
1915
|
+
...at.subjectId !== void 0 ? { subjectId: at.subjectId } : {}
|
|
1916
|
+
});
|
|
1917
|
+
}
|
|
1918
|
+
async finish(outcome) {
|
|
1919
|
+
if (this.id === void 0) return;
|
|
1920
|
+
const id = this.id;
|
|
1921
|
+
this.id = void 0;
|
|
1922
|
+
await this.send(
|
|
1923
|
+
"PATCH",
|
|
1924
|
+
`/${encodeURIComponent(id)}/state`,
|
|
1925
|
+
outcome.ok ? { state: "completed" } : { state: "failed", reason: outcome.reason.slice(0, JOB_REASON_MAX) }
|
|
1926
|
+
);
|
|
1927
|
+
}
|
|
1928
|
+
};
|
|
1929
|
+
|
|
1858
1930
|
// src/cli/commands/sync.ts
|
|
1859
1931
|
var NotLoggedInError = class extends Error {
|
|
1860
1932
|
constructor() {
|
|
@@ -1892,58 +1964,6 @@ var SyncHttpError = class extends Error {
|
|
|
1892
1964
|
}
|
|
1893
1965
|
status;
|
|
1894
1966
|
};
|
|
1895
|
-
var JOB_KIND = "cli-sync";
|
|
1896
|
-
var JOB_TIMEOUT_MS = 5e3;
|
|
1897
|
-
var JobReporter = class {
|
|
1898
|
-
constructor(doFetch, base, token) {
|
|
1899
|
-
this.doFetch = doFetch;
|
|
1900
|
-
this.base = base;
|
|
1901
|
-
this.token = token;
|
|
1902
|
-
}
|
|
1903
|
-
doFetch;
|
|
1904
|
-
base;
|
|
1905
|
-
token;
|
|
1906
|
-
id;
|
|
1907
|
-
async send(method, url, body) {
|
|
1908
|
-
try {
|
|
1909
|
-
const res = await this.doFetch(url, {
|
|
1910
|
-
method,
|
|
1911
|
-
headers: { "content-type": "application/json", authorization: `Bearer ${this.token}` },
|
|
1912
|
-
body: JSON.stringify(body),
|
|
1913
|
-
signal: AbortSignal.timeout(JOB_TIMEOUT_MS)
|
|
1914
|
-
});
|
|
1915
|
-
const json = await res.json().catch(() => void 0);
|
|
1916
|
-
return res.ok ? json ?? {} : void 0;
|
|
1917
|
-
} catch {
|
|
1918
|
-
return void 0;
|
|
1919
|
-
}
|
|
1920
|
-
}
|
|
1921
|
-
async open(subjectName) {
|
|
1922
|
-
const body = await this.send("POST", `${this.base}/v1/jobs`, {
|
|
1923
|
-
kind: JOB_KIND,
|
|
1924
|
-
subjectName,
|
|
1925
|
-
progress: 0
|
|
1926
|
-
});
|
|
1927
|
-
if (typeof body?.id === "string") this.id = body.id;
|
|
1928
|
-
}
|
|
1929
|
-
async report(progress, cursor) {
|
|
1930
|
-
if (this.id === void 0) return;
|
|
1931
|
-
await this.send("PATCH", `${this.base}/v1/jobs/${encodeURIComponent(this.id)}`, {
|
|
1932
|
-
progress,
|
|
1933
|
-
...cursor !== void 0 ? { cursor } : {}
|
|
1934
|
-
});
|
|
1935
|
-
}
|
|
1936
|
-
async finish(outcome) {
|
|
1937
|
-
if (this.id === void 0) return;
|
|
1938
|
-
const id = this.id;
|
|
1939
|
-
this.id = void 0;
|
|
1940
|
-
await this.send(
|
|
1941
|
-
"PATCH",
|
|
1942
|
-
`${this.base}/v1/jobs/${encodeURIComponent(id)}`,
|
|
1943
|
-
outcome.ok ? { status: "succeeded", progress: 100 } : { status: "failed", reason: outcome.reason }
|
|
1944
|
-
);
|
|
1945
|
-
}
|
|
1946
|
-
};
|
|
1947
1967
|
function readRun(cwd, file) {
|
|
1948
1968
|
const target = file !== void 0 ? path5.resolve(cwd, file) : path5.join(cwd, ".plune", "last-run.json");
|
|
1949
1969
|
let raw;
|
|
@@ -1985,11 +2005,12 @@ async function handleSync(deps = {}) {
|
|
|
1985
2005
|
const payload = JSON.stringify(run);
|
|
1986
2006
|
const idempotencyKey = (0, import_node_crypto2.createHash)("sha256").update(payload).digest("hex");
|
|
1987
2007
|
const bearer = token;
|
|
1988
|
-
const job = new JobReporter(doFetch, apiUrl, bearer);
|
|
1989
|
-
await job.open(path5.basename(deps.file !== void 0 ? path5.resolve(cwd, deps.file) : cwd));
|
|
2008
|
+
const job = new JobReporter(doFetch, apiUrl, bearer, "sync");
|
|
2009
|
+
await job.open(path5.basename(deps.file !== void 0 ? path5.resolve(cwd, deps.file) : cwd), run.summary.total);
|
|
1990
2010
|
try {
|
|
1991
2011
|
const result = await upload();
|
|
1992
2012
|
await job.finish({ ok: true });
|
|
2013
|
+
if (job.problem !== void 0) result.jobProblem = job.problem;
|
|
1993
2014
|
return result;
|
|
1994
2015
|
} catch (err) {
|
|
1995
2016
|
await job.finish({ ok: false, reason: err instanceof Error ? err.message : "Sync failed." });
|
|
@@ -2018,7 +2039,7 @@ async function handleSync(deps = {}) {
|
|
|
2018
2039
|
const body = await res.json().catch(() => ({}));
|
|
2019
2040
|
const id = typeof body.id === "string" ? body.id : "";
|
|
2020
2041
|
const result = { id, url: `${endpoint}/${id}` };
|
|
2021
|
-
await job.
|
|
2042
|
+
await job.uploaded({ cursor: id || void 0 });
|
|
2022
2043
|
if (deps.failOnFailures === true) {
|
|
2023
2044
|
result.gate = gateExit(await readVerdict(doFetch, `${endpoint}/${id}/summary`, bearer));
|
|
2024
2045
|
}
|
|
@@ -2046,6 +2067,9 @@ function reportSyncFailure(err, write) {
|
|
|
2046
2067
|
return null;
|
|
2047
2068
|
}
|
|
2048
2069
|
|
|
2070
|
+
// src/cli/commands/ingest.ts
|
|
2071
|
+
var path7 = __toESM(require("path"), 1);
|
|
2072
|
+
|
|
2049
2073
|
// src/cli/commands/cairn-artifact.ts
|
|
2050
2074
|
var fs7 = __toESM(require("fs/promises"), 1);
|
|
2051
2075
|
var path6 = __toESM(require("path"), 1);
|
|
@@ -2240,28 +2264,42 @@ async function handleIngest(deps) {
|
|
|
2240
2264
|
const payload = await readCairnRun(dir);
|
|
2241
2265
|
const doFetch = deps.fetchImpl ?? fetch;
|
|
2242
2266
|
const endpoint = `${apiUrl}/v1/ingest/cairn`;
|
|
2243
|
-
|
|
2267
|
+
const job = new JobReporter(doFetch, apiUrl, token, "ingest");
|
|
2268
|
+
await job.open(path7.basename(path7.resolve(dir)), payload.cases.length);
|
|
2244
2269
|
try {
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
} catch {
|
|
2251
|
-
|
|
2270
|
+
const result = await upload();
|
|
2271
|
+
await job.uploaded({ subjectId: result.runId || void 0 });
|
|
2272
|
+
await job.finish({ ok: true });
|
|
2273
|
+
if (job.problem !== void 0) result.jobProblem = job.problem;
|
|
2274
|
+
return result;
|
|
2275
|
+
} catch (err) {
|
|
2276
|
+
await job.finish({ ok: false, reason: err instanceof Error && err.message.trim() || "Ingest failed." });
|
|
2277
|
+
throw err;
|
|
2252
2278
|
}
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2279
|
+
async function upload() {
|
|
2280
|
+
let res;
|
|
2281
|
+
try {
|
|
2282
|
+
res = await doFetch(endpoint, {
|
|
2283
|
+
method: "POST",
|
|
2284
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
|
2285
|
+
body: JSON.stringify(payload)
|
|
2286
|
+
});
|
|
2287
|
+
} catch {
|
|
2288
|
+
throw new IngestNetworkError(apiUrl);
|
|
2289
|
+
}
|
|
2290
|
+
if (!res.ok) {
|
|
2291
|
+
const detail = await safeErrorDetail2(res);
|
|
2292
|
+
if (res.status === 401) throw new IngestTokenRejectedError();
|
|
2293
|
+
throw new IngestHttpError(res.status, detail);
|
|
2294
|
+
}
|
|
2295
|
+
const body = await res.json().catch(() => ({}));
|
|
2296
|
+
return {
|
|
2297
|
+
runId: typeof body.runId === "string" ? body.runId : "",
|
|
2298
|
+
linked: typeof body.linked === "number" ? body.linked : 0,
|
|
2299
|
+
proposed: typeof body.proposed === "number" ? body.proposed : 0,
|
|
2300
|
+
skipped: typeof body.skipped === "number" ? body.skipped : 0
|
|
2301
|
+
};
|
|
2257
2302
|
}
|
|
2258
|
-
const body = await res.json().catch(() => ({}));
|
|
2259
|
-
return {
|
|
2260
|
-
runId: typeof body.runId === "string" ? body.runId : "",
|
|
2261
|
-
linked: typeof body.linked === "number" ? body.linked : 0,
|
|
2262
|
-
proposed: typeof body.proposed === "number" ? body.proposed : 0,
|
|
2263
|
-
skipped: typeof body.skipped === "number" ? body.skipped : 0
|
|
2264
|
-
};
|
|
2265
2303
|
}
|
|
2266
2304
|
function reportIngestFailure(err, write) {
|
|
2267
2305
|
if (err instanceof IngestNotLoggedInError || err instanceof IngestTokenRejectedError || err instanceof CairnRunNotFoundError || err instanceof NoCairnRunsError || err instanceof CairnRunVersionError || err instanceof CairnRunPartialError || err instanceof CairnEvidenceAmbiguousError) {
|
package/dist/index.d.cts
CHANGED
|
@@ -364,6 +364,8 @@ interface SyncResult {
|
|
|
364
364
|
code: number;
|
|
365
365
|
line: string;
|
|
366
366
|
};
|
|
367
|
+
/** Why the platform's job queue did not take this sync's report: the first refusal or error. The upload went in. */
|
|
368
|
+
jobProblem?: string;
|
|
367
369
|
}
|
|
368
370
|
/**
|
|
369
371
|
* Upload the latest (or `--file`) local run to `POST {apiUrl}/v1/runs` with the stored Bearer token.
|
|
@@ -688,6 +690,8 @@ interface IngestResult {
|
|
|
688
690
|
linked: number;
|
|
689
691
|
proposed: number;
|
|
690
692
|
skipped: number;
|
|
693
|
+
/** Why the platform's job queue did not take this ingest's report: the first refusal or error. The run is recorded. */
|
|
694
|
+
jobProblem?: string;
|
|
691
695
|
}
|
|
692
696
|
declare function handleIngest(deps: IngestDeps): Promise<IngestResult>;
|
|
693
697
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -364,6 +364,8 @@ interface SyncResult {
|
|
|
364
364
|
code: number;
|
|
365
365
|
line: string;
|
|
366
366
|
};
|
|
367
|
+
/** Why the platform's job queue did not take this sync's report: the first refusal or error. The upload went in. */
|
|
368
|
+
jobProblem?: string;
|
|
367
369
|
}
|
|
368
370
|
/**
|
|
369
371
|
* Upload the latest (or `--file`) local run to `POST {apiUrl}/v1/runs` with the stored Bearer token.
|
|
@@ -688,6 +690,8 @@ interface IngestResult {
|
|
|
688
690
|
linked: number;
|
|
689
691
|
proposed: number;
|
|
690
692
|
skipped: number;
|
|
693
|
+
/** Why the platform's job queue did not take this ingest's report: the first refusal or error. The run is recorded. */
|
|
694
|
+
jobProblem?: string;
|
|
691
695
|
}
|
|
692
696
|
declare function handleIngest(deps: IngestDeps): Promise<IngestResult>;
|
|
693
697
|
/**
|
package/dist/index.js
CHANGED
|
@@ -315,7 +315,7 @@ function normalizeProviderError(err, secret, envVar) {
|
|
|
315
315
|
}
|
|
316
316
|
|
|
317
317
|
// src/providers/retry.ts
|
|
318
|
-
var defaultSleep = (ms) => new Promise((
|
|
318
|
+
var defaultSleep = (ms) => new Promise((resolve6) => setTimeout(resolve6, ms));
|
|
319
319
|
function retryAfterMs(err) {
|
|
320
320
|
if (typeof err !== "object" || err === null || !("headers" in err)) return void 0;
|
|
321
321
|
const raw = readHeader(err.headers, "retry-after");
|
|
@@ -1795,6 +1795,78 @@ function gateExit(verdict, runnerCode) {
|
|
|
1795
1795
|
return n("failed") + n("broken") > 0 || notRun > 0 ? { code: 1, line: `plune: gate failed \u2014 ${said}` } : { code: 0, line: `plune: gate passed \u2014 ${said}` };
|
|
1796
1796
|
}
|
|
1797
1797
|
|
|
1798
|
+
// src/cli/commands/job-reporter.ts
|
|
1799
|
+
var JOB_TIMEOUT_MS = 5e3;
|
|
1800
|
+
var JOB_SUBJECT_NAME_MAX = 200;
|
|
1801
|
+
var JOB_REASON_MAX = 2e3;
|
|
1802
|
+
var JobReporter = class {
|
|
1803
|
+
constructor(doFetch, base, token, kind) {
|
|
1804
|
+
this.doFetch = doFetch;
|
|
1805
|
+
this.base = base;
|
|
1806
|
+
this.token = token;
|
|
1807
|
+
this.kind = kind;
|
|
1808
|
+
}
|
|
1809
|
+
doFetch;
|
|
1810
|
+
base;
|
|
1811
|
+
token;
|
|
1812
|
+
kind;
|
|
1813
|
+
id;
|
|
1814
|
+
/** The upload's rows, the job's `total`; `null` while there are none to count. */
|
|
1815
|
+
size = null;
|
|
1816
|
+
/** The first report the platform did not take, for the CLI to say once. */
|
|
1817
|
+
problem;
|
|
1818
|
+
async send(method, route, body) {
|
|
1819
|
+
try {
|
|
1820
|
+
const res = await this.doFetch(`${this.base}/v1/jobs${route}`, {
|
|
1821
|
+
method,
|
|
1822
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${this.token}` },
|
|
1823
|
+
body: JSON.stringify(body),
|
|
1824
|
+
signal: AbortSignal.timeout(JOB_TIMEOUT_MS)
|
|
1825
|
+
});
|
|
1826
|
+
const json = await res.json().catch(() => void 0);
|
|
1827
|
+
if (res.ok) return json ?? {};
|
|
1828
|
+
if (route === "" && (res.status === 404 || res.status === 405)) return void 0;
|
|
1829
|
+
this.problem ??= `HTTP ${res.status}${typeof json?.error === "string" ? ` \u2014 ${json.error}` : ""}`;
|
|
1830
|
+
} catch (err) {
|
|
1831
|
+
this.problem ??= err instanceof Error ? err.message : String(err);
|
|
1832
|
+
}
|
|
1833
|
+
return void 0;
|
|
1834
|
+
}
|
|
1835
|
+
async open(subjectName, rows) {
|
|
1836
|
+
this.size = Number.isInteger(rows) && rows > 0 ? rows : null;
|
|
1837
|
+
const name = subjectName.trim().slice(0, JOB_SUBJECT_NAME_MAX);
|
|
1838
|
+
const body = await this.send("POST", "", {
|
|
1839
|
+
kind: this.kind,
|
|
1840
|
+
...name !== "" ? { subjectName: name } : {},
|
|
1841
|
+
...this.size !== null ? { progress: { done: 0, total: this.size } } : {}
|
|
1842
|
+
});
|
|
1843
|
+
if (typeof body?.id === "string") this.id = body.id;
|
|
1844
|
+
}
|
|
1845
|
+
/**
|
|
1846
|
+
* The upload is in: every row is on the platform. `subjectId` is what the job works on, learnt from the upload's
|
|
1847
|
+
* answer (the run an ingest landed in): the platform takes it while the job names none (#1279).
|
|
1848
|
+
*/
|
|
1849
|
+
async uploaded(at) {
|
|
1850
|
+
if (this.id === void 0) return;
|
|
1851
|
+
await this.send("PATCH", `/${encodeURIComponent(this.id)}/progress`, {
|
|
1852
|
+
done: this.size,
|
|
1853
|
+
total: this.size,
|
|
1854
|
+
...at.cursor !== void 0 ? { cursor: at.cursor } : {},
|
|
1855
|
+
...at.subjectId !== void 0 ? { subjectId: at.subjectId } : {}
|
|
1856
|
+
});
|
|
1857
|
+
}
|
|
1858
|
+
async finish(outcome) {
|
|
1859
|
+
if (this.id === void 0) return;
|
|
1860
|
+
const id = this.id;
|
|
1861
|
+
this.id = void 0;
|
|
1862
|
+
await this.send(
|
|
1863
|
+
"PATCH",
|
|
1864
|
+
`/${encodeURIComponent(id)}/state`,
|
|
1865
|
+
outcome.ok ? { state: "completed" } : { state: "failed", reason: outcome.reason.slice(0, JOB_REASON_MAX) }
|
|
1866
|
+
);
|
|
1867
|
+
}
|
|
1868
|
+
};
|
|
1869
|
+
|
|
1798
1870
|
// src/cli/commands/sync.ts
|
|
1799
1871
|
var NotLoggedInError = class extends Error {
|
|
1800
1872
|
constructor() {
|
|
@@ -1832,58 +1904,6 @@ var SyncHttpError = class extends Error {
|
|
|
1832
1904
|
}
|
|
1833
1905
|
status;
|
|
1834
1906
|
};
|
|
1835
|
-
var JOB_KIND = "cli-sync";
|
|
1836
|
-
var JOB_TIMEOUT_MS = 5e3;
|
|
1837
|
-
var JobReporter = class {
|
|
1838
|
-
constructor(doFetch, base, token) {
|
|
1839
|
-
this.doFetch = doFetch;
|
|
1840
|
-
this.base = base;
|
|
1841
|
-
this.token = token;
|
|
1842
|
-
}
|
|
1843
|
-
doFetch;
|
|
1844
|
-
base;
|
|
1845
|
-
token;
|
|
1846
|
-
id;
|
|
1847
|
-
async send(method, url, body) {
|
|
1848
|
-
try {
|
|
1849
|
-
const res = await this.doFetch(url, {
|
|
1850
|
-
method,
|
|
1851
|
-
headers: { "content-type": "application/json", authorization: `Bearer ${this.token}` },
|
|
1852
|
-
body: JSON.stringify(body),
|
|
1853
|
-
signal: AbortSignal.timeout(JOB_TIMEOUT_MS)
|
|
1854
|
-
});
|
|
1855
|
-
const json = await res.json().catch(() => void 0);
|
|
1856
|
-
return res.ok ? json ?? {} : void 0;
|
|
1857
|
-
} catch {
|
|
1858
|
-
return void 0;
|
|
1859
|
-
}
|
|
1860
|
-
}
|
|
1861
|
-
async open(subjectName) {
|
|
1862
|
-
const body = await this.send("POST", `${this.base}/v1/jobs`, {
|
|
1863
|
-
kind: JOB_KIND,
|
|
1864
|
-
subjectName,
|
|
1865
|
-
progress: 0
|
|
1866
|
-
});
|
|
1867
|
-
if (typeof body?.id === "string") this.id = body.id;
|
|
1868
|
-
}
|
|
1869
|
-
async report(progress, cursor) {
|
|
1870
|
-
if (this.id === void 0) return;
|
|
1871
|
-
await this.send("PATCH", `${this.base}/v1/jobs/${encodeURIComponent(this.id)}`, {
|
|
1872
|
-
progress,
|
|
1873
|
-
...cursor !== void 0 ? { cursor } : {}
|
|
1874
|
-
});
|
|
1875
|
-
}
|
|
1876
|
-
async finish(outcome) {
|
|
1877
|
-
if (this.id === void 0) return;
|
|
1878
|
-
const id = this.id;
|
|
1879
|
-
this.id = void 0;
|
|
1880
|
-
await this.send(
|
|
1881
|
-
"PATCH",
|
|
1882
|
-
`${this.base}/v1/jobs/${encodeURIComponent(id)}`,
|
|
1883
|
-
outcome.ok ? { status: "succeeded", progress: 100 } : { status: "failed", reason: outcome.reason }
|
|
1884
|
-
);
|
|
1885
|
-
}
|
|
1886
|
-
};
|
|
1887
1907
|
function readRun(cwd, file) {
|
|
1888
1908
|
const target = file !== void 0 ? path5.resolve(cwd, file) : path5.join(cwd, ".plune", "last-run.json");
|
|
1889
1909
|
let raw;
|
|
@@ -1925,11 +1945,12 @@ async function handleSync(deps = {}) {
|
|
|
1925
1945
|
const payload = JSON.stringify(run);
|
|
1926
1946
|
const idempotencyKey = createHash2("sha256").update(payload).digest("hex");
|
|
1927
1947
|
const bearer = token;
|
|
1928
|
-
const job = new JobReporter(doFetch, apiUrl, bearer);
|
|
1929
|
-
await job.open(path5.basename(deps.file !== void 0 ? path5.resolve(cwd, deps.file) : cwd));
|
|
1948
|
+
const job = new JobReporter(doFetch, apiUrl, bearer, "sync");
|
|
1949
|
+
await job.open(path5.basename(deps.file !== void 0 ? path5.resolve(cwd, deps.file) : cwd), run.summary.total);
|
|
1930
1950
|
try {
|
|
1931
1951
|
const result = await upload();
|
|
1932
1952
|
await job.finish({ ok: true });
|
|
1953
|
+
if (job.problem !== void 0) result.jobProblem = job.problem;
|
|
1933
1954
|
return result;
|
|
1934
1955
|
} catch (err) {
|
|
1935
1956
|
await job.finish({ ok: false, reason: err instanceof Error ? err.message : "Sync failed." });
|
|
@@ -1958,7 +1979,7 @@ async function handleSync(deps = {}) {
|
|
|
1958
1979
|
const body = await res.json().catch(() => ({}));
|
|
1959
1980
|
const id = typeof body.id === "string" ? body.id : "";
|
|
1960
1981
|
const result = { id, url: `${endpoint}/${id}` };
|
|
1961
|
-
await job.
|
|
1982
|
+
await job.uploaded({ cursor: id || void 0 });
|
|
1962
1983
|
if (deps.failOnFailures === true) {
|
|
1963
1984
|
result.gate = gateExit(await readVerdict(doFetch, `${endpoint}/${id}/summary`, bearer));
|
|
1964
1985
|
}
|
|
@@ -1986,6 +2007,9 @@ function reportSyncFailure(err, write) {
|
|
|
1986
2007
|
return null;
|
|
1987
2008
|
}
|
|
1988
2009
|
|
|
2010
|
+
// src/cli/commands/ingest.ts
|
|
2011
|
+
import * as path7 from "path";
|
|
2012
|
+
|
|
1989
2013
|
// src/cli/commands/cairn-artifact.ts
|
|
1990
2014
|
import * as fs7 from "fs/promises";
|
|
1991
2015
|
import * as path6 from "path";
|
|
@@ -2180,28 +2204,42 @@ async function handleIngest(deps) {
|
|
|
2180
2204
|
const payload = await readCairnRun(dir);
|
|
2181
2205
|
const doFetch = deps.fetchImpl ?? fetch;
|
|
2182
2206
|
const endpoint = `${apiUrl}/v1/ingest/cairn`;
|
|
2183
|
-
|
|
2207
|
+
const job = new JobReporter(doFetch, apiUrl, token, "ingest");
|
|
2208
|
+
await job.open(path7.basename(path7.resolve(dir)), payload.cases.length);
|
|
2184
2209
|
try {
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
} catch {
|
|
2191
|
-
|
|
2210
|
+
const result = await upload();
|
|
2211
|
+
await job.uploaded({ subjectId: result.runId || void 0 });
|
|
2212
|
+
await job.finish({ ok: true });
|
|
2213
|
+
if (job.problem !== void 0) result.jobProblem = job.problem;
|
|
2214
|
+
return result;
|
|
2215
|
+
} catch (err) {
|
|
2216
|
+
await job.finish({ ok: false, reason: err instanceof Error && err.message.trim() || "Ingest failed." });
|
|
2217
|
+
throw err;
|
|
2192
2218
|
}
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2219
|
+
async function upload() {
|
|
2220
|
+
let res;
|
|
2221
|
+
try {
|
|
2222
|
+
res = await doFetch(endpoint, {
|
|
2223
|
+
method: "POST",
|
|
2224
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
|
2225
|
+
body: JSON.stringify(payload)
|
|
2226
|
+
});
|
|
2227
|
+
} catch {
|
|
2228
|
+
throw new IngestNetworkError(apiUrl);
|
|
2229
|
+
}
|
|
2230
|
+
if (!res.ok) {
|
|
2231
|
+
const detail = await safeErrorDetail2(res);
|
|
2232
|
+
if (res.status === 401) throw new IngestTokenRejectedError();
|
|
2233
|
+
throw new IngestHttpError(res.status, detail);
|
|
2234
|
+
}
|
|
2235
|
+
const body = await res.json().catch(() => ({}));
|
|
2236
|
+
return {
|
|
2237
|
+
runId: typeof body.runId === "string" ? body.runId : "",
|
|
2238
|
+
linked: typeof body.linked === "number" ? body.linked : 0,
|
|
2239
|
+
proposed: typeof body.proposed === "number" ? body.proposed : 0,
|
|
2240
|
+
skipped: typeof body.skipped === "number" ? body.skipped : 0
|
|
2241
|
+
};
|
|
2197
2242
|
}
|
|
2198
|
-
const body = await res.json().catch(() => ({}));
|
|
2199
|
-
return {
|
|
2200
|
-
runId: typeof body.runId === "string" ? body.runId : "",
|
|
2201
|
-
linked: typeof body.linked === "number" ? body.linked : 0,
|
|
2202
|
-
proposed: typeof body.proposed === "number" ? body.proposed : 0,
|
|
2203
|
-
skipped: typeof body.skipped === "number" ? body.skipped : 0
|
|
2204
|
-
};
|
|
2205
2243
|
}
|
|
2206
2244
|
function reportIngestFailure(err, write) {
|
|
2207
2245
|
if (err instanceof IngestNotLoggedInError || err instanceof IngestTokenRejectedError || err instanceof CairnRunNotFoundError || err instanceof NoCairnRunsError || err instanceof CairnRunVersionError || err instanceof CairnRunPartialError || err instanceof CairnEvidenceAmbiguousError) {
|
package/dist/reporter-core.cjs
CHANGED
|
@@ -315,6 +315,25 @@ function shardOf(env) {
|
|
|
315
315
|
error: `PLUNE_SHARD=${JSON.stringify(raw.slice(0, 40))} is not "k/N" with 1 <= k <= N <= ${SHARDS_MAX} (for example 2/4) - nothing was sent.`
|
|
316
316
|
};
|
|
317
317
|
}
|
|
318
|
+
function configurationOf(env) {
|
|
319
|
+
const raw = value(env, "PLUNE_CONFIGURATION");
|
|
320
|
+
if (raw === void 0) return {};
|
|
321
|
+
const pairs = [];
|
|
322
|
+
for (const pair of raw.split(";").map((s) => s.trim()).filter((s) => s !== "")) {
|
|
323
|
+
const at = pair.indexOf("=");
|
|
324
|
+
const name = at < 0 ? "" : pair.slice(0, at).trim();
|
|
325
|
+
const label = at < 0 ? "" : pair.slice(at + 1).trim();
|
|
326
|
+
const twice = pairs.find(([seen]) => seen.toLowerCase() === name.toLowerCase());
|
|
327
|
+
const why = at < 0 ? 'has no "="' : name === "" ? "has no group name" : label === "" ? "has no value" : twice !== void 0 ? `names the group ${JSON.stringify(twice[0])} a second time` : void 0;
|
|
328
|
+
if (why !== void 0) {
|
|
329
|
+
return {
|
|
330
|
+
error: `PLUNE_CONFIGURATION: ${JSON.stringify(pair.slice(0, 40))} ${why} - write it as "Group=Value;Group=Value" (for example "Browser=Chrome;Operating system=Windows") - nothing was sent.`
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
pairs.push([name, label]);
|
|
334
|
+
}
|
|
335
|
+
return pairs.length === 0 ? {} : { configuration: Object.fromEntries(pairs) };
|
|
336
|
+
}
|
|
318
337
|
var UPLOAD_CONCURRENCY = { min: 1, max: 32, default: 16 };
|
|
319
338
|
var UPLOAD_RETRIES = { min: 0, max: 20, default: 6 };
|
|
320
339
|
var IMPORT_TIMEOUT = { min: 0, max: 86400, default: 600 };
|
|
@@ -362,6 +381,7 @@ function readEnv(env = process.env) {
|
|
|
362
381
|
const offerDiscovered = flag(env, "PLUNE_CREATE");
|
|
363
382
|
const full = flag(env, "PLUNE_FULL_RUN");
|
|
364
383
|
const { shard, error: shardError } = shardOf(env);
|
|
384
|
+
const { configuration, error: configurationError } = configurationOf(env);
|
|
365
385
|
return {
|
|
366
386
|
config: {
|
|
367
387
|
...externalKey !== void 0 ? { externalKey } : {},
|
|
@@ -374,6 +394,7 @@ function readEnv(env = process.env) {
|
|
|
374
394
|
...title !== void 0 ? { title } : {},
|
|
375
395
|
...environment !== void 0 ? { environment } : {},
|
|
376
396
|
...labels !== void 0 ? { labels } : {},
|
|
397
|
+
...configuration !== void 0 ? { configuration } : {},
|
|
377
398
|
// Only when set: `false` here would outrank a config file that asked for it, and an unset
|
|
378
399
|
// variable is not an instruction.
|
|
379
400
|
...offerDiscovered ? { offerDiscovered } : {},
|
|
@@ -387,6 +408,7 @@ function readEnv(env = process.env) {
|
|
|
387
408
|
keepOpen: flag(env, "PLUNE_SHARED_RUN") || flag(env, "PLUNE_PROCEED") || shard !== void 0 || shardError !== void 0,
|
|
388
409
|
...shard !== void 0 ? { shard } : {},
|
|
389
410
|
...shardError !== void 0 ? { shardError } : {},
|
|
411
|
+
...configurationError !== void 0 ? { configurationError } : {},
|
|
390
412
|
...importTimeout !== void 0 ? { importTimeout } : {},
|
|
391
413
|
ignored: UNSUPPORTED_VARS.filter((name) => value(env, name) !== void 0),
|
|
392
414
|
warnings
|
|
@@ -597,6 +619,7 @@ function appendBatch(file, batch) {
|
|
|
597
619
|
externalKey: batch.externalKey,
|
|
598
620
|
...batch.session !== void 0 ? { session: batch.session } : {},
|
|
599
621
|
...batch.meta !== void 0 ? { meta: batch.meta } : {},
|
|
622
|
+
...batch.configuration !== void 0 ? { configuration: batch.configuration } : {},
|
|
600
623
|
results: batch.results
|
|
601
624
|
};
|
|
602
625
|
fs.appendFileSync(file, `${JSON.stringify(line)}
|
|
@@ -747,8 +770,10 @@ function warnIfDropped(sent, run, log) {
|
|
|
747
770
|
async function startRun(passed, declared) {
|
|
748
771
|
const env = readEnv();
|
|
749
772
|
if (env.shardError !== void 0) throw new Error(env.shardError);
|
|
773
|
+
if (env.configurationError !== void 0 && passed.configuration === void 0) throw new Error(env.configurationError);
|
|
750
774
|
const stated = Object.fromEntries(Object.entries(passed).filter(([, v]) => v !== void 0));
|
|
751
775
|
const cfg = { ...env.config, ...stated };
|
|
776
|
+
const configuration = cfg.configuration !== void 0 && Object.keys(cfg.configuration).length > 0 ? cfg.configuration : void 0;
|
|
752
777
|
const log = cfg.log ?? ((line) => void process.stderr.write(`${line}
|
|
753
778
|
`));
|
|
754
779
|
const fallbackPath = cfg.fallbackPath ?? DEFAULT_FALLBACK_PATH;
|
|
@@ -832,6 +857,7 @@ async function startRun(passed, declared) {
|
|
|
832
857
|
externalKey,
|
|
833
858
|
session: sessionMarker,
|
|
834
859
|
...cfg.meta !== void 0 ? { meta: cfg.meta } : {},
|
|
860
|
+
...configuration !== void 0 ? { configuration } : {},
|
|
835
861
|
results: results.map(onDisk)
|
|
836
862
|
});
|
|
837
863
|
stats.deferred += results.length;
|
|
@@ -856,7 +882,7 @@ async function startRun(passed, declared) {
|
|
|
856
882
|
if (!offline) {
|
|
857
883
|
if (expected !== void 0) await resolveKeys(expected.flat());
|
|
858
884
|
const stated2 = expected?.slice(0, EXPECTED_MAX) ?? [];
|
|
859
|
-
const
|
|
885
|
+
const configuration2 = stated2.length === 0 ? void 0 : {
|
|
860
886
|
expected: stated2.flatMap((keys) => {
|
|
861
887
|
const hit = keys.map((k) => resolved.get(k.value)).find((v) => typeof v === "string");
|
|
862
888
|
if (typeof hit === "string") return [{ testCaseId: hit }];
|
|
@@ -880,7 +906,7 @@ async function startRun(passed, declared) {
|
|
|
880
906
|
kind: cfg.kind ?? "automated",
|
|
881
907
|
...externalKey !== null ? { externalKey } : {},
|
|
882
908
|
...cfg.meta !== void 0 ? { meta: cfg.meta } : {},
|
|
883
|
-
...
|
|
909
|
+
...configuration2 !== void 0 ? { configuration: configuration2 } : {},
|
|
884
910
|
// Named even when nobody named it — the list has to read without opening a row. Only what
|
|
885
911
|
// the caller DESCRIBED goes to `warnIfDropped` below: a title this side invented is not one
|
|
886
912
|
// a person will miss, and a deployment older than the field must not be nagged on every run.
|
|
@@ -888,7 +914,7 @@ async function startRun(passed, declared) {
|
|
|
888
914
|
...described
|
|
889
915
|
};
|
|
890
916
|
let start = await client.post("/v1/runs", request);
|
|
891
|
-
if (!start.ok && start.status === 413 &&
|
|
917
|
+
if (!start.ok && start.status === 413 && configuration2 !== void 0) {
|
|
892
918
|
log(
|
|
893
919
|
`plune: the platform refused the run's list of expected tests as too large (${start.detail || start.kind}) \u2014 starting without it: the results still land, but this run counts no tests that did not run, and claims no full run.`
|
|
894
920
|
);
|
|
@@ -1434,7 +1460,8 @@ async function startRun(passed, declared) {
|
|
|
1434
1460
|
}
|
|
1435
1461
|
const out = await client.post(
|
|
1436
1462
|
`/v1/runs/${runId}/results`,
|
|
1437
|
-
|
|
1463
|
+
// The job's combination on every batch (plune-ai/plune#1251); a refusal of it is a 422 said below as it came.
|
|
1464
|
+
{ results: part, ...configuration !== void 0 ? { configuration } : {} }
|
|
1438
1465
|
);
|
|
1439
1466
|
const counts = out.ok ? out.body?.counts : void 0;
|
|
1440
1467
|
const told = COUNTED.filter((k) => typeof counts?.[k] === "number");
|
package/dist/reporter-core.d.cts
CHANGED
|
@@ -324,6 +324,12 @@ interface ReporterConfig {
|
|
|
324
324
|
environment?: string;
|
|
325
325
|
/** How the run is marked — smoke, nightly, a team. */
|
|
326
326
|
labels?: string[];
|
|
327
|
+
/**
|
|
328
|
+
* The combination this job runs, group name → value label (`PLUNE_CONFIGURATION`, plune-ai/plune#1251):
|
|
329
|
+
* sent as `configuration` on every results batch. Not the run's own `configuration` (what it expects).
|
|
330
|
+
* Stated, it is all there is: the variable is not read, and `{}` sends none (what a replay passes for a line that recorded none).
|
|
331
|
+
*/
|
|
332
|
+
configuration?: Record<string, string>;
|
|
327
333
|
/** Results per request. The platform's ceiling is 500. */
|
|
328
334
|
batchSize?: number;
|
|
329
335
|
/** Files in flight at once (`PLUNE_UPLOAD_CONCURRENCY`, 1 to 32, default 16). */
|
|
@@ -445,6 +451,8 @@ interface EnvSettings {
|
|
|
445
451
|
shard?: ShardOf;
|
|
446
452
|
/** `PLUNE_SHARD` was set to something that is not `k/N`. A configuration error: the caller refuses to start. */
|
|
447
453
|
shardError?: string;
|
|
454
|
+
/** `PLUNE_CONFIGURATION` was set to something that is not `Group=Value;…`. Refused the same way. */
|
|
455
|
+
configurationError?: string;
|
|
448
456
|
/** `PLUNE_IMPORT_TIMEOUT` in seconds, when set and usable. */
|
|
449
457
|
importTimeout?: number;
|
|
450
458
|
/** Variables that were set and have nowhere to land. The caller says so once. */
|
|
@@ -498,12 +506,17 @@ declare const DEFAULT_FALLBACK_PATH: string;
|
|
|
498
506
|
* A run of 24 tests at a batch of 10 is three lines, and replay opens one run for the lines that share a
|
|
499
507
|
* marker rather than one for each. A line written before the field existed has none, and is one run's
|
|
500
508
|
* worth by itself — nothing in it says otherwise.
|
|
509
|
+
*
|
|
510
|
+
* `configuration` is the combination the job named (`PLUNE_CONFIGURATION`, plune-ai/plune#1251), recorded for
|
|
511
|
+
* the same reason as `meta`: replay sends the batch into the combination it ran in, whatever the machine that
|
|
512
|
+
* replays it has set. A line without it was written by a job that named none, or before the field existed.
|
|
501
513
|
*/
|
|
502
514
|
interface DeferredBatch {
|
|
503
515
|
runId: string | null;
|
|
504
516
|
externalKey: string | null;
|
|
505
517
|
session?: string;
|
|
506
518
|
meta?: RunMeta;
|
|
519
|
+
configuration?: Record<string, string>;
|
|
507
520
|
results: DeferredResult[];
|
|
508
521
|
}
|
|
509
522
|
/**
|