@crvouga/atlas 0.17.0 → 1.1.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 +12 -0
- package/README.md +52 -8
- package/dist/core/src/cli.d.ts +1 -1
- package/dist/core/src/cli.js +1 -1
- package/dist/reports/src/garbage.d.ts +39 -0
- package/dist/reports/src/garbage.js +82 -0
- package/dist/reports/src/report-index.d.ts +6 -0
- package/dist/reports/src/report-index.js +14 -0
- package/dist/schema/src/index.d.ts +1 -0
- package/dist/schema/src/index.js +1 -0
- package/dist/schema/src/server-config.d.ts +7 -49
- package/dist/schema/src/server-config.js +8 -21
- package/dist/schema/src/user-state.d.ts +115 -0
- package/dist/schema/src/user-state.js +54 -0
- package/dist/server/src/app.d.ts +16 -2
- package/dist/server/src/app.js +62 -0
- package/dist/server/src/config.d.ts +16 -21
- package/dist/server/src/config.js +48 -57
- package/dist/server/src/dev.d.ts +1 -1
- package/dist/server/src/dev.js +20 -7
- package/dist/server/src/index.d.ts +5 -1
- package/dist/server/src/index.js +5 -1
- package/dist/server/src/main.d.ts +23 -7
- package/dist/server/src/main.js +72 -26
- package/dist/server/src/openapi.js +24 -5
- package/dist/server/src/projects.js +89 -14
- package/dist/server/src/settings.d.ts +23 -7
- package/dist/server/src/settings.js +50 -60
- package/dist/server/src/storage.d.ts +68 -0
- package/dist/server/src/storage.js +125 -0
- package/dist/server/src/user-state.d.ts +31 -0
- package/dist/server/src/user-state.js +74 -0
- package/docs/agents.md +1 -1
- package/docs/deploying.md +38 -39
- package/docs/reference/cli.md +12 -7
- package/docs/reference/http-api.md +30 -2
- package/docs/reference/reports.md +71 -0
- package/docs/reference/schema.md +102 -16
- package/docs/reference/server.md +263 -41
- package/docs/report-backends.md +0 -144
- package/docs/report-contract.md +1 -1
- package/docs/runners.md +1 -1
- package/docs/server.md +141 -36
- package/docs/statecharts.md +1 -1
- package/docs/ui.md +8 -1
- package/docs/workspaces.md +5 -2
- package/llms-full.txt +238 -234
- package/llms.txt +1 -1
- package/package.json +2 -2
- package/packages/reports/STORAGE.md +1 -1
- package/packages/schema/schemas/gc-policy.schema.json +36 -0
- package/packages/schema/schemas/gc-run-result.schema.json +106 -0
- package/packages/schema/schemas/gc-snapshot.schema.json +122 -0
- package/packages/schema/schemas/server-config.schema.json +0 -41
- package/packages/schema/schemas/server-editable-settings.schema.json +0 -41
- package/packages/schema/schemas/server-settings-snapshot.schema.json +0 -82
- package/packages/schema/schemas/user-state.schema.json +140 -0
- package/packages/server/README.md +11 -6
- package/packages/server/openapi.json +510 -126
- package/packages/visualizer/src/admin/SettingsPage.tsx +183 -88
- package/packages/visualizer/src/evidence/check-detail.tsx +6 -1
- package/packages/visualizer/src/evidence/evidence-context.tsx +9 -2
- package/packages/visualizer/src/evidence/load-state.tsx +65 -0
- package/packages/visualizer/src/evidence/overview.tsx +4 -6
- package/packages/visualizer/src/evidence/recording-page.tsx +17 -7
- package/packages/visualizer/src/evidence/spec-header.tsx +8 -6
- package/packages/visualizer/src/evidence/spec-page.tsx +98 -15
- package/packages/visualizer/src/find/Finder.tsx +45 -7
- package/packages/visualizer/src/spec-kind.test.ts +14 -1
- package/packages/visualizer/src/spec-kind.ts +28 -2
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Required S3 storage configuration
|
|
3
|
+
*
|
|
4
|
+
* The Atlas server keeps ALL reports and ALL user state (the garbage collection policy and saved settings) in
|
|
5
|
+
* S3-compatible object storage, and nowhere else: no local folder, no memory, no database. So the storage settings
|
|
6
|
+
* are required, come only from environment variables (they hold secrets), and have no defaults for where or as whom
|
|
7
|
+
* to connect. `resolveS3Config` checks every variable in one pass and reports all the problems together;
|
|
8
|
+
* `startupFailure` renders them as a banner that cannot be missed. `S3_VARIABLES` is the single description of the
|
|
9
|
+
* variables: the banner, `.env.example` and the documentation table are all made from it.
|
|
10
|
+
*
|
|
11
|
+
* @see docs/server.md#required-s3-storage
|
|
12
|
+
*/
|
|
13
|
+
type Env = Readonly<Record<string, string | undefined>>;
|
|
14
|
+
/** One environment variable of the storage configuration. */
|
|
15
|
+
export type S3Variable = {
|
|
16
|
+
readonly name: string;
|
|
17
|
+
readonly required: boolean;
|
|
18
|
+
/** Shown in the banner and the documentation. Never a real secret. */
|
|
19
|
+
readonly meaning: string;
|
|
20
|
+
readonly example: string;
|
|
21
|
+
/** Secrets are never printed, not even when invalid. */
|
|
22
|
+
readonly secret?: true;
|
|
23
|
+
/** Returns what is wrong with a value that is set, or null when it is fine. */
|
|
24
|
+
readonly check: (value: string) => string | null;
|
|
25
|
+
};
|
|
26
|
+
/** Every variable that configures storage, required ones first. */
|
|
27
|
+
export declare const S3_VARIABLES: readonly S3Variable[];
|
|
28
|
+
/** The storage settings once checked: everything needed to open the bucket, with credentials spelled out. */
|
|
29
|
+
export type S3Config = {
|
|
30
|
+
readonly endpoint: string;
|
|
31
|
+
readonly bucket: string;
|
|
32
|
+
readonly region: string;
|
|
33
|
+
readonly prefix: string;
|
|
34
|
+
readonly pathStyle: boolean;
|
|
35
|
+
readonly accessKeyId: string;
|
|
36
|
+
readonly secretAccessKey: string;
|
|
37
|
+
readonly sessionToken?: string;
|
|
38
|
+
};
|
|
39
|
+
/** The state of one variable after checking, for the banner. */
|
|
40
|
+
export type S3Finding = {
|
|
41
|
+
readonly variable: S3Variable;
|
|
42
|
+
readonly status: 'ok' | 'missing' | 'invalid' | 'unset';
|
|
43
|
+
readonly problem?: string;
|
|
44
|
+
};
|
|
45
|
+
/** The storage configuration could not be used. `findings` has every variable, not only the first problem. */
|
|
46
|
+
export declare class S3ConfigError extends Error {
|
|
47
|
+
readonly findings: readonly S3Finding[];
|
|
48
|
+
readonly reason: string;
|
|
49
|
+
constructor(findings: readonly S3Finding[], reason?: string);
|
|
50
|
+
}
|
|
51
|
+
/** Looks at every storage variable and returns what each is. Nothing here throws. */
|
|
52
|
+
export declare function inspectS3Env(env: Env): readonly S3Finding[];
|
|
53
|
+
/**
|
|
54
|
+
* The storage settings from the environment. Throws {@link S3ConfigError}, whose message lists every variable and
|
|
55
|
+
* marks each missing or invalid one, when any required variable is absent or any set variable is malformed.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* const s3 = resolveS3Config(process.env);
|
|
59
|
+
*/
|
|
60
|
+
export declare function resolveS3Config(env: Env): S3Config;
|
|
61
|
+
/** Where the storage is, without a secret: what the log may say. */
|
|
62
|
+
export declare const describeS3: (config: S3Config) => string;
|
|
63
|
+
/**
|
|
64
|
+
* The banner printed when the server cannot start because of storage: why storage is required, then EVERY variable by
|
|
65
|
+
* name with what is wrong with it. Values of secrets are never printed.
|
|
66
|
+
*/
|
|
67
|
+
export declare function startupFailure(findings: readonly S3Finding[], reason: string, detail?: string): string;
|
|
68
|
+
export {};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Required S3 storage configuration
|
|
3
|
+
*
|
|
4
|
+
* The Atlas server keeps ALL reports and ALL user state (the garbage collection policy and saved settings) in
|
|
5
|
+
* S3-compatible object storage, and nowhere else: no local folder, no memory, no database. So the storage settings
|
|
6
|
+
* are required, come only from environment variables (they hold secrets), and have no defaults for where or as whom
|
|
7
|
+
* to connect. `resolveS3Config` checks every variable in one pass and reports all the problems together;
|
|
8
|
+
* `startupFailure` renders them as a banner that cannot be missed. `S3_VARIABLES` is the single description of the
|
|
9
|
+
* variables: the banner, `.env.example` and the documentation table are all made from it.
|
|
10
|
+
*
|
|
11
|
+
* @see docs/server.md#required-s3-storage
|
|
12
|
+
*/
|
|
13
|
+
const hostOf = (value) => {
|
|
14
|
+
try {
|
|
15
|
+
const url = new URL(value);
|
|
16
|
+
if (url.protocol !== 'https:' && url.protocol !== 'http:')
|
|
17
|
+
return 'must start with https:// (or http:// for a local S3 server)';
|
|
18
|
+
if (url.username || url.password)
|
|
19
|
+
return 'must not contain a user name or password; credentials have their own variables';
|
|
20
|
+
if (url.search || url.hash)
|
|
21
|
+
return 'must be just the address, with no query or fragment';
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return 'is not a URL; write it like https://s3.eu-west-1.amazonaws.com';
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
/** Every variable that configures storage, required ones first. */
|
|
29
|
+
export const S3_VARIABLES = [
|
|
30
|
+
{ name: 'ATLAS_S3_ENDPOINT', required: true, meaning: 'S3 API endpoint of the object storage service', example: 'https://s3.eu-west-1.amazonaws.com', check: hostOf },
|
|
31
|
+
{ name: 'ATLAS_S3_BUCKET', required: true, meaning: 'Bucket that holds reports and user state; it must already exist', example: 'my-atlas-reports',
|
|
32
|
+
check: value => (/^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/.test(value) ? null : 'must be 3-63 lowercase letters, digits, dots or hyphens, starting and ending with a letter or digit') },
|
|
33
|
+
{ name: 'ATLAS_S3_REGION', required: true, meaning: 'Region of the bucket (`auto` for Cloudflare R2, `us-east-1` for most self-hosted servers)', example: 'eu-west-1',
|
|
34
|
+
check: value => (/^[a-z0-9][a-z0-9-]*$/.test(value) ? null : 'must be lowercase letters, digits and hyphens, like eu-west-1') },
|
|
35
|
+
{ name: 'ATLAS_S3_ACCESS_KEY_ID', required: true, meaning: 'Access key id of an account that may list, read, write and delete in the bucket', example: 'AKIAEXAMPLE', secret: true,
|
|
36
|
+
check: value => (/^\S+$/.test(value) ? null : 'must not contain spaces or line breaks') },
|
|
37
|
+
{ name: 'ATLAS_S3_SECRET_ACCESS_KEY', required: true, meaning: 'Secret key that goes with the access key id', example: 'change-me', secret: true,
|
|
38
|
+
check: value => (/^\S+$/.test(value) ? null : 'must not contain spaces or line breaks') },
|
|
39
|
+
{ name: 'ATLAS_S3_PREFIX', required: false, meaning: 'Folder inside the bucket for everything Atlas writes (default: the bucket root)', example: 'atlas',
|
|
40
|
+
check: value => (/^[\w./-]+$/.test(value) && !value.split('/').includes('..') ? null : 'may contain only letters, digits, `.`, `_`, `-` and `/`, and no `..`') },
|
|
41
|
+
{ name: 'ATLAS_S3_PATH_STYLE', required: false, meaning: 'Address the bucket as endpoint/bucket/key (default: yes; set 0 for virtual-hosted style)', example: '1',
|
|
42
|
+
check: value => (['1', '0', 'true', 'false'].includes(value) ? null : 'must be 1, 0, true or false') },
|
|
43
|
+
{ name: 'ATLAS_S3_SESSION_TOKEN', required: false, meaning: 'Session token, only for temporary credentials', example: '', secret: true,
|
|
44
|
+
check: value => (/^\S+$/.test(value) ? null : 'must not contain spaces or line breaks') }
|
|
45
|
+
];
|
|
46
|
+
/** The storage configuration could not be used. `findings` has every variable, not only the first problem. */
|
|
47
|
+
export class S3ConfigError extends Error {
|
|
48
|
+
findings;
|
|
49
|
+
reason;
|
|
50
|
+
constructor(findings, reason = 'the S3 configuration is missing or invalid') {
|
|
51
|
+
super(startupFailure(findings, reason));
|
|
52
|
+
this.findings = findings;
|
|
53
|
+
this.reason = reason;
|
|
54
|
+
this.name = 'S3ConfigError';
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** Looks at every storage variable and returns what each is. Nothing here throws. */
|
|
58
|
+
export function inspectS3Env(env) {
|
|
59
|
+
return S3_VARIABLES.map(variable => {
|
|
60
|
+
const raw = env[variable.name];
|
|
61
|
+
if (raw === undefined || raw.trim() === '')
|
|
62
|
+
return { variable, status: variable.required ? 'missing' : 'unset' };
|
|
63
|
+
const problem = variable.check(raw.trim());
|
|
64
|
+
return problem ? { variable, status: 'invalid', problem } : { variable, status: 'ok' };
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The storage settings from the environment. Throws {@link S3ConfigError}, whose message lists every variable and
|
|
69
|
+
* marks each missing or invalid one, when any required variable is absent or any set variable is malformed.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* const s3 = resolveS3Config(process.env);
|
|
73
|
+
*/
|
|
74
|
+
export function resolveS3Config(env) {
|
|
75
|
+
const findings = inspectS3Env(env);
|
|
76
|
+
if (findings.some(finding => finding.status === 'missing' || finding.status === 'invalid'))
|
|
77
|
+
throw new S3ConfigError(findings);
|
|
78
|
+
const value = (name) => env[name]?.trim() ?? '';
|
|
79
|
+
const style = value('ATLAS_S3_PATH_STYLE');
|
|
80
|
+
return {
|
|
81
|
+
endpoint: value('ATLAS_S3_ENDPOINT').replace(/\/+$/, ''), bucket: value('ATLAS_S3_BUCKET'), region: value('ATLAS_S3_REGION'),
|
|
82
|
+
prefix: value('ATLAS_S3_PREFIX').replace(/^\/+|\/+$/g, ''), pathStyle: style === '' ? true : style === '1' || style === 'true',
|
|
83
|
+
accessKeyId: value('ATLAS_S3_ACCESS_KEY_ID'), secretAccessKey: value('ATLAS_S3_SECRET_ACCESS_KEY'),
|
|
84
|
+
...(value('ATLAS_S3_SESSION_TOKEN') ? { sessionToken: value('ATLAS_S3_SESSION_TOKEN') } : {})
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/** Where the storage is, without a secret: what the log may say. */
|
|
88
|
+
export const describeS3 = (config) => `s3://${config.bucket}/${config.prefix ? `${config.prefix}/` : ''} on ${new URL(config.endpoint).host} (${config.region})`;
|
|
89
|
+
const RULE = '='.repeat(78);
|
|
90
|
+
/**
|
|
91
|
+
* The banner printed when the server cannot start because of storage: why storage is required, then EVERY variable by
|
|
92
|
+
* name with what is wrong with it. Values of secrets are never printed.
|
|
93
|
+
*/
|
|
94
|
+
export function startupFailure(findings, reason, detail) {
|
|
95
|
+
const width = Math.max(...findings.map(finding => finding.variable.name.length));
|
|
96
|
+
const mark = (finding) => finding.status === 'missing' ? '[MISSING]' : finding.status === 'invalid' ? '[INVALID]' : finding.status === 'ok' ? '[ok] ' : '[not set]';
|
|
97
|
+
const line = (finding) => {
|
|
98
|
+
const { variable } = finding;
|
|
99
|
+
const note = finding.status === 'invalid' ? `${finding.problem}` : variable.meaning;
|
|
100
|
+
return ` ${mark(finding)} ${variable.name.padEnd(width)} ${note}`;
|
|
101
|
+
};
|
|
102
|
+
const required = findings.filter(finding => finding.variable.required);
|
|
103
|
+
const optional = findings.filter(finding => !finding.variable.required);
|
|
104
|
+
const bad = findings.filter(finding => finding.status === 'missing' || finding.status === 'invalid').map(finding => finding.variable.name);
|
|
105
|
+
return [
|
|
106
|
+
'', RULE,
|
|
107
|
+
' ATLAS CANNOT START: S3 STORAGE IS NOT USABLE',
|
|
108
|
+
RULE,
|
|
109
|
+
` Why: ${reason}.`,
|
|
110
|
+
' Reason: Atlas stores ALL reports and ALL user state (the garbage collection policy and saved',
|
|
111
|
+
' settings) in S3-compatible object storage, and nowhere else. There is no local-disk,',
|
|
112
|
+
' in-memory or database fallback, so the server refuses to start without working S3.',
|
|
113
|
+
...(detail ? ['', ` S3 said: ${detail}`] : []),
|
|
114
|
+
'',
|
|
115
|
+
' Required environment variables:',
|
|
116
|
+
...required.map(line),
|
|
117
|
+
'',
|
|
118
|
+
' Optional environment variables:',
|
|
119
|
+
...optional.map(line),
|
|
120
|
+
'',
|
|
121
|
+
bad.length ? ` Fix these ${bad.length}: ${bad.join(', ')}` : ' Every variable is well-formed, so the values themselves are wrong; check them against the storage service.',
|
|
122
|
+
' See .env.example for a complete example and docs/server.md#required-s3-storage.',
|
|
123
|
+
' Exiting with status 2.', RULE, ''
|
|
124
|
+
].join('\n');
|
|
125
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ObjectStore } from '../../reports/src/object-store.js';
|
|
2
|
+
import { type UserState } from '../../schema/src/user-state.js';
|
|
3
|
+
/** Where the user state lives, relative to the store (the bucket plus `ATLAS_S3_PREFIX`). */
|
|
4
|
+
export declare const USER_STATE_KEY = "server/state.json";
|
|
5
|
+
/** The user state as stored: the document, its `revision` and whether the object `exists` yet. */
|
|
6
|
+
export type StoredUserState = {
|
|
7
|
+
readonly state: UserState;
|
|
8
|
+
readonly revision: string;
|
|
9
|
+
readonly exists: boolean;
|
|
10
|
+
};
|
|
11
|
+
/** Raised when the stored user state cannot be trusted. The message says where it is and what to do. */
|
|
12
|
+
export declare class UserStateError extends Error {
|
|
13
|
+
}
|
|
14
|
+
/** The user state of one server: read it, or change it against the revision that was read. */
|
|
15
|
+
export type UserStateStore = {
|
|
16
|
+
/** Reads the document. Throws {@link UserStateError} when it exists but is unreadable or invalid. */
|
|
17
|
+
read(): Promise<StoredUserState>;
|
|
18
|
+
/**
|
|
19
|
+
* Applies `change` to the stored state and writes the result, if `revision` is still the stored one. Throws a
|
|
20
|
+
* `UserStateError` starting with "Settings changed" for a stale revision.
|
|
21
|
+
*/
|
|
22
|
+
update(revision: string, change: (state: UserState) => UserState): Promise<StoredUserState>;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* The user state kept in `store` at {@link USER_STATE_KEY}.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* const userState = userStateStore(s3Backend.store);
|
|
29
|
+
* const { state } = await userState.read();
|
|
30
|
+
*/
|
|
31
|
+
export declare function userStateStore(store: ObjectStore): UserStateStore;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module User state in S3
|
|
3
|
+
*
|
|
4
|
+
* Reads and writes the user state document (`UserStateSchema`: the garbage collection policy and the saved settings) as
|
|
5
|
+
* one JSON object at `server/state.json` in the object store, which in a running server is the S3 bucket. This is the
|
|
6
|
+
* only place user state is kept: not a file, not a database, not the browser. A missing object is an empty state
|
|
7
|
+
* ("nothing chosen yet"); an object that cannot be read or does not match the schema is an error that stops the
|
|
8
|
+
* server from starting and is never overwritten, so a transient or corrupt read can never silently reset anyone's choices.
|
|
9
|
+
* Saving refuses a stale revision (the SHA-256 of the stored text), serialised within this process. An object store has
|
|
10
|
+
* no conditional writes, so two servers saving at the same instant could still race; run one server per bucket.
|
|
11
|
+
*
|
|
12
|
+
* @see docs/server.md#user-state-and-garbage-collection
|
|
13
|
+
*/
|
|
14
|
+
import { createHash } from 'node:crypto';
|
|
15
|
+
import { readUtf8, writeUtf8 } from '../../reports/src/object-io.js';
|
|
16
|
+
import { UserStateSchema } from '../../schema/src/user-state.js';
|
|
17
|
+
/** Where the user state lives, relative to the store (the bucket plus `ATLAS_S3_PREFIX`). */
|
|
18
|
+
export const USER_STATE_KEY = 'server/state.json';
|
|
19
|
+
/** Raised when the stored user state cannot be trusted. The message says where it is and what to do. */
|
|
20
|
+
export class UserStateError extends Error {
|
|
21
|
+
}
|
|
22
|
+
const digest = (text) => createHash('sha256').update(text).digest('hex');
|
|
23
|
+
/**
|
|
24
|
+
* The user state kept in `store` at {@link USER_STATE_KEY}.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* const userState = userStateStore(s3Backend.store);
|
|
28
|
+
* const { state } = await userState.read();
|
|
29
|
+
*/
|
|
30
|
+
export function userStateStore(store) {
|
|
31
|
+
let pending = Promise.resolve();
|
|
32
|
+
const read = async () => {
|
|
33
|
+
let text;
|
|
34
|
+
try {
|
|
35
|
+
text = await readUtf8(store, USER_STATE_KEY);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
throw new UserStateError(`The user state ${USER_STATE_KEY} could not be read from S3: ${error instanceof Error ? error.message : String(error)}`);
|
|
39
|
+
}
|
|
40
|
+
if (text === null)
|
|
41
|
+
return { state: { schemaVersion: 1 }, revision: digest(''), exists: false };
|
|
42
|
+
let json;
|
|
43
|
+
try {
|
|
44
|
+
json = JSON.parse(text);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
throw new UserStateError(`The user state ${USER_STATE_KEY} in S3 is not valid JSON. Atlas will not overwrite it: fix or delete that object, then restart.`);
|
|
48
|
+
}
|
|
49
|
+
const parsed = UserStateSchema.safeParse(json);
|
|
50
|
+
if (!parsed.success)
|
|
51
|
+
throw new UserStateError(`The user state ${USER_STATE_KEY} in S3 is invalid (${parsed.error.issues.map(issue => `${issue.path.join('.') || '(document)'}: ${issue.message}`).join('; ')}). Atlas will not overwrite it: fix or delete that object, then restart.`);
|
|
52
|
+
// The document as written, once it is known to be valid: defaults are not filled in, so a setting nobody chose stays unchosen.
|
|
53
|
+
return { state: json, revision: digest(text), exists: true };
|
|
54
|
+
};
|
|
55
|
+
return {
|
|
56
|
+
read,
|
|
57
|
+
update(revision, change) {
|
|
58
|
+
const run = pending.then(async () => {
|
|
59
|
+
const held = await read();
|
|
60
|
+
if (held.revision !== revision)
|
|
61
|
+
throw new UserStateError('Settings changed. Reload them before saving.');
|
|
62
|
+
const next = { ...change(held.state), schemaVersion: 1 };
|
|
63
|
+
const checked = UserStateSchema.safeParse(next);
|
|
64
|
+
if (!checked.success)
|
|
65
|
+
throw new UserStateError(checked.error.issues.map(issue => `${issue.path.join('.') || '(document)'}: ${issue.message}`).join('; '));
|
|
66
|
+
const text = `${JSON.stringify(next, null, 2)}\n`;
|
|
67
|
+
await writeUtf8(store, USER_STATE_KEY, text, 'application/json');
|
|
68
|
+
return { state: next, revision: digest(text), exists: true };
|
|
69
|
+
});
|
|
70
|
+
pending = run.catch(() => undefined);
|
|
71
|
+
return run;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
package/docs/agents.md
CHANGED
|
@@ -144,7 +144,7 @@ workspace selection and test scripts. An existing `atlas.yml` not generated by A
|
|
|
144
144
|
workflows may still need manual integration. Non-JavaScript suites need their language/tool setup in CI.
|
|
145
145
|
|
|
146
146
|
Without a server, start with [local verification](quickstart.md). To run a server later, `atlas serve`
|
|
147
|
-
starts one (`http://127.0.0.1:8080
|
|
147
|
+
starts one (`http://127.0.0.1:8080`; it needs S3-compatible storage configured with the `ATLAS_S3_*` variables, see `.env.example`); [the server guide](server.md) covers configuration
|
|
148
148
|
and authentication. Keep synthetic failure checks in a scratch store rather than a shared production report.
|
|
149
149
|
|
|
150
150
|
## What an agent can rely on
|
package/docs/deploying.md
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
This is the production setup. A project runs **one Atlas server**. Developers' machines and every CI job send
|
|
4
4
|
their results to it, as they finish or when they are done, and it shows everyone, developers and the business
|
|
5
|
-
alike, the whole project at once, live. It keeps results
|
|
6
|
-
|
|
5
|
+
alike, the whole project at once, live. It keeps results (and all its user state) in an S3-compatible bucket and
|
|
6
|
+
nowhere else; nothing is partitioned by run, branch or person: results are layered. The server's API,
|
|
7
7
|
configuration and protocol are in **[the Atlas server](server.md)**; this page is the path to production.
|
|
8
8
|
|
|
9
9
|
```
|
|
10
10
|
a developer's worktree ─┐ ┌─▶ developers (browser, live)
|
|
11
|
-
a CI job, any branch ───┼─ atlas report publish|stream ─▶ SERVER ◀┤ atlas serve: keeps layers in
|
|
12
|
-
a device lab, nightly ──┘ (immutable layers, over HTTP)
|
|
11
|
+
a CI job, any branch ───┼─ atlas report publish|stream ─▶ SERVER ◀┤ atlas serve: keeps layers in S3
|
|
12
|
+
a device lab, nightly ──┘ (immutable layers, over HTTP) ──▶ S3 ├─▶ business (required, nothing else)
|
|
13
13
|
└─▶ CI gate atlas report status
|
|
14
14
|
```
|
|
15
15
|
|
|
@@ -18,7 +18,7 @@ Why this holds together:
|
|
|
18
18
|
- **Publishing never conflicts.** A layer is one immutable file named by its content. Two machines that
|
|
19
19
|
publish at once write different files; a retry writes the same one. There is no lock, no index to
|
|
20
20
|
corrupt, nothing to merge.
|
|
21
|
-
- **The server is replaceable.** What it knows is its
|
|
21
|
+
- **The server is replaceable.** What it knows is in its bucket. Replace it, restore it
|
|
22
22
|
from a backup, or run a copy on a laptop: they show the same report, because a report is a function of the
|
|
23
23
|
layers ([the contract](report-contract.md)).
|
|
24
24
|
- **One view.** The newest completed result of every check, in every environment, from anyone. A
|
|
@@ -36,13 +36,15 @@ Why this holds together:
|
|
|
36
36
|
|
|
37
37
|
## 1. Where results are kept
|
|
38
38
|
|
|
39
|
-
**
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
**The server keeps everything in one S3-compatible bucket** (reports and user state, such as the garbage collection
|
|
40
|
+
policy), set by the required `ATLAS_S3_ENDPOINT`, `ATLAS_S3_BUCKET`, `ATLAS_S3_REGION`, `ATLAS_S3_ACCESS_KEY_ID` and
|
|
41
|
+
`ATLAS_S3_SECRET_ACCESS_KEY` variables ([details](server.md#required-s3-storage), template in
|
|
42
|
+
[`.env.example`](https://github.com/crvouga/atlas/blob/main/.env.example)). Without them it refuses to start, listing
|
|
43
|
+
every variable and what is wrong. There is no disk or database to fall back to, so the server is replaceable:
|
|
44
|
+
a new one started on the same bucket shows the same report.
|
|
42
45
|
|
|
43
|
-
**
|
|
44
|
-
|
|
45
|
-
The rest of this section is that setup. A store is named by a URL. Keys never go in it.
|
|
46
|
+
**Clients and CI** name stores by URL, and a client may publish straight to a bucket or folder as well as to the
|
|
47
|
+
server. A store is named by a URL. Keys never go in it.
|
|
46
48
|
|
|
47
49
|
| Store | URL |
|
|
48
50
|
| --- | --- |
|
|
@@ -190,18 +192,16 @@ A run is uploaded only when its privacy scan passed: the same rule the static cl
|
|
|
190
192
|
## 4. The server
|
|
191
193
|
|
|
192
194
|
```sh
|
|
193
|
-
atlas serve #
|
|
195
|
+
atlas serve # ATLAS_S3_* says where; listens on :8080
|
|
194
196
|
```
|
|
195
197
|
|
|
196
|
-
It serves the client, the report and the project's specs and runs from
|
|
197
|
-
accepts uploads and announces new results on its event stream.
|
|
198
|
-
|
|
198
|
+
It serves the client, the report and the project's specs and runs from its bucket. It also
|
|
199
|
+
accepts uploads and announces new results on its event stream. The bucket's credentials need list, read, write and delete
|
|
200
|
+
permissions (`atlas report policy` prints a least-privilege policy; delete is for the garbage collection you define in the UI).
|
|
199
201
|
|
|
200
202
|
| Flag | Environment | Meaning |
|
|
201
203
|
| --- | --- | --- |
|
|
202
|
-
|
|
|
203
|
-
| `--data <dir>` | `ATLAS_DATA` | Local upload backend. Default `./atlas-data`. |
|
|
204
|
-
| `--write <id>` | `ATLAS_WRITE` | Backend that accepts uploads. Default `data`. |
|
|
204
|
+
| | `ATLAS_S3_ENDPOINT`, `ATLAS_S3_BUCKET`, `ATLAS_S3_REGION`, `ATLAS_S3_ACCESS_KEY_ID`, `ATLAS_S3_SECRET_ACCESS_KEY` | **Required.** The S3-compatible bucket that holds all reports and user state. Optional: `ATLAS_S3_PREFIX`, `ATLAS_S3_PATH_STYLE`, `ATLAS_S3_SESSION_TOKEN`. |
|
|
205
205
|
| `--port <n>` | `ATLAS_PORT` or `PORT` | Default 8080. `0` picks a free one. |
|
|
206
206
|
| `--host <h>` | `ATLAS_HOST` or `HOST` | Default `127.0.0.1`. Listening beyond this machine requires tokens or `--insecure`. |
|
|
207
207
|
| `--site <dir>` | `ATLAS_SITE` | The built client. Default: `./atlas-site`, else built once into `~/.cache/atlas/site`. |
|
|
@@ -228,9 +228,10 @@ updated `latest`. Pull request builds and failed publications are labelled accor
|
|
|
228
228
|
Leave the start command unset: the image includes the server and the prebuilt viewer.
|
|
229
229
|
2. Add `ATLAS_TOKEN` with a long random value (generate one with `openssl rand -hex 24`). It grants admin
|
|
230
230
|
access; optionally add separate `ATLAS_PUBLISH_TOKEN` and `ATLAS_READ_TOKEN` for publishers and readers.
|
|
231
|
-
3.
|
|
232
|
-
|
|
233
|
-
|
|
231
|
+
3. Add the five required storage variables (`ATLAS_S3_ENDPOINT`, `ATLAS_S3_BUCKET`, `ATLAS_S3_REGION`,
|
|
232
|
+
`ATLAS_S3_ACCESS_KEY_ID`, `ATLAS_S3_SECRET_ACCESS_KEY`) for a bucket you own. The service keeps nothing on its own
|
|
233
|
+
disk, so no volume is needed and a replaced image loses nothing; without the variables it exits and lists each one.
|
|
234
|
+
Keep one replica.
|
|
234
235
|
4. Set the healthcheck path to `/readyz` and generate a public domain. The image listens on `0.0.0.0` and
|
|
235
236
|
honors Railway's `PORT`; leave `ATLAS_PORT` unset. Open the domain and sign in with your token as the
|
|
236
237
|
password (any username), or use the viewer's token sign-in.
|
|
@@ -244,8 +245,7 @@ Projects publish to that domain with `ATLAS_STORE=https://YOUR-DOMAIN` and their
|
|
|
244
245
|
[sending results](#3-send-results-from-ci-and-from-laptops). Hosting Atlas needs no project checkout; publishing
|
|
245
246
|
your project's test results still needs the integration described there.
|
|
246
247
|
|
|
247
|
-
|
|
248
|
-
and the bucket credentials. Keep the `/data` volume for server settings and local state.
|
|
248
|
+
Then open `/admin` to define the garbage collection policy; until you save one, nothing is deleted.
|
|
249
249
|
|
|
250
250
|
### Deploy without authentication
|
|
251
251
|
|
|
@@ -261,12 +261,11 @@ In Railway, create a **Docker image** service with:
|
|
|
261
261
|
ghcr.io/crvouga/atlas-insecure:latest
|
|
262
262
|
```
|
|
263
263
|
|
|
264
|
-
Leave the start command unset.
|
|
264
|
+
Leave the start command unset. Add the required `ATLAS_S3_*` storage variables, keep one
|
|
265
265
|
replica, set the healthcheck to `/readyz` and generate a public domain. Leave `ATLAS_PORT` unset so Railway's
|
|
266
266
|
`PORT` is used. Enable **Settings → Source → Configure Auto Updates → Anytime** to follow `latest`.
|
|
267
267
|
Do not set `ATLAS_TOKEN`, `ATLAS_PUBLISH_TOKEN` or `ATLAS_READ_TOKEN`, or add `auth.tokens` to
|
|
268
|
-
|
|
269
|
-
authenticated deployment, remove its saved auth tokens first. Open the domain directly, including `/admin`
|
|
268
|
+
a configuration file: configured tokens still enable authentication. Open the domain directly, including `/admin`
|
|
270
269
|
for settings (leave the token field blank and choose **Load settings**).
|
|
271
270
|
|
|
272
271
|
Verify anonymous reads and uploads without an Authorization header:
|
|
@@ -287,7 +286,7 @@ leave its `ATLAS_TOKEN` unset. Use the same publishing commands described in
|
|
|
287
286
|
For local Docker:
|
|
288
287
|
|
|
289
288
|
```sh
|
|
290
|
-
docker run --pull always -p 8080:8080 -
|
|
289
|
+
docker run --pull always -p 8080:8080 --env-file .env ghcr.io/crvouga/atlas-insecure:latest
|
|
291
290
|
```
|
|
292
291
|
|
|
293
292
|
Both images publish full commit tags for pinning or rollback. A new GHCR package must be made Public
|
|
@@ -297,10 +296,11 @@ once before Railway can pull without registry credentials; see
|
|
|
297
296
|
### Docker
|
|
298
297
|
|
|
299
298
|
The same published image works anywhere Docker runs. It already contains the client (the same
|
|
300
|
-
for every project: the server tells the page where its data is), and keeps
|
|
299
|
+
for every project: the server tells the page where its data is), and keeps nothing on its own disk: it needs the
|
|
300
|
+
`ATLAS_S3_*` variables (copy `.env.example` to `.env` and fill it in):
|
|
301
301
|
|
|
302
302
|
```sh
|
|
303
|
-
docker run --pull always -p 8080:8080 -
|
|
303
|
+
docker run --pull always -p 8080:8080 --env-file .env \
|
|
304
304
|
-e ATLAS_TOKEN=$(openssl rand -hex 24) ghcr.io/crvouga/atlas:latest
|
|
305
305
|
```
|
|
306
306
|
|
|
@@ -309,17 +309,15 @@ back, replace `latest` with the full commit SHA. To build from this repository,
|
|
|
309
309
|
[`examples/deploy/Dockerfile`](https://github.com/crvouga/atlas/blob/main/examples/deploy/Dockerfile) is the
|
|
310
310
|
alternative for building from an npm version or a local package tarball.
|
|
311
311
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
instance and set no keys.
|
|
312
|
+
The bucket's account needs the combined **both** policy (including `server/live/*` for producer leases and `server/state.json`
|
|
313
|
+
for user state) plus `s3:DeleteObject` for garbage collection. The server does not use ambient AWS roles: pass the keys.
|
|
315
314
|
|
|
316
315
|
**Try it on one machine.** [`examples/deploy/compose.yaml`](https://github.com/crvouga/atlas/blob/main/examples/deploy/compose.yaml)
|
|
317
|
-
starts the published image
|
|
316
|
+
starts the published image with an S3 server (SeaweedFS) and a bucket as its storage; `compose.s3.yaml` is the same stack without tokens.
|
|
318
317
|
Publish to `http://localhost:8080` from your project and open it.
|
|
319
318
|
|
|
320
319
|
**Kubernetes.** [`examples/deploy/kubernetes.yaml`](https://github.com/crvouga/atlas/blob/main/examples/deploy/kubernetes.yaml):
|
|
321
|
-
one copy (the server is a singleton for the project), results in the bucket, readiness on `/readyz
|
|
322
|
-
service account.
|
|
320
|
+
one copy (the server is a singleton for the project), results in the bucket (keys from a Secret), readiness on `/readyz`.
|
|
323
321
|
|
|
324
322
|
**Without a container.** `bun add -d @crvouga/atlas`, then `bunx atlas client build --no-data --out
|
|
325
323
|
atlas-site` and `bunx atlas serve`. Everything it can be told is in
|
|
@@ -392,12 +390,13 @@ Narrow it by choice, per consumer, never by moving data:
|
|
|
392
390
|
listing (one request per thousand layers) runs at the start and every 30 refreshes (five minutes by
|
|
393
391
|
default), which is also how a layer recorded with an old timestamp is found. One small `GET` per new
|
|
394
392
|
layer, once.
|
|
395
|
-
- **Retention.** Atlas deletes nothing
|
|
396
|
-
|
|
393
|
+
- **Retention.** Atlas deletes nothing until you define a garbage collection policy in the UI (`/admin`); it is kept in the bucket as JSON and
|
|
394
|
+
then removes only superseded layers older than you allow ([details](server.md#user-state-and-garbage-collection)). `atlas report doctor` shows what the bucket holds.
|
|
395
|
+
- **Disaster recovery.** The bucket is the only state (reports and user state). Back it up, replicate or version it as you
|
|
397
396
|
do any data; a server is rebuilt from nothing in the time it takes to read the layers (below). It listens at once
|
|
398
397
|
and answers `/readyz` 503 until it has, so a liveness probe never kills one that is still reading.
|
|
399
|
-
- **A second bucket.**
|
|
400
|
-
report,
|
|
398
|
+
- **A second bucket.** The server reads one bucket. To combine another team's layers or an archive into the
|
|
399
|
+
report, copy them in with `atlas report merge --into <store>`; clients and `atlas report status` may read several stores.
|
|
401
400
|
|
|
402
401
|
## 7. Performance
|
|
403
402
|
|
package/docs/reference/cli.md
CHANGED
|
@@ -16,7 +16,7 @@ atlas — executable statecharts
|
|
|
16
16
|
[--no-retry] [--output <dir>]
|
|
17
17
|
atlas merge [--config atlas.config.ts] [--output <dir>] <run-dir> <run-dir>...
|
|
18
18
|
atlas export --specs <dir> --format scxml|mermaid|markdown|json [--out <file>]
|
|
19
|
-
atlas server [--
|
|
19
|
+
atlas server [--port 8080] one Atlas node: API, projects and client; needs ATLAS_S3_* (atlas serve --help)
|
|
20
20
|
atlas client build [--no-data] [--out <dir>] build the project-agnostic Atlas client
|
|
21
21
|
atlas dev [--port 5180] develop the Atlas server and client on one host
|
|
22
22
|
atlas init [--server <url>] [--write] integrate this project: detect its suites, print the plan (atlas init --help)
|
|
@@ -53,14 +53,18 @@ atlas init — integrate a project with an Atlas server
|
|
|
53
53
|
```text
|
|
54
54
|
atlas serve: the Atlas server
|
|
55
55
|
|
|
56
|
-
atlas serve [--config atlas.server.json] [--
|
|
57
|
-
[--store <url|id=url>]... [--write <id>] [--site <dir>]
|
|
56
|
+
atlas serve [--config atlas.server.json] [--port 8080] [--host 127.0.0.1] [--site <dir>]
|
|
58
57
|
[--outline-branch <b>] [--branch <name>]... [--ignore-modified] [--refresh <seconds>]
|
|
59
58
|
[--anonymous-read] [--insecure] [--no-log]
|
|
60
59
|
|
|
61
60
|
One process serves the client and the API. Test results are uploaded to it from anywhere (laptops, CI) and it
|
|
62
|
-
shows everyone the combined report, live.
|
|
63
|
-
|
|
61
|
+
shows everyone the combined report, live.
|
|
62
|
+
|
|
63
|
+
Storage is required. ALL reports and ALL user state (the garbage collection policy, saved settings) are kept in
|
|
64
|
+
S3-compatible object storage and nowhere else; there is no local-disk, in-memory or database fallback. Without
|
|
65
|
+
these environment variables the server prints every one of them with what is wrong, and exits with status 2:
|
|
66
|
+
ATLAS_S3_ENDPOINT ATLAS_S3_BUCKET ATLAS_S3_REGION ATLAS_S3_ACCESS_KEY_ID ATLAS_S3_SECRET_ACCESS_KEY
|
|
67
|
+
Optional: ATLAS_S3_PREFIX, ATLAS_S3_PATH_STYLE, ATLAS_S3_SESSION_TOKEN. See .env.example.
|
|
64
68
|
|
|
65
69
|
Tokens (ATLAS_TOKEN admin, ATLAS_PUBLISH_TOKEN, ATLAS_READ_TOKEN, or "auth.tokens" in the config) are required
|
|
66
70
|
to listen beyond this machine unless --insecure. Every option has an environment variable (docs/deploying.md).
|
|
@@ -71,11 +75,12 @@ atlas serve: the Atlas server
|
|
|
71
75
|
```text
|
|
72
76
|
atlas dev: the Atlas server and the client, for working on either
|
|
73
77
|
|
|
74
|
-
atlas dev [--port 5180] [--host 127.0.0.1] [--
|
|
78
|
+
atlas dev [--port 5180] [--host 127.0.0.1] [--config atlas.server.json]
|
|
75
79
|
|
|
76
80
|
Starts the server (API only, on a port of its own) and the client's dev server, which proxies /api to it, so the
|
|
77
81
|
browser talks to one host and a change to the client shows at once. In production `atlas serve` does both from
|
|
78
|
-
one process.
|
|
82
|
+
one process. Storage is the same as for `atlas serve`: the required ATLAS_S3_* environment variables name the S3
|
|
83
|
+
bucket that holds all reports and user state; without them it exits with status 2 and lists every variable.
|
|
79
84
|
```
|
|
80
85
|
|
|
81
86
|
## atlas report
|
|
@@ -116,7 +116,7 @@ Responses: `200` Settings editor.; `401` No token, or an unknown one. Send `Auth
|
|
|
116
116
|
|
|
117
117
|
`GET /api/settings` · role: admin
|
|
118
118
|
|
|
119
|
-
Returns editable
|
|
119
|
+
Returns the editable report and live-run settings saved in the user state in S3, active settings, a revision, and configuration precedence. Storage and authentication settings are excluded. Requires an admin Bearer token. Embedded applications may manage settings outside this API.
|
|
120
120
|
|
|
121
121
|
Responses: `200` Settings and revision.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `403` The token is known but its role may not do this.; `503` Configuration is managed by the embedding application.
|
|
122
122
|
|
|
@@ -124,12 +124,40 @@ Responses: `200` Settings and revision.; `401` No token, or an unknown one. Send
|
|
|
124
124
|
|
|
125
125
|
`PUT /api/settings` · role: admin
|
|
126
126
|
|
|
127
|
-
Replaces editable
|
|
127
|
+
Replaces the editable report and live-run settings in the user state in S3 (`server/state.json`), preserving the garbage collection policy and other fields. Persists a validated configuration; no report data is moved. Environment variables and flags override these values after restart. Requires an admin Bearer token, same-origin requests in browsers, and a current revision.
|
|
128
128
|
|
|
129
129
|
- `If-Match` (header, required): The quoted revision returned by GET /api/settings.
|
|
130
130
|
|
|
131
131
|
Responses: `200` Saved settings and new revision. Restart required.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `403` The token is known but its role may not do this.; `409` Settings changed; reload before saving.; `413` Settings exceed the body limit.; `422` Invalid configuration.; `428` A quoted settings revision is required.; `503` Configuration is managed by the embedding application.
|
|
132
132
|
|
|
133
|
+
### Read the garbage collection policy
|
|
134
|
+
|
|
135
|
+
`GET /api/settings/gc` · role: admin
|
|
136
|
+
|
|
137
|
+
Returns the policy kept in the user state in S3 (`server/state.json`), or `policy: null, saved: false` when none has been saved yet, in which case garbage collection is off and nothing is deleted. Also returns the revision to send back when saving and the last pass over each project. Requires an admin Bearer token. If the stored user state is unreadable the answer is 503 and the stored object is left untouched.
|
|
138
|
+
|
|
139
|
+
Responses: `200` The policy, its revision and the last passes.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `403` The token is known but its role may not do this.; `503` The user state in S3 cannot be read, or settings are managed by the embedding application.
|
|
140
|
+
|
|
141
|
+
### Save the garbage collection policy
|
|
142
|
+
|
|
143
|
+
`PUT /api/settings/gc` · role: admin
|
|
144
|
+
|
|
145
|
+
Replaces the policy in the user state in S3 and leaves the rest of that document alone. The server reads the policy from S3 each time it collects, so the new policy applies at the next pass, without a restart. Requires an admin Bearer token, same-origin requests in browsers, and the current revision.
|
|
146
|
+
|
|
147
|
+
- `If-Match` (header, required): The quoted revision returned by GET /api/settings/gc.
|
|
148
|
+
|
|
149
|
+
Responses: `200` The saved policy and the new revision.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `403` The token is known but its role may not do this.; `409` The state changed; reload before saving.; `413` The body exceeds the limit.; `422` Invalid policy.; `428` A quoted revision is required.; `503` The user state in S3 cannot be read, or settings are managed by the embedding application.
|
|
150
|
+
|
|
151
|
+
### Collect garbage now, or preview it
|
|
152
|
+
|
|
153
|
+
`POST /api/settings/gc/run` · role: admin
|
|
154
|
+
|
|
155
|
+
Applies the policy saved in S3 to every project now, instead of waiting for the schedule. Only report layers that were read and no longer decide any result, older than the policy allows and beyond its `keepLatest`, are deleted, with the captured files only they used. With `dryRun=1` nothing is deleted and each run reports what it would delete (a disabled policy is previewed as if enabled). Answers 409 when no policy has been saved, and, without `dryRun`, when the saved policy is disabled. Requires an admin Bearer token and a same-origin request in browsers.
|
|
156
|
+
|
|
157
|
+
- `dryRun` (query): `1` or `true` to only count what would be deleted.
|
|
158
|
+
|
|
159
|
+
Responses: `200` One run per project.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `403` The token is known but its role may not do this.; `409` No policy has been saved, or it is disabled.
|
|
160
|
+
|
|
133
161
|
### Is the process up
|
|
134
162
|
|
|
135
163
|
`GET /healthz` · role: none
|