@crvouga/atlas 0.17.0 → 1.0.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/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 +1 -1
- package/docs/workspaces.md +5 -2
- package/llms-full.txt +231 -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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog: @crvouga/atlas
|
|
2
2
|
|
|
3
|
+
## 1.0.0 (2026-10-28)
|
|
4
|
+
|
|
5
|
+
### ⚠️ Breaking changes
|
|
6
|
+
|
|
7
|
+
- store all reports and user state in S3 only, with a GC policy set in the UI (#51) ([157ea67](https://github.com/crvouga/atlas/commit/157ea6734439f9d890b3de992aad72f4bca2d597))
|
|
8
|
+
|
|
3
9
|
## 0.17.0 (2026-10-28)
|
|
4
10
|
|
|
5
11
|
### Features
|
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ backends, and tells you in one command whether the product still does what it wa
|
|
|
18
18
|
versioned layer format with a golden fixture, and an `ObjectStore` interface any bucket fits.
|
|
19
19
|
- **One node, many projects.** `atlas server` runs a Bun API and serves the Atlas client from the same host. Reports, specs, recordings and live progress are partitioned by project slug.
|
|
20
20
|
Every laptop and CI job sends results to it as they finish (`atlas report publish`, or `atlas report stream` while
|
|
21
|
-
a suite runs), it keeps them
|
|
21
|
+
a suite runs), it keeps them in an S3-compatible bucket (AWS S3, R2, MinIO; required, nothing else is used), and every client sees the combined report
|
|
22
22
|
update live, receiving only what changed ([the server](docs/server.md), [deploying](docs/deploying.md)).
|
|
23
23
|
- **Fast and fail-closed.** Publishing is two small writes. A thousand layers of a hundred checks
|
|
24
24
|
combine in about a second. An unreadable layer, a downed backend or a check still running is never
|
|
@@ -67,7 +67,7 @@ bun atlas # starts the project-agnostic Atlas node at http://localhost:
|
|
|
67
67
|
bun example:run # runs tests and streams results, current steps and progress as they execute
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
-
The server starts independently
|
|
70
|
+
The server starts independently; it never loads example specs or reports. **It needs S3-compatible storage** (see [Required S3 storage](#required-s3-storage)): copy `.env.example` to `.env` and fill in the `ATLAS_S3_*` variables first, or it exits at once and lists every one that is missing. `bun example:run` publishes the Todo example under `todo-app`. Select a project using the top-right picker, then use the spec dropdown breadcrumbs to navigate. Other projects set `ATLAS_SERVER_URL`, `ATLAS_PROJECT` (for example `checkout-service`) and their publish token. Every uploader talks to the node; storage credentials stay on the server.
|
|
71
71
|
|
|
72
72
|
Every Gherkin scenario runs, including sharing and renaming. Test reports show running spinners,
|
|
73
73
|
live step logs and a Running status filter. Producers send heartbeats every
|
|
@@ -88,8 +88,11 @@ or package installation. [`latest` on GHCR](https://github.com/users/crvouga/pac
|
|
|
88
88
|
the newest checked `main` image, independently of npm releases.
|
|
89
89
|
|
|
90
90
|
1. Leave the start command unset. Add a long random secret as `ATLAS_TOKEN` (at least 12 characters;
|
|
91
|
-
use 48 or more)
|
|
92
|
-
2.
|
|
91
|
+
use 48 or more).
|
|
92
|
+
2. Add the five required storage variables (`ATLAS_S3_ENDPOINT`, `ATLAS_S3_BUCKET`, `ATLAS_S3_REGION`,
|
|
93
|
+
`ATLAS_S3_ACCESS_KEY_ID`, `ATLAS_S3_SECRET_ACCESS_KEY`; see [Required S3 storage](#required-s3-storage)) for a
|
|
94
|
+
bucket you own, and keep one replica. All reports and settings live in that bucket, so no volume is needed and
|
|
95
|
+
the service refuses to start, listing what is missing, without them.
|
|
93
96
|
3. Set the healthcheck path to `/readyz` and generate a public domain. Railway supplies `PORT`;
|
|
94
97
|
leave `ATLAS_PORT` unset. Open the domain and sign in with the token as the password (any username).
|
|
95
98
|
4. Enable **Settings → Source → Configure Auto Updates → Anytime** to follow `latest`.
|
|
@@ -100,7 +103,7 @@ For the exact image from a run, open **[CI](https://github.com/crvouga/atlas/act
|
|
|
100
103
|
run → Summary → Atlas CI**. The summary includes a copyable full commit tag, the immutable registry digest,
|
|
101
104
|
whether that run updated `latest`, all job results, npm release details and artifact links. Use the commit
|
|
102
105
|
tag to pin or roll back a deployment. The [deployment guide](docs/deploying.md#run-it) covers optional reader
|
|
103
|
-
and publisher tokens, bucket
|
|
106
|
+
and publisher tokens, the S3 bucket and sending results from your projects.
|
|
104
107
|
|
|
105
108
|
### Deploy without authentication
|
|
106
109
|
|
|
@@ -231,8 +234,8 @@ flag).
|
|
|
231
234
|
|
|
232
235
|
**The server** is `atlas serve`: one process that serves the client, takes in results and streams the report to
|
|
233
236
|
every client. Point `ATLAS_STORE` at it and every developer's laptop and every CI job sends results with the same
|
|
234
|
-
commands that write to a folder; everyone then looks at one report, live. Results are kept in
|
|
235
|
-
|
|
237
|
+
commands that write to a folder; everyone then looks at one report, live. Results are kept in an S3-compatible bucket
|
|
238
|
+
and nowhere else ([required storage](#required-s3-storage)). `atlas dev` runs the same server with the client's dev server in front, so
|
|
236
239
|
development is one host too. See [the server](docs/server.md) and
|
|
237
240
|
[running Atlas for a project](docs/deploying.md).
|
|
238
241
|
|
|
@@ -240,7 +243,7 @@ Deploy the server using the [Railway setup above](#deploy-to-railway-without-cod
|
|
|
240
243
|
[deployment guide](docs/deploying.md#run-it) for other hosts.
|
|
241
244
|
|
|
242
245
|
```sh
|
|
243
|
-
atlas serve # the project's server, at :8080 (ATLAS_TOKEN says who may upload)
|
|
246
|
+
atlas serve # the project's server, at :8080 (needs ATLAS_S3_*; ATLAS_TOKEN says who may upload)
|
|
244
247
|
|
|
245
248
|
export ATLAS_STORE=https://atlas.example.com ATLAS_TOKEN=… # on every laptop and CI job
|
|
246
249
|
atlas report publish --producer unit --junit out.xml --specs ./specs # when a suite is done
|
|
@@ -248,6 +251,47 @@ my-suite --json | atlas report stream --producer e2e --environment ios # or as
|
|
|
248
251
|
atlas report status --branch main --ignore-modified # the gate, answered by the server
|
|
249
252
|
```
|
|
250
253
|
|
|
254
|
+
### Required S3 storage
|
|
255
|
+
|
|
256
|
+
The Atlas server keeps **all reports and all user state in S3-compatible object storage, and nowhere else**: no local
|
|
257
|
+
disk, no memory, no database, no browser storage. *User state* is one JSON document in the bucket,
|
|
258
|
+
`server/state.json`, that holds the **garbage collection policy** (defined in the Atlas UI at `/admin`) and the
|
|
259
|
+
settings saved there. The server reads the policy from that document each time it collects. Until someone saves a
|
|
260
|
+
policy, garbage collection is off and nothing is ever deleted.
|
|
261
|
+
|
|
262
|
+
Because of that, the storage settings are **required environment variables with no defaults** (see
|
|
263
|
+
[`.env.example`](https://github.com/crvouga/atlas/blob/main/.env.example)), and the server **refuses to start** (exit status 2) without them:
|
|
264
|
+
|
|
265
|
+
| Variable | Required | Meaning |
|
|
266
|
+
| --- | --- | --- |
|
|
267
|
+
| `ATLAS_S3_ENDPOINT` | yes | S3 API endpoint, e.g. `https://s3.eu-west-1.amazonaws.com` |
|
|
268
|
+
| `ATLAS_S3_BUCKET` | yes | Bucket for reports and user state; it must already exist |
|
|
269
|
+
| `ATLAS_S3_REGION` | yes | Bucket region (`auto` for Cloudflare R2) |
|
|
270
|
+
| `ATLAS_S3_ACCESS_KEY_ID` | yes | Access key of an account that may list, read, write and delete in the bucket |
|
|
271
|
+
| `ATLAS_S3_SECRET_ACCESS_KEY` | yes | Its secret key |
|
|
272
|
+
| `ATLAS_S3_PREFIX` | no | Folder inside the bucket (default: the bucket root) |
|
|
273
|
+
| `ATLAS_S3_PATH_STYLE` | no | `1` (default) for `endpoint/bucket/key`, `0` for virtual-hosted style |
|
|
274
|
+
| `ATLAS_S3_SESSION_TOKEN` | no | Only for temporary credentials |
|
|
275
|
+
|
|
276
|
+
If a variable is missing or malformed, or S3 rejects the settings, Atlas lists **every** variable with `[MISSING]`
|
|
277
|
+
or `[INVALID]` next to the ones that are wrong, says why storage is required, and exits non-zero:
|
|
278
|
+
|
|
279
|
+
```text
|
|
280
|
+
==============================================================================
|
|
281
|
+
ATLAS CANNOT START: S3 STORAGE IS NOT USABLE
|
|
282
|
+
==============================================================================
|
|
283
|
+
Why: the S3 configuration is missing or invalid.
|
|
284
|
+
Reason: Atlas stores ALL reports and ALL user state (the garbage collection policy and saved
|
|
285
|
+
settings) in S3-compatible object storage, and nowhere else. …
|
|
286
|
+
Required environment variables:
|
|
287
|
+
[MISSING] ATLAS_S3_ENDPOINT S3 API endpoint of the object storage service
|
|
288
|
+
[ok] ATLAS_S3_BUCKET Bucket that holds reports and user state; it must already exist
|
|
289
|
+
…
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
See [the server guide](docs/server.md#required-s3-storage) for the details and
|
|
293
|
+
[user state and garbage collection](docs/server.md#user-state-and-garbage-collection) for how the policy works.
|
|
294
|
+
|
|
251
295
|
**The client** shows each spec (Gherkin, Quint, DMN, Alloy, OpenSLO, and the rest by file type) with
|
|
252
296
|
its combined result per environment and the layer behind it. See
|
|
253
297
|
[integrating Atlas](docs/integrating.md#6-browse-it).
|
package/dist/core/src/cli.d.ts
CHANGED
|
@@ -9,6 +9,6 @@
|
|
|
9
9
|
* @see docs/reference/cli.md
|
|
10
10
|
*/
|
|
11
11
|
/** The text `atlas --help` prints: every command, and the flags of the statechart commands. */
|
|
12
|
-
export declare const USAGE = "atlas \u2014 executable statecharts\n\n atlas lint [--config atlas.config.ts | --specs <dir>] [--require-implementations]\n atlas plan [--config atlas.config.ts] [--state <name>]\n atlas run [--config atlas.config.ts] [--mode fast|showcase] [--workers n] [--state <name>]\n [--seed <name>]... [--paths id,id] [--shard i/n] [--rerun-failed | --reuse <run-dir>]\n [--no-retry] [--output <dir>]\n atlas merge [--config atlas.config.ts] [--output <dir>] <run-dir> <run-dir>...\n atlas export --specs <dir> --format scxml|mermaid|markdown|json [--out <file>]\n atlas server [--
|
|
12
|
+
export declare const USAGE = "atlas \u2014 executable statecharts\n\n atlas lint [--config atlas.config.ts | --specs <dir>] [--require-implementations]\n atlas plan [--config atlas.config.ts] [--state <name>]\n atlas run [--config atlas.config.ts] [--mode fast|showcase] [--workers n] [--state <name>]\n [--seed <name>]... [--paths id,id] [--shard i/n] [--rerun-failed | --reuse <run-dir>]\n [--no-retry] [--output <dir>]\n atlas merge [--config atlas.config.ts] [--output <dir>] <run-dir> <run-dir>...\n atlas export --specs <dir> --format scxml|mermaid|markdown|json [--out <file>]\n atlas server [--port 8080] one Atlas node: API, projects and client; needs ATLAS_S3_* (atlas serve --help)\n atlas client build [--no-data] [--out <dir>] build the project-agnostic Atlas client\n atlas dev [--port 5180] develop the Atlas server and client on one host\n atlas init [--server <url>] [--write] integrate this project: detect its suites, print the plan (atlas init --help)\n atlas report publish|stream|status|merge|doctor|policy \u2026 layered evidence for any kind of spec (atlas report --help)\n\n --workers path attempts that run at once (default: one per CPU, capped by the driver)\n --seed only paths that start from this seed (\"start\" for paths setup starts); repeatable\n --shard 2/4 run the second quarter of the plan; merge the shards' runs afterwards\n --rerun-failed keep the newest run's passed paths and run only the rest\n";
|
|
13
13
|
/** Runs `atlas` with `argv` (default: the process arguments) and returns the exit code. Throws for a command that is wrong, which the `atlas` binary reports as a message and exit code 2. */
|
|
14
14
|
export declare function main(argv?: string[]): Promise<number>;
|
package/dist/core/src/cli.js
CHANGED
|
@@ -28,7 +28,7 @@ export const USAGE = `atlas — executable statecharts
|
|
|
28
28
|
[--no-retry] [--output <dir>]
|
|
29
29
|
atlas merge [--config atlas.config.ts] [--output <dir>] <run-dir> <run-dir>...
|
|
30
30
|
atlas export --specs <dir> --format scxml|mermaid|markdown|json [--out <file>]
|
|
31
|
-
atlas server [--
|
|
31
|
+
atlas server [--port 8080] one Atlas node: API, projects and client; needs ATLAS_S3_* (atlas serve --help)
|
|
32
32
|
atlas client build [--no-data] [--out <dir>] build the project-agnostic Atlas client
|
|
33
33
|
atlas dev [--port 5180] develop the Atlas server and client on one host
|
|
34
34
|
atlas init [--server <url>] [--write] integrate this project: detect its suites, print the plan (atlas init --help)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ObjectStore } from './object-store.js';
|
|
2
|
+
import type { GcPolicy } from '../../schema/src/user-state.js';
|
|
3
|
+
/** One layer the policy would delete. */
|
|
4
|
+
export type GcCandidate = {
|
|
5
|
+
readonly id: string;
|
|
6
|
+
readonly catalogKey: string;
|
|
7
|
+
readonly recordedAtMs: number;
|
|
8
|
+
};
|
|
9
|
+
/** What a policy would delete from a store right now. */
|
|
10
|
+
export type GcPlan = {
|
|
11
|
+
/** Layers to delete, oldest first. */
|
|
12
|
+
readonly candidates: readonly GcCandidate[];
|
|
13
|
+
/** Layers in the catalog that stay. */
|
|
14
|
+
readonly kept: number;
|
|
15
|
+
};
|
|
16
|
+
/** The outcome of applying a plan. */
|
|
17
|
+
export type GcOutcome = {
|
|
18
|
+
readonly deletedLayers: number;
|
|
19
|
+
readonly deletedBlobs: number;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Decides which layers a policy deletes, without deleting anything. `eligible` is the set of layer ids the caller has
|
|
23
|
+
* read and knows no longer decide any result (`ReportIndex.superseded()`); a layer not in it is never a candidate.
|
|
24
|
+
* Lists the catalog only. With `policy.enabled === false` the plan is empty.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* const plan = await planGarbage(store, { policy, nowMs: Date.now(), eligible: index.superseded() });
|
|
28
|
+
*/
|
|
29
|
+
export declare function planGarbage(store: ObjectStore, input: {
|
|
30
|
+
readonly policy: GcPolicy;
|
|
31
|
+
readonly nowMs: number;
|
|
32
|
+
readonly eligible: ReadonlySet<string>;
|
|
33
|
+
}): Promise<GcPlan>;
|
|
34
|
+
/**
|
|
35
|
+
* Deletes the layers of a plan, then the blobs only they used. Store errors throw after whatever was already deleted;
|
|
36
|
+
* running the same plan again finishes the job, because every delete is idempotent and the catalog entry (what
|
|
37
|
+
* makes a layer visible) always goes before the content.
|
|
38
|
+
*/
|
|
39
|
+
export declare function collectGarbage(store: ObjectStore, plan: GcPlan): Promise<GcOutcome>;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Layer garbage collection
|
|
3
|
+
*
|
|
4
|
+
* Applies a garbage collection policy (`GcPolicy`, chosen by a person in the Atlas UI and kept in the user state) to the
|
|
5
|
+
* layers in a store. Atlas is otherwise deletion-free, so this is deliberately conservative: a layer is deleted only when
|
|
6
|
+
* (1) the caller says the report has read it and every result it carries has been replaced by a newer layer (`eligible`),
|
|
7
|
+
* (2) it was recorded longer ago than the policy allows, and (3) it is not among the newest `keepLatest` layers. Deleting
|
|
8
|
+
* it therefore cannot change the combined report. Its catalog entry goes first, so nothing visible ever points at a
|
|
9
|
+
* missing manifest; its manifest next; then the captured-file blobs that no remaining layer refers to. If any remaining
|
|
10
|
+
* layer cannot be read, no blob is deleted: what cannot be read is unknown, never unused. Specs, runs and commit
|
|
11
|
+
* inventories are not touched.
|
|
12
|
+
*
|
|
13
|
+
* @see docs/server.md#user-state-and-garbage-collection
|
|
14
|
+
*/
|
|
15
|
+
import { eachLimited } from './completion-order.js';
|
|
16
|
+
import { LAYER_INDEX, layerKey, readLayer } from './layer-store.js';
|
|
17
|
+
const DAY_MS = 86_400_000;
|
|
18
|
+
const CATALOG = /^layers\/index\/(\d{16})-([0-9a-f]{64})\.json$/;
|
|
19
|
+
/**
|
|
20
|
+
* Decides which layers a policy deletes, without deleting anything. `eligible` is the set of layer ids the caller has
|
|
21
|
+
* read and knows no longer decide any result (`ReportIndex.superseded()`); a layer not in it is never a candidate.
|
|
22
|
+
* Lists the catalog only. With `policy.enabled === false` the plan is empty.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* const plan = await planGarbage(store, { policy, nowMs: Date.now(), eligible: index.superseded() });
|
|
26
|
+
*/
|
|
27
|
+
export async function planGarbage(store, input) {
|
|
28
|
+
const entries = (await store.listKeys(LAYER_INDEX)).flatMap(key => {
|
|
29
|
+
const match = CATALOG.exec(key);
|
|
30
|
+
return match ? [{ id: match[2], catalogKey: key, recordedAtMs: Number.MAX_SAFE_INTEGER - Number(match[1]) }] : [];
|
|
31
|
+
});
|
|
32
|
+
// Catalog keys sort newest first (the time is inverted), so the layers to always keep are the first `keepLatest`.
|
|
33
|
+
entries.sort((a, b) => b.recordedAtMs - a.recordedAtMs || (a.id < b.id ? -1 : 1));
|
|
34
|
+
if (!input.policy.enabled)
|
|
35
|
+
return { candidates: [], kept: entries.length };
|
|
36
|
+
const cutoff = input.nowMs - input.policy.maxAgeDays * DAY_MS;
|
|
37
|
+
const candidates = entries.slice(input.policy.keepLatest).filter(entry => entry.recordedAtMs < cutoff && input.eligible.has(entry.id));
|
|
38
|
+
return { candidates: candidates.reverse(), kept: entries.length - candidates.length };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Deletes the layers of a plan, then the blobs only they used. Store errors throw after whatever was already deleted;
|
|
42
|
+
* running the same plan again finishes the job, because every delete is idempotent and the catalog entry (what
|
|
43
|
+
* makes a layer visible) always goes before the content.
|
|
44
|
+
*/
|
|
45
|
+
export async function collectGarbage(store, plan) {
|
|
46
|
+
if (!plan.candidates.length)
|
|
47
|
+
return { deletedLayers: 0, deletedBlobs: 0 };
|
|
48
|
+
const doomed = new Set(plan.candidates.map(candidate => candidate.id));
|
|
49
|
+
// Which blobs the doomed layers used, read before anything is deleted. A layer that cannot be read stays: unknown is not garbage.
|
|
50
|
+
const used = new Map();
|
|
51
|
+
const readable = [];
|
|
52
|
+
await eachLimited(plan.candidates, 16, async (candidate) => {
|
|
53
|
+
const layer = await readLayer(store, candidate.id).catch(() => null);
|
|
54
|
+
if (!layer)
|
|
55
|
+
return;
|
|
56
|
+
used.set(candidate.id, new Set(Object.values(layer.bundle).flatMap(file => file.parts.map(part => part.blob))));
|
|
57
|
+
readable.push(candidate);
|
|
58
|
+
});
|
|
59
|
+
if (!readable.length)
|
|
60
|
+
return { deletedLayers: 0, deletedBlobs: 0 };
|
|
61
|
+
await store.deleteMany(readable.map(candidate => candidate.catalogKey));
|
|
62
|
+
await store.deleteMany(readable.map(candidate => layerKey(candidate.id)));
|
|
63
|
+
const removed = new Set(readable.map(candidate => candidate.id));
|
|
64
|
+
const orphanable = new Set([...removed].flatMap(id => [...used.get(id)]));
|
|
65
|
+
// Blobs that any remaining layer still points at must stay. If a remaining layer cannot be read, keep every blob.
|
|
66
|
+
const remaining = (await store.listKeys(LAYER_INDEX)).flatMap(key => CATALOG.exec(key)?.[2] ?? []).filter(id => !removed.has(id));
|
|
67
|
+
let unreadable = false;
|
|
68
|
+
await eachLimited(remaining, 16, async (id) => {
|
|
69
|
+
const layer = await readLayer(store, id).catch(() => null);
|
|
70
|
+
if (!layer) {
|
|
71
|
+
unreadable = true;
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
for (const file of Object.values(layer.bundle))
|
|
75
|
+
for (const part of file.parts)
|
|
76
|
+
orphanable.delete(part.blob);
|
|
77
|
+
});
|
|
78
|
+
if (unreadable)
|
|
79
|
+
return { deletedLayers: removed.size, deletedBlobs: 0 };
|
|
80
|
+
await store.deleteMany([...orphanable]);
|
|
81
|
+
return { deletedLayers: removed.size, deletedBlobs: orphanable.size };
|
|
82
|
+
}
|
|
@@ -93,6 +93,12 @@ export declare class ReportIndex {
|
|
|
93
93
|
* and the run list are built from. Read-only for callers; `history.rev` changes whenever a layer is taken in.
|
|
94
94
|
*/
|
|
95
95
|
get history(): TestHistory;
|
|
96
|
+
/**
|
|
97
|
+
* The ids of layers this index has read and that no longer decide any result. Removing one from its store cannot
|
|
98
|
+
* change the report (every slot it filled is held by a newer layer), which is what makes it safe for garbage
|
|
99
|
+
* collection to delete. A layer that failed to load, is hidden by `include`, or still holds a result is not here.
|
|
100
|
+
*/
|
|
101
|
+
superseded(): ReadonlySet<string>;
|
|
96
102
|
/** For `/readyz` and logs. */
|
|
97
103
|
stats(): {
|
|
98
104
|
backends: number;
|
|
@@ -50,6 +50,8 @@ export class ReportIndex {
|
|
|
50
50
|
/** Layers that stopped holding any winner, and when. Older removals are forgotten; `#forgotten` says how far. */
|
|
51
51
|
#removed = [];
|
|
52
52
|
#forgotten = 0;
|
|
53
|
+
/** Layers that were read, took part in the report, and no longer decide any result: all their checks were replaced by newer layers. */
|
|
54
|
+
#superseded = new Set();
|
|
53
55
|
#refreshing = null;
|
|
54
56
|
#timer = null;
|
|
55
57
|
#refreshedAt = 0;
|
|
@@ -248,6 +250,9 @@ export class ReportIndex {
|
|
|
248
250
|
if (wins.size) {
|
|
249
251
|
this.#held.set(id, { evidence, wins, rev: ++this.#rev, entry: null, json: null, digest: null });
|
|
250
252
|
}
|
|
253
|
+
else {
|
|
254
|
+
this.#superseded.add(id);
|
|
255
|
+
}
|
|
251
256
|
}
|
|
252
257
|
/** What this layer sends has changed. */
|
|
253
258
|
#touch(held) {
|
|
@@ -264,6 +269,7 @@ export class ReportIndex {
|
|
|
264
269
|
return;
|
|
265
270
|
}
|
|
266
271
|
this.#held.delete(layerId);
|
|
272
|
+
this.#superseded.add(layerId);
|
|
267
273
|
this.#removed.push({ id: layerId, rev: ++this.#rev });
|
|
268
274
|
if (this.#removed.length > REMEMBERED_REMOVALS)
|
|
269
275
|
this.#forgotten = this.#removed.splice(0, this.#removed.length - REMEMBERED_REMOVALS).at(-1).rev;
|
|
@@ -342,6 +348,14 @@ export class ReportIndex {
|
|
|
342
348
|
get history() {
|
|
343
349
|
return this.#history;
|
|
344
350
|
}
|
|
351
|
+
/**
|
|
352
|
+
* The ids of layers this index has read and that no longer decide any result. Removing one from its store cannot
|
|
353
|
+
* change the report (every slot it filled is held by a newer layer), which is what makes it safe for garbage
|
|
354
|
+
* collection to delete. A layer that failed to load, is hidden by `include`, or still holds a result is not here.
|
|
355
|
+
*/
|
|
356
|
+
superseded() {
|
|
357
|
+
return this.#superseded;
|
|
358
|
+
}
|
|
345
359
|
/** For `/readyz` and logs. */
|
|
346
360
|
stats() {
|
|
347
361
|
return { backends: this.#backends.length, layers: this.#attribution.size, slots: this.#winners.size, holding: this.#held.size, failed: this.#failed.size, refreshedAt: this.#refreshedAt };
|
|
@@ -15,6 +15,7 @@ export * from './spec.js';
|
|
|
15
15
|
export * from './report-sources.js';
|
|
16
16
|
export * from './layer.js';
|
|
17
17
|
export * from './server-config.js';
|
|
18
|
+
export * from './user-state.js';
|
|
18
19
|
export * from './api.js';
|
|
19
20
|
export * from './cli-results.js';
|
|
20
21
|
export * from './live-run.js';
|
package/dist/schema/src/index.js
CHANGED
|
@@ -15,6 +15,7 @@ export * from './spec.js';
|
|
|
15
15
|
export * from './report-sources.js';
|
|
16
16
|
export * from './layer.js';
|
|
17
17
|
export * from './server-config.js';
|
|
18
|
+
export * from './user-state.js';
|
|
18
19
|
export * from './api.js';
|
|
19
20
|
export * from './cli-results.js';
|
|
20
21
|
export * from './live-run.js';
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
* @module Server configuration schema
|
|
3
3
|
*
|
|
4
4
|
* The schema of `atlas.server.json`, the file that configures `atlas serve` and `atlas dev`: where to listen, which
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* a
|
|
5
|
+
* bearer tokens may read, publish or administer, and limits. Where reports are kept is not here: the server stores
|
|
6
|
+
* everything in S3-compatible storage named by required `ATLAS_S3_*` environment variables, never in this file.
|
|
7
|
+
* Every field has a default. Unlike the report format, unknown keys are an error, so a typo (or a removed `data`,
|
|
8
|
+
* `backends` or `write` key) is reported instead of ignored. Flags and environment variables override the file.
|
|
8
9
|
*
|
|
9
10
|
* @see docs/deploying.md
|
|
10
11
|
* @see docs/server.md
|
|
@@ -18,16 +19,6 @@ export declare const ServerRoleSchema: z.ZodEnum<{
|
|
|
18
19
|
}>;
|
|
19
20
|
/** One of the roles of `ServerRoleSchema`. */
|
|
20
21
|
export type ServerRole = z.infer<typeof ServerRoleSchema>;
|
|
21
|
-
/**
|
|
22
|
-
* A further store the server reads together with its data folder, and optionally writes to: an id, a label and a
|
|
23
|
-
* location URL.
|
|
24
|
-
*/
|
|
25
|
-
export declare const ServerBackendSchema: z.ZodObject<{
|
|
26
|
-
id: z.ZodString;
|
|
27
|
-
label: z.ZodOptional<z.ZodString>;
|
|
28
|
-
url: z.ZodString;
|
|
29
|
-
write: z.ZodOptional<z.ZodBoolean>;
|
|
30
|
-
}, z.core.$strict>;
|
|
31
22
|
/**
|
|
32
23
|
* A bearer token: a name for logs, a role, and the token itself or the name of the environment variable that holds it
|
|
33
24
|
* (exactly one of the two).
|
|
@@ -43,21 +34,12 @@ export declare const ServerTokenSchema: z.ZodObject<{
|
|
|
43
34
|
tokenEnv: z.ZodOptional<z.ZodString>;
|
|
44
35
|
}, z.core.$strict>;
|
|
45
36
|
/**
|
|
46
|
-
* The whole `atlas.server.json`: listening address,
|
|
47
|
-
* viewer to serve, which layers take part in the report, refresh and heartbeat intervals, request limits and logging.
|
|
37
|
+
* The whole `atlas.server.json`: listening address, authentication, the viewer to serve, which layers take part in the report, refresh and heartbeat intervals, request limits and logging.
|
|
48
38
|
*/
|
|
49
39
|
export declare const ServerConfigSchema: z.ZodObject<{
|
|
50
40
|
$schema: z.ZodOptional<z.ZodString>;
|
|
51
41
|
host: z.ZodDefault<z.ZodString>;
|
|
52
42
|
port: z.ZodDefault<z.ZodNumber>;
|
|
53
|
-
data: z.ZodDefault<z.ZodString>;
|
|
54
|
-
backends: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
55
|
-
id: z.ZodString;
|
|
56
|
-
label: z.ZodOptional<z.ZodString>;
|
|
57
|
-
url: z.ZodString;
|
|
58
|
-
write: z.ZodOptional<z.ZodBoolean>;
|
|
59
|
-
}, z.core.$strict>>>;
|
|
60
|
-
write: z.ZodOptional<z.ZodString>;
|
|
61
43
|
auth: z.ZodDefault<z.ZodObject<{
|
|
62
44
|
tokens: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
63
45
|
name: z.ZodString;
|
|
@@ -93,17 +75,9 @@ export declare const ServerConfigSchema: z.ZodObject<{
|
|
|
93
75
|
export type ServerConfig = z.output<typeof ServerConfigSchema>;
|
|
94
76
|
/** A server configuration as written, before defaults are applied. */
|
|
95
77
|
export type ServerConfigInput = z.input<typeof ServerConfigSchema>;
|
|
96
|
-
/**
|
|
78
|
+
/** Report and live-run settings editable by an admin. Storage, authentication, listeners and the served site stay deployment-owned. Replaces the saved portion of the user state; omitted fields use defaults after restart. */
|
|
97
79
|
export declare const ServerEditableSettingsSchema: z.ZodObject<{
|
|
98
|
-
data: z.ZodOptional<z.ZodDefault<z.ZodString>>;
|
|
99
|
-
backends: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
100
|
-
id: z.ZodString;
|
|
101
|
-
label: z.ZodOptional<z.ZodString>;
|
|
102
|
-
url: z.ZodString;
|
|
103
|
-
write: z.ZodOptional<z.ZodBoolean>;
|
|
104
|
-
}, z.core.$strict>>>>;
|
|
105
80
|
log: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
|
|
106
|
-
write: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
107
81
|
outlineBranch: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
108
82
|
include: z.ZodOptional<z.ZodDefault<z.ZodObject<{
|
|
109
83
|
branches: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
@@ -119,19 +93,11 @@ export declare const ServerEditableSettingsSchema: z.ZodObject<{
|
|
|
119
93
|
runStallSeconds: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
|
|
120
94
|
heartbeatSeconds: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
|
|
121
95
|
}, z.core.$strict>;
|
|
122
|
-
/** The settings API response:
|
|
96
|
+
/** The settings API response: editable configuration saved in the user state in S3, active values, concurrency revision and restart/precedence guidance. Authentication token settings are excluded. */
|
|
123
97
|
export declare const ServerSettingsSnapshotSchema: z.ZodObject<{
|
|
124
98
|
revision: z.ZodString;
|
|
125
99
|
settings: z.ZodObject<{
|
|
126
|
-
data: z.ZodOptional<z.ZodDefault<z.ZodString>>;
|
|
127
|
-
backends: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
128
|
-
id: z.ZodString;
|
|
129
|
-
label: z.ZodOptional<z.ZodString>;
|
|
130
|
-
url: z.ZodString;
|
|
131
|
-
write: z.ZodOptional<z.ZodBoolean>;
|
|
132
|
-
}, z.core.$strict>>>>;
|
|
133
100
|
log: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
|
|
134
|
-
write: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
135
101
|
outlineBranch: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
136
102
|
include: z.ZodOptional<z.ZodDefault<z.ZodObject<{
|
|
137
103
|
branches: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
@@ -148,15 +114,7 @@ export declare const ServerSettingsSnapshotSchema: z.ZodObject<{
|
|
|
148
114
|
heartbeatSeconds: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
|
|
149
115
|
}, z.core.$strict>;
|
|
150
116
|
active: z.ZodObject<{
|
|
151
|
-
data: z.ZodOptional<z.ZodDefault<z.ZodString>>;
|
|
152
|
-
backends: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
153
|
-
id: z.ZodString;
|
|
154
|
-
label: z.ZodOptional<z.ZodString>;
|
|
155
|
-
url: z.ZodString;
|
|
156
|
-
write: z.ZodOptional<z.ZodBoolean>;
|
|
157
|
-
}, z.core.$strict>>>>;
|
|
158
117
|
log: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
|
|
159
|
-
write: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
160
118
|
outlineBranch: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
161
119
|
include: z.ZodOptional<z.ZodDefault<z.ZodObject<{
|
|
162
120
|
branches: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
* @module Server configuration schema
|
|
3
3
|
*
|
|
4
4
|
* The schema of `atlas.server.json`, the file that configures `atlas serve` and `atlas dev`: where to listen, which
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* a
|
|
5
|
+
* bearer tokens may read, publish or administer, and limits. Where reports are kept is not here: the server stores
|
|
6
|
+
* everything in S3-compatible storage named by required `ATLAS_S3_*` environment variables, never in this file.
|
|
7
|
+
* Every field has a default. Unlike the report format, unknown keys are an error, so a typo (or a removed `data`,
|
|
8
|
+
* `backends` or `write` key) is reported instead of ignored. Flags and environment variables override the file.
|
|
8
9
|
*
|
|
9
10
|
* @see docs/deploying.md
|
|
10
11
|
* @see docs/server.md
|
|
@@ -12,16 +13,6 @@
|
|
|
12
13
|
import { z } from 'zod/v4';
|
|
13
14
|
/** What a bearer token may do: `read` (look), `publish` (also upload) or `admin` (also remove scratch objects). */
|
|
14
15
|
export const ServerRoleSchema = z.enum(['read', 'publish', 'admin']);
|
|
15
|
-
/**
|
|
16
|
-
* A further store the server reads together with its data folder, and optionally writes to: an id, a label and a
|
|
17
|
-
* location URL.
|
|
18
|
-
*/
|
|
19
|
-
export const ServerBackendSchema = z.strictObject({
|
|
20
|
-
id: z.string().regex(/^[a-z0-9-]+$/).describe('Lowercase letters, digits and hyphens. Names where a result came from'),
|
|
21
|
-
label: z.string().optional(),
|
|
22
|
-
url: z.string().min(1).describe('A folder, s3://bucket/prefix, r2://, gs://, or another Atlas server (https://…)'),
|
|
23
|
-
write: z.boolean().optional().describe('Uploads go here instead of the data folder. At most one backend is the write target')
|
|
24
|
-
});
|
|
25
16
|
/**
|
|
26
17
|
* A bearer token: a name for logs, a role, and the token itself or the name of the environment variable that holds it
|
|
27
18
|
* (exactly one of the two).
|
|
@@ -35,16 +26,12 @@ export const ServerTokenSchema = z
|
|
|
35
26
|
})
|
|
36
27
|
.refine((value) => (value.token === undefined) !== (value.tokenEnv === undefined), { message: 'Give exactly one of token or tokenEnv' });
|
|
37
28
|
/**
|
|
38
|
-
* The whole `atlas.server.json`: listening address,
|
|
39
|
-
* viewer to serve, which layers take part in the report, refresh and heartbeat intervals, request limits and logging.
|
|
29
|
+
* The whole `atlas.server.json`: listening address, authentication, the viewer to serve, which layers take part in the report, refresh and heartbeat intervals, request limits and logging.
|
|
40
30
|
*/
|
|
41
31
|
export const ServerConfigSchema = z.strictObject({
|
|
42
32
|
$schema: z.string().optional(),
|
|
43
33
|
host: z.string().default('127.0.0.1').describe('Where to listen. 127.0.0.1 keeps it on this machine; 0.0.0.0 is every interface and needs tokens (or `auth.insecure`)'),
|
|
44
34
|
port: z.number().int().min(0).max(65535).default(8080),
|
|
45
|
-
data: z.string().default('./atlas-data').describe('The folder that is the default backend and the default write target: layers, specs and runs live here'),
|
|
46
|
-
backends: z.array(ServerBackendSchema).default([]).describe('Further backends, read together with the data folder: S3, another server, a device lab'),
|
|
47
|
-
write: z.string().optional().describe('The id of the backend uploads go to. Default: the data folder'),
|
|
48
35
|
auth: z
|
|
49
36
|
.strictObject({
|
|
50
37
|
tokens: z.array(ServerTokenSchema).default([]),
|
|
@@ -73,9 +60,9 @@ export const ServerConfigSchema = z.strictObject({
|
|
|
73
60
|
heartbeatSeconds: z.number().min(1).default(15).describe('How often a live connection is touched, so proxies keep it open'),
|
|
74
61
|
log: z.boolean().default(true).describe('One JSON line per request')
|
|
75
62
|
});
|
|
76
|
-
/**
|
|
77
|
-
export const ServerEditableSettingsSchema = ServerConfigSchema.pick({
|
|
78
|
-
/** The settings API response:
|
|
63
|
+
/** Report and live-run settings editable by an admin. Storage, authentication, listeners and the served site stay deployment-owned. Replaces the saved portion of the user state; omitted fields use defaults after restart. */
|
|
64
|
+
export const ServerEditableSettingsSchema = ServerConfigSchema.pick({ outlineBranch: true, include: true, refreshSeconds: true, limits: true, heartbeatSeconds: true, runTimeoutSeconds: true, runStallSeconds: true, log: true }).partial();
|
|
65
|
+
/** The settings API response: editable configuration saved in the user state in S3, active values, concurrency revision and restart/precedence guidance. Authentication token settings are excluded. */
|
|
79
66
|
export const ServerSettingsSnapshotSchema = z.looseObject({
|
|
80
67
|
revision: z.string().regex(/^[a-f0-9]{64}$/),
|
|
81
68
|
settings: ServerEditableSettingsSchema,
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module User state schema
|
|
3
|
+
*
|
|
4
|
+
* The one JSON document that holds everything a person using Atlas has chosen, as opposed to what an operator deployed:
|
|
5
|
+
* the garbage collection policy and the saved server settings. The server keeps it in S3-compatible object storage
|
|
6
|
+
* (`server/state.json` under the configured prefix) and nowhere else: not in a file, a database or the browser. A
|
|
7
|
+
* document that does not exist yet means nothing has been chosen; a document that cannot be read is an error, never
|
|
8
|
+
* an empty state. Unknown keys are kept, so an older server does not drop what a newer one wrote.
|
|
9
|
+
*
|
|
10
|
+
* @see docs/server.md#user-state-and-garbage-collection
|
|
11
|
+
*/
|
|
12
|
+
import { z } from 'zod/v4';
|
|
13
|
+
/**
|
|
14
|
+
* The garbage collection policy: which report layers the server may delete from S3, and when. Only layers that the
|
|
15
|
+
* report has loaded and that no longer decide any result (every check they carry has since been superseded by a newer
|
|
16
|
+
* layer) are ever candidates, and the newest `keepLatest` layers are always kept. With `enabled: false` nothing is deleted.
|
|
17
|
+
*/
|
|
18
|
+
export declare const GcPolicySchema: z.ZodObject<{
|
|
19
|
+
enabled: z.ZodBoolean;
|
|
20
|
+
maxAgeDays: z.ZodNumber;
|
|
21
|
+
keepLatest: z.ZodDefault<z.ZodNumber>;
|
|
22
|
+
intervalHours: z.ZodDefault<z.ZodNumber>;
|
|
23
|
+
}, z.core.$strict>;
|
|
24
|
+
/** A garbage collection policy as stored, defaults applied. */
|
|
25
|
+
export type GcPolicy = z.output<typeof GcPolicySchema>;
|
|
26
|
+
/** A garbage collection policy as written, before defaults are applied. */
|
|
27
|
+
export type GcPolicyInput = z.input<typeof GcPolicySchema>;
|
|
28
|
+
/** What one pass of garbage collection found and did, for one project. */
|
|
29
|
+
export declare const GcRunSchema: z.ZodObject<{
|
|
30
|
+
project: z.ZodString;
|
|
31
|
+
dryRun: z.ZodBoolean;
|
|
32
|
+
at: z.ZodString;
|
|
33
|
+
candidates: z.ZodNumber;
|
|
34
|
+
deletedLayers: z.ZodNumber;
|
|
35
|
+
deletedBlobs: z.ZodNumber;
|
|
36
|
+
keptLayers: z.ZodNumber;
|
|
37
|
+
error: z.ZodOptional<z.ZodString>;
|
|
38
|
+
}, z.core.$loose>;
|
|
39
|
+
/** One pass of garbage collection, see {@link GcRunSchema}. */
|
|
40
|
+
export type GcRun = z.output<typeof GcRunSchema>;
|
|
41
|
+
/** The user state document stored in S3. Both parts are optional: absent means "never chosen". */
|
|
42
|
+
export declare const UserStateSchema: z.ZodObject<{
|
|
43
|
+
schemaVersion: z.ZodLiteral<1>;
|
|
44
|
+
gc: z.ZodOptional<z.ZodObject<{
|
|
45
|
+
enabled: z.ZodBoolean;
|
|
46
|
+
maxAgeDays: z.ZodNumber;
|
|
47
|
+
keepLatest: z.ZodDefault<z.ZodNumber>;
|
|
48
|
+
intervalHours: z.ZodDefault<z.ZodNumber>;
|
|
49
|
+
}, z.core.$strict>>;
|
|
50
|
+
settings: z.ZodOptional<z.ZodObject<{
|
|
51
|
+
log: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
|
|
52
|
+
outlineBranch: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
53
|
+
include: z.ZodOptional<z.ZodDefault<z.ZodObject<{
|
|
54
|
+
branches: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
55
|
+
ignoreModified: z.ZodOptional<z.ZodBoolean>;
|
|
56
|
+
}, z.core.$strict>>>;
|
|
57
|
+
refreshSeconds: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
|
|
58
|
+
limits: z.ZodOptional<z.ZodDefault<z.ZodObject<{
|
|
59
|
+
maxObjectMiB: z.ZodDefault<z.ZodNumber>;
|
|
60
|
+
maxJsonMiB: z.ZodDefault<z.ZodNumber>;
|
|
61
|
+
maxStreams: z.ZodDefault<z.ZodNumber>;
|
|
62
|
+
}, z.core.$strict>>>;
|
|
63
|
+
runTimeoutSeconds: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
|
|
64
|
+
runStallSeconds: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
|
|
65
|
+
heartbeatSeconds: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
|
|
66
|
+
}, z.core.$strict>>;
|
|
67
|
+
}, z.core.$loose>;
|
|
68
|
+
/** The user state document, see {@link UserStateSchema}. */
|
|
69
|
+
export type UserState = z.output<typeof UserStateSchema>;
|
|
70
|
+
/**
|
|
71
|
+
* `GET /api/settings/gc` and `PUT /api/settings/gc`: the policy as saved in S3 (`null` when none has been saved), the
|
|
72
|
+
* revision to send back in `If-Match`, and the result of the last pass this server ran.
|
|
73
|
+
*/
|
|
74
|
+
export declare const GcSnapshotSchema: z.ZodObject<{
|
|
75
|
+
revision: z.ZodString;
|
|
76
|
+
policy: z.ZodNullable<z.ZodObject<{
|
|
77
|
+
enabled: z.ZodBoolean;
|
|
78
|
+
maxAgeDays: z.ZodNumber;
|
|
79
|
+
keepLatest: z.ZodDefault<z.ZodNumber>;
|
|
80
|
+
intervalHours: z.ZodDefault<z.ZodNumber>;
|
|
81
|
+
}, z.core.$strict>>;
|
|
82
|
+
saved: z.ZodBoolean;
|
|
83
|
+
lastRuns: z.ZodArray<z.ZodObject<{
|
|
84
|
+
project: z.ZodString;
|
|
85
|
+
dryRun: z.ZodBoolean;
|
|
86
|
+
at: z.ZodString;
|
|
87
|
+
candidates: z.ZodNumber;
|
|
88
|
+
deletedLayers: z.ZodNumber;
|
|
89
|
+
deletedBlobs: z.ZodNumber;
|
|
90
|
+
keptLayers: z.ZodNumber;
|
|
91
|
+
error: z.ZodOptional<z.ZodString>;
|
|
92
|
+
}, z.core.$loose>>;
|
|
93
|
+
stateKey: z.ZodString;
|
|
94
|
+
}, z.core.$loose>;
|
|
95
|
+
/** The garbage collection settings response, see {@link GcSnapshotSchema}. */
|
|
96
|
+
export type GcSnapshot = z.output<typeof GcSnapshotSchema>;
|
|
97
|
+
/** `POST /api/settings/gc/run`: the passes that ran, one per project. */
|
|
98
|
+
export declare const GcRunResultSchema: z.ZodObject<{
|
|
99
|
+
runs: z.ZodArray<z.ZodObject<{
|
|
100
|
+
project: z.ZodString;
|
|
101
|
+
dryRun: z.ZodBoolean;
|
|
102
|
+
at: z.ZodString;
|
|
103
|
+
candidates: z.ZodNumber;
|
|
104
|
+
deletedLayers: z.ZodNumber;
|
|
105
|
+
deletedBlobs: z.ZodNumber;
|
|
106
|
+
keptLayers: z.ZodNumber;
|
|
107
|
+
error: z.ZodOptional<z.ZodString>;
|
|
108
|
+
}, z.core.$loose>>;
|
|
109
|
+
policy: z.ZodNullable<z.ZodObject<{
|
|
110
|
+
enabled: z.ZodBoolean;
|
|
111
|
+
maxAgeDays: z.ZodNumber;
|
|
112
|
+
keepLatest: z.ZodDefault<z.ZodNumber>;
|
|
113
|
+
intervalHours: z.ZodDefault<z.ZodNumber>;
|
|
114
|
+
}, z.core.$strict>>;
|
|
115
|
+
}, z.core.$loose>;
|