@wtfalch/foundry 0.47.0 → 0.49.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 +1 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.js +3 -2
- package/dist/migrations/0059_org_builtin_blue_green.sql +9 -0
- package/dist/migrations/0060_hosting_repos.sql +7 -0
- package/dist/org.d.ts +25 -1
- package/dist/org.js +51 -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/github-release-hook.d.ts +81 -0
- package/dist/plan/github-release-hook.js +159 -0
- package/dist/plan/index.d.ts +4 -1
- package/dist/plan/index.js +5 -1
- package/dist/plan/live.d.ts +1 -1
- 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.d.ts +59 -0
- package/dist/plan/platform-diff.js +100 -2
- package/dist/plan/platform-providers.d.ts +10 -0
- package/dist/plan/platform-providers.js +73 -1
- 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 +32 -2
- package/dist/store/schema.d.ts +58 -1
- package/dist/store/schema.js +5 -0
- package/package.json +1 -1
package/dist/release/decide.d.ts
CHANGED
|
@@ -92,4 +92,9 @@ export declare function decide(input: {
|
|
|
92
92
|
stepHoldMinutes?: number | undefined;
|
|
93
93
|
now: Date;
|
|
94
94
|
observation: Observation;
|
|
95
|
+
/**
|
|
96
|
+
* The app has no reporting health read (a static app). Only hard fails count: it never reports a relative
|
|
97
|
+
* fail, steps up by time, and the window closes after its minimum time.
|
|
98
|
+
*/
|
|
99
|
+
hardOnly?: boolean;
|
|
95
100
|
}): Decision;
|
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, normalizeHostingRepos, 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: {
|
|
@@ -170,6 +176,7 @@ function rowToOrg(row) {
|
|
|
170
176
|
issuerApps: row.hostingIssuerApps,
|
|
171
177
|
entries: row.hostingEntries.map((entry) => storedEntryToEntry(entry, row.hostingUsesDomain)),
|
|
172
178
|
gateLimit: row.hostingGateLimit,
|
|
179
|
+
repos: row.hostingRepos,
|
|
173
180
|
},
|
|
174
181
|
sso: { enabled: row.ssoEnabled },
|
|
175
182
|
guests: { enabled: row.guestsEnabled },
|
|
@@ -193,6 +200,13 @@ function readVersionPin(row) {
|
|
|
193
200
|
}
|
|
194
201
|
return pin;
|
|
195
202
|
}
|
|
203
|
+
function readBlueGreen(stored, app) {
|
|
204
|
+
const reason = validateBlueGreen(stored, app);
|
|
205
|
+
if (reason) {
|
|
206
|
+
throw new DeclaredOrgError(`a stored org has an invalid ${app}.blueGreen: ${reason}`);
|
|
207
|
+
}
|
|
208
|
+
return stored;
|
|
209
|
+
}
|
|
196
210
|
function orgToRow(org) {
|
|
197
211
|
return {
|
|
198
212
|
id: org.id,
|
|
@@ -215,6 +229,8 @@ function orgToRow(org) {
|
|
|
215
229
|
signInInApp: org.signIn?.inApp ?? false,
|
|
216
230
|
customersEnabled: org.customers?.enabled ?? false,
|
|
217
231
|
customersSignUp: org.customers?.signUp ?? false,
|
|
232
|
+
bouleBlueGreen: org.boule?.blueGreen ?? null,
|
|
233
|
+
agoraBlueGreen: org.agora?.blueGreen ?? null,
|
|
218
234
|
servicesOwn: org.services?.own ?? false,
|
|
219
235
|
servicesSfuSlot: org.services?.sfuSlot ?? null,
|
|
220
236
|
versionPin: org.versionPin && Object.keys(org.versionPin).length > 0 ? org.versionPin : null,
|
|
@@ -226,6 +242,7 @@ function orgToRow(org) {
|
|
|
226
242
|
hostingIssuerApps: org.hosting.issuerApps,
|
|
227
243
|
hostingEntries: org.hosting.entries,
|
|
228
244
|
hostingGateLimit: org.hosting.gateLimit,
|
|
245
|
+
hostingRepos: normalizeHostingRepos(org.hosting.repos),
|
|
229
246
|
ssoEnabled: org.sso.enabled,
|
|
230
247
|
guestsEnabled: org.guests.enabled,
|
|
231
248
|
soloEnabled: org.solo.enabled,
|
|
@@ -422,6 +439,10 @@ export function createPostgresDeclaredOrgStore(db) {
|
|
|
422
439
|
if (customersRefusal) {
|
|
423
440
|
throw new DeclaredOrgError(`customers invalid: ${customersRefusal.message}`, customersRefusal);
|
|
424
441
|
}
|
|
442
|
+
const builtinRefusal = refuseBuiltinBlueGreen(org);
|
|
443
|
+
if (builtinRefusal) {
|
|
444
|
+
throw new DeclaredOrgError(`blue/green invalid: ${builtinRefusal.message}`, builtinRefusal);
|
|
445
|
+
}
|
|
425
446
|
const slot = org.services?.sfuSlot ?? null;
|
|
426
447
|
if (slot === null)
|
|
427
448
|
return;
|
|
@@ -550,7 +571,16 @@ export function createPostgresDeclaredOrgStore(db) {
|
|
|
550
571
|
// just be re-litigating a write this update isn't making.
|
|
551
572
|
if (patch.slug !== undefined && patch.slug !== current.slug)
|
|
552
573
|
requireValidSlug(patch.slug);
|
|
553
|
-
const
|
|
574
|
+
const merged = {
|
|
575
|
+
...current,
|
|
576
|
+
...patch,
|
|
577
|
+
id: current.id,
|
|
578
|
+
version: current.version,
|
|
579
|
+
};
|
|
580
|
+
const next = {
|
|
581
|
+
...merged,
|
|
582
|
+
hosting: { ...merged.hosting, repos: normalizeHostingRepos(merged.hosting.repos) },
|
|
583
|
+
};
|
|
554
584
|
requireValidDomain(next.domain);
|
|
555
585
|
requireValidLimits(next);
|
|
556
586
|
requireValidBuyer(next);
|
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";
|
|
@@ -654,6 +692,25 @@ export declare const declaredOrgs: import("drizzle-orm/pg-core").PgTableWithColu
|
|
|
654
692
|
identity: undefined;
|
|
655
693
|
generated: undefined;
|
|
656
694
|
}, {}, {}>;
|
|
695
|
+
hostingRepos: import("drizzle-orm/pg-core").PgColumn<{
|
|
696
|
+
name: "hosting_repos";
|
|
697
|
+
tableName: "foundry_declared_orgs";
|
|
698
|
+
dataType: "json";
|
|
699
|
+
columnType: "PgJsonb";
|
|
700
|
+
data: string[];
|
|
701
|
+
driverParam: unknown;
|
|
702
|
+
notNull: true;
|
|
703
|
+
hasDefault: true;
|
|
704
|
+
isPrimaryKey: false;
|
|
705
|
+
isAutoincrement: false;
|
|
706
|
+
hasRuntimeDefault: false;
|
|
707
|
+
enumValues: undefined;
|
|
708
|
+
baseColumn: never;
|
|
709
|
+
identity: undefined;
|
|
710
|
+
generated: undefined;
|
|
711
|
+
}, {}, {
|
|
712
|
+
$type: string[];
|
|
713
|
+
}>;
|
|
657
714
|
ssoEnabled: import("drizzle-orm/pg-core").PgColumn<{
|
|
658
715
|
name: "sso_enabled";
|
|
659
716
|
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.
|
|
@@ -71,6 +74,8 @@ export const declaredOrgs = pgTable('foundry_declared_orgs', {
|
|
|
71
74
|
hostingEntries: jsonb('hosting_entries').notNull().default([]).$type(),
|
|
72
75
|
// HostingConfig.gateLimit (org.ts, #603); migrations/0048. Default 3 is DEFAULT_GATE_LIMIT.
|
|
73
76
|
hostingGateLimit: integer('hosting_gate_limit').notNull().default(3),
|
|
77
|
+
// HostingConfig.repos (org.ts); migrations/0060. Lowercase `wtfalch/<name>` strings.
|
|
78
|
+
hostingRepos: jsonb('hosting_repos').notNull().default([]).$type(),
|
|
74
79
|
ssoEnabled: boolean('sso_enabled').notNull().default(false),
|
|
75
80
|
guestsEnabled: boolean('guests_enabled').notNull().default(false),
|
|
76
81
|
soloEnabled: boolean('solo_enabled').notNull().default(false),
|