@crvouga/atlas 0.13.0 → 0.14.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/CHANGELOG.md +6 -0
- package/dist/playwright-reporter/src/index.d.ts +11 -0
- package/dist/playwright-reporter/src/index.js +4 -2
- package/dist/reports/src/evidence-types.d.ts +3 -0
- package/dist/reports/src/layer-merge.d.ts +1 -1
- package/dist/reports/src/layer-merge.js +3 -2
- package/dist/reports/src/layer-types.d.ts +3 -0
- package/dist/reports/src/publish-checks.js +1 -0
- package/dist/reports/src/report-cli.d.ts +1 -1
- package/dist/reports/src/report-cli.js +30 -2
- package/dist/reports/src/reporter.d.ts +1 -1
- package/dist/reports/src/test-catalog.d.ts +20 -4
- package/dist/reports/src/test-catalog.js +31 -8
- package/dist/schema/src/api.d.ts +49 -1
- package/dist/schema/src/api.js +7 -3
- package/dist/schema/src/layer.d.ts +26 -0
- package/dist/schema/src/layer.js +17 -0
- package/docs/integrating.md +8 -0
- package/docs/reference/cli.md +7 -0
- package/docs/reference/playwright-reporter.md +6 -6
- package/docs/reference/reports.md +18 -6
- package/docs/reference/schema.md +20 -1
- package/docs/report-contract.md +1 -0
- package/llms-full.txt +16 -0
- package/package.json +1 -1
- package/packages/schema/schemas/effective-delta.schema.json +30 -0
- package/packages/schema/schemas/effective-evidence.schema.json +30 -0
- package/packages/schema/schemas/evidence-page.schema.json +30 -0
- package/packages/schema/schemas/layer.schema.json +30 -0
- package/packages/schema/schemas/publish-checks.schema.json +30 -0
- package/packages/schema/schemas/report-run.schema.json +17 -1
- package/packages/schema/schemas/report-runs.schema.json +17 -1
- package/packages/server/openapi.json +154 -2
- package/packages/visualizer/src/explorer/RunsView.tsx +23 -4
- package/packages/visualizer/src/explorer/parts.tsx +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog: @crvouga/atlas
|
|
2
2
|
|
|
3
|
+
## 0.14.0 (2026-10-28)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- identify local demo executions separately from branch and commit ([543bf9f](https://github.com/crvouga/atlas/commit/543bf9f00bacb3deb5d32d78c3b61437e04497df))
|
|
8
|
+
|
|
3
9
|
## 0.13.0 (2026-10-28)
|
|
4
10
|
|
|
5
11
|
### Features
|
|
@@ -35,6 +35,17 @@ export type AtlasPlaywrightReporterOptions = {
|
|
|
35
35
|
readonly branch?: string;
|
|
36
36
|
/** Where to read the CI run. Default the GitHub Actions run (or the CI's own run URL variable). */
|
|
37
37
|
readonly runUrl?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Names this execution, for a local or manual run with no CI run URL: every shard and project that gets the same `id`
|
|
40
|
+
* is one run, and the next invocation gets another. Default: `ATLAS_RUN_ID` (and `ATLAS_RUN_TITLE`) when set. Only
|
|
41
|
+
* groups and labels runs; it never changes what a check is or which result wins.
|
|
42
|
+
*/
|
|
43
|
+
readonly run?: {
|
|
44
|
+
readonly id: string;
|
|
45
|
+
readonly title?: string;
|
|
46
|
+
readonly startedAtMs?: number;
|
|
47
|
+
readonly endedAtMs?: number;
|
|
48
|
+
};
|
|
38
49
|
/** The spec a test belongs to, relative to the repository. Default: the `.feature` of a playwright-bdd test, else the test file. */
|
|
39
50
|
readonly specPath?: (test: TestCase) => string | undefined;
|
|
40
51
|
/** Paths are relative to this directory. Default the git root above Playwright's `rootDir`, else `rootDir`. */
|
|
@@ -123,15 +123,16 @@ export default class AtlasPlaywrightReporter {
|
|
|
123
123
|
}
|
|
124
124
|
if (!this.store)
|
|
125
125
|
return;
|
|
126
|
+
const run = options.run ?? (env.ATLAS_RUN_ID ? { id: env.ATLAS_RUN_ID, ...(env.ATLAS_RUN_TITLE ? { title: env.ATLAS_RUN_TITLE } : {}) } : undefined);
|
|
126
127
|
try {
|
|
127
128
|
const explicitSha = options.sha ?? env.ATLAS_SHA;
|
|
128
129
|
const origin = detectOrigin(env);
|
|
129
130
|
const sha = explicitSha ?? origin.sha;
|
|
130
131
|
const freshness = detectFreshness({ sha, ci: origin.ci && !explicitSha });
|
|
131
|
-
this.identity = { sha, branch: options.branch ?? env.ATLAS_BRANCH ?? origin.branch, runUrl: options.runUrl ?? origin.runUrl, ...(freshness ? { freshness } : {}) };
|
|
132
|
+
this.identity = { sha, branch: options.branch ?? env.ATLAS_BRANCH ?? origin.branch, runUrl: options.runUrl ?? origin.runUrl, ...(run ? { run } : {}), ...(freshness ? { freshness } : {}) };
|
|
132
133
|
}
|
|
133
134
|
catch {
|
|
134
|
-
this.identity = { sha: options.sha ?? env.ATLAS_SHA ?? null, branch: options.branch ?? env.ATLAS_BRANCH ?? null, runUrl: options.runUrl ?? null, freshness: 'unknown' };
|
|
135
|
+
this.identity = { sha: options.sha ?? env.ATLAS_SHA ?? null, branch: options.branch ?? env.ATLAS_BRANCH ?? null, runUrl: options.runUrl ?? null, ...(run ? { run } : {}), freshness: 'unknown' };
|
|
135
136
|
}
|
|
136
137
|
}
|
|
137
138
|
specPathOf(test) {
|
|
@@ -195,6 +196,7 @@ export default class AtlasPlaywrightReporter {
|
|
|
195
196
|
sha: this.identity.sha,
|
|
196
197
|
branch: this.identity.branch,
|
|
197
198
|
runUrl: this.identity.runUrl,
|
|
199
|
+
...(this.identity.run ? { run: this.identity.run } : {}),
|
|
198
200
|
...(this.identity.freshness ? { freshness: this.identity.freshness } : {}),
|
|
199
201
|
environment,
|
|
200
202
|
parameters: { ...parameters },
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* @see docs/report-contract.md
|
|
10
10
|
*/
|
|
11
11
|
import type { EvidenceMeasurement, EvidenceStatus } from './explorer-types.js';
|
|
12
|
+
import type { LayerRun } from '../../schema/src/layer.js';
|
|
12
13
|
import type { LayerParameters } from './layer-types.js';
|
|
13
14
|
/** Where a recorded test's timeline and captured files are read from. */
|
|
14
15
|
export type RecordingAddress = {
|
|
@@ -77,6 +78,8 @@ export type LayerHeader = {
|
|
|
77
78
|
readonly parameters: LayerParameters;
|
|
78
79
|
/** `modified`: recorded from a checkout with uncommitted changes; `unknown`: origin not captured; absent: clean. Advisory; never changes a status. */
|
|
79
80
|
readonly freshness?: 'unknown' | 'modified';
|
|
81
|
+
/** The execution the producer named for it (one local or manual invocation), or absent. Grouping and display only; never part of the slot. */
|
|
82
|
+
readonly run?: LayerRun;
|
|
80
83
|
/** Where this layer's recorded timelines and captured files are read from, or null when it has none. */
|
|
81
84
|
readonly recording: RecordingAddress | null;
|
|
82
85
|
/** From a spec run that has not been collected into a layer yet. */
|
|
@@ -129,7 +129,7 @@ export declare function distinctEvidence(layers: readonly LayerEvidence[]): Laye
|
|
|
129
129
|
/** Drops each mutable layer whose execution is already archived as an immutable one, and keeps one summary per id. */
|
|
130
130
|
export declare function distinctLayers(layers: readonly LayerSummary[]): LayerSummary[];
|
|
131
131
|
/**
|
|
132
|
-
* A string equal for the mutable and the archived address of one execution: its producer, sha, time and
|
|
132
|
+
* A string equal for the mutable and the archived address of one execution: its producer, sha, time, parts and, when the producer named one, its execution id.
|
|
133
133
|
* Object key order is ignored at every depth, including check evidence and extensions; array order is preserved.
|
|
134
134
|
* This is an advisory execution identity, not the content digest that addresses an immutable layer in storage.
|
|
135
135
|
*/
|
|
@@ -97,6 +97,7 @@ export function layerEvidence(summary, backend) {
|
|
|
97
97
|
const layerSummaryEvidence = summarizeEvidence(summary, ...summary.parts);
|
|
98
98
|
const layer = {
|
|
99
99
|
...(summary.freshness ? { freshness: summary.freshness } : {}),
|
|
100
|
+
...(summary.run ? { run: summary.run } : {}),
|
|
100
101
|
id: summary.id,
|
|
101
102
|
backend,
|
|
102
103
|
producer: summary.producer,
|
|
@@ -270,12 +271,12 @@ export function distinctLayers(layers) {
|
|
|
270
271
|
return [...new Map(layers.filter(layer => archived(layer.id) || !kept.has(layerExecutionKey(layer))).map(layer => [layer.id, layer])).values()];
|
|
271
272
|
}
|
|
272
273
|
/**
|
|
273
|
-
* A string equal for the mutable and the archived address of one execution: its producer, sha, time and
|
|
274
|
+
* A string equal for the mutable and the archived address of one execution: its producer, sha, time, parts and, when the producer named one, its execution id.
|
|
274
275
|
* Object key order is ignored at every depth, including check evidence and extensions; array order is preserved.
|
|
275
276
|
* This is an advisory execution identity, not the content digest that addresses an immutable layer in storage.
|
|
276
277
|
*/
|
|
277
278
|
export function layerExecutionKey(layer) {
|
|
278
|
-
return JSON.stringify([layer.producer, layer.sha, layer.recordedAtMs, orderedValue(layer.parts)]);
|
|
279
|
+
return JSON.stringify([layer.producer, layer.sha, layer.recordedAtMs, orderedValue(layer.parts), ...(layer.run ? [layer.run.id] : [])]);
|
|
279
280
|
}
|
|
280
281
|
/** Sort JSON objects recursively without changing array order or interpreting extension keys as prototypes. */
|
|
281
282
|
function orderedValue(value) {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @see docs/report-contract.md#the-layer-document
|
|
10
10
|
*/
|
|
11
|
+
import type { LayerRun } from '../../schema/src/layer.js';
|
|
11
12
|
import type { BundleIndex } from './bundle.js';
|
|
12
13
|
import type { ReportPart, SpecEntry } from './explorer-types.js';
|
|
13
14
|
import type { SourceRecord } from './protocol.js';
|
|
@@ -33,6 +34,8 @@ export type LayerSummary = LayerExtras & {
|
|
|
33
34
|
readonly environment?: string | null;
|
|
34
35
|
/** Provenance: the CI run that produced the layer, or `null`. Never part of the slot. */
|
|
35
36
|
readonly runUrl: string | null;
|
|
37
|
+
/** The execution (one invocation of a local or manual run) the layer belongs to, when the producer named one. Grouping and display only; never part of the slot. Absent: the run is told by `runUrl`, else `branch@sha`. */
|
|
38
|
+
readonly run?: LayerRun;
|
|
36
39
|
/** Free choices that make a result its own slot. The `environment` parameter is read as the environment (see `layerPlace`). */
|
|
37
40
|
readonly parameters: LayerParameters;
|
|
38
41
|
/** The specs the layer was recorded against, with fingerprints of their text. */
|
|
@@ -103,6 +103,7 @@ export async function publishChecks(store, input, options = {}) {
|
|
|
103
103
|
sha: args.sha,
|
|
104
104
|
branch: args.branch,
|
|
105
105
|
runUrl: args.runUrl,
|
|
106
|
+
...(args.run ? { run: args.run } : {}),
|
|
106
107
|
...(environment ? { environment } : {}),
|
|
107
108
|
parameters: Object.fromEntries(Object.entries(parameters).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))),
|
|
108
109
|
specs: args.specs,
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* @see docs/agents.md
|
|
14
14
|
*/
|
|
15
15
|
/** The text `atlas report --help` prints: every subcommand, its flags, what each does and the exit codes. Also the source of the command reference in `docs/reference/cli.md`. */
|
|
16
|
-
export declare const REPORT_USAGE = "atlas report \u2014 layered evidence for any kind of spec\n\n atlas report publish [--store <dir|s3://bucket/prefix>] [--producer <name>]\n ( --ctrf <file> | --junit <file> | --manifest <file> | --checks <file.json> )\n [--environment <name>] [--branch <b>] [--sha <s>] [--run-url <u>]\n [--spec <path> | --specs <dir>] [--upload-run]\n [--param key=value]... [--kind <k>] [--title <t>] [--recorded-at <iso|ms>]\n [--attach [role=]<file>]... [--label key=value]... [--link [name=]<url>]...\n [--attach-root <dir>]... [--max-attachment-mb <n>] [--max-total-attachment-mb <n>]\n atlas report stream [--store <dir|s3://\u2026|https://atlas.example.com>] --producer <name> [--environment <name>]\n [--branch <b>] [--sha <s>] [--run-url <u>] [--kind <k>] [--param key=value]...\n [--flush-ms 1000] [--batch 100] (checks as JSON lines on standard input)\n atlas report status [--store <dir|s3://\u2026|id=url>]... [--backends <module>]\n [--specs <dir>] [--format text|json|markdown]\n [--require-environments a,b] [--require-all-specs]\n [--branch <name>]... [--ignore-modified]\n atlas report merge [--store \u2026]... [--backends <module>] --into <dir|s3://\u2026>\n atlas report doctor [--store \u2026]... [--role publisher|reader|both]\n atlas report policy --store s3://bucket/prefix [--role publisher|reader|both]\n\n A store is a directory or s3://bucket/prefix (add ?region=\u2026&endpoint=\u2026 for R2, MinIO and the\n rest). Select a project on an Atlas node with --project <slug> or ATLAS_PROJECT. This scopes specs, reports and recordings together.\n When no --store is given, ATLAS_STORE is used. Keys never go in the URL: set\n AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY, or sign in with `aws sso login`, or use a role.\n\n publish Write one immutable layer: the checks of one producer, on one environment. Safe to repeat.\n --producer is required, unless the --checks file names one\n --spec <path> every check in the report belongs to this spec (a report per feature file)\n --specs <dir> link checks to the spec files in <dir> by path, or by the suite, class or\n file name the report gives; also publishes the specs themselves, so the\n node can show them without a checkout\n --upload-run with --manifest: also upload the run's screenshots and clips for the viewer\n --attach <file> attach a file to the whole layer: a CI log, a coverage report, a trace archive\n (screenshots, videos and traces of a single test go in its CTRF or checks file)\n --label k=v a fact about the run, not part of what it checks: build, os, browser\n --link [n=]<url> a link to the run, a dashboard, an issue\n Files a report names (CTRF attachments, JUnit [[ATTACHMENT|path]] lines) come from the report's own\n folder and each --attach-root, and never from a hidden file or a link out: a report is data from a\n test run, not instructions. One that is missing or not allowed is skipped with a warning.\n From a checkout with uncommitted changes the layer is marked \"modified\".\n stream Send checks as they finish: one JSON object per line on standard input (the fields of one\n check: id, title, status, and any evidence). Each second's worth becomes one layer, so a server\n shows the run while it is still going. A check sent again with the same id (running, then\n passed) is replaced by the later. Ends when the input does; exit 1 if anything could not be sent.\n Point it at an Atlas server with --store https://\u2026 and a token in ATLAS_TOKEN.\n status Combine every layer of every store, and exit 1 unless the report passes.\n --specs <dir> lay the results over these spec files (default: the specs in the store)\n --require-all-specs fail while any spec has no results\n --branch <name> only layers recorded on this branch (repeatable)\n --ignore-modified leave out layers from modified checkouts and unknown origins\n merge Copy the layers of several stores into one.\n doctor Check that a store works: credentials, listing, writing, and what it holds.\n policy Print the least-privilege IAM policy for publishers, readers, or both.\n\n --backends <module> A TypeScript or JavaScript module whose default export is a list of\n { id, label?, store } (or a function returning it): a device lab, anything\n that implements ObjectStore.\n\n --format json (alias --json) publish, stream, merge, doctor and status print one JSON document instead of prose\n (schemas: publish-result, stream-result, merge-result, doctor-result, report-status). A command\n that is wrong is {\"ok\":false,\"error\":{\"message\":\u2026,\"code\":\"usage\"}} on standard error.\n A store may be an Atlas server (https://\u2026): ATLAS_STORE=https://atlas.example.com, with ATLAS_TOKEN.\n\n Exit codes: 0 ok, 1 the report does not pass (or a check failed), 2 the command was wrong.\n";
|
|
16
|
+
export declare const REPORT_USAGE = "atlas report \u2014 layered evidence for any kind of spec\n\n atlas report publish [--store <dir|s3://bucket/prefix>] [--producer <name>]\n ( --ctrf <file> | --junit <file> | --manifest <file> | --checks <file.json> )\n [--environment <name>] [--branch <b>] [--sha <s>] [--run-url <u>]\n [--run-id <id>] [--run-title <t>] [--run-started-at <iso|ms>] [--run-ended-at <iso|ms>]\n [--spec <path> | --specs <dir>] [--upload-run]\n [--param key=value]... [--kind <k>] [--title <t>] [--recorded-at <iso|ms>]\n [--attach [role=]<file>]... [--label key=value]... [--link [name=]<url>]...\n [--attach-root <dir>]... [--max-attachment-mb <n>] [--max-total-attachment-mb <n>]\n atlas report stream [--store <dir|s3://\u2026|https://atlas.example.com>] --producer <name> [--environment <name>]\n [--branch <b>] [--sha <s>] [--run-url <u>] [--kind <k>] [--param key=value]...\n [--run-id <id>] [--run-title <t>]\n [--flush-ms 1000] [--batch 100] (checks as JSON lines on standard input)\n atlas report status [--store <dir|s3://\u2026|id=url>]... [--backends <module>]\n [--specs <dir>] [--format text|json|markdown]\n [--require-environments a,b] [--require-all-specs]\n [--branch <name>]... [--ignore-modified]\n atlas report merge [--store \u2026]... [--backends <module>] --into <dir|s3://\u2026>\n atlas report doctor [--store \u2026]... [--role publisher|reader|both]\n atlas report policy --store s3://bucket/prefix [--role publisher|reader|both]\n\n A store is a directory or s3://bucket/prefix (add ?region=\u2026&endpoint=\u2026 for R2, MinIO and the\n rest). Select a project on an Atlas node with --project <slug> or ATLAS_PROJECT. This scopes specs, reports and recordings together.\n When no --store is given, ATLAS_STORE is used. Keys never go in the URL: set\n AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY, or sign in with `aws sso login`, or use a role.\n\n publish Write one immutable layer: the checks of one producer, on one environment. Safe to repeat.\n --producer is required, unless the --checks file names one\n --spec <path> every check in the report belongs to this spec (a report per feature file)\n --specs <dir> link checks to the spec files in <dir> by path, or by the suite, class or\n file name the report gives; also publishes the specs themselves, so the\n node can show them without a checkout\n --run-id <id> name this execution (a local or manual run with no CI run URL; also ATLAS_RUN_ID).\n Layers that share an id are one run, however many shards or batches uploaded them;\n another invocation at the same commit gets another id. Never part of what a check is\n --run-title <t> a human name for the run (also ATLAS_RUN_TITLE); --run-started-at and --run-ended-at\n record when it ran. Without --run-id a run is its CI run URL, else branch@sha\n --upload-run with --manifest: also upload the run's screenshots and clips for the viewer\n --attach <file> attach a file to the whole layer: a CI log, a coverage report, a trace archive\n (screenshots, videos and traces of a single test go in its CTRF or checks file)\n --label k=v a fact about the run, not part of what it checks: build, os, browser\n --link [n=]<url> a link to the run, a dashboard, an issue\n Files a report names (CTRF attachments, JUnit [[ATTACHMENT|path]] lines) come from the report's own\n folder and each --attach-root, and never from a hidden file or a link out: a report is data from a\n test run, not instructions. One that is missing or not allowed is skipped with a warning.\n From a checkout with uncommitted changes the layer is marked \"modified\".\n stream Send checks as they finish: one JSON object per line on standard input (the fields of one\n check: id, title, status, and any evidence). Each second's worth becomes one layer, so a server\n shows the run while it is still going. A check sent again with the same id (running, then\n passed) is replaced by the later. Ends when the input does; exit 1 if anything could not be sent.\n Point it at an Atlas server with --store https://\u2026 and a token in ATLAS_TOKEN.\n status Combine every layer of every store, and exit 1 unless the report passes.\n --specs <dir> lay the results over these spec files (default: the specs in the store)\n --require-all-specs fail while any spec has no results\n --branch <name> only layers recorded on this branch (repeatable)\n --ignore-modified leave out layers from modified checkouts and unknown origins\n merge Copy the layers of several stores into one.\n doctor Check that a store works: credentials, listing, writing, and what it holds.\n policy Print the least-privilege IAM policy for publishers, readers, or both.\n\n --backends <module> A TypeScript or JavaScript module whose default export is a list of\n { id, label?, store } (or a function returning it): a device lab, anything\n that implements ObjectStore.\n\n --format json (alias --json) publish, stream, merge, doctor and status print one JSON document instead of prose\n (schemas: publish-result, stream-result, merge-result, doctor-result, report-status). A command\n that is wrong is {\"ok\":false,\"error\":{\"message\":\u2026,\"code\":\"usage\"}} on standard error.\n A store may be an Atlas server (https://\u2026): ATLAS_STORE=https://atlas.example.com, with ATLAS_TOKEN.\n\n Exit codes: 0 ok, 1 the report does not pass (or a check failed), 2 the command was wrong.\n";
|
|
17
17
|
type Env = Readonly<Record<string, string | undefined>>;
|
|
18
18
|
type Git = (args: readonly string[]) => string | null;
|
|
19
19
|
/** What `reportMain` is given besides arguments: where output goes (`out`), and, for tests and embedding, the environment, working directory, git, clock and standard input (`stdin`, where `stream` reads checks from). Defaults are the real ones. */
|
|
@@ -38,12 +38,14 @@ export const REPORT_USAGE = `atlas report — layered evidence for any kind of s
|
|
|
38
38
|
atlas report publish [--store <dir|s3://bucket/prefix>] [--producer <name>]
|
|
39
39
|
( --ctrf <file> | --junit <file> | --manifest <file> | --checks <file.json> )
|
|
40
40
|
[--environment <name>] [--branch <b>] [--sha <s>] [--run-url <u>]
|
|
41
|
+
[--run-id <id>] [--run-title <t>] [--run-started-at <iso|ms>] [--run-ended-at <iso|ms>]
|
|
41
42
|
[--spec <path> | --specs <dir>] [--upload-run]
|
|
42
43
|
[--param key=value]... [--kind <k>] [--title <t>] [--recorded-at <iso|ms>]
|
|
43
44
|
[--attach [role=]<file>]... [--label key=value]... [--link [name=]<url>]...
|
|
44
45
|
[--attach-root <dir>]... [--max-attachment-mb <n>] [--max-total-attachment-mb <n>]
|
|
45
46
|
atlas report stream [--store <dir|s3://…|https://atlas.example.com>] --producer <name> [--environment <name>]
|
|
46
47
|
[--branch <b>] [--sha <s>] [--run-url <u>] [--kind <k>] [--param key=value]...
|
|
48
|
+
[--run-id <id>] [--run-title <t>]
|
|
47
49
|
[--flush-ms 1000] [--batch 100] (checks as JSON lines on standard input)
|
|
48
50
|
atlas report status [--store <dir|s3://…|id=url>]... [--backends <module>]
|
|
49
51
|
[--specs <dir>] [--format text|json|markdown]
|
|
@@ -64,6 +66,11 @@ export const REPORT_USAGE = `atlas report — layered evidence for any kind of s
|
|
|
64
66
|
--specs <dir> link checks to the spec files in <dir> by path, or by the suite, class or
|
|
65
67
|
file name the report gives; also publishes the specs themselves, so the
|
|
66
68
|
node can show them without a checkout
|
|
69
|
+
--run-id <id> name this execution (a local or manual run with no CI run URL; also ATLAS_RUN_ID).
|
|
70
|
+
Layers that share an id are one run, however many shards or batches uploaded them;
|
|
71
|
+
another invocation at the same commit gets another id. Never part of what a check is
|
|
72
|
+
--run-title <t> a human name for the run (also ATLAS_RUN_TITLE); --run-started-at and --run-ended-at
|
|
73
|
+
record when it ran. Without --run-id a run is its CI run URL, else branch@sha
|
|
67
74
|
--upload-run with --manifest: also upload the run's screenshots and clips for the viewer
|
|
68
75
|
--attach <file> attach a file to the whole layer: a CI log, a coverage report, a trace archive
|
|
69
76
|
(screenshots, videos and traces of a single test go in its CTRF or checks file)
|
|
@@ -151,15 +158,32 @@ function linkSpec(spec) {
|
|
|
151
158
|
const named = /^([^=/:]+)=(.+)$/.exec(spec);
|
|
152
159
|
return named ? { name: named[1], url: named[2] } : { url: spec };
|
|
153
160
|
}
|
|
154
|
-
function parseTime(text) {
|
|
161
|
+
function parseTime(text, flag = '--recorded-at') {
|
|
155
162
|
if (text === undefined)
|
|
156
163
|
return null;
|
|
157
164
|
const numeric = Number(text);
|
|
158
165
|
const parsed = Number.isFinite(numeric) ? numeric : Date.parse(text);
|
|
159
166
|
if (!Number.isFinite(parsed) || parsed < 0)
|
|
160
|
-
throw new Error(
|
|
167
|
+
throw new Error(`${flag} takes an ISO date or epoch milliseconds (got "${text}")`);
|
|
161
168
|
return parsed;
|
|
162
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* The execution the producer named, from `--run-id`, `--run-title`, `--run-started-at`, `--run-ended-at` and then
|
|
172
|
+
* `ATLAS_RUN_ID` / `ATLAS_RUN_TITLE`, laid over a `run` the checks file gave. `undefined` when nothing names one, so the
|
|
173
|
+
* documented grouping (CI run URL, else branch@sha) applies. A title or time without an id is refused: it would have nothing to belong to.
|
|
174
|
+
*/
|
|
175
|
+
function executionOf(rest, env, given) {
|
|
176
|
+
const file = typeof given === 'object' && given !== null && !Array.isArray(given) ? given : {};
|
|
177
|
+
const id = value(rest, 'run-id') ?? env.ATLAS_RUN_ID ?? (typeof file.id === 'string' ? file.id : undefined);
|
|
178
|
+
const title = value(rest, 'run-title') ?? env.ATLAS_RUN_TITLE;
|
|
179
|
+
const startedAtMs = parseTime(value(rest, 'run-started-at'), '--run-started-at');
|
|
180
|
+
const endedAtMs = parseTime(value(rest, 'run-ended-at'), '--run-ended-at');
|
|
181
|
+
if (id === undefined && (title !== undefined || startedAtMs !== null || endedAtMs !== null))
|
|
182
|
+
throw new Error('--run-title, --run-started-at and --run-ended-at go with --run-id <id> (or ATLAS_RUN_ID): a run needs an identity first');
|
|
183
|
+
if (id === undefined)
|
|
184
|
+
return undefined;
|
|
185
|
+
return { ...file, id, ...(title !== undefined ? { title } : {}), ...(startedAtMs !== null ? { startedAtMs } : {}), ...(endedAtMs !== null ? { endedAtMs } : {}) };
|
|
186
|
+
}
|
|
163
187
|
function ingest(argv, base) {
|
|
164
188
|
const read = (file) => readFileSync(path.resolve(base, file), 'utf8');
|
|
165
189
|
const sources = ['ctrf', 'junit', 'manifest', 'checks'].filter(name => value(argv, name) !== undefined);
|
|
@@ -258,6 +282,7 @@ export async function reportMain(argv, io = { out: text => process.stdout.write(
|
|
|
258
282
|
}
|
|
259
283
|
const labels = { ...(ingested.labels ?? {}), ...(extra.labels ?? {}), ...Object.fromEntries(values(rest, 'label').map(pair => keyValue(pair, '--label'))) };
|
|
260
284
|
const links = [...(extra.links ?? []), ...values(rest, 'link').map(linkSpec)];
|
|
285
|
+
const execution = executionOf(rest, env, extra.run);
|
|
261
286
|
const layer = await publishChecks(target.store, {
|
|
262
287
|
...extra,
|
|
263
288
|
producer,
|
|
@@ -272,6 +297,7 @@ export async function reportMain(argv, io = { out: text => process.stdout.write(
|
|
|
272
297
|
...(value(rest, 'title') ?? extra.title ? { title: (value(rest, 'title') ?? extra.title) } : {}),
|
|
273
298
|
sha, branch,
|
|
274
299
|
runUrl: value(rest, 'run-url') ?? extra.runUrl ?? origin.runUrl,
|
|
300
|
+
...(execution ? { run: execution } : {}),
|
|
275
301
|
...(freshness ? { freshness } : {}),
|
|
276
302
|
...(value(rest, 'environment') ?? extra.environment ? { environment: (value(rest, 'environment') ?? extra.environment) } : {}),
|
|
277
303
|
parameters: { ...(extra.parameters ?? {}), ...parameters }
|
|
@@ -329,6 +355,7 @@ export async function reportMain(argv, io = { out: text => process.stdout.write(
|
|
|
329
355
|
const branch = value(rest, 'branch') ?? origin.branch;
|
|
330
356
|
const freshness = detectFreshness({ sha, ci: origin.ci && !value(rest, 'sha') }, io.git);
|
|
331
357
|
const environment = value(rest, 'environment');
|
|
358
|
+
const execution = executionOf(rest, env);
|
|
332
359
|
const warnings = [];
|
|
333
360
|
const reporter = createReporter({
|
|
334
361
|
producer,
|
|
@@ -336,6 +363,7 @@ export async function reportMain(argv, io = { out: text => process.stdout.write(
|
|
|
336
363
|
...(value(rest, 'title') ? { title: value(rest, 'title') } : {}),
|
|
337
364
|
sha, branch,
|
|
338
365
|
runUrl: value(rest, 'run-url') ?? origin.runUrl,
|
|
366
|
+
...(execution ? { run: execution } : {}),
|
|
339
367
|
...(freshness ? { freshness } : {}),
|
|
340
368
|
...(environment ? { environment } : {}),
|
|
341
369
|
parameters: Object.fromEntries(values(rest, 'param').map(pair => keyValue(pair, '--param')))
|
|
@@ -13,7 +13,7 @@ import { type PublishChecksInput } from '../../schema/src/layer.js';
|
|
|
13
13
|
import type { ObjectStore } from './object-store.js';
|
|
14
14
|
type Check = PublishChecksInput['checks'][number];
|
|
15
15
|
/** What every layer of one stream shares: who is reporting, from where. Whatever `publishChecks` takes, except the checks and the time. */
|
|
16
|
-
export type ReporterIdentity = Pick<PublishChecksInput, 'producer' | 'kind' | 'title' | 'sha' | 'branch' | 'runUrl' | 'environment' | 'parameters' | 'freshness' | 'specs' | 'tags' | 'labels' | 'links' | 'attachments' | 'annotations' | 'extensions'>;
|
|
16
|
+
export type ReporterIdentity = Pick<PublishChecksInput, 'producer' | 'kind' | 'title' | 'sha' | 'branch' | 'runUrl' | 'run' | 'environment' | 'parameters' | 'freshness' | 'specs' | 'tags' | 'labels' | 'links' | 'attachments' | 'annotations' | 'extensions'>;
|
|
17
17
|
/** How a reporter sends: the `store` (a folder, S3, or `httpStore` for a server), how often (`flushMs`, `batchSize`), how hard it tries (`attempts`, `backoffMs`), what it tells (`onError`, `onPublished`), and the clock and sleep for tests. */
|
|
18
18
|
export type ReporterOptions = {
|
|
19
19
|
/** Where layers go: a directory, S3, or `httpStore` for an Atlas server. */
|
|
@@ -234,10 +234,19 @@ export type TestHistoryDocument = {
|
|
|
234
234
|
readonly test: TestSummary;
|
|
235
235
|
readonly history: readonly TestResultEntry[];
|
|
236
236
|
};
|
|
237
|
-
/**
|
|
237
|
+
/**
|
|
238
|
+
* A run: the layers that name the same execution (`run.id`), else share a CI run URL, else a branch and commit. When,
|
|
239
|
+
* where, and how its tests went.
|
|
240
|
+
*/
|
|
238
241
|
export type RunSummary = {
|
|
239
|
-
/** Stable key: the run URL,
|
|
242
|
+
/** Stable key: `run:<id>` for an execution a producer named, else the run URL, else `branch@sha`. */
|
|
240
243
|
readonly key: string;
|
|
244
|
+
/** The execution id a producer named for it, when it did. Layers that share it are one run, however many uploads they took; another invocation at the same commit has another. */
|
|
245
|
+
readonly runId?: string;
|
|
246
|
+
/** The human title the producer gave the execution (the newest layer's). Presentation only: the key, the commit and `freshness` are unaffected. */
|
|
247
|
+
readonly title?: string;
|
|
248
|
+
/** `modified` when any of its layers was recorded from a checkout with uncommitted changes, else `unknown` when any origin was not captured; absent when every layer is from a clean commit. Advisory; never changes a status. */
|
|
249
|
+
readonly freshness?: 'unknown' | 'modified';
|
|
241
250
|
readonly runUrl: string | null;
|
|
242
251
|
readonly branch: string | null;
|
|
243
252
|
readonly sha: string | null;
|
|
@@ -290,8 +299,15 @@ export type RunDetail = {
|
|
|
290
299
|
readonly run: RunSummary;
|
|
291
300
|
readonly results: readonly RunResult[];
|
|
292
301
|
};
|
|
293
|
-
/**
|
|
294
|
-
|
|
302
|
+
/**
|
|
303
|
+
* The run a layer belongs to: the execution its producer named (`run:<id>`), else its run URL, else `branch@sha`, else
|
|
304
|
+
* `local`. Layers with the same key are one run. Only grouping: it is never part of the slot a result fills.
|
|
305
|
+
*/
|
|
306
|
+
export declare function runKey(layer: Pick<LayerHeader, 'runUrl' | 'branch' | 'sha'> & {
|
|
307
|
+
readonly run?: {
|
|
308
|
+
readonly id: string;
|
|
309
|
+
};
|
|
310
|
+
}): string;
|
|
295
311
|
/**
|
|
296
312
|
* Every recorded result of every test (the newest `HISTORY_PER_TEST` per test, leaving out `running` progress) and the
|
|
297
313
|
* runs they came from. A run's results are read from that history, so memory follows the number of tests and not the age
|
|
@@ -8,8 +8,13 @@ export const HISTORY_PER_TEST = 100;
|
|
|
8
8
|
const FAILURE_RANK = { passed: 0, 'not-run': 1, running: 2, failed: 3 };
|
|
9
9
|
/** How tests are ordered. */
|
|
10
10
|
export const TEST_SORTS = ['spec', 'title', 'recent', 'slowest', 'failures', 'flaky', 'runs'];
|
|
11
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* The run a layer belongs to: the execution its producer named (`run:<id>`), else its run URL, else `branch@sha`, else
|
|
13
|
+
* `local`. Layers with the same key are one run. Only grouping: it is never part of the slot a result fills.
|
|
14
|
+
*/
|
|
12
15
|
export function runKey(layer) {
|
|
16
|
+
if (layer.run?.id)
|
|
17
|
+
return `run:${layer.run.id}`;
|
|
13
18
|
if (layer.runUrl)
|
|
14
19
|
return layer.runUrl;
|
|
15
20
|
if (layer.branch || layer.sha)
|
|
@@ -98,17 +103,29 @@ export class TestHistory {
|
|
|
98
103
|
const key = runKey(layer);
|
|
99
104
|
let run = this.#runs.get(key);
|
|
100
105
|
if (!run) {
|
|
101
|
-
run = { key, runUrl: layer.runUrl, branch: layer.branch, sha: layer.sha, start: layer.recordedAtMs, end: layer.recordedAtMs, first: layer.id, producers: new Set(), environments: new Set(), layers: 0 };
|
|
106
|
+
run = { key, runUrl: layer.runUrl, branch: layer.branch, sha: layer.sha, start: layer.recordedAtMs, end: layer.recordedAtMs, firstAt: layer.recordedAtMs, first: layer.id, producers: new Set(), environments: new Set(), layers: 0, runId: layer.run?.id, title: undefined, titledAt: 0, titledBy: '', freshness: undefined };
|
|
102
107
|
this.#runs.set(key, run);
|
|
103
108
|
}
|
|
104
109
|
// A run is told by its first layer (by time, then id), whatever order its layers arrive in.
|
|
105
|
-
if (layer.recordedAtMs < run.
|
|
106
|
-
run.
|
|
110
|
+
if (layer.recordedAtMs < run.firstAt || (layer.recordedAtMs === run.firstAt && layer.id < run.first)) {
|
|
111
|
+
run.firstAt = layer.recordedAtMs;
|
|
107
112
|
run.first = layer.id;
|
|
108
113
|
run.branch = layer.branch;
|
|
109
114
|
run.sha = layer.sha;
|
|
115
|
+
run.runUrl = layer.runUrl;
|
|
110
116
|
}
|
|
111
|
-
run.
|
|
117
|
+
run.start = Math.min(run.start, layer.recordedAtMs, layer.run?.startedAtMs ?? Infinity);
|
|
118
|
+
run.end = Math.max(run.end, layer.recordedAtMs, layer.run?.endedAtMs ?? -Infinity);
|
|
119
|
+
// The newest title wins, so a person can rename a run by sending its id again with another title.
|
|
120
|
+
const title = layer.run?.title;
|
|
121
|
+
if (title && (layer.recordedAtMs > run.titledAt || (layer.recordedAtMs === run.titledAt && layer.id > run.titledBy))) {
|
|
122
|
+
run.title = title;
|
|
123
|
+
run.titledAt = layer.recordedAtMs;
|
|
124
|
+
run.titledBy = layer.id;
|
|
125
|
+
}
|
|
126
|
+
// Local changes are never lost to a clean sibling: the run is as modified as its most modified layer.
|
|
127
|
+
if (layer.freshness === 'modified' || (layer.freshness === 'unknown' && run.freshness !== 'modified'))
|
|
128
|
+
run.freshness = layer.freshness;
|
|
112
129
|
run.producers.add(layer.producer);
|
|
113
130
|
if (layer.environment)
|
|
114
131
|
run.environments.add(layer.environment);
|
|
@@ -227,7 +244,8 @@ function runSummary(run, cells) {
|
|
|
227
244
|
failed.sort((a, b) => compareText(a.i.slot, b.i.slot) || compareText(a.r.o.place, b.r.o.place) || compareText(a.r.k, b.r.k));
|
|
228
245
|
const failing = failed.slice(0, 20).map(cell => ({ key: cell.i.slot, title: cell.i.title, environment: cell.r.o.environment, detail: (cell.r.t.split('\n')[0] ?? '').slice(0, 500) }));
|
|
229
246
|
return {
|
|
230
|
-
key: run.key,
|
|
247
|
+
key: run.key, ...(run.runId ? { runId: run.runId } : {}), ...(run.title ? { title: run.title } : {}), ...(run.freshness ? { freshness: run.freshness } : {}),
|
|
248
|
+
runUrl: run.runUrl, branch: run.branch, sha: run.sha,
|
|
231
249
|
startedAtMs: run.start, endedAtMs: run.end, spanMs: run.end - run.start,
|
|
232
250
|
producers: [...run.producers].sort(), environments: [...run.environments].sort(),
|
|
233
251
|
layers: run.layers, tests: tests.size, counts, totalDurationMs: measured ? total : null, failing,
|
|
@@ -631,6 +649,10 @@ export function historyMarkdown(document) {
|
|
|
631
649
|
...document.history.map(e => `| ${iso(e.at)} | ${e.environment ?? '—'} | ${e.status} | ${formatMs(e.durationMs)} | ${cell(e.branch ?? '—')} | ${e.sha?.slice(0, 10) ?? '—'} | ${cell(e.detail.split('\n')[0] ?? '')} |`),
|
|
632
650
|
].join('\n')}\n`;
|
|
633
651
|
}
|
|
652
|
+
/** What to say beside a commit that was not recorded from a clean checkout: nothing, ` (modified)` or ` (origin unknown)`. */
|
|
653
|
+
function freshnessNote(freshness) {
|
|
654
|
+
return freshness === 'modified' ? ' (modified: local changes)' : freshness === 'unknown' ? ' (origin unknown)' : '';
|
|
655
|
+
}
|
|
634
656
|
/** The run list as Markdown. */
|
|
635
657
|
export function runsMarkdown(list) {
|
|
636
658
|
return `${[
|
|
@@ -638,7 +660,7 @@ export function runsMarkdown(list) {
|
|
|
638
660
|
'',
|
|
639
661
|
'| Ended | Branch | Commit | Environments | Tests | Passed | Failed | Not run | Span | Run |',
|
|
640
662
|
'|---|---|---|---|---:|---:|---:|---:|---:|---|',
|
|
641
|
-
...list.runs.map(r => `| ${iso(r.endedAtMs)} | ${cell(r.branch ?? '—')} | ${r.sha?.slice(0, 10) ?? '—'} | ${r.environments.join(', ') || '—'} | ${r.tests} | ${r.counts.passed} | ${r.counts.failed} | ${r.counts['not-run']} | ${formatMs(r.spanMs)} | ${cell(r.key)} |`),
|
|
663
|
+
...list.runs.map(r => `| ${iso(r.endedAtMs)} | ${cell(r.branch ?? '—')} | ${r.sha?.slice(0, 10) ?? '—'}${freshnessNote(r.freshness)} | ${r.environments.join(', ') || '—'} | ${r.tests} | ${r.counts.passed} | ${r.counts.failed} | ${r.counts['not-run']} | ${formatMs(r.spanMs)} | ${cell(r.title ? `${r.title} (${r.key})` : r.key)} |`),
|
|
642
664
|
].join('\n')}\n`;
|
|
643
665
|
}
|
|
644
666
|
/** One run as Markdown. */
|
|
@@ -647,7 +669,8 @@ export function runMarkdown(detail) {
|
|
|
647
669
|
return `${[
|
|
648
670
|
`# Run ${run.key}`,
|
|
649
671
|
'',
|
|
650
|
-
|
|
672
|
+
...(run.title ? [`- Title: ${run.title}`] : []),
|
|
673
|
+
`- Branch ${run.branch ?? '—'}, commit ${run.sha ?? '—'}${freshnessNote(run.freshness)}`,
|
|
651
674
|
`- ${iso(run.startedAtMs)} to ${iso(run.endedAtMs)} (${formatMs(run.spanMs)}); ${run.layers} layers from ${run.producers.join(', ')}`,
|
|
652
675
|
`- Environments: ${run.environments.join(', ') || '—'}`,
|
|
653
676
|
`- ${run.tests} tests: ${counted(run.counts)}${run.totalDurationMs === null ? '' : `; ${formatMs(run.totalDurationMs)} of test time`}`,
|
package/dist/schema/src/api.d.ts
CHANGED
|
@@ -107,6 +107,12 @@ export declare const LayerHeaderSchema: z.ZodObject<{
|
|
|
107
107
|
unknown: "unknown";
|
|
108
108
|
modified: "modified";
|
|
109
109
|
}>>;
|
|
110
|
+
run: z.ZodOptional<z.ZodObject<{
|
|
111
|
+
id: z.ZodString;
|
|
112
|
+
title: z.ZodOptional<z.ZodString>;
|
|
113
|
+
startedAtMs: z.ZodOptional<z.ZodNumber>;
|
|
114
|
+
endedAtMs: z.ZodOptional<z.ZodNumber>;
|
|
115
|
+
}, z.core.$loose>>;
|
|
110
116
|
recording: z.ZodNullable<z.ZodUnknown>;
|
|
111
117
|
live: z.ZodBoolean;
|
|
112
118
|
counts: z.ZodRecord<z.ZodEnum<{
|
|
@@ -248,6 +254,12 @@ export declare const LayerEvidenceSchema: z.ZodObject<{
|
|
|
248
254
|
unknown: "unknown";
|
|
249
255
|
modified: "modified";
|
|
250
256
|
}>>;
|
|
257
|
+
run: z.ZodOptional<z.ZodObject<{
|
|
258
|
+
id: z.ZodString;
|
|
259
|
+
title: z.ZodOptional<z.ZodString>;
|
|
260
|
+
startedAtMs: z.ZodOptional<z.ZodNumber>;
|
|
261
|
+
endedAtMs: z.ZodOptional<z.ZodNumber>;
|
|
262
|
+
}, z.core.$loose>>;
|
|
251
263
|
recording: z.ZodNullable<z.ZodUnknown>;
|
|
252
264
|
live: z.ZodBoolean;
|
|
253
265
|
counts: z.ZodRecord<z.ZodEnum<{
|
|
@@ -326,6 +338,12 @@ export declare const EffectiveEvidenceSchema: z.ZodObject<{
|
|
|
326
338
|
unknown: "unknown";
|
|
327
339
|
modified: "modified";
|
|
328
340
|
}>>;
|
|
341
|
+
run: z.ZodOptional<z.ZodObject<{
|
|
342
|
+
id: z.ZodString;
|
|
343
|
+
title: z.ZodOptional<z.ZodString>;
|
|
344
|
+
startedAtMs: z.ZodOptional<z.ZodNumber>;
|
|
345
|
+
endedAtMs: z.ZodOptional<z.ZodNumber>;
|
|
346
|
+
}, z.core.$loose>>;
|
|
329
347
|
recording: z.ZodNullable<z.ZodUnknown>;
|
|
330
348
|
live: z.ZodBoolean;
|
|
331
349
|
counts: z.ZodRecord<z.ZodEnum<{
|
|
@@ -425,6 +443,12 @@ export declare const EffectiveDeltaSchema: z.ZodObject<{
|
|
|
425
443
|
unknown: "unknown";
|
|
426
444
|
modified: "modified";
|
|
427
445
|
}>>;
|
|
446
|
+
run: z.ZodOptional<z.ZodObject<{
|
|
447
|
+
id: z.ZodString;
|
|
448
|
+
title: z.ZodOptional<z.ZodString>;
|
|
449
|
+
startedAtMs: z.ZodOptional<z.ZodNumber>;
|
|
450
|
+
endedAtMs: z.ZodOptional<z.ZodNumber>;
|
|
451
|
+
}, z.core.$loose>>;
|
|
428
452
|
recording: z.ZodNullable<z.ZodUnknown>;
|
|
429
453
|
live: z.ZodBoolean;
|
|
430
454
|
counts: z.ZodRecord<z.ZodEnum<{
|
|
@@ -485,6 +509,12 @@ export declare const EvidencePageSchema: z.ZodObject<{
|
|
|
485
509
|
unknown: "unknown";
|
|
486
510
|
modified: "modified";
|
|
487
511
|
}>>;
|
|
512
|
+
run: z.ZodOptional<z.ZodObject<{
|
|
513
|
+
id: z.ZodString;
|
|
514
|
+
title: z.ZodOptional<z.ZodString>;
|
|
515
|
+
startedAtMs: z.ZodOptional<z.ZodNumber>;
|
|
516
|
+
endedAtMs: z.ZodOptional<z.ZodNumber>;
|
|
517
|
+
}, z.core.$loose>>;
|
|
488
518
|
recording: z.ZodNullable<z.ZodUnknown>;
|
|
489
519
|
live: z.ZodBoolean;
|
|
490
520
|
counts: z.ZodRecord<z.ZodEnum<{
|
|
@@ -1354,9 +1384,15 @@ export declare const TestHistorySchema: z.ZodObject<{
|
|
|
1354
1384
|
}, z.core.$loose>>;
|
|
1355
1385
|
}, z.core.$loose>>;
|
|
1356
1386
|
}, z.core.$loose>;
|
|
1357
|
-
/** A run: the layers that share a run URL
|
|
1387
|
+
/** A run: the layers that name the same execution (else share a run URL, else a branch and commit), with what they recorded. */
|
|
1358
1388
|
export declare const ReportRunSchema: z.ZodObject<{
|
|
1359
1389
|
key: z.ZodString;
|
|
1390
|
+
runId: z.ZodOptional<z.ZodString>;
|
|
1391
|
+
title: z.ZodOptional<z.ZodString>;
|
|
1392
|
+
freshness: z.ZodOptional<z.ZodEnum<{
|
|
1393
|
+
unknown: "unknown";
|
|
1394
|
+
modified: "modified";
|
|
1395
|
+
}>>;
|
|
1360
1396
|
runUrl: z.ZodNullable<z.ZodString>;
|
|
1361
1397
|
branch: z.ZodNullable<z.ZodString>;
|
|
1362
1398
|
sha: z.ZodNullable<z.ZodString>;
|
|
@@ -1388,6 +1424,12 @@ export declare const RunListSchema: z.ZodObject<{
|
|
|
1388
1424
|
total: z.ZodNumber;
|
|
1389
1425
|
runs: z.ZodArray<z.ZodObject<{
|
|
1390
1426
|
key: z.ZodString;
|
|
1427
|
+
runId: z.ZodOptional<z.ZodString>;
|
|
1428
|
+
title: z.ZodOptional<z.ZodString>;
|
|
1429
|
+
freshness: z.ZodOptional<z.ZodEnum<{
|
|
1430
|
+
unknown: "unknown";
|
|
1431
|
+
modified: "modified";
|
|
1432
|
+
}>>;
|
|
1391
1433
|
runUrl: z.ZodNullable<z.ZodString>;
|
|
1392
1434
|
branch: z.ZodNullable<z.ZodString>;
|
|
1393
1435
|
sha: z.ZodNullable<z.ZodString>;
|
|
@@ -1419,6 +1461,12 @@ export declare const RunDetailSchema: z.ZodObject<{
|
|
|
1419
1461
|
version: z.ZodString;
|
|
1420
1462
|
run: z.ZodObject<{
|
|
1421
1463
|
key: z.ZodString;
|
|
1464
|
+
runId: z.ZodOptional<z.ZodString>;
|
|
1465
|
+
title: z.ZodOptional<z.ZodString>;
|
|
1466
|
+
freshness: z.ZodOptional<z.ZodEnum<{
|
|
1467
|
+
unknown: "unknown";
|
|
1468
|
+
modified: "modified";
|
|
1469
|
+
}>>;
|
|
1422
1470
|
runUrl: z.ZodNullable<z.ZodString>;
|
|
1423
1471
|
branch: z.ZodNullable<z.ZodString>;
|
|
1424
1472
|
sha: z.ZodNullable<z.ZodString>;
|
package/dist/schema/src/api.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* @see docs/server.md
|
|
15
15
|
*/
|
|
16
16
|
import { z } from 'zod/v4';
|
|
17
|
-
import { CheckSchema } from './layer.js';
|
|
17
|
+
import { CheckSchema, LayerRunSchema } from './layer.js';
|
|
18
18
|
/** One of the statuses a check has in a report: `passed`, `failed`, `running` (not finished) or `not-run`. Closed. */
|
|
19
19
|
export const EvidenceStatusSchema = z.enum(['passed', 'failed', 'running', 'not-run']);
|
|
20
20
|
/**
|
|
@@ -86,6 +86,7 @@ export const LayerHeaderSchema = z.looseObject({
|
|
|
86
86
|
runUrl: z.string().nullable(),
|
|
87
87
|
parameters: z.record(z.string(), z.string()).describe('Other choices that make a result its own slot'),
|
|
88
88
|
freshness: z.enum(['unknown', 'modified']).optional().describe('`modified`: recorded from a checkout with uncommitted changes. `unknown`: origin not captured. Absent: clean'),
|
|
89
|
+
run: LayerRunSchema.optional().describe('The execution the producer named for it (one local or manual invocation). Grouping and display only; never part of the slot'),
|
|
89
90
|
recording: z.unknown().nullable().describe('Where recorded timelines are read from, or null'),
|
|
90
91
|
live: z.boolean(),
|
|
91
92
|
counts: z.record(EvidenceStatusSchema, z.number().int().nonnegative()),
|
|
@@ -332,9 +333,12 @@ export const TestHistorySchema = z.looseObject({
|
|
|
332
333
|
test: TestSummarySchema,
|
|
333
334
|
history: z.array(TestResultEntrySchema)
|
|
334
335
|
});
|
|
335
|
-
/** A run: the layers that share a run URL
|
|
336
|
+
/** A run: the layers that name the same execution (else share a run URL, else a branch and commit), with what they recorded. */
|
|
336
337
|
export const ReportRunSchema = z.looseObject({
|
|
337
|
-
key: z.string().describe('
|
|
338
|
+
key: z.string().describe('`run:<id>` for an execution a producer named, else the run URL, else `branch@sha`. Pass it to `/api/reports/runs/detail?key=` or `/api/reports/tests?run=`'),
|
|
339
|
+
runId: z.string().optional().describe('The execution id the producer named, when it did. Layers that share it are one run however many uploads they took'),
|
|
340
|
+
title: z.string().optional().describe('The human title of the execution (the newest layer\'s). Presentation only: the key, commit and `freshness` are unaffected'),
|
|
341
|
+
freshness: z.enum(['unknown', 'modified']).optional().describe('`modified` when any layer of the run was recorded from a checkout with uncommitted changes; `unknown` when an origin was not captured; absent when all are clean. Shown beside the commit, never replaced by the title'),
|
|
338
342
|
runUrl: z.string().nullable(), branch: z.string().nullable(), sha: z.string().nullable(),
|
|
339
343
|
startedAtMs: z.number(), endedAtMs: z.number(), spanMs: z.number(),
|
|
340
344
|
producers: z.array(z.string()), environments: z.array(z.string()),
|
|
@@ -60,6 +60,20 @@ export type CheckStatus = z.infer<typeof CheckStatusSchema>;
|
|
|
60
60
|
* changes only with a new `schemaVersion`.
|
|
61
61
|
*/
|
|
62
62
|
export declare const FRESHNESS: readonly ["unknown", "modified"];
|
|
63
|
+
/**
|
|
64
|
+
* The execution a layer belongs to, named by the producer: one invocation of a local or manual run, which several
|
|
65
|
+
* layers (shards, batches, producers) may share. Presentation and history grouping only: it never takes part in the
|
|
66
|
+
* slot a result fills, in which result wins, or in a status. Absent, a run is told by its CI run URL, else `branch@sha`.
|
|
67
|
+
* Within one project, layers that carry the same `id` are one run; a later invocation gets another `id`.
|
|
68
|
+
*/
|
|
69
|
+
export declare const LayerRunSchema: z.ZodObject<{
|
|
70
|
+
id: z.ZodString;
|
|
71
|
+
title: z.ZodOptional<z.ZodString>;
|
|
72
|
+
startedAtMs: z.ZodOptional<z.ZodNumber>;
|
|
73
|
+
endedAtMs: z.ZodOptional<z.ZodNumber>;
|
|
74
|
+
}, z.core.$loose>;
|
|
75
|
+
/** The execution of a layer, parsed from `LayerRunSchema`. */
|
|
76
|
+
export type LayerRun = z.output<typeof LayerRunSchema>;
|
|
63
77
|
/**
|
|
64
78
|
* A number a check measured, such as a latency or a size, with its unit and an optional allowance. A measurement over
|
|
65
79
|
* its `limit` is reported and never changes the check's status.
|
|
@@ -1211,6 +1225,12 @@ export declare const LayerDocumentSchema: z.ZodObject<{
|
|
|
1211
1225
|
unknown: "unknown";
|
|
1212
1226
|
modified: "modified";
|
|
1213
1227
|
}>>;
|
|
1228
|
+
run: z.ZodOptional<z.ZodObject<{
|
|
1229
|
+
id: z.ZodString;
|
|
1230
|
+
title: z.ZodOptional<z.ZodString>;
|
|
1231
|
+
startedAtMs: z.ZodOptional<z.ZodNumber>;
|
|
1232
|
+
endedAtMs: z.ZodOptional<z.ZodNumber>;
|
|
1233
|
+
}, z.core.$loose>>;
|
|
1214
1234
|
specs: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
1215
1235
|
path: z.ZodString;
|
|
1216
1236
|
domain: z.ZodString;
|
|
@@ -2109,6 +2129,12 @@ export declare const PublishChecksSchema: z.ZodObject<{
|
|
|
2109
2129
|
unknown: "unknown";
|
|
2110
2130
|
modified: "modified";
|
|
2111
2131
|
}>>;
|
|
2132
|
+
run: z.ZodOptional<z.ZodObject<{
|
|
2133
|
+
id: z.ZodString;
|
|
2134
|
+
title: z.ZodOptional<z.ZodString>;
|
|
2135
|
+
startedAtMs: z.ZodOptional<z.ZodNumber>;
|
|
2136
|
+
endedAtMs: z.ZodOptional<z.ZodNumber>;
|
|
2137
|
+
}, z.core.$loose>>;
|
|
2112
2138
|
specs: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
2113
2139
|
path: z.ZodString;
|
|
2114
2140
|
domain: z.ZodString;
|