@wtfalch/foundry 0.60.0 → 0.62.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/dist/apps/cloudflare.d.ts +173 -0
- package/dist/apps/cloudflare.js +17 -0
- package/dist/apps/deploy.d.ts +116 -0
- package/dist/apps/deploy.js +791 -0
- package/dist/apps/github.d.ts +47 -0
- package/dist/apps/github.js +201 -0
- package/dist/apps/index.d.ts +9 -0
- package/dist/apps/index.js +9 -0
- package/dist/apps/migrations.d.ts +110 -0
- package/dist/apps/migrations.js +458 -0
- package/dist/audit/connector.d.ts +43 -0
- package/dist/audit/connector.js +22 -0
- package/dist/audit/evidence.d.ts +49 -0
- package/dist/audit/evidence.js +56 -0
- package/dist/audit/index.d.ts +9 -0
- package/dist/audit/index.js +9 -0
- package/dist/audit/migrations.d.ts +5 -0
- package/dist/audit/migrations.js +5 -0
- package/dist/audit/registry.d.ts +49 -0
- package/dist/audit/registry.js +44 -0
- package/dist/audit/seal.d.ts +49 -0
- package/dist/audit/seal.js +131 -0
- package/dist/audit/sealer-role.d.ts +33 -0
- package/dist/audit/sealer-role.js +56 -0
- package/dist/audit/signer.d.ts +40 -0
- package/dist/audit/signer.js +78 -0
- package/dist/audit/tick.d.ts +36 -0
- package/dist/audit/tick.js +87 -0
- package/dist/catalogue/copy-en.d.ts +1 -0
- package/dist/catalogue/copy-en.js +129 -0
- package/dist/catalogue/index.d.ts +9 -0
- package/dist/catalogue/index.js +21 -0
- package/dist/catalogue/integrations.d.ts +4 -0
- package/dist/catalogue/integrations.js +104 -0
- package/dist/catalogue/systems.d.ts +7 -0
- package/dist/catalogue/systems.js +31 -0
- package/dist/catalogue/types.d.ts +110 -0
- package/dist/catalogue/types.js +1 -0
- package/dist/entry-rules.d.ts +53 -0
- package/dist/entry-rules.js +71 -0
- package/dist/hosted-label.d.ts +13 -0
- package/dist/hosted-label.js +81 -0
- package/dist/index.d.ts +14 -5
- package/dist/index.js +7 -3
- package/dist/migrations/0062_hosting_github.sql +6 -0
- package/dist/migrations/0063_boule_apps.sql +7 -0
- package/dist/migrations/0064_requested_changes_boule_apps.sql +6 -0
- package/dist/migrations/0065_native_build_runs.sql +32 -0
- package/dist/migrations/0066_org_kosmos.sql +10 -0
- package/dist/modules/ai.d.ts +3 -0
- package/dist/modules/ai.js +64 -0
- package/dist/modules/assistants.d.ts +3 -0
- package/dist/modules/assistants.js +72 -0
- package/dist/modules/audit-ledger.d.ts +29 -0
- package/dist/modules/audit-ledger.js +49 -0
- package/dist/modules/audit.d.ts +7 -0
- package/dist/modules/audit.js +44 -0
- package/dist/modules/database.d.ts +23 -0
- package/dist/modules/database.js +189 -0
- package/dist/modules/email.d.ts +3 -0
- package/dist/modules/email.js +18 -0
- package/dist/modules/entry-parts.d.ts +23 -0
- package/dist/modules/entry-parts.js +63 -0
- package/dist/modules/files.d.ts +3 -0
- package/dist/modules/files.js +64 -0
- package/dist/modules/index.d.ts +74 -0
- package/dist/modules/index.js +774 -0
- package/dist/modules/jobs.d.ts +3 -0
- package/dist/modules/jobs.js +23 -0
- package/dist/modules/organisations.d.ts +3 -0
- package/dist/modules/organisations.js +20 -0
- package/dist/modules/outputs.d.ts +9 -0
- package/dist/modules/outputs.js +38 -0
- package/dist/modules/sign-in.d.ts +3 -0
- package/dist/modules/sign-in.js +197 -0
- package/dist/modules/signed-records.d.ts +3 -0
- package/dist/modules/signed-records.js +32 -0
- package/dist/modules/types.d.ts +194 -0
- package/dist/modules/types.js +1 -0
- package/dist/modules/workspaces.d.ts +3 -0
- package/dist/modules/workspaces.js +75 -0
- package/dist/native-build.d.ts +70 -0
- package/dist/native-build.js +92 -0
- package/dist/org.d.ts +184 -24
- package/dist/org.js +444 -49
- package/dist/plan/agora.d.ts +1 -1
- package/dist/plan/agora.js +4 -0
- package/dist/plan/apps-db-route.d.ts +90 -0
- package/dist/plan/apps-db-route.js +168 -0
- package/dist/plan/apps-zones.d.ts +56 -0
- package/dist/plan/apps-zones.js +118 -0
- package/dist/plan/archon-blue-green.js +11 -1
- package/dist/plan/blue-green-apply.d.ts +37 -1
- package/dist/plan/blue-green-apply.js +55 -15
- package/dist/plan/blue-green.d.ts +60 -8
- package/dist/plan/blue-green.js +115 -10
- package/dist/plan/boule.d.ts +23 -1
- package/dist/plan/boule.js +35 -2
- package/dist/plan/broker-entries.d.ts +28 -3
- package/dist/plan/broker-entries.js +74 -3
- package/dist/plan/broker-registrar.d.ts +168 -0
- package/dist/plan/broker-registrar.js +440 -0
- package/dist/plan/broker-workspaces-database.d.ts +48 -0
- package/dist/plan/broker-workspaces-database.js +87 -0
- package/dist/plan/deploy-health.js +9 -2
- package/dist/plan/desktop-callback.js +15 -7
- package/dist/plan/diff.d.ts +8 -2
- package/dist/plan/diff.js +552 -104
- package/dist/plan/edge-access.d.ts +6 -0
- package/dist/plan/edge-access.js +1 -0
- package/dist/plan/edge-dns.d.ts +6 -0
- package/dist/plan/edge-dns.js +9 -7
- package/dist/plan/entry-ai-key.d.ts +1 -1
- package/dist/plan/entry-ai-key.js +2 -2
- package/dist/plan/entry-audit.d.ts +58 -0
- package/dist/plan/entry-audit.js +122 -0
- package/dist/plan/entry-customer-grants.d.ts +75 -0
- package/dist/plan/entry-customer-grants.js +154 -0
- package/dist/plan/entry-edge-key.d.ts +26 -1
- package/dist/plan/entry-edge-key.js +42 -3
- package/dist/plan/entry-jobs.d.ts +64 -0
- package/dist/plan/entry-jobs.js +92 -0
- package/dist/plan/entry-repo-key-apply.d.ts +55 -0
- package/dist/plan/entry-repo-key-apply.js +185 -0
- package/dist/plan/entry-repo-key.d.ts +92 -0
- package/dist/plan/entry-repo-key.js +235 -0
- package/dist/plan/entry-workspaces.d.ts +55 -0
- package/dist/plan/entry-workspaces.js +90 -0
- package/dist/plan/executor.d.ts +231 -19
- package/dist/plan/executor.js +1648 -129
- package/dist/plan/gates.d.ts +1 -1
- package/dist/plan/gates.js +62 -9
- package/dist/plan/github-deploy-keys.d.ts +78 -0
- package/dist/plan/github-deploy-keys.js +305 -0
- package/dist/plan/github-source.d.ts +53 -0
- package/dist/plan/github-source.js +140 -0
- package/dist/plan/held-key-refs.d.ts +12 -0
- package/dist/plan/held-key-refs.js +12 -0
- package/dist/plan/hostnames.d.ts +9 -0
- package/dist/plan/hostnames.js +28 -1
- package/dist/plan/index.d.ts +9 -0
- package/dist/plan/index.js +6 -0
- package/dist/plan/jobs-scheduler-http.js +3 -0
- package/dist/plan/keys-registrar-http.d.ts +32 -0
- package/dist/plan/keys-registrar-http.js +105 -0
- package/dist/plan/keys-registrar.d.ts +67 -0
- package/dist/plan/keys-registrar.js +103 -0
- package/dist/plan/kosmos.d.ts +56 -0
- package/dist/plan/kosmos.js +84 -0
- package/dist/plan/live.d.ts +1 -1
- package/dist/plan/locked-hostnames.d.ts +2 -2
- package/dist/plan/locked-hostnames.js +7 -4
- package/dist/plan/login-host.js +11 -5
- package/dist/plan/machine-paths.d.ts +36 -0
- package/dist/plan/machine-paths.js +137 -0
- package/dist/plan/mcp.d.ts +1 -1
- package/dist/plan/mcp.js +49 -17
- package/dist/plan/org-apps-admin.d.ts +108 -0
- package/dist/plan/org-apps-admin.js +434 -0
- package/dist/plan/org-names.d.ts +3 -3
- package/dist/plan/org-names.js +15 -2
- package/dist/plan/org-postgres-migrate.js +1 -0
- package/dist/plan/own-diff.d.ts +23 -0
- package/dist/plan/own-diff.js +53 -0
- package/dist/plan/own-hostnames.d.ts +2 -0
- package/dist/plan/own-hostnames.js +1 -0
- package/dist/plan/own-jobs-route.d.ts +47 -0
- package/dist/plan/own-jobs-route.js +91 -0
- package/dist/plan/own-limits.d.ts +6 -0
- package/dist/plan/own-limits.js +1 -0
- package/dist/plan/own-services.d.ts +4 -0
- package/dist/plan/own-services.js +4 -0
- package/dist/plan/plan-apply.d.ts +39 -0
- package/dist/plan/plan-apply.js +163 -9
- package/dist/plan/platform-diff.d.ts +2 -2
- package/dist/plan/platform-diff.js +7 -2
- package/dist/plan/platform-org.d.ts +30 -0
- package/dist/plan/platform-org.js +12 -1
- package/dist/plan/platform-providers.d.ts +2 -0
- package/dist/plan/platform-providers.js +13 -2
- package/dist/plan/post-logout-uris.d.ts +1 -1
- package/dist/plan/post-logout-uris.js +12 -3
- package/dist/plan/private-pages.d.ts +26 -0
- package/dist/plan/private-pages.js +39 -0
- package/dist/plan/providers-readers.js +20 -1
- package/dist/plan/proxy-labels.d.ts +12 -1
- package/dist/plan/proxy-labels.js +32 -2
- package/dist/plan/release-host.d.ts +6 -2
- package/dist/plan/release-host.js +36 -8
- package/dist/plan/stage-env.d.ts +19 -1
- package/dist/plan/stage-env.js +25 -1
- package/dist/plan/stage-guards.d.ts +20 -0
- package/dist/plan/stage-guards.js +24 -0
- package/dist/plan/stage-isolation.d.ts +24 -0
- package/dist/plan/stage-isolation.js +29 -1
- package/dist/plan/worker-app-database.d.ts +46 -42
- package/dist/plan/worker-app-database.js +150 -153
- package/dist/plan/worker-app.d.ts +5 -65
- package/dist/plan/worker-app.js +28 -105
- package/dist/plan/worker.d.ts +2 -2
- package/dist/plan/worker.js +2 -1
- package/dist/plan/workspace-client.d.ts +25 -0
- package/dist/plan/workspace-client.js +48 -0
- package/dist/plan/workspace-sign-in.d.ts +55 -0
- package/dist/plan/workspace-sign-in.js +273 -0
- package/dist/plan/workspace-worker.d.ts +4 -0
- package/dist/plan/workspace-worker.js +13 -0
- package/dist/plan/workspace.d.ts +101 -0
- package/dist/plan/workspace.js +305 -0
- package/dist/release/decide.d.ts +30 -1
- package/dist/release/decide.js +73 -11
- package/dist/release/health.d.ts +8 -3
- package/dist/release/health.js +58 -5
- package/dist/release/migration-compatibility.d.ts +66 -0
- package/dist/release/migration-compatibility.js +340 -0
- package/dist/release/run.d.ts +2 -5
- package/dist/release/run.js +4 -0
- package/dist/release/state.d.ts +3 -1
- package/dist/release/state.js +6 -0
- package/dist/release/types.d.ts +20 -0
- package/dist/requested-changes.d.ts +11 -2
- package/dist/requested-changes.js +13 -1
- package/dist/service-labels.d.ts +4 -0
- package/dist/service-labels.js +11 -0
- package/dist/slug.d.ts +2 -0
- package/dist/slug.js +20 -7
- package/dist/store/postgres.js +25 -2
- package/dist/store/schema.d.ts +73 -1
- package/dist/store/schema.js +10 -0
- package/dist/worker-app-config.d.ts +1 -11
- package/dist/worker-app-config.js +4 -17
- package/dist/worker-app-migrations.d.ts +105 -0
- package/dist/worker-app-migrations.js +166 -0
- package/dist/worker-app-upload.d.ts +103 -0
- package/dist/worker-app-upload.js +128 -0
- package/package.json +19 -2
package/dist/release/health.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ import type { HealthRead } from './decide.js';
|
|
|
4
4
|
export declare const HEALTH_READ_TIMEOUT_MS = 10000;
|
|
5
5
|
/** The color's own address check has this long to answer. */
|
|
6
6
|
export declare const ADDRESS_CHECK_TIMEOUT_MS = 10000;
|
|
7
|
+
/** A health body larger than this is refused unread (a runaway or hostile response must not fill memory). */
|
|
8
|
+
export declare const HEALTH_BODY_MAX_BYTES: number;
|
|
7
9
|
/** C1's 200 body. */
|
|
8
10
|
export declare const healthBody: z.ZodObject<{
|
|
9
11
|
colors: z.ZodObject<{
|
|
@@ -40,14 +42,17 @@ export declare function readReleaseHealth(input: {
|
|
|
40
42
|
fetchImpl?: typeof fetch;
|
|
41
43
|
}): Promise<HealthRead | null>;
|
|
42
44
|
/**
|
|
43
|
-
* An HTTP check of the color's health path on its own address. A redirect is a failure: it is
|
|
44
|
-
* wall (Cloudflare Access) answers a machine.
|
|
45
|
+
* An HTTP check of the color's health path on its own address. A redirect to another origin is a failure: it is
|
|
46
|
+
* how a login wall (Cloudflare Access) answers a machine. One same-origin redirect (a trailing-slash 308) is
|
|
47
|
+
* followed and the answer judged; a second redirect fails. The body is read under {@link HEALTH_BODY_MAX_BYTES}.
|
|
48
|
+
* With `expectJson` it must parse as JSON. Without, it must not be Access's login page (a 200 that
|
|
49
|
+
* carries `cloudflareaccess.com` or `/cdn-cgi/access/`), so a login wall is never read as a healthy static app.
|
|
45
50
|
*/
|
|
46
51
|
export declare function checkColorAddress(input: {
|
|
47
52
|
address: string;
|
|
48
53
|
healthPath: string | null;
|
|
49
54
|
headers?: Record<string, string>;
|
|
50
|
-
/** True for an app whose health path answers JSON; false for a static app's `/`. */
|
|
55
|
+
/** True for an app with a server, whose health path answers JSON; false for a static app's `/`. */
|
|
51
56
|
expectJson?: boolean;
|
|
52
57
|
fetchImpl?: typeof fetch;
|
|
53
58
|
}): Promise<'ok' | 'failed'>;
|
package/dist/release/health.js
CHANGED
|
@@ -3,6 +3,36 @@ import { z } from 'zod';
|
|
|
3
3
|
export const HEALTH_READ_TIMEOUT_MS = 10_000;
|
|
4
4
|
/** The color's own address check has this long to answer. */
|
|
5
5
|
export const ADDRESS_CHECK_TIMEOUT_MS = 10_000;
|
|
6
|
+
/** A health body larger than this is refused unread (a runaway or hostile response must not fill memory). */
|
|
7
|
+
export const HEALTH_BODY_MAX_BYTES = 64 * 1024;
|
|
8
|
+
/**
|
|
9
|
+
* The response body as text, or null when it is larger than {@link HEALTH_BODY_MAX_BYTES}. Reads in chunks and
|
|
10
|
+
* stops at the cap, so an endless body is never held.
|
|
11
|
+
*/
|
|
12
|
+
async function readCappedText(res) {
|
|
13
|
+
const declared = Number(res.headers.get('content-length'));
|
|
14
|
+
if (Number.isFinite(declared) && declared > HEALTH_BODY_MAX_BYTES)
|
|
15
|
+
return null;
|
|
16
|
+
if (!res.body)
|
|
17
|
+
return '';
|
|
18
|
+
const reader = res.body.getReader();
|
|
19
|
+
const chunks = [];
|
|
20
|
+
let total = 0;
|
|
21
|
+
for (;;) {
|
|
22
|
+
const { done, value } = await reader.read();
|
|
23
|
+
if (done)
|
|
24
|
+
break;
|
|
25
|
+
total += value.byteLength;
|
|
26
|
+
if (total > HEALTH_BODY_MAX_BYTES) {
|
|
27
|
+
await reader.cancel().catch(() => undefined);
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
chunks.push(value);
|
|
31
|
+
}
|
|
32
|
+
return new TextDecoder().decode(Buffer.concat(chunks));
|
|
33
|
+
}
|
|
34
|
+
/** What Cloudflare Access's login page carries: its hosted domain and its path prefix. */
|
|
35
|
+
const ACCESS_PAGE_MARKERS = ['cloudflareaccess.com', '/cdn-cgi/access/'];
|
|
6
36
|
const counts = z.object({
|
|
7
37
|
requests: z.number().int().nonnegative(),
|
|
8
38
|
errors: z.number().int().nonnegative(),
|
|
@@ -35,7 +65,10 @@ export async function readReleaseHealth(input) {
|
|
|
35
65
|
});
|
|
36
66
|
if (!res.ok)
|
|
37
67
|
return null;
|
|
38
|
-
const
|
|
68
|
+
const text = await readCappedText(res);
|
|
69
|
+
if (text === null)
|
|
70
|
+
return null;
|
|
71
|
+
const parsed = healthBody.safeParse(JSON.parse(text));
|
|
39
72
|
if (!parsed.success)
|
|
40
73
|
return null;
|
|
41
74
|
return { colors: parsed.data.colors, daily: parsed.data.daily };
|
|
@@ -46,20 +79,40 @@ export async function readReleaseHealth(input) {
|
|
|
46
79
|
}
|
|
47
80
|
}
|
|
48
81
|
/**
|
|
49
|
-
* An HTTP check of the color's health path on its own address. A redirect is a failure: it is
|
|
50
|
-
* wall (Cloudflare Access) answers a machine.
|
|
82
|
+
* An HTTP check of the color's health path on its own address. A redirect to another origin is a failure: it is
|
|
83
|
+
* how a login wall (Cloudflare Access) answers a machine. One same-origin redirect (a trailing-slash 308) is
|
|
84
|
+
* followed and the answer judged; a second redirect fails. The body is read under {@link HEALTH_BODY_MAX_BYTES}.
|
|
85
|
+
* With `expectJson` it must parse as JSON. Without, it must not be Access's login page (a 200 that
|
|
86
|
+
* carries `cloudflareaccess.com` or `/cdn-cgi/access/`), so a login wall is never read as a healthy static app.
|
|
51
87
|
*/
|
|
52
88
|
export async function checkColorAddress(input) {
|
|
53
89
|
try {
|
|
54
|
-
const
|
|
90
|
+
const doFetch = input.fetchImpl ?? fetch;
|
|
91
|
+
const get = (url) => doFetch(url, {
|
|
55
92
|
...(input.headers ? { headers: input.headers } : {}),
|
|
56
93
|
redirect: 'manual',
|
|
57
94
|
signal: AbortSignal.timeout(ADDRESS_CHECK_TIMEOUT_MS),
|
|
58
95
|
});
|
|
96
|
+
const first = new URL(input.healthPath ?? '/', input.address);
|
|
97
|
+
let res = await get(first);
|
|
98
|
+
if (res.status >= 300 && res.status < 400) {
|
|
99
|
+
const location = res.headers.get('location');
|
|
100
|
+
if (!location)
|
|
101
|
+
return 'failed';
|
|
102
|
+
const next = new URL(location, first);
|
|
103
|
+
if (next.origin !== first.origin)
|
|
104
|
+
return 'failed';
|
|
105
|
+
res = await get(next);
|
|
106
|
+
}
|
|
59
107
|
if (!res.ok)
|
|
60
108
|
return 'failed';
|
|
109
|
+
const text = await readCappedText(res);
|
|
110
|
+
if (text === null)
|
|
111
|
+
return 'failed';
|
|
61
112
|
if (input.expectJson)
|
|
62
|
-
|
|
113
|
+
JSON.parse(text);
|
|
114
|
+
else if (ACCESS_PAGE_MARKERS.some((marker) => text.includes(marker)))
|
|
115
|
+
return 'failed';
|
|
63
116
|
return 'ok';
|
|
64
117
|
}
|
|
65
118
|
catch {
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { type WorkerAppConfig } from '../worker-app-config.js';
|
|
2
|
+
/**
|
|
3
|
+
* Whether a release's database changes may go to idle (foundry#934, estate DECISIONS.md,
|
|
4
|
+
* 2026-10-21 "Releases and versions"). Idle shares the live database with whatever release is
|
|
5
|
+
* live, so a release may only add -- never drop, narrow, rename or retype something the live
|
|
6
|
+
* release still reads or writes, and never delete a Durable Object class the live release still
|
|
7
|
+
* uses. A removal needs two releases: one that stops using the object, a later one -- once that
|
|
8
|
+
* first release is no longer on live, idle or draining anywhere -- that removes it.
|
|
9
|
+
*
|
|
10
|
+
* Pure: no database connection, no network call, no data read. The caller supplies everything it
|
|
11
|
+
* already has: what the previous live release applied (`liveMigrationIds`, `liveDoTag`), what
|
|
12
|
+
* release tags are deployed anywhere right now (`deployedReleases`), and which removals this
|
|
13
|
+
* release declares as the drained second step of an earlier one (`declaredRemovals`). There is no
|
|
14
|
+
* override beyond that declaration: a destructive step with no declaration, or whose declared
|
|
15
|
+
* release has not drained, is refused every time.
|
|
16
|
+
*
|
|
17
|
+
* A SQL statement declares its own removal with a `-- removal-since: <release>` comment directly
|
|
18
|
+
* above it (the same convention `@wtfalch/db`'s migration safety check uses for
|
|
19
|
+
* `-- destructive: <reason>`); a Durable Object class removal is declared in `declaredRemovals`,
|
|
20
|
+
* since `deleted_classes` carries no comment of its own.
|
|
21
|
+
*
|
|
22
|
+
* The SQL classifier is pattern-based, not a real SQL parser: it is a safety rail against an
|
|
23
|
+
* accidental incompatible release, not a boundary against someone deliberately hiding a drop
|
|
24
|
+
* inside a string. A statement it cannot classify is refused rather than guessed as safe.
|
|
25
|
+
*/
|
|
26
|
+
/** A Durable Object class removal this release declares as the drained second step of an earlier one. */
|
|
27
|
+
export interface DeclaredClassRemoval {
|
|
28
|
+
/** The class name a `deleted_classes` step of this release removes. */
|
|
29
|
+
className: string;
|
|
30
|
+
/** The earlier release that stopped using the class. Must have fully drained before this passes. */
|
|
31
|
+
sinceRelease: string;
|
|
32
|
+
}
|
|
33
|
+
export interface MigrationCompatibilityInput {
|
|
34
|
+
/** This release's own `migrations.json` (SQL), or `null` for a release with none. */
|
|
35
|
+
migrationsJson: string | null;
|
|
36
|
+
/** The SQL migration ids the previous live release already applied, in order. Default: none. */
|
|
37
|
+
liveMigrationIds?: readonly string[];
|
|
38
|
+
/** This release's full, declared Durable Object migrations, in order. Default: none. */
|
|
39
|
+
doMigrations?: WorkerAppConfig['durableObjects']['migrations'];
|
|
40
|
+
/** The Durable Object migration tag the previous live release holds, if any. */
|
|
41
|
+
liveDoTag?: string;
|
|
42
|
+
/** Durable Object class removals this release declares. Default: none. */
|
|
43
|
+
declaredRemovals?: readonly DeclaredClassRemoval[];
|
|
44
|
+
/** Release tags that are, right now, on live, idle or draining anywhere for this app. Default: none. */
|
|
45
|
+
deployedReleases?: readonly string[];
|
|
46
|
+
}
|
|
47
|
+
export interface MigrationCompatibilityProblem {
|
|
48
|
+
/** The SQL migration id or Durable Object migration tag the statement is in. */
|
|
49
|
+
migration: string;
|
|
50
|
+
/** The statement or step that is refused, trimmed to one line. */
|
|
51
|
+
statement: string;
|
|
52
|
+
/** Why it is refused, and what to do about it, in plain words. */
|
|
53
|
+
reason: string;
|
|
54
|
+
}
|
|
55
|
+
export type MigrationCompatibilityResult = {
|
|
56
|
+
ok: true;
|
|
57
|
+
} | {
|
|
58
|
+
ok: false;
|
|
59
|
+
problems: MigrationCompatibilityProblem[];
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Whether a release's migrations (SQL and Durable Object) may go to idle, given what the
|
|
63
|
+
* previous live release already applied and which releases are deployed anywhere right now. See
|
|
64
|
+
* the module docstring for the rule and the removal convention.
|
|
65
|
+
*/
|
|
66
|
+
export declare function checkMigrationCompatibility(input: MigrationCompatibilityInput): MigrationCompatibilityResult;
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { foldMigrationClasses } from '../worker-app-config.js';
|
|
2
|
+
import { parseMigrations, unappliedDatabaseMigrations, } from '../worker-app-migrations.js';
|
|
3
|
+
const REMOVAL_MARKER = /^--\s*removal-since:\s*(\S.*)$/i;
|
|
4
|
+
const STATEMENT_MAX_CHARS = 160;
|
|
5
|
+
/** Collapses whitespace and cuts a statement down to a readable, one-line length for a message. */
|
|
6
|
+
function oneLine(text) {
|
|
7
|
+
const collapsed = text.replace(/\s+/g, ' ').trim();
|
|
8
|
+
return collapsed.length > STATEMENT_MAX_CHARS
|
|
9
|
+
? `${collapsed.slice(0, STATEMENT_MAX_CHARS - 1)}…`
|
|
10
|
+
: collapsed;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Splits `sql` into standalone statements at a semicolon that sits outside a line comment, a
|
|
14
|
+
* block comment, a single- or double-quoted literal, and a dollar-quoted body (a function's
|
|
15
|
+
* `$$ ... $$`, which may itself hold semicolons). Each statement keeps any comment lines directly
|
|
16
|
+
* above it, so a `-- removal-since:` marker travels with the statement it excuses.
|
|
17
|
+
*/
|
|
18
|
+
function splitStatements(sql) {
|
|
19
|
+
const statements = [];
|
|
20
|
+
let current = '';
|
|
21
|
+
let i = 0;
|
|
22
|
+
const n = sql.length;
|
|
23
|
+
while (i < n) {
|
|
24
|
+
const ch = sql[i];
|
|
25
|
+
if (ch === '-' && sql[i + 1] === '-') {
|
|
26
|
+
const end = sql.indexOf('\n', i);
|
|
27
|
+
const end2 = end === -1 ? n : end + 1;
|
|
28
|
+
current += sql.slice(i, end2);
|
|
29
|
+
i = end2;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (ch === '/' && sql[i + 1] === '*') {
|
|
33
|
+
const end = sql.indexOf('*/', i + 2);
|
|
34
|
+
const end2 = end === -1 ? n : end + 2;
|
|
35
|
+
current += sql.slice(i, end2);
|
|
36
|
+
i = end2;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (ch === "'" || ch === '"') {
|
|
40
|
+
let j = i + 1;
|
|
41
|
+
while (j < n) {
|
|
42
|
+
if (sql[j] === ch) {
|
|
43
|
+
if (sql[j + 1] === ch) {
|
|
44
|
+
j += 2;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
j += 1;
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
j += 1;
|
|
51
|
+
}
|
|
52
|
+
current += sql.slice(i, j);
|
|
53
|
+
i = j;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (ch === '$') {
|
|
57
|
+
const tagMatch = /^\$[A-Za-z_][A-Za-z0-9_]*\$|^\$\$/.exec(sql.slice(i));
|
|
58
|
+
if (tagMatch) {
|
|
59
|
+
const tag = tagMatch[0];
|
|
60
|
+
const end = sql.indexOf(tag, i + tag.length);
|
|
61
|
+
const end2 = end === -1 ? n : end + tag.length;
|
|
62
|
+
current += sql.slice(i, end2);
|
|
63
|
+
i = end2;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (ch === ';') {
|
|
68
|
+
statements.push(current);
|
|
69
|
+
current = '';
|
|
70
|
+
i += 1;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
current += ch;
|
|
74
|
+
i += 1;
|
|
75
|
+
}
|
|
76
|
+
if (current.trim() !== '')
|
|
77
|
+
statements.push(current);
|
|
78
|
+
return statements;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Splits `code` at a top-level comma (outside a quoted literal or parentheses), so each element
|
|
82
|
+
* of an `ALTER TABLE`'s comma-separated action list -- `ADD COLUMN a text DEFAULT 'x', ADD COLUMN
|
|
83
|
+
* b integer NOT NULL` -- can be judged on its own. Without this, a `DEFAULT` on one added column
|
|
84
|
+
* hid a `NOT NULL` with no default on a different one, because the two regexes ran against the
|
|
85
|
+
* whole statement (foundry#941 review).
|
|
86
|
+
*/
|
|
87
|
+
function splitTopLevelCommas(code) {
|
|
88
|
+
const parts = [];
|
|
89
|
+
let current = '';
|
|
90
|
+
let depth = 0;
|
|
91
|
+
let i = 0;
|
|
92
|
+
const n = code.length;
|
|
93
|
+
while (i < n) {
|
|
94
|
+
const ch = code[i];
|
|
95
|
+
if (ch === "'" || ch === '"') {
|
|
96
|
+
let j = i + 1;
|
|
97
|
+
while (j < n) {
|
|
98
|
+
if (code[j] === ch) {
|
|
99
|
+
if (code[j + 1] === ch) {
|
|
100
|
+
j += 2;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
j += 1;
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
j += 1;
|
|
107
|
+
}
|
|
108
|
+
current += code.slice(i, j);
|
|
109
|
+
i = j;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (ch === '(') {
|
|
113
|
+
depth += 1;
|
|
114
|
+
current += ch;
|
|
115
|
+
i += 1;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (ch === ')') {
|
|
119
|
+
depth = Math.max(0, depth - 1);
|
|
120
|
+
current += ch;
|
|
121
|
+
i += 1;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (ch === ',' && depth === 0) {
|
|
125
|
+
parts.push(current);
|
|
126
|
+
current = '';
|
|
127
|
+
i += 1;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
current += ch;
|
|
131
|
+
i += 1;
|
|
132
|
+
}
|
|
133
|
+
parts.push(current);
|
|
134
|
+
return parts;
|
|
135
|
+
}
|
|
136
|
+
/** The statement with its leading blank and comment lines removed, so classification reads code, not a marker. */
|
|
137
|
+
function withoutLeadingComments(statement) {
|
|
138
|
+
const lines = statement.split('\n');
|
|
139
|
+
let i = 0;
|
|
140
|
+
while (i < lines.length) {
|
|
141
|
+
const trimmed = lines[i].trim();
|
|
142
|
+
if (trimmed === '' || trimmed.startsWith('--')) {
|
|
143
|
+
i += 1;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
return lines.slice(i).join('\n').trim();
|
|
149
|
+
}
|
|
150
|
+
/** The release named in a `-- removal-since: <release>` line directly above the statement, or `null`. */
|
|
151
|
+
function declaredSince(statement) {
|
|
152
|
+
for (const line of statement.split('\n')) {
|
|
153
|
+
const trimmed = line.trim();
|
|
154
|
+
if (trimmed === '')
|
|
155
|
+
continue;
|
|
156
|
+
const match = REMOVAL_MARKER.exec(trimmed);
|
|
157
|
+
if (match)
|
|
158
|
+
return match[1].trim();
|
|
159
|
+
if (!trimmed.startsWith('--'))
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Classifies one statement's code (comments already stripped) as additive or destructive, or
|
|
166
|
+
* `null` when it cannot tell: an unrecognised statement is never assumed to be additive.
|
|
167
|
+
*/
|
|
168
|
+
function classifyStatement(code) {
|
|
169
|
+
if (code === '')
|
|
170
|
+
return null;
|
|
171
|
+
if (/^CREATE\s+(OR\s+REPLACE\s+)?(TABLE|SCHEMA|EXTENSION|SEQUENCE|VIEW)\b/i.test(code)) {
|
|
172
|
+
return { destructive: false, reason: 'creates something new' };
|
|
173
|
+
}
|
|
174
|
+
if (/^CREATE\s+(UNIQUE\s+)?INDEX(\s+CONCURRENTLY)?\b/i.test(code)) {
|
|
175
|
+
return { destructive: false, reason: 'adds an index' };
|
|
176
|
+
}
|
|
177
|
+
if (/^CREATE\s+(OR\s+REPLACE\s+)?(FUNCTION|PROCEDURE)\b/i.test(code)) {
|
|
178
|
+
return { destructive: false, reason: 'adds or replaces a function' };
|
|
179
|
+
}
|
|
180
|
+
if (/^CREATE\s+(OR\s+REPLACE\s+)?TRIGGER\b/i.test(code)) {
|
|
181
|
+
return { destructive: false, reason: 'adds a trigger' };
|
|
182
|
+
}
|
|
183
|
+
if (/^(COMMENT\s+ON|GRANT)\b/i.test(code)) {
|
|
184
|
+
return {
|
|
185
|
+
destructive: false,
|
|
186
|
+
reason: 'changes neither data nor structure a running release reads',
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
if (/^ALTER\s+TABLE\b/i.test(code)) {
|
|
190
|
+
if (/\bDROP\s+COLUMN\b/i.test(code))
|
|
191
|
+
return { destructive: true, reason: 'drops a column' };
|
|
192
|
+
if (/\bDROP\s+CONSTRAINT\b/i.test(code))
|
|
193
|
+
return { destructive: true, reason: 'drops a constraint' };
|
|
194
|
+
if (/\bRENAME\b/i.test(code))
|
|
195
|
+
return { destructive: true, reason: 'renames a table or column' };
|
|
196
|
+
if (/\bALTER\s+COLUMN\b[\s\S]*\b(SET\s+DATA\s+)?TYPE\b/i.test(code)) {
|
|
197
|
+
return { destructive: true, reason: "changes a column's type" };
|
|
198
|
+
}
|
|
199
|
+
if (/\bSET\s+NOT\s+NULL\b/i.test(code)) {
|
|
200
|
+
return { destructive: true, reason: 'adds a NOT NULL constraint to an existing column' };
|
|
201
|
+
}
|
|
202
|
+
if (/\bADD\s+COLUMN\b/i.test(code)) {
|
|
203
|
+
for (const clause of splitTopLevelCommas(code)) {
|
|
204
|
+
if (!/\bADD\s+COLUMN\b/i.test(clause))
|
|
205
|
+
continue;
|
|
206
|
+
if (/\bNOT\s+NULL\b/i.test(clause) && !/\bDEFAULT\b/i.test(clause)) {
|
|
207
|
+
return { destructive: true, reason: 'adds a NOT NULL column with no default' };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return { destructive: false, reason: 'adds a column' };
|
|
211
|
+
}
|
|
212
|
+
if (/\bADD\s+CONSTRAINT\b/i.test(code))
|
|
213
|
+
return { destructive: false, reason: 'adds a constraint' };
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
if (/^DROP\s+(TABLE|INDEX|TYPE|SCHEMA|FUNCTION|PROCEDURE|TRIGGER|VIEW|MATERIALIZED\s+VIEW|EXTENSION|SEQUENCE)\b/i.test(code)) {
|
|
217
|
+
return { destructive: true, reason: 'drops something the live release may still read' };
|
|
218
|
+
}
|
|
219
|
+
if (/^(TRUNCATE|DELETE\s+FROM|UPDATE\s+\S+\s+SET)\b/i.test(code)) {
|
|
220
|
+
return { destructive: true, reason: 'removes or rewrites existing rows' };
|
|
221
|
+
}
|
|
222
|
+
if (/^ALTER\s+(INDEX|SEQUENCE|VIEW|TYPE|SCHEMA)\b[\s\S]*\bRENAME\b/i.test(code)) {
|
|
223
|
+
return { destructive: true, reason: 'renames something' };
|
|
224
|
+
}
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
const splitHelp = (what) => `${what}; split this into two releases: stop using it in one, remove it in a later release once that one is no longer on live, idle or draining`;
|
|
228
|
+
/** A destructive statement or step named `since` is drained once that release deploys nowhere. */
|
|
229
|
+
function hasDrained(since, deployedReleases) {
|
|
230
|
+
return !deployedReleases.includes(since);
|
|
231
|
+
}
|
|
232
|
+
function sqlProblems(migrationsJson, liveMigrationIds, deployedReleases) {
|
|
233
|
+
let declared;
|
|
234
|
+
try {
|
|
235
|
+
declared = parseMigrations(migrationsJson);
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
return [{ migration: '', statement: '', reason: error.message }];
|
|
239
|
+
}
|
|
240
|
+
let pending;
|
|
241
|
+
try {
|
|
242
|
+
pending = unappliedDatabaseMigrations(declared, liveMigrationIds);
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
return [{ migration: '', statement: '', reason: error.message }];
|
|
246
|
+
}
|
|
247
|
+
const problems = [];
|
|
248
|
+
for (const migration of pending) {
|
|
249
|
+
for (const statement of splitStatements(migration.sql)) {
|
|
250
|
+
const code = withoutLeadingComments(statement);
|
|
251
|
+
const classified = classifyStatement(code);
|
|
252
|
+
if (classified === null) {
|
|
253
|
+
problems.push({
|
|
254
|
+
migration: migration.id,
|
|
255
|
+
statement: oneLine(code),
|
|
256
|
+
reason: 'this statement is not one foundry recognises as additive; split it into its own release and ask a person',
|
|
257
|
+
});
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (!classified.destructive)
|
|
261
|
+
continue;
|
|
262
|
+
const since = declaredSince(statement);
|
|
263
|
+
if (since === null) {
|
|
264
|
+
problems.push({
|
|
265
|
+
migration: migration.id,
|
|
266
|
+
statement: oneLine(code),
|
|
267
|
+
reason: splitHelp(classified.reason),
|
|
268
|
+
});
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
if (!hasDrained(since, deployedReleases)) {
|
|
272
|
+
problems.push({
|
|
273
|
+
migration: migration.id,
|
|
274
|
+
statement: oneLine(code),
|
|
275
|
+
reason: `${classified.reason}; declared as the second step after release ${since}, but ${since} is still on live, idle or draining -- wait for it to fully drain, then release this one`,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return problems;
|
|
281
|
+
}
|
|
282
|
+
function doProblems(doMigrations, liveDoTag, declaredRemovals, deployedReleases) {
|
|
283
|
+
if (doMigrations.length === 0)
|
|
284
|
+
return [];
|
|
285
|
+
let appliedCount = 0;
|
|
286
|
+
if (liveDoTag !== undefined) {
|
|
287
|
+
const index = doMigrations.findIndex((m) => m.tag === liveDoTag);
|
|
288
|
+
if (index < 0) {
|
|
289
|
+
return [
|
|
290
|
+
{
|
|
291
|
+
migration: liveDoTag,
|
|
292
|
+
statement: '',
|
|
293
|
+
reason: "the live release holds a Durable Object migration that is not in this release's declared list; migrations are append-only",
|
|
294
|
+
},
|
|
295
|
+
];
|
|
296
|
+
}
|
|
297
|
+
appliedCount = index + 1;
|
|
298
|
+
}
|
|
299
|
+
const folded = foldMigrationClasses(doMigrations.slice(0, appliedCount));
|
|
300
|
+
if ('error' in folded)
|
|
301
|
+
return [{ migration: '', statement: '', reason: folded.error }];
|
|
302
|
+
const problems = [];
|
|
303
|
+
for (const migration of doMigrations.slice(appliedCount)) {
|
|
304
|
+
for (const className of migration.deletedClasses ?? []) {
|
|
305
|
+
if (!folded.classes.has(className))
|
|
306
|
+
continue; // never reached live: nothing running still uses it
|
|
307
|
+
const removal = declaredRemovals.find((r) => r.className === className);
|
|
308
|
+
const statement = `deleted_classes: ${className}`;
|
|
309
|
+
if (!removal) {
|
|
310
|
+
problems.push({
|
|
311
|
+
migration: migration.tag,
|
|
312
|
+
statement,
|
|
313
|
+
reason: splitHelp(`deletes the Durable Object class '${className}', which the live release still uses`),
|
|
314
|
+
});
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
if (!hasDrained(removal.sinceRelease, deployedReleases)) {
|
|
318
|
+
problems.push({
|
|
319
|
+
migration: migration.tag,
|
|
320
|
+
statement,
|
|
321
|
+
reason: `deletes the Durable Object class '${className}', declared as the second step after release ${removal.sinceRelease}, but ${removal.sinceRelease} is still on live, idle or draining -- wait for it to fully drain, then release this one`,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return problems;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Whether a release's migrations (SQL and Durable Object) may go to idle, given what the
|
|
330
|
+
* previous live release already applied and which releases are deployed anywhere right now. See
|
|
331
|
+
* the module docstring for the rule and the removal convention.
|
|
332
|
+
*/
|
|
333
|
+
export function checkMigrationCompatibility(input) {
|
|
334
|
+
const deployedReleases = input.deployedReleases ?? [];
|
|
335
|
+
const problems = [
|
|
336
|
+
...sqlProblems(input.migrationsJson, input.liveMigrationIds ?? [], deployedReleases),
|
|
337
|
+
...doProblems(input.doMigrations ?? [], input.liveDoTag, input.declaredRemovals ?? [], deployedReleases),
|
|
338
|
+
];
|
|
339
|
+
return problems.length === 0 ? { ok: true } : { ok: false, problems };
|
|
340
|
+
}
|
package/dist/release/run.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Decision, type Observation, type ReleaseEffect } from './decide.js';
|
|
1
|
+
import { type Decision, type Observation, type ReleaseEffect, type ReleaseEntryConfig } from './decide.js';
|
|
2
2
|
import { type ReleaseStore } from './state.js';
|
|
3
3
|
import type { Color } from './types.js';
|
|
4
4
|
/** What the runner needs from the outside. Each call is bounded by its own timeout. */
|
|
@@ -31,10 +31,7 @@ export declare function runRelease(input: {
|
|
|
31
31
|
store: ReleaseStore;
|
|
32
32
|
orgId: string;
|
|
33
33
|
entryKey: string;
|
|
34
|
-
entry:
|
|
35
|
-
steps?: number[];
|
|
36
|
-
stepHoldMinutes?: number;
|
|
37
|
-
};
|
|
34
|
+
entry: ReleaseEntryConfig;
|
|
38
35
|
now: Date;
|
|
39
36
|
deps: ReleaseDeps;
|
|
40
37
|
/** No reporting health read: only hard fails count (see `decide`). */
|
package/dist/release/run.js
CHANGED
|
@@ -25,6 +25,10 @@ export async function runRelease(input) {
|
|
|
25
25
|
actions,
|
|
26
26
|
steps: input.entry.steps,
|
|
27
27
|
stepHoldMinutes: input.entry.stepHoldMinutes,
|
|
28
|
+
advance: input.entry.advance,
|
|
29
|
+
minRequests: input.entry.minRequests,
|
|
30
|
+
errorMargin: input.entry.errorMargin,
|
|
31
|
+
staffFirst: input.entry.staffFirst,
|
|
28
32
|
now,
|
|
29
33
|
observation: { health, ...seen },
|
|
30
34
|
...(input.hardOnly ? { hardOnly: true } : {}),
|
package/dist/release/state.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import type
|
|
2
|
+
import { type Recorded, resolveReleaseConfig } from './decide.js';
|
|
3
3
|
import type { Color, ReleaseAction, ReleaseSignal, ReleaseState, WindowState } from './types.js';
|
|
4
4
|
/**
|
|
5
5
|
* The persistence the release runner and Archon share. `getScopeIds` is the
|
|
@@ -52,6 +52,8 @@ export declare function readSignal(ids: Record<string, string>, entryKey: string
|
|
|
52
52
|
export declare function readReleaseState(store: ReleaseStore, input: {
|
|
53
53
|
orgId: string;
|
|
54
54
|
entryKey: string;
|
|
55
|
+
/** The entry's `blueGreen` as declared; defaults are resolved into `config`. */
|
|
56
|
+
entry?: Parameters<typeof resolveReleaseConfig>[0];
|
|
55
57
|
}): Promise<ReleaseState | null>;
|
|
56
58
|
declare const actionSchema: z.ZodType<ReleaseAction>;
|
|
57
59
|
/** C2: ask the runner to apply an action on its next run. Archon audits it. */
|
package/dist/release/state.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { STAGE_REF_PATTERN, resolveReleaseConfig } from './decide.js';
|
|
2
3
|
/** Scope keys (C3). `entry` is a hosting entry key: never `.` or `:`. */
|
|
3
4
|
export function releaseKeys(entry) {
|
|
4
5
|
if (!/^[a-z0-9][a-z0-9-]*$/.test(entry))
|
|
@@ -33,6 +34,7 @@ const storedWindow = z.discriminatedUnion('kind', [
|
|
|
33
34
|
z.object({ kind: z.literal('passed'), at: z.string() }),
|
|
34
35
|
z.object({ kind: z.literal('rolled_back'), at: z.string(), reason: z.string() }),
|
|
35
36
|
z.object({ kind: z.literal('cap'), since: z.string() }),
|
|
37
|
+
z.object({ kind: z.literal('staff'), since: z.string() }),
|
|
36
38
|
]);
|
|
37
39
|
const releaseField = z
|
|
38
40
|
.object({
|
|
@@ -110,6 +112,7 @@ export async function readReleaseState(store, input) {
|
|
|
110
112
|
window: r.window,
|
|
111
113
|
signal: readSignal(ids, input.entryKey),
|
|
112
114
|
held: r.held,
|
|
115
|
+
config: resolveReleaseConfig(input.entry),
|
|
113
116
|
};
|
|
114
117
|
}
|
|
115
118
|
const actionSchema = z.discriminatedUnion('kind', [
|
|
@@ -117,6 +120,9 @@ const actionSchema = z.discriminatedUnion('kind', [
|
|
|
117
120
|
z.object({ kind: z.literal('rollback') }),
|
|
118
121
|
z.object({ kind: z.literal('clear_held'), digest: z.string().min(1) }),
|
|
119
122
|
z.object({ kind: z.literal('decide_at_cap'), decision: z.enum(['pass', 'rollback']) }),
|
|
123
|
+
z.object({ kind: z.literal('approve_step') }),
|
|
124
|
+
z.object({ kind: z.literal('open') }),
|
|
125
|
+
z.object({ kind: z.literal('deploy_stage'), ref: z.string().regex(STAGE_REF_PATTERN) }),
|
|
120
126
|
]);
|
|
121
127
|
/** C2: ask the runner to apply an action on its next run. Archon audits it. */
|
|
122
128
|
export async function requestReleaseAction(store, input) {
|
package/dist/release/types.d.ts
CHANGED
|
@@ -22,6 +22,11 @@ export type WindowState = {
|
|
|
22
22
|
} | {
|
|
23
23
|
kind: 'cap';
|
|
24
24
|
since: string;
|
|
25
|
+
}
|
|
26
|
+
/** Deployed on the idle color at weight 0, waiting for an `open` action (staffFirst). */
|
|
27
|
+
| {
|
|
28
|
+
kind: 'staff';
|
|
29
|
+
since: string;
|
|
25
30
|
};
|
|
26
31
|
export interface ReleaseSignal {
|
|
27
32
|
requests: number;
|
|
@@ -29,6 +34,13 @@ export interface ReleaseSignal {
|
|
|
29
34
|
baselineRate: number | null;
|
|
30
35
|
verdict: 'pass' | 'fail' | 'hard_fail' | 'inconclusive';
|
|
31
36
|
}
|
|
37
|
+
/** The release kind of an entry, defaults resolved. */
|
|
38
|
+
export interface ReleaseConfig {
|
|
39
|
+
advance: 'time' | 'signal' | 'approval';
|
|
40
|
+
steps: number[];
|
|
41
|
+
stepHoldMinutes: number;
|
|
42
|
+
staffFirst: boolean;
|
|
43
|
+
}
|
|
32
44
|
export interface ReleaseState {
|
|
33
45
|
entryKey: string;
|
|
34
46
|
live: Color;
|
|
@@ -38,6 +50,7 @@ export interface ReleaseState {
|
|
|
38
50
|
window: WindowState;
|
|
39
51
|
signal: ReleaseSignal | null;
|
|
40
52
|
held: string[];
|
|
53
|
+
config: ReleaseConfig;
|
|
41
54
|
}
|
|
42
55
|
export type ReleaseAction = {
|
|
43
56
|
kind: 'promote';
|
|
@@ -49,6 +62,13 @@ export type ReleaseAction = {
|
|
|
49
62
|
} | {
|
|
50
63
|
kind: 'decide_at_cap';
|
|
51
64
|
decision: 'pass' | 'rollback';
|
|
65
|
+
} | {
|
|
66
|
+
kind: 'approve_step';
|
|
67
|
+
} | {
|
|
68
|
+
kind: 'open';
|
|
69
|
+
} | {
|
|
70
|
+
kind: 'deploy_stage';
|
|
71
|
+
ref: string;
|
|
52
72
|
};
|
|
53
73
|
export declare const COLORS: readonly Color[];
|
|
54
74
|
export declare function otherColor(color: Color): Color;
|