@plune-ai/cli 0.30.2 → 0.32.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 +71 -10
- package/dist/cli.cjs +279 -175
- package/dist/index.cjs +90 -66
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +90 -66
- 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,58 +1855,23 @@ 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/
|
|
1859
|
-
var NotLoggedInError = class extends Error {
|
|
1860
|
-
constructor() {
|
|
1861
|
-
super('Not logged in. Run "plune login" first, or set PLUNE_TOKEN.');
|
|
1862
|
-
this.name = "NotLoggedInError";
|
|
1863
|
-
}
|
|
1864
|
-
};
|
|
1865
|
-
var TokenRejectedError = class extends Error {
|
|
1866
|
-
constructor() {
|
|
1867
|
-
super(
|
|
1868
|
-
'Your API token was rejected (revoked or invalid). Run "plune login" with a fresh token.'
|
|
1869
|
-
);
|
|
1870
|
-
this.name = "TokenRejectedError";
|
|
1871
|
-
}
|
|
1872
|
-
};
|
|
1873
|
-
var SyncFileError = class extends Error {
|
|
1874
|
-
constructor(message) {
|
|
1875
|
-
super(message);
|
|
1876
|
-
this.name = "SyncFileError";
|
|
1877
|
-
}
|
|
1878
|
-
};
|
|
1879
|
-
var SyncNetworkError = class extends Error {
|
|
1880
|
-
constructor(apiUrl) {
|
|
1881
|
-
super(
|
|
1882
|
-
`Could not reach the Plune API at ${apiUrl}. Check your connection (or PLUNE_API_URL) and try again.`
|
|
1883
|
-
);
|
|
1884
|
-
this.name = "SyncNetworkError";
|
|
1885
|
-
}
|
|
1886
|
-
};
|
|
1887
|
-
var SyncHttpError = class extends Error {
|
|
1888
|
-
constructor(status, detail) {
|
|
1889
|
-
super(`Sync failed \u2014 the server returned HTTP ${status}${detail ? ` (${detail})` : ""}.`);
|
|
1890
|
-
this.status = status;
|
|
1891
|
-
this.name = "SyncHttpError";
|
|
1892
|
-
}
|
|
1893
|
-
status;
|
|
1894
|
-
};
|
|
1895
|
-
var JOB_KIND = "sync";
|
|
1858
|
+
// src/cli/commands/job-reporter.ts
|
|
1896
1859
|
var JOB_TIMEOUT_MS = 5e3;
|
|
1897
1860
|
var JOB_SUBJECT_NAME_MAX = 200;
|
|
1898
1861
|
var JOB_REASON_MAX = 2e3;
|
|
1899
1862
|
var JobReporter = class {
|
|
1900
|
-
constructor(doFetch, base, token) {
|
|
1863
|
+
constructor(doFetch, base, token, kind) {
|
|
1901
1864
|
this.doFetch = doFetch;
|
|
1902
1865
|
this.base = base;
|
|
1903
1866
|
this.token = token;
|
|
1867
|
+
this.kind = kind;
|
|
1904
1868
|
}
|
|
1905
1869
|
doFetch;
|
|
1906
1870
|
base;
|
|
1907
1871
|
token;
|
|
1872
|
+
kind;
|
|
1908
1873
|
id;
|
|
1909
|
-
/** The
|
|
1874
|
+
/** The upload's rows, the job's `total`; `null` while there are none to count. */
|
|
1910
1875
|
size = null;
|
|
1911
1876
|
/** The first report the platform did not take, for the CLI to say once. */
|
|
1912
1877
|
problem;
|
|
@@ -1931,19 +1896,23 @@ var JobReporter = class {
|
|
|
1931
1896
|
this.size = Number.isInteger(rows) && rows > 0 ? rows : null;
|
|
1932
1897
|
const name = subjectName.trim().slice(0, JOB_SUBJECT_NAME_MAX);
|
|
1933
1898
|
const body = await this.send("POST", "", {
|
|
1934
|
-
kind:
|
|
1899
|
+
kind: this.kind,
|
|
1935
1900
|
...name !== "" ? { subjectName: name } : {},
|
|
1936
1901
|
...this.size !== null ? { progress: { done: 0, total: this.size } } : {}
|
|
1937
1902
|
});
|
|
1938
1903
|
if (typeof body?.id === "string") this.id = body.id;
|
|
1939
1904
|
}
|
|
1940
|
-
/**
|
|
1941
|
-
|
|
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) {
|
|
1942
1910
|
if (this.id === void 0) return;
|
|
1943
1911
|
await this.send("PATCH", `/${encodeURIComponent(this.id)}/progress`, {
|
|
1944
1912
|
done: this.size,
|
|
1945
1913
|
total: this.size,
|
|
1946
|
-
...cursor !== void 0 ? { cursor } : {}
|
|
1914
|
+
...at.cursor !== void 0 ? { cursor: at.cursor } : {},
|
|
1915
|
+
...at.subjectId !== void 0 ? { subjectId: at.subjectId } : {}
|
|
1947
1916
|
});
|
|
1948
1917
|
}
|
|
1949
1918
|
async finish(outcome) {
|
|
@@ -1957,6 +1926,44 @@ var JobReporter = class {
|
|
|
1957
1926
|
);
|
|
1958
1927
|
}
|
|
1959
1928
|
};
|
|
1929
|
+
|
|
1930
|
+
// src/cli/commands/sync.ts
|
|
1931
|
+
var NotLoggedInError = class extends Error {
|
|
1932
|
+
constructor() {
|
|
1933
|
+
super('Not logged in. Run "plune login" first, or set PLUNE_TOKEN.');
|
|
1934
|
+
this.name = "NotLoggedInError";
|
|
1935
|
+
}
|
|
1936
|
+
};
|
|
1937
|
+
var TokenRejectedError = class extends Error {
|
|
1938
|
+
constructor() {
|
|
1939
|
+
super(
|
|
1940
|
+
'Your API token was rejected (revoked or invalid). Run "plune login" with a fresh token.'
|
|
1941
|
+
);
|
|
1942
|
+
this.name = "TokenRejectedError";
|
|
1943
|
+
}
|
|
1944
|
+
};
|
|
1945
|
+
var SyncFileError = class extends Error {
|
|
1946
|
+
constructor(message) {
|
|
1947
|
+
super(message);
|
|
1948
|
+
this.name = "SyncFileError";
|
|
1949
|
+
}
|
|
1950
|
+
};
|
|
1951
|
+
var SyncNetworkError = class extends Error {
|
|
1952
|
+
constructor(apiUrl) {
|
|
1953
|
+
super(
|
|
1954
|
+
`Could not reach the Plune API at ${apiUrl}. Check your connection (or PLUNE_API_URL) and try again.`
|
|
1955
|
+
);
|
|
1956
|
+
this.name = "SyncNetworkError";
|
|
1957
|
+
}
|
|
1958
|
+
};
|
|
1959
|
+
var SyncHttpError = class extends Error {
|
|
1960
|
+
constructor(status, detail) {
|
|
1961
|
+
super(`Sync failed \u2014 the server returned HTTP ${status}${detail ? ` (${detail})` : ""}.`);
|
|
1962
|
+
this.status = status;
|
|
1963
|
+
this.name = "SyncHttpError";
|
|
1964
|
+
}
|
|
1965
|
+
status;
|
|
1966
|
+
};
|
|
1960
1967
|
function readRun(cwd, file) {
|
|
1961
1968
|
const target = file !== void 0 ? path5.resolve(cwd, file) : path5.join(cwd, ".plune", "last-run.json");
|
|
1962
1969
|
let raw;
|
|
@@ -1998,7 +2005,7 @@ async function handleSync(deps = {}) {
|
|
|
1998
2005
|
const payload = JSON.stringify(run);
|
|
1999
2006
|
const idempotencyKey = (0, import_node_crypto2.createHash)("sha256").update(payload).digest("hex");
|
|
2000
2007
|
const bearer = token;
|
|
2001
|
-
const job = new JobReporter(doFetch, apiUrl, bearer);
|
|
2008
|
+
const job = new JobReporter(doFetch, apiUrl, bearer, "sync");
|
|
2002
2009
|
await job.open(path5.basename(deps.file !== void 0 ? path5.resolve(cwd, deps.file) : cwd), run.summary.total);
|
|
2003
2010
|
try {
|
|
2004
2011
|
const result = await upload();
|
|
@@ -2032,7 +2039,7 @@ async function handleSync(deps = {}) {
|
|
|
2032
2039
|
const body = await res.json().catch(() => ({}));
|
|
2033
2040
|
const id = typeof body.id === "string" ? body.id : "";
|
|
2034
2041
|
const result = { id, url: `${endpoint}/${id}` };
|
|
2035
|
-
await job.uploaded(id || void 0);
|
|
2042
|
+
await job.uploaded({ cursor: id || void 0 });
|
|
2036
2043
|
if (deps.failOnFailures === true) {
|
|
2037
2044
|
result.gate = gateExit(await readVerdict(doFetch, `${endpoint}/${id}/summary`, bearer));
|
|
2038
2045
|
}
|
|
@@ -2060,6 +2067,9 @@ function reportSyncFailure(err, write) {
|
|
|
2060
2067
|
return null;
|
|
2061
2068
|
}
|
|
2062
2069
|
|
|
2070
|
+
// src/cli/commands/ingest.ts
|
|
2071
|
+
var path7 = __toESM(require("path"), 1);
|
|
2072
|
+
|
|
2063
2073
|
// src/cli/commands/cairn-artifact.ts
|
|
2064
2074
|
var fs7 = __toESM(require("fs/promises"), 1);
|
|
2065
2075
|
var path6 = __toESM(require("path"), 1);
|
|
@@ -2254,28 +2264,42 @@ async function handleIngest(deps) {
|
|
|
2254
2264
|
const payload = await readCairnRun(dir);
|
|
2255
2265
|
const doFetch = deps.fetchImpl ?? fetch;
|
|
2256
2266
|
const endpoint = `${apiUrl}/v1/ingest/cairn`;
|
|
2257
|
-
|
|
2267
|
+
const job = new JobReporter(doFetch, apiUrl, token, "ingest");
|
|
2268
|
+
await job.open(path7.basename(path7.resolve(dir)), payload.cases.length);
|
|
2258
2269
|
try {
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
} catch {
|
|
2265
|
-
|
|
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;
|
|
2266
2278
|
}
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
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
|
+
};
|
|
2271
2302
|
}
|
|
2272
|
-
const body = await res.json().catch(() => ({}));
|
|
2273
|
-
return {
|
|
2274
|
-
runId: typeof body.runId === "string" ? body.runId : "",
|
|
2275
|
-
linked: typeof body.linked === "number" ? body.linked : 0,
|
|
2276
|
-
proposed: typeof body.proposed === "number" ? body.proposed : 0,
|
|
2277
|
-
skipped: typeof body.skipped === "number" ? body.skipped : 0
|
|
2278
|
-
};
|
|
2279
2303
|
}
|
|
2280
2304
|
function reportIngestFailure(err, write) {
|
|
2281
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
|
@@ -690,6 +690,8 @@ interface IngestResult {
|
|
|
690
690
|
linked: number;
|
|
691
691
|
proposed: number;
|
|
692
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;
|
|
693
695
|
}
|
|
694
696
|
declare function handleIngest(deps: IngestDeps): Promise<IngestResult>;
|
|
695
697
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -690,6 +690,8 @@ interface IngestResult {
|
|
|
690
690
|
linked: number;
|
|
691
691
|
proposed: number;
|
|
692
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;
|
|
693
695
|
}
|
|
694
696
|
declare function handleIngest(deps: IngestDeps): Promise<IngestResult>;
|
|
695
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,58 +1795,23 @@ 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/
|
|
1799
|
-
var NotLoggedInError = class extends Error {
|
|
1800
|
-
constructor() {
|
|
1801
|
-
super('Not logged in. Run "plune login" first, or set PLUNE_TOKEN.');
|
|
1802
|
-
this.name = "NotLoggedInError";
|
|
1803
|
-
}
|
|
1804
|
-
};
|
|
1805
|
-
var TokenRejectedError = class extends Error {
|
|
1806
|
-
constructor() {
|
|
1807
|
-
super(
|
|
1808
|
-
'Your API token was rejected (revoked or invalid). Run "plune login" with a fresh token.'
|
|
1809
|
-
);
|
|
1810
|
-
this.name = "TokenRejectedError";
|
|
1811
|
-
}
|
|
1812
|
-
};
|
|
1813
|
-
var SyncFileError = class extends Error {
|
|
1814
|
-
constructor(message) {
|
|
1815
|
-
super(message);
|
|
1816
|
-
this.name = "SyncFileError";
|
|
1817
|
-
}
|
|
1818
|
-
};
|
|
1819
|
-
var SyncNetworkError = class extends Error {
|
|
1820
|
-
constructor(apiUrl) {
|
|
1821
|
-
super(
|
|
1822
|
-
`Could not reach the Plune API at ${apiUrl}. Check your connection (or PLUNE_API_URL) and try again.`
|
|
1823
|
-
);
|
|
1824
|
-
this.name = "SyncNetworkError";
|
|
1825
|
-
}
|
|
1826
|
-
};
|
|
1827
|
-
var SyncHttpError = class extends Error {
|
|
1828
|
-
constructor(status, detail) {
|
|
1829
|
-
super(`Sync failed \u2014 the server returned HTTP ${status}${detail ? ` (${detail})` : ""}.`);
|
|
1830
|
-
this.status = status;
|
|
1831
|
-
this.name = "SyncHttpError";
|
|
1832
|
-
}
|
|
1833
|
-
status;
|
|
1834
|
-
};
|
|
1835
|
-
var JOB_KIND = "sync";
|
|
1798
|
+
// src/cli/commands/job-reporter.ts
|
|
1836
1799
|
var JOB_TIMEOUT_MS = 5e3;
|
|
1837
1800
|
var JOB_SUBJECT_NAME_MAX = 200;
|
|
1838
1801
|
var JOB_REASON_MAX = 2e3;
|
|
1839
1802
|
var JobReporter = class {
|
|
1840
|
-
constructor(doFetch, base, token) {
|
|
1803
|
+
constructor(doFetch, base, token, kind) {
|
|
1841
1804
|
this.doFetch = doFetch;
|
|
1842
1805
|
this.base = base;
|
|
1843
1806
|
this.token = token;
|
|
1807
|
+
this.kind = kind;
|
|
1844
1808
|
}
|
|
1845
1809
|
doFetch;
|
|
1846
1810
|
base;
|
|
1847
1811
|
token;
|
|
1812
|
+
kind;
|
|
1848
1813
|
id;
|
|
1849
|
-
/** The
|
|
1814
|
+
/** The upload's rows, the job's `total`; `null` while there are none to count. */
|
|
1850
1815
|
size = null;
|
|
1851
1816
|
/** The first report the platform did not take, for the CLI to say once. */
|
|
1852
1817
|
problem;
|
|
@@ -1871,19 +1836,23 @@ var JobReporter = class {
|
|
|
1871
1836
|
this.size = Number.isInteger(rows) && rows > 0 ? rows : null;
|
|
1872
1837
|
const name = subjectName.trim().slice(0, JOB_SUBJECT_NAME_MAX);
|
|
1873
1838
|
const body = await this.send("POST", "", {
|
|
1874
|
-
kind:
|
|
1839
|
+
kind: this.kind,
|
|
1875
1840
|
...name !== "" ? { subjectName: name } : {},
|
|
1876
1841
|
...this.size !== null ? { progress: { done: 0, total: this.size } } : {}
|
|
1877
1842
|
});
|
|
1878
1843
|
if (typeof body?.id === "string") this.id = body.id;
|
|
1879
1844
|
}
|
|
1880
|
-
/**
|
|
1881
|
-
|
|
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) {
|
|
1882
1850
|
if (this.id === void 0) return;
|
|
1883
1851
|
await this.send("PATCH", `/${encodeURIComponent(this.id)}/progress`, {
|
|
1884
1852
|
done: this.size,
|
|
1885
1853
|
total: this.size,
|
|
1886
|
-
...cursor !== void 0 ? { cursor } : {}
|
|
1854
|
+
...at.cursor !== void 0 ? { cursor: at.cursor } : {},
|
|
1855
|
+
...at.subjectId !== void 0 ? { subjectId: at.subjectId } : {}
|
|
1887
1856
|
});
|
|
1888
1857
|
}
|
|
1889
1858
|
async finish(outcome) {
|
|
@@ -1897,6 +1866,44 @@ var JobReporter = class {
|
|
|
1897
1866
|
);
|
|
1898
1867
|
}
|
|
1899
1868
|
};
|
|
1869
|
+
|
|
1870
|
+
// src/cli/commands/sync.ts
|
|
1871
|
+
var NotLoggedInError = class extends Error {
|
|
1872
|
+
constructor() {
|
|
1873
|
+
super('Not logged in. Run "plune login" first, or set PLUNE_TOKEN.');
|
|
1874
|
+
this.name = "NotLoggedInError";
|
|
1875
|
+
}
|
|
1876
|
+
};
|
|
1877
|
+
var TokenRejectedError = class extends Error {
|
|
1878
|
+
constructor() {
|
|
1879
|
+
super(
|
|
1880
|
+
'Your API token was rejected (revoked or invalid). Run "plune login" with a fresh token.'
|
|
1881
|
+
);
|
|
1882
|
+
this.name = "TokenRejectedError";
|
|
1883
|
+
}
|
|
1884
|
+
};
|
|
1885
|
+
var SyncFileError = class extends Error {
|
|
1886
|
+
constructor(message) {
|
|
1887
|
+
super(message);
|
|
1888
|
+
this.name = "SyncFileError";
|
|
1889
|
+
}
|
|
1890
|
+
};
|
|
1891
|
+
var SyncNetworkError = class extends Error {
|
|
1892
|
+
constructor(apiUrl) {
|
|
1893
|
+
super(
|
|
1894
|
+
`Could not reach the Plune API at ${apiUrl}. Check your connection (or PLUNE_API_URL) and try again.`
|
|
1895
|
+
);
|
|
1896
|
+
this.name = "SyncNetworkError";
|
|
1897
|
+
}
|
|
1898
|
+
};
|
|
1899
|
+
var SyncHttpError = class extends Error {
|
|
1900
|
+
constructor(status, detail) {
|
|
1901
|
+
super(`Sync failed \u2014 the server returned HTTP ${status}${detail ? ` (${detail})` : ""}.`);
|
|
1902
|
+
this.status = status;
|
|
1903
|
+
this.name = "SyncHttpError";
|
|
1904
|
+
}
|
|
1905
|
+
status;
|
|
1906
|
+
};
|
|
1900
1907
|
function readRun(cwd, file) {
|
|
1901
1908
|
const target = file !== void 0 ? path5.resolve(cwd, file) : path5.join(cwd, ".plune", "last-run.json");
|
|
1902
1909
|
let raw;
|
|
@@ -1938,7 +1945,7 @@ async function handleSync(deps = {}) {
|
|
|
1938
1945
|
const payload = JSON.stringify(run);
|
|
1939
1946
|
const idempotencyKey = createHash2("sha256").update(payload).digest("hex");
|
|
1940
1947
|
const bearer = token;
|
|
1941
|
-
const job = new JobReporter(doFetch, apiUrl, bearer);
|
|
1948
|
+
const job = new JobReporter(doFetch, apiUrl, bearer, "sync");
|
|
1942
1949
|
await job.open(path5.basename(deps.file !== void 0 ? path5.resolve(cwd, deps.file) : cwd), run.summary.total);
|
|
1943
1950
|
try {
|
|
1944
1951
|
const result = await upload();
|
|
@@ -1972,7 +1979,7 @@ async function handleSync(deps = {}) {
|
|
|
1972
1979
|
const body = await res.json().catch(() => ({}));
|
|
1973
1980
|
const id = typeof body.id === "string" ? body.id : "";
|
|
1974
1981
|
const result = { id, url: `${endpoint}/${id}` };
|
|
1975
|
-
await job.uploaded(id || void 0);
|
|
1982
|
+
await job.uploaded({ cursor: id || void 0 });
|
|
1976
1983
|
if (deps.failOnFailures === true) {
|
|
1977
1984
|
result.gate = gateExit(await readVerdict(doFetch, `${endpoint}/${id}/summary`, bearer));
|
|
1978
1985
|
}
|
|
@@ -2000,6 +2007,9 @@ function reportSyncFailure(err, write) {
|
|
|
2000
2007
|
return null;
|
|
2001
2008
|
}
|
|
2002
2009
|
|
|
2010
|
+
// src/cli/commands/ingest.ts
|
|
2011
|
+
import * as path7 from "path";
|
|
2012
|
+
|
|
2003
2013
|
// src/cli/commands/cairn-artifact.ts
|
|
2004
2014
|
import * as fs7 from "fs/promises";
|
|
2005
2015
|
import * as path6 from "path";
|
|
@@ -2194,28 +2204,42 @@ async function handleIngest(deps) {
|
|
|
2194
2204
|
const payload = await readCairnRun(dir);
|
|
2195
2205
|
const doFetch = deps.fetchImpl ?? fetch;
|
|
2196
2206
|
const endpoint = `${apiUrl}/v1/ingest/cairn`;
|
|
2197
|
-
|
|
2207
|
+
const job = new JobReporter(doFetch, apiUrl, token, "ingest");
|
|
2208
|
+
await job.open(path7.basename(path7.resolve(dir)), payload.cases.length);
|
|
2198
2209
|
try {
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
} catch {
|
|
2205
|
-
|
|
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;
|
|
2206
2218
|
}
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
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
|
+
};
|
|
2211
2242
|
}
|
|
2212
|
-
const body = await res.json().catch(() => ({}));
|
|
2213
|
-
return {
|
|
2214
|
-
runId: typeof body.runId === "string" ? body.runId : "",
|
|
2215
|
-
linked: typeof body.linked === "number" ? body.linked : 0,
|
|
2216
|
-
proposed: typeof body.proposed === "number" ? body.proposed : 0,
|
|
2217
|
-
skipped: typeof body.skipped === "number" ? body.skipped : 0
|
|
2218
|
-
};
|
|
2219
2243
|
}
|
|
2220
2244
|
function reportIngestFailure(err, write) {
|
|
2221
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
|
/**
|