@wtfalch/foundry 0.47.0 → 0.48.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/index.d.ts +3 -1
- package/dist/index.js +1 -0
- package/dist/migrations/0059_org_builtin_blue_green.sql +9 -0
- package/dist/org.d.ts +17 -1
- package/dist/org.js +24 -3
- package/dist/plan/archon-blue-green.js +15 -1
- package/dist/plan/blue-green-apply.d.ts +35 -0
- package/dist/plan/blue-green-apply.js +167 -16
- package/dist/plan/blue-green.d.ts +97 -2
- package/dist/plan/blue-green.js +214 -13
- package/dist/plan/database-dump.d.ts +37 -0
- package/dist/plan/database-dump.js +101 -0
- package/dist/plan/diff.js +3 -0
- package/dist/plan/executor.d.ts +48 -0
- package/dist/plan/executor.js +79 -20
- package/dist/plan/index.d.ts +2 -0
- package/dist/plan/index.js +4 -0
- package/dist/plan/locked-hostnames.d.ts +1 -0
- package/dist/plan/locked-hostnames.js +1 -0
- package/dist/plan/plan-apply.js +16 -7
- package/dist/plan/platform-diff.js +2 -1
- package/dist/plan/platform-providers.js +2 -0
- package/dist/plan/proxy-labels.d.ts +13 -1
- package/dist/plan/proxy-labels.js +26 -3
- package/dist/plan/release-host.d.ts +1 -1
- package/dist/plan/release-host.js +36 -14
- package/dist/plan/stage-env.d.ts +38 -0
- package/dist/plan/stage-env.js +52 -0
- package/dist/plan/stage-guards.d.ts +104 -0
- package/dist/plan/stage-guards.js +149 -0
- package/dist/plan/stage-isolation.d.ts +57 -0
- package/dist/plan/stage-isolation.js +248 -0
- package/dist/refuse-org.d.ts +1 -1
- package/dist/refuse-org.js +5 -3
- package/dist/release/decide.d.ts +5 -0
- package/dist/release/decide.js +10 -6
- package/dist/release/health.d.ts +6 -1
- package/dist/release/health.js +10 -2
- package/dist/release/run.d.ts +2 -0
- package/dist/release/run.js +2 -1
- package/dist/store/postgres.js +20 -1
- package/dist/store/schema.d.ts +39 -1
- package/dist/store/schema.js +3 -0
- package/package.json +1 -1
package/dist/release/decide.js
CHANGED
|
@@ -150,7 +150,7 @@ export function decide(input) {
|
|
|
150
150
|
r = rollback(r, now, 'deploy timeout');
|
|
151
151
|
}
|
|
152
152
|
else {
|
|
153
|
-
const health = observation.health;
|
|
153
|
+
const health = input.hardOnly ? null : observation.health;
|
|
154
154
|
const newC = health?.colors[rel.to] ?? { requests: 0, errors: 0 };
|
|
155
155
|
const atWindow = r.window.kind === 'window' || r.window.kind === 'cap';
|
|
156
156
|
let baseline = null;
|
|
@@ -162,6 +162,8 @@ export function decide(input) {
|
|
|
162
162
|
let verdict;
|
|
163
163
|
if (hard || hardRate)
|
|
164
164
|
verdict = 'hard_fail';
|
|
165
|
+
else if (input.hardOnly)
|
|
166
|
+
verdict = 'pass';
|
|
165
167
|
else if (!health || baseline === null || newC.requests < RELATIVE_MIN_REQUESTS)
|
|
166
168
|
verdict = 'inconclusive';
|
|
167
169
|
else
|
|
@@ -200,11 +202,13 @@ export function decide(input) {
|
|
|
200
202
|
}
|
|
201
203
|
else {
|
|
202
204
|
const required = Math.max(1, Math.ceil(health?.daily.requestsPerDay ?? 0));
|
|
203
|
-
const closable =
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
205
|
+
const closable = input.hardOnly
|
|
206
|
+
? elapsed >= WINDOW_MIN_HOURS * HOUR
|
|
207
|
+
: health !== null &&
|
|
208
|
+
baseline !== null &&
|
|
209
|
+
elapsed >= WINDOW_MIN_HOURS * HOUR &&
|
|
210
|
+
newC.requests >= required &&
|
|
211
|
+
rate(newC) <= baseline + ERROR_RATE_MARGIN;
|
|
208
212
|
if (closable) {
|
|
209
213
|
out.effects.push({ kind: 'stop', color: rel.from });
|
|
210
214
|
r = { ...r, window: { kind: 'passed', at: now.toISOString() }, release: null };
|
package/dist/release/health.d.ts
CHANGED
|
@@ -39,10 +39,15 @@ export declare function readReleaseHealth(input: {
|
|
|
39
39
|
headers?: Record<string, string>;
|
|
40
40
|
fetchImpl?: typeof fetch;
|
|
41
41
|
}): Promise<HealthRead | null>;
|
|
42
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* An HTTP check of the color's health path on its own address. A redirect is a failure: it is how a login
|
|
44
|
+
* wall (Cloudflare Access) answers a machine. With `expectJson` the body must also parse as JSON, so a login page served with 200 is not read as a healthy color.
|
|
45
|
+
*/
|
|
43
46
|
export declare function checkColorAddress(input: {
|
|
44
47
|
address: string;
|
|
45
48
|
healthPath: string | null;
|
|
46
49
|
headers?: Record<string, string>;
|
|
50
|
+
/** True for an app whose health path answers JSON; false for a static app's `/`. */
|
|
51
|
+
expectJson?: boolean;
|
|
47
52
|
fetchImpl?: typeof fetch;
|
|
48
53
|
}): Promise<'ok' | 'failed'>;
|
package/dist/release/health.js
CHANGED
|
@@ -45,14 +45,22 @@ export async function readReleaseHealth(input) {
|
|
|
45
45
|
return null;
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* An HTTP check of the color's health path on its own address. A redirect is a failure: it is how a login
|
|
50
|
+
* wall (Cloudflare Access) answers a machine. With `expectJson` the body must also parse as JSON, so a login page served with 200 is not read as a healthy color.
|
|
51
|
+
*/
|
|
49
52
|
export async function checkColorAddress(input) {
|
|
50
53
|
try {
|
|
51
54
|
const res = await (input.fetchImpl ?? fetch)(new URL(input.healthPath ?? '/', input.address), {
|
|
52
55
|
...(input.headers ? { headers: input.headers } : {}),
|
|
56
|
+
redirect: 'manual',
|
|
53
57
|
signal: AbortSignal.timeout(ADDRESS_CHECK_TIMEOUT_MS),
|
|
54
58
|
});
|
|
55
|
-
|
|
59
|
+
if (!res.ok)
|
|
60
|
+
return 'failed';
|
|
61
|
+
if (input.expectJson)
|
|
62
|
+
await res.json();
|
|
63
|
+
return 'ok';
|
|
56
64
|
}
|
|
57
65
|
catch {
|
|
58
66
|
// Unreachable counts as a failed check.
|
package/dist/release/run.d.ts
CHANGED
package/dist/release/run.js
CHANGED
|
@@ -16,7 +16,7 @@ export async function runRelease(input) {
|
|
|
16
16
|
const actions = open.map((r) => r.action);
|
|
17
17
|
const range = healthRange(recorded, now);
|
|
18
18
|
const target = recorded.release?.to ?? null;
|
|
19
|
-
const health = range ? await deps.readHealth(range) : null;
|
|
19
|
+
const health = range && !input.hardOnly ? await deps.readHealth(range) : null;
|
|
20
20
|
const seen = target
|
|
21
21
|
? await deps.observeColor(target)
|
|
22
22
|
: { status: 'other', address: 'unchecked' };
|
|
@@ -27,6 +27,7 @@ export async function runRelease(input) {
|
|
|
27
27
|
stepHoldMinutes: input.entry.stepHoldMinutes,
|
|
28
28
|
now,
|
|
29
29
|
observation: { health, ...seen },
|
|
30
|
+
...(input.hardOnly ? { hardOnly: true } : {}),
|
|
30
31
|
});
|
|
31
32
|
for (const effect of decision.effects)
|
|
32
33
|
await deps.effect(effect);
|
package/dist/store/postgres.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { and, asc, desc, eq, inArray, isNull, ne, sql } from 'drizzle-orm';
|
|
2
|
-
import { APP_ENTRY_KEY, DEFAULT_MAIL_APPS, DEFAULT_NO_REPLY_LOCAL_PART, refuseCustomers, refuseHosting, validateBlueGreen, validateBuyerIdentity, validateCalls, validateDomain, validateDomainClaim, validateMail, validateOwners, validateServices, validateSignIn, validateSite, validateVersionPin, } from '../org.js';
|
|
2
|
+
import { APP_ENTRY_KEY, DEFAULT_MAIL_APPS, DEFAULT_NO_REPLY_LOCAL_PART, refuseBuiltinBlueGreen, refuseCustomers, refuseHosting, validateBlueGreen, validateBuyerIdentity, validateCalls, validateDomain, validateDomainClaim, validateMail, validateOwners, validateServices, validateSignIn, validateSite, validateVersionPin, } from '../org.js';
|
|
3
3
|
import { hostingNames } from '../plan/locked-hostnames.js';
|
|
4
4
|
import { findNameClashes, nameClashMessage } from '../plan/org-names.js';
|
|
5
5
|
import { validatePlatform } from '../platform.js';
|
|
@@ -161,6 +161,12 @@ function rowToOrg(row) {
|
|
|
161
161
|
? { customers: { enabled: row.customersEnabled, signUp: row.customersSignUp } }
|
|
162
162
|
: {}),
|
|
163
163
|
// Omitted while unset. Validated on read: a stored value is not trusted.
|
|
164
|
+
...(row.bouleBlueGreen
|
|
165
|
+
? { boule: { blueGreen: readBlueGreen(row.bouleBlueGreen, 'boule') } }
|
|
166
|
+
: {}),
|
|
167
|
+
...(row.agoraBlueGreen
|
|
168
|
+
? { agora: { blueGreen: readBlueGreen(row.agoraBlueGreen, 'agora') } }
|
|
169
|
+
: {}),
|
|
164
170
|
...(row.versionPin ? { versionPin: readVersionPin(row) } : {}),
|
|
165
171
|
site: { enabled: row.siteEnabled, usesDomain: row.siteUsesDomain },
|
|
166
172
|
hosting: {
|
|
@@ -193,6 +199,13 @@ function readVersionPin(row) {
|
|
|
193
199
|
}
|
|
194
200
|
return pin;
|
|
195
201
|
}
|
|
202
|
+
function readBlueGreen(stored, app) {
|
|
203
|
+
const reason = validateBlueGreen(stored, app);
|
|
204
|
+
if (reason) {
|
|
205
|
+
throw new DeclaredOrgError(`a stored org has an invalid ${app}.blueGreen: ${reason}`);
|
|
206
|
+
}
|
|
207
|
+
return stored;
|
|
208
|
+
}
|
|
196
209
|
function orgToRow(org) {
|
|
197
210
|
return {
|
|
198
211
|
id: org.id,
|
|
@@ -215,6 +228,8 @@ function orgToRow(org) {
|
|
|
215
228
|
signInInApp: org.signIn?.inApp ?? false,
|
|
216
229
|
customersEnabled: org.customers?.enabled ?? false,
|
|
217
230
|
customersSignUp: org.customers?.signUp ?? false,
|
|
231
|
+
bouleBlueGreen: org.boule?.blueGreen ?? null,
|
|
232
|
+
agoraBlueGreen: org.agora?.blueGreen ?? null,
|
|
218
233
|
servicesOwn: org.services?.own ?? false,
|
|
219
234
|
servicesSfuSlot: org.services?.sfuSlot ?? null,
|
|
220
235
|
versionPin: org.versionPin && Object.keys(org.versionPin).length > 0 ? org.versionPin : null,
|
|
@@ -422,6 +437,10 @@ export function createPostgresDeclaredOrgStore(db) {
|
|
|
422
437
|
if (customersRefusal) {
|
|
423
438
|
throw new DeclaredOrgError(`customers invalid: ${customersRefusal.message}`, customersRefusal);
|
|
424
439
|
}
|
|
440
|
+
const builtinRefusal = refuseBuiltinBlueGreen(org);
|
|
441
|
+
if (builtinRefusal) {
|
|
442
|
+
throw new DeclaredOrgError(`blue/green invalid: ${builtinRefusal.message}`, builtinRefusal);
|
|
443
|
+
}
|
|
425
444
|
const slot = org.services?.sfuSlot ?? null;
|
|
426
445
|
if (slot === null)
|
|
427
446
|
return;
|
package/dist/store/schema.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AppEnvSource, HostingEntry, IssuerApp, OwnerIdentity } from '../org.js';
|
|
1
|
+
import type { AppEnvSource, BlueGreenConfig, HostingEntry, IssuerApp, OwnerIdentity } from '../org.js';
|
|
2
2
|
import type { Plan } from '../plan.js';
|
|
3
3
|
import type { Checkpoint, DestroyRecord } from '../state.js';
|
|
4
4
|
/**
|
|
@@ -406,6 +406,44 @@ export declare const declaredOrgs: import("drizzle-orm/pg-core").PgTableWithColu
|
|
|
406
406
|
identity: undefined;
|
|
407
407
|
generated: undefined;
|
|
408
408
|
}, {}, {}>;
|
|
409
|
+
bouleBlueGreen: import("drizzle-orm/pg-core").PgColumn<{
|
|
410
|
+
name: "boule_blue_green";
|
|
411
|
+
tableName: "foundry_declared_orgs";
|
|
412
|
+
dataType: "json";
|
|
413
|
+
columnType: "PgJsonb";
|
|
414
|
+
data: BlueGreenConfig | null;
|
|
415
|
+
driverParam: unknown;
|
|
416
|
+
notNull: false;
|
|
417
|
+
hasDefault: false;
|
|
418
|
+
isPrimaryKey: false;
|
|
419
|
+
isAutoincrement: false;
|
|
420
|
+
hasRuntimeDefault: false;
|
|
421
|
+
enumValues: undefined;
|
|
422
|
+
baseColumn: never;
|
|
423
|
+
identity: undefined;
|
|
424
|
+
generated: undefined;
|
|
425
|
+
}, {}, {
|
|
426
|
+
$type: BlueGreenConfig | null;
|
|
427
|
+
}>;
|
|
428
|
+
agoraBlueGreen: import("drizzle-orm/pg-core").PgColumn<{
|
|
429
|
+
name: "agora_blue_green";
|
|
430
|
+
tableName: "foundry_declared_orgs";
|
|
431
|
+
dataType: "json";
|
|
432
|
+
columnType: "PgJsonb";
|
|
433
|
+
data: BlueGreenConfig | null;
|
|
434
|
+
driverParam: unknown;
|
|
435
|
+
notNull: false;
|
|
436
|
+
hasDefault: false;
|
|
437
|
+
isPrimaryKey: false;
|
|
438
|
+
isAutoincrement: false;
|
|
439
|
+
hasRuntimeDefault: false;
|
|
440
|
+
enumValues: undefined;
|
|
441
|
+
baseColumn: never;
|
|
442
|
+
identity: undefined;
|
|
443
|
+
generated: undefined;
|
|
444
|
+
}, {}, {
|
|
445
|
+
$type: BlueGreenConfig | null;
|
|
446
|
+
}>;
|
|
409
447
|
versionPin: import("drizzle-orm/pg-core").PgColumn<{
|
|
410
448
|
name: "version_pin";
|
|
411
449
|
tableName: "foundry_declared_orgs";
|
package/dist/store/schema.js
CHANGED
|
@@ -43,6 +43,9 @@ export const declaredOrgs = pgTable('foundry_declared_orgs', {
|
|
|
43
43
|
// CustomersConfig (org.ts); migrations/0057.
|
|
44
44
|
customersEnabled: boolean('customers_enabled').notNull().default(false),
|
|
45
45
|
customersSignUp: boolean('customers_sign_up').notNull().default(false),
|
|
46
|
+
// OrgBuiltinApp.blueGreen (org.ts) of Boule and Agora; `migrations/0059_org_builtin_blue_green.sql`. Null is off.
|
|
47
|
+
bouleBlueGreen: jsonb('boule_blue_green').$type(),
|
|
48
|
+
agoraBlueGreen: jsonb('agora_blue_green').$type(),
|
|
46
49
|
// VersionPin (org.ts): null follows the platform. `migrations/0053_org_version_pin.sql`.
|
|
47
50
|
versionPin: jsonb('version_pin').$type(),
|
|
48
51
|
// SiteConfig (org.ts, T10). A brand new org has no cms site.
|