@wtfalch/foundry 0.61.0 → 0.63.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/bootstrap/declare.js +4 -1
- package/dist/bootstrap/versions.d.ts +2 -0
- package/dist/index.d.ts +9 -1
- package/dist/index.js +3 -0
- package/dist/migrations/0065_native_build_runs.sql +32 -0
- package/dist/migrations/0066_org_kosmos.sql +10 -0
- package/dist/migrations/0067_requested_changes_kosmos.sql +6 -0
- package/dist/migrations/0068_platform_registry.sql +17 -0
- package/dist/modules/sign-in.js +1 -1
- package/dist/native-build.d.ts +50 -0
- package/dist/native-build.js +72 -0
- package/dist/org.d.ts +22 -3
- package/dist/org.js +25 -2
- package/dist/plan/agora.d.ts +1 -1
- package/dist/plan/agora.js +4 -0
- package/dist/plan/archon-blue-green.d.ts +5 -0
- package/dist/plan/archon-blue-green.js +2 -0
- package/dist/plan/blue-green-apply.d.ts +5 -0
- package/dist/plan/blue-green-apply.js +197 -22
- package/dist/plan/blue-green.d.ts +80 -9
- package/dist/plan/blue-green.js +115 -14
- package/dist/plan/boule.d.ts +1 -1
- package/dist/plan/boule.js +2 -0
- package/dist/plan/broker-entries.d.ts +9 -0
- package/dist/plan/broker-entries.js +29 -0
- package/dist/plan/deploy-health.js +9 -2
- package/dist/plan/desktop-callback.js +4 -1
- package/dist/plan/diff.d.ts +5 -0
- package/dist/plan/diff.js +184 -63
- package/dist/plan/edge-access.d.ts +2 -3
- package/dist/plan/edge-access.js +8 -0
- package/dist/plan/executor.d.ts +111 -19
- package/dist/plan/executor.js +578 -72
- package/dist/plan/index.d.ts +2 -0
- package/dist/plan/index.js +1 -0
- package/dist/plan/jobs-scheduler-http.d.ts +2 -0
- package/dist/plan/jobs-scheduler-http.js +11 -0
- package/dist/plan/kosmos.d.ts +56 -0
- package/dist/plan/kosmos.js +84 -0
- package/dist/plan/locked-hostnames.js +5 -2
- package/dist/plan/org-names.d.ts +3 -3
- package/dist/plan/org-names.js +3 -1
- package/dist/plan/org-postgres-migrate.js +1 -0
- package/dist/plan/own-limits.d.ts +12 -0
- package/dist/plan/own-limits.js +2 -0
- package/dist/plan/plan-apply.d.ts +51 -1
- package/dist/plan/plan-apply.js +298 -25
- package/dist/plan/platform-diff.d.ts +6 -0
- package/dist/plan/platform-diff.js +74 -0
- package/dist/plan/platform-org.d.ts +2 -0
- package/dist/plan/platform-providers.d.ts +46 -1
- package/dist/plan/platform-providers.js +162 -1
- package/dist/plan/platform-registry-keys.d.ts +58 -0
- package/dist/plan/platform-registry-keys.js +69 -0
- package/dist/plan/platform-registry.d.ts +67 -0
- package/dist/plan/platform-registry.js +298 -0
- package/dist/plan/post-logout-uris.d.ts +1 -1
- package/dist/plan/post-logout-uris.js +5 -2
- package/dist/plan/providers-readers.js +20 -1
- package/dist/plan/release-host.d.ts +31 -2
- package/dist/plan/release-host.js +114 -6
- package/dist/plan/stage-copy-host.d.ts +104 -0
- package/dist/plan/stage-copy-host.js +259 -0
- package/dist/plan/stage-copy.js +44 -0
- package/dist/plan/stage-env.d.ts +9 -0
- package/dist/plan/stage-env.js +38 -0
- package/dist/plan/stage-guards.d.ts +57 -1
- package/dist/plan/stage-guards.js +106 -0
- package/dist/plan/stage-isolation.js +56 -0
- package/dist/plan/stage-services.d.ts +93 -0
- package/dist/plan/stage-services.js +153 -0
- package/dist/plan/stage-signin.d.ts +152 -0
- package/dist/plan/stage-signin.js +133 -0
- package/dist/plan/stage-sink.d.ts +2 -0
- package/dist/plan/stage-sink.js +6 -1
- package/dist/platform.d.ts +7 -1
- package/dist/platform.js +1 -0
- package/dist/release/decide.d.ts +12 -0
- package/dist/release/decide.js +37 -2
- package/dist/release/migration-compatibility.d.ts +66 -0
- package/dist/release/migration-compatibility.js +340 -0
- package/dist/release/run.d.ts +71 -1
- package/dist/release/run.js +92 -8
- package/dist/release/state.d.ts +14 -1
- package/dist/release/state.js +43 -0
- package/dist/release/types.d.ts +20 -0
- package/dist/release/types.js +2 -0
- package/dist/requested-changes.d.ts +11 -2
- package/dist/requested-changes.js +13 -1
- package/dist/slug.d.ts +1 -1
- package/dist/slug.js +11 -2
- package/dist/store/postgres.js +21 -0
- package/dist/store/schema.d.ts +87 -0
- package/dist/store/schema.js +10 -0
- package/package.json +3 -2
- package/versions.digests.json +1 -0
- package/versions.json +1 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { bgKey } from './blue-green.js';
|
|
3
|
+
/** Tries the broker write this many times (CLAUDE.md, Platform steps, rule 1). */
|
|
4
|
+
export const STAGE_SIGNIN_DELIVERY_TRIES = 3;
|
|
5
|
+
/** The guard value for a subject that does not sign in. */
|
|
6
|
+
export const STAGE_SIGNIN_NONE = 'none';
|
|
7
|
+
/** Fixed text and ids only: safe to log, record and return. */
|
|
8
|
+
export class StageSignInError extends Error {
|
|
9
|
+
name = 'StageSignInError';
|
|
10
|
+
}
|
|
11
|
+
const sha256 = (value) => createHash('sha256').update(value).digest('hex');
|
|
12
|
+
/**
|
|
13
|
+
* Makes stage's client, mints its app key once and writes its broker entry. Order: the client first (its ids
|
|
14
|
+
* recorded and persisted at once, a lost answer is adopted by name), then the key (held, its ref recorded under
|
|
15
|
+
* {@link bgKey.stageSignInKey} and persisted before anything is delivered), then the broker entry (retried).
|
|
16
|
+
* A run that failed halfway finds both and does the rest; the key is never minted twice. When the ref is
|
|
17
|
+
* recorded but the held store no longer has the key, this stops: it never mints another.
|
|
18
|
+
*/
|
|
19
|
+
export async function ensureStageSignIn(input) {
|
|
20
|
+
const { id, port } = input;
|
|
21
|
+
const at = `stage sign-in for ${id}`;
|
|
22
|
+
let appId = input.recorded()[bgKey.stageClient(id)];
|
|
23
|
+
let clientId = input.recorded()[bgKey.stageClientId(id)];
|
|
24
|
+
if (!appId || !clientId) {
|
|
25
|
+
const found = await port.findClient(port.spec.clientName);
|
|
26
|
+
if (found) {
|
|
27
|
+
if (!found.clientId) {
|
|
28
|
+
throw new StageSignInError(`${at}: client ${found.appId} exists but its id cannot be read`);
|
|
29
|
+
}
|
|
30
|
+
appId = found.appId;
|
|
31
|
+
clientId = found.clientId;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
const created = await port.createClient(port.spec);
|
|
35
|
+
appId = created.appId;
|
|
36
|
+
clientId = created.clientId;
|
|
37
|
+
}
|
|
38
|
+
input.record(bgKey.stageClient(id), appId);
|
|
39
|
+
input.record(bgKey.stageClientId(id), clientId);
|
|
40
|
+
await input.persist();
|
|
41
|
+
}
|
|
42
|
+
// One mint: the key goes to the held store, its ref is recorded and persisted, and only then is it used.
|
|
43
|
+
let appKey = await port.held.resolve();
|
|
44
|
+
if (appKey) {
|
|
45
|
+
input.secret(appKey);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
if (input.recorded()[bgKey.stageSignInKey(id)]) {
|
|
49
|
+
throw new StageSignInError(`${at}: key ${port.held.ref} was minted but is no longer held; restore the held value`);
|
|
50
|
+
}
|
|
51
|
+
appKey = input.generate();
|
|
52
|
+
input.secret(appKey);
|
|
53
|
+
await port.held.put(appKey);
|
|
54
|
+
input.record(bgKey.stageSignInKey(id), port.held.ref);
|
|
55
|
+
await input.persist();
|
|
56
|
+
}
|
|
57
|
+
const entry = {
|
|
58
|
+
name: port.spec.brokerName,
|
|
59
|
+
organizationId: port.spec.organizationId,
|
|
60
|
+
clientId,
|
|
61
|
+
origins: port.spec.origins,
|
|
62
|
+
from: port.spec.from,
|
|
63
|
+
registration: port.spec.registration,
|
|
64
|
+
...(port.spec.emailLink === undefined ? {} : { emailLink: port.spec.emailLink }),
|
|
65
|
+
};
|
|
66
|
+
const sleep = input.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
67
|
+
for (let attempt = 1;; attempt++) {
|
|
68
|
+
try {
|
|
69
|
+
await port.writeBrokerEntry(entry, appKey);
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// The cause can echo the key; the ref is all a reader needs.
|
|
74
|
+
if (attempt >= STAGE_SIGNIN_DELIVERY_TRIES) {
|
|
75
|
+
throw new StageSignInError(`${at}: delivering key ${port.held.ref} to the broker entry ${port.spec.brokerName} failed after ${STAGE_SIGNIN_DELIVERY_TRIES} tries`);
|
|
76
|
+
}
|
|
77
|
+
await sleep(1000 * attempt);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
input.record(bgKey.stageBroker(id), JSON.stringify({ name: entry.name, organizationId: entry.organizationId }));
|
|
81
|
+
await input.persist();
|
|
82
|
+
return {
|
|
83
|
+
env: [
|
|
84
|
+
{ key: 'AUTH_CLIENT_ID', value: clientId },
|
|
85
|
+
{ key: 'AUTH_APP_KEY', value: appKey },
|
|
86
|
+
],
|
|
87
|
+
guard: stageSignInGuard({ appId, clientId, brokerName: entry.name, origins: entry.origins }),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/** `ok:<sha256 of the ids>`: the client, the app, the broker entry's name and its origins. No value goes in. */
|
|
91
|
+
export function stageSignInGuard(input) {
|
|
92
|
+
return `ok:${sha256(JSON.stringify([input.appId, input.clientId, input.brokerName, [...input.origins].sort()]))}`;
|
|
93
|
+
}
|
|
94
|
+
/** Whether the recorded `bg.<id>.stagesignin` lets the copy go on. */
|
|
95
|
+
export const stageSignInReady = (value) => value === STAGE_SIGNIN_NONE || value?.startsWith('ok:') === true;
|
|
96
|
+
/**
|
|
97
|
+
* Makes the public client of a static SPA's stage (Agora): found by name or created, its ids recorded and
|
|
98
|
+
* persisted at once. There is no key and no broker entry, so nothing secret exists. The guard hashes the app id,
|
|
99
|
+
* the client id, the client name and the origins.
|
|
100
|
+
*/
|
|
101
|
+
export async function ensureStagePublicClient(input) {
|
|
102
|
+
const { id, port } = input;
|
|
103
|
+
const at = `stage sign-in for ${id}`;
|
|
104
|
+
let appId = input.recorded()[bgKey.stageClient(id)];
|
|
105
|
+
let clientId = input.recorded()[bgKey.stageClientId(id)];
|
|
106
|
+
if (!appId || !clientId) {
|
|
107
|
+
const found = await port.findClient(port.spec.clientName);
|
|
108
|
+
if (found) {
|
|
109
|
+
if (!found.clientId) {
|
|
110
|
+
throw new StageSignInError(`${at}: client ${found.appId} exists but its id cannot be read`);
|
|
111
|
+
}
|
|
112
|
+
appId = found.appId;
|
|
113
|
+
clientId = found.clientId;
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
const created = await port.createClient(port.spec);
|
|
117
|
+
appId = created.appId;
|
|
118
|
+
clientId = created.clientId;
|
|
119
|
+
}
|
|
120
|
+
input.record(bgKey.stageClient(id), appId);
|
|
121
|
+
input.record(bgKey.stageClientId(id), clientId);
|
|
122
|
+
await input.persist();
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
clientId,
|
|
126
|
+
guard: stageSignInGuard({
|
|
127
|
+
appId,
|
|
128
|
+
clientId,
|
|
129
|
+
brokerName: port.spec.clientName,
|
|
130
|
+
origins: port.spec.origins,
|
|
131
|
+
}),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
@@ -26,6 +26,8 @@ type SinkOrg = Pick<DeclaredOrg, 'slug'>;
|
|
|
26
26
|
export declare const stageSinkAppName: (org: SinkOrg) => string;
|
|
27
27
|
/** The sink's HTTP address on the company's Docker network. */
|
|
28
28
|
export declare const stageSinkInternalUrl: (org: SinkOrg) => string;
|
|
29
|
+
/** The sender stage mails carry. Apps send only when `MAIL_FROM` is set; the address cannot receive mail. */
|
|
30
|
+
export declare const STAGE_MAIL_FROM = "Stage <noreply@stage.invalid>";
|
|
29
31
|
/**
|
|
30
32
|
* The only mail env stage holds. `@wtfalch/mailer` 0.6.0 posts every message to `MAIL_SINK_URL` when it is set,
|
|
31
33
|
* so stage holds no mail token and no Lettermint setting.
|
package/dist/plan/stage-sink.js
CHANGED
|
@@ -24,10 +24,15 @@ export const STAGE_SINK_VERSION = 1;
|
|
|
24
24
|
export const stageSinkAppName = (org) => `${org.slug}-stage-sink`;
|
|
25
25
|
/** The sink's HTTP address on the company's Docker network. */
|
|
26
26
|
export const stageSinkInternalUrl = (org) => `http://${stageSinkAppName(org)}:${STAGE_SINK_HTTP_PORT}`;
|
|
27
|
+
/** The sender stage mails carry. Apps send only when `MAIL_FROM` is set; the address cannot receive mail. */
|
|
28
|
+
export const STAGE_MAIL_FROM = 'Stage <noreply@stage.invalid>';
|
|
27
29
|
/**
|
|
28
30
|
* The only mail env stage holds. `@wtfalch/mailer` 0.6.0 posts every message to `MAIL_SINK_URL` when it is set,
|
|
29
31
|
* so stage holds no mail token and no Lettermint setting.
|
|
30
32
|
*/
|
|
31
33
|
export function stageMailEnv(org) {
|
|
32
|
-
return [
|
|
34
|
+
return [
|
|
35
|
+
{ key: 'MAIL_SINK_URL', value: stageSinkInternalUrl(org) },
|
|
36
|
+
{ key: 'MAIL_FROM', value: STAGE_MAIL_FROM },
|
|
37
|
+
];
|
|
33
38
|
}
|
package/dist/platform.d.ts
CHANGED
|
@@ -72,7 +72,7 @@ export declare function uptimeArtifactUrl(tag: string): string;
|
|
|
72
72
|
* holds the Cloudflare token `<username>.<org domain>` claims use, so no
|
|
73
73
|
* hosting entry ever holds that credential itself.
|
|
74
74
|
*/
|
|
75
|
-
export type PlatformServiceKey = 'auth' | 'mail' | 'sfu' | 'files' | 'ai' | 'jobs' | 'integrations' | 'archon' | 'edge' | 'hc' | 'coolify';
|
|
75
|
+
export type PlatformServiceKey = 'auth' | 'mail' | 'sfu' | 'files' | 'ai' | 'jobs' | 'integrations' | 'archon' | 'edge' | 'hc' | 'registry' | 'coolify';
|
|
76
76
|
export declare const PLATFORM_SERVICE_KEYS: readonly PlatformServiceKey[];
|
|
77
77
|
/**
|
|
78
78
|
* The one shared platform every org's services run on -- decided in #165 as
|
|
@@ -123,6 +123,12 @@ export interface DeclaredPlatform {
|
|
|
123
123
|
* `plan/platform-diff.ts`'s `planHeartbeats`.
|
|
124
124
|
*/
|
|
125
125
|
hc: PlatformService;
|
|
126
|
+
/**
|
|
127
|
+
* The platform's image registry (`registry:3`, htpasswd sign-in): the images the build workflow
|
|
128
|
+
* pushes and the company servers pull. `gitRef` is the image tag, pinned by digest in
|
|
129
|
+
* `versions.json` like `hc`'s. See `plan/platform-registry.ts`.
|
|
130
|
+
*/
|
|
131
|
+
registry: PlatformService;
|
|
126
132
|
/**
|
|
127
133
|
* The Coolify panel. Its address is declared so the platform's hostnames
|
|
128
134
|
* include it; foundry never deploys it, so `enabled` stays false and
|
package/dist/platform.js
CHANGED
package/dist/release/decide.d.ts
CHANGED
|
@@ -51,6 +51,8 @@ export interface Recorded {
|
|
|
51
51
|
/** Set while window.kind is step, inconclusive, window or cap. */
|
|
52
52
|
release: ActiveRelease | null;
|
|
53
53
|
held: string[];
|
|
54
|
+
/** Stage holds the sink env. Absent reads as no. */
|
|
55
|
+
stageSink?: boolean;
|
|
54
56
|
}
|
|
55
57
|
export interface ColorCounts {
|
|
56
58
|
requests: number;
|
|
@@ -85,6 +87,14 @@ export type ReleaseEffect = {
|
|
|
85
87
|
| {
|
|
86
88
|
kind: 'deploy_stage';
|
|
87
89
|
ref: string;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Run the stage copy (or undo it) for the first open request of that kind. The runner does not execute it: it
|
|
93
|
+
* hands it to its caller, who runs it after this run's other effects, writes and weights are done.
|
|
94
|
+
*/
|
|
95
|
+
| {
|
|
96
|
+
kind: 'stage_copy';
|
|
97
|
+
mode: 'copy' | 'undo';
|
|
88
98
|
};
|
|
89
99
|
export interface Decision {
|
|
90
100
|
recorded: Recorded;
|
|
@@ -94,6 +104,8 @@ export interface Decision {
|
|
|
94
104
|
blocked: string[];
|
|
95
105
|
/** Actions that changed nothing, with a fixed reason. */
|
|
96
106
|
refused: string[];
|
|
107
|
+
/** The reason a `copy_prod_to_stage` request was refused here, for `bg.<id>.stagecopy`; else null. */
|
|
108
|
+
stageRefusal: string | null;
|
|
97
109
|
}
|
|
98
110
|
export declare function normalizeSteps(steps: readonly number[] | undefined): number[];
|
|
99
111
|
/** The time a health read should cover: from the step or window start to now. */
|
package/dist/release/decide.js
CHANGED
|
@@ -81,6 +81,7 @@ function applyActions(r, actions, now, out, kind) {
|
|
|
81
81
|
let ended = false;
|
|
82
82
|
let approved = false;
|
|
83
83
|
let opened = false;
|
|
84
|
+
const stageRequests = [];
|
|
84
85
|
for (const a of actions) {
|
|
85
86
|
if (a.kind === 'deploy_stage') {
|
|
86
87
|
if (typeof a.ref === 'string' && STAGE_REF_PATTERN.test(a.ref))
|
|
@@ -135,6 +136,9 @@ function applyActions(r, actions, now, out, kind) {
|
|
|
135
136
|
promoted = true;
|
|
136
137
|
}
|
|
137
138
|
}
|
|
139
|
+
else if (a.kind === 'copy_prod_to_stage' || a.kind === 'undo_stage_copy') {
|
|
140
|
+
stageRequests.push(a.kind);
|
|
141
|
+
}
|
|
138
142
|
else if (a.kind === 'rollback') {
|
|
139
143
|
if (!cur.release)
|
|
140
144
|
out.refused.push('rollback refused: no release is running');
|
|
@@ -157,6 +161,26 @@ function applyActions(r, actions, now, out, kind) {
|
|
|
157
161
|
}
|
|
158
162
|
}
|
|
159
163
|
}
|
|
164
|
+
// The stage requests are judged last, against the release the other actions of this run left.
|
|
165
|
+
for (const kind of stageRequests) {
|
|
166
|
+
const copy = kind === 'copy_prod_to_stage';
|
|
167
|
+
const name = copy ? 'copy' : 'undo';
|
|
168
|
+
if (out.effects.some((e) => e.kind === 'stage_copy')) {
|
|
169
|
+
out.refused.push(`${name} refused: one stage copy request runs at a time`);
|
|
170
|
+
}
|
|
171
|
+
else if (cur.release) {
|
|
172
|
+
// A copy takes up to ~45 minutes; a release that is stepping needs the runner free to roll back.
|
|
173
|
+
out.refused.push(`${name} refused: a release is running`);
|
|
174
|
+
out.stageRefusal = 'release';
|
|
175
|
+
}
|
|
176
|
+
else if (copy && !cur.stageSink) {
|
|
177
|
+
out.refused.push('copy refused: stage sink is not applied');
|
|
178
|
+
out.stageRefusal = 'sink';
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
out.effects.push({ kind: 'stage_copy', mode: copy ? 'copy' : 'undo' });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
160
184
|
return { recorded: cur, promoted, ended, approved, opened };
|
|
161
185
|
}
|
|
162
186
|
/**
|
|
@@ -168,7 +192,11 @@ export function decide(input) {
|
|
|
168
192
|
const { now, observation } = input;
|
|
169
193
|
const steps = normalizeSteps(input.steps);
|
|
170
194
|
const holdMs = (input.stepHoldMinutes ?? DEFAULT_STEP_HOLD_MINUTES) * MINUTE;
|
|
171
|
-
const out = {
|
|
195
|
+
const out = {
|
|
196
|
+
effects: [],
|
|
197
|
+
refused: [],
|
|
198
|
+
stageRefusal: null,
|
|
199
|
+
};
|
|
172
200
|
const advance = input.advance ?? DEFAULT_ADVANCE;
|
|
173
201
|
const minRequests = input.minRequests ?? RELATIVE_MIN_REQUESTS;
|
|
174
202
|
const errorMargin = input.errorMargin ?? ERROR_RATE_MARGIN;
|
|
@@ -285,7 +313,14 @@ export function decide(input) {
|
|
|
285
313
|
}
|
|
286
314
|
if (r.window.kind === 'cap')
|
|
287
315
|
blocked.push('release window reached the cap without enough data: pass or roll back in Archon');
|
|
288
|
-
return {
|
|
316
|
+
return {
|
|
317
|
+
recorded: r,
|
|
318
|
+
signal,
|
|
319
|
+
effects: out.effects,
|
|
320
|
+
blocked,
|
|
321
|
+
refused: out.refused,
|
|
322
|
+
stageRefusal: out.stageRefusal,
|
|
323
|
+
};
|
|
289
324
|
}
|
|
290
325
|
function stepTo(r, rel, percent, now) {
|
|
291
326
|
const at = now.toISOString();
|
|
@@ -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;
|