@wtfalch/foundry 0.42.1 → 0.43.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.
Files changed (50) hide show
  1. package/dist/bootstrap/versions.d.ts +2 -0
  2. package/dist/index.d.ts +5 -1
  3. package/dist/index.js +3 -1
  4. package/dist/migrations/0055_release_requests.sql +20 -0
  5. package/dist/migrations/0056_platform_archon_blue_green.sql +7 -0
  6. package/dist/org.d.ts +13 -0
  7. package/dist/org.js +38 -1
  8. package/dist/plan/archon-blue-green.d.ts +54 -0
  9. package/dist/plan/archon-blue-green.js +72 -0
  10. package/dist/plan/blue-green-apply.d.ts +82 -0
  11. package/dist/plan/blue-green-apply.js +360 -0
  12. package/dist/plan/blue-green.d.ts +223 -0
  13. package/dist/plan/blue-green.js +444 -0
  14. package/dist/plan/cloudflared-connector.d.ts +26 -0
  15. package/dist/plan/cloudflared-connector.js +37 -0
  16. package/dist/plan/diff.d.ts +6 -0
  17. package/dist/plan/diff.js +31 -2
  18. package/dist/plan/executor.d.ts +34 -0
  19. package/dist/plan/executor.js +244 -9
  20. package/dist/plan/plan-apply.d.ts +51 -0
  21. package/dist/plan/plan-apply.js +222 -0
  22. package/dist/plan/platform-diff.d.ts +9 -0
  23. package/dist/plan/platform-diff.js +24 -1
  24. package/dist/plan/platform-providers.d.ts +15 -1
  25. package/dist/plan/platform-providers.js +90 -5
  26. package/dist/plan/proxy-labels.d.ts +2 -2
  27. package/dist/plan/proxy-labels.js +12 -4
  28. package/dist/plan/release-host.d.ts +59 -0
  29. package/dist/plan/release-host.js +239 -0
  30. package/dist/plan/remote-database.d.ts +124 -0
  31. package/dist/plan/remote-database.js +373 -0
  32. package/dist/platform.d.ts +11 -1
  33. package/dist/platform.js +4 -1
  34. package/dist/release/decide.d.ts +95 -0
  35. package/dist/release/decide.js +242 -0
  36. package/dist/release/health.d.ts +48 -0
  37. package/dist/release/health.js +61 -0
  38. package/dist/release/run.d.ts +40 -0
  39. package/dist/release/run.js +56 -0
  40. package/dist/release/state.d.ts +64 -0
  41. package/dist/release/state.js +126 -0
  42. package/dist/release/types.d.ts +54 -0
  43. package/dist/release/types.js +4 -0
  44. package/dist/store/postgres.d.ts +8 -0
  45. package/dist/store/postgres.js +56 -3
  46. package/dist/store/schema.d.ts +154 -0
  47. package/dist/store/schema.js +12 -0
  48. package/package.json +1 -1
  49. package/versions.digests.json +2 -1
  50. package/versions.json +2 -1
@@ -0,0 +1,54 @@
1
+ export type Color = 'blue' | 'green';
2
+ export type WindowState = {
3
+ kind: 'idle';
4
+ } | {
5
+ kind: 'step';
6
+ percent: number;
7
+ since: string;
8
+ } | {
9
+ kind: 'inconclusive';
10
+ percent: number;
11
+ since: string;
12
+ } | {
13
+ kind: 'window';
14
+ since: string;
15
+ } | {
16
+ kind: 'passed';
17
+ at: string;
18
+ } | {
19
+ kind: 'rolled_back';
20
+ at: string;
21
+ reason: string;
22
+ } | {
23
+ kind: 'cap';
24
+ since: string;
25
+ };
26
+ export interface ReleaseSignal {
27
+ requests: number;
28
+ errors: number;
29
+ baselineRate: number | null;
30
+ verdict: 'pass' | 'fail' | 'hard_fail' | 'inconclusive';
31
+ }
32
+ export interface ReleaseState {
33
+ entryKey: string;
34
+ live: Color;
35
+ weights: Record<Color, number>;
36
+ digests: Record<Color, string | null>;
37
+ stageDigest: string | null;
38
+ window: WindowState;
39
+ signal: ReleaseSignal | null;
40
+ held: string[];
41
+ }
42
+ export type ReleaseAction = {
43
+ kind: 'promote';
44
+ } | {
45
+ kind: 'rollback';
46
+ } | {
47
+ kind: 'clear_held';
48
+ digest: string;
49
+ } | {
50
+ kind: 'decide_at_cap';
51
+ decision: 'pass' | 'rollback';
52
+ };
53
+ export declare const COLORS: readonly Color[];
54
+ export declare function otherColor(color: Color): Color;
@@ -0,0 +1,4 @@
1
+ export const COLORS = ['blue', 'green'];
2
+ export function otherColor(color) {
3
+ return color === 'blue' ? 'green' : 'blue';
4
+ }
@@ -1,4 +1,5 @@
1
1
  import type { PgDatabase, PgQueryResultHKT } from 'drizzle-orm/pg-core';
2
+ import { type ReleaseStore } from '../release/state.js';
2
3
  import { type AppliedVersionsStore, type DeclaredOrgStore, type PlatformStore, type RequestedChangeStore } from '../store.js';
3
4
  /**
4
5
  * The host's own drizzle handle or a transaction open on it, same shape
@@ -98,3 +99,10 @@ export declare function createPostgresRequestedChangeStore(db: FoundryStoreDb):
98
99
  * lost the way `foundry_applied_state`'s one row per service would lose it.
99
100
  */
100
101
  export declare function createPostgresAppliedVersionsStore(db: FoundryStoreDb): AppliedVersionsStore;
102
+ /**
103
+ * `ReleaseStore` (`../release/state.ts`) on Postgres: the recorded `bg.*`
104
+ * keys come from an org's applied state, or the platform's for the orgId
105
+ * `platform`; requests are rows in `foundry_release_requests`. Reading leaves
106
+ * the open rows consumed only when the caller says its run succeeded (`consumeRequests`).
107
+ */
108
+ export declare function createPostgresReleaseStore(db: FoundryStoreDb): ReleaseStore;
@@ -1,12 +1,13 @@
1
- import { and, asc, desc, eq, ne, sql } from 'drizzle-orm';
2
- import { APP_ENTRY_KEY, DEFAULT_MAIL_APPS, DEFAULT_NO_REPLY_LOCAL_PART, validateBuyerIdentity, validateCalls, validateDomain, validateDomainClaim, validateHosting, validateMail, validateOwners, validateServices, validateSignIn, validateSite, validateVersionPin, } from '../org.js';
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, validateBlueGreen, validateBuyerIdentity, validateCalls, validateDomain, validateDomainClaim, validateHosting, validateMail, validateOwners, validateServices, validateSignIn, validateSite, validateVersionPin, } from '../org.js';
3
3
  import { lockedHostnames } from '../plan/locked-hostnames.js';
4
4
  import { findNameClashes, nameClashMessage } from '../plan/org-names.js';
5
5
  import { validatePlatform } from '../platform.js';
6
+ import { releaseActionSchema } from '../release/state.js';
6
7
  import { RequestedChangeError, classifyChange } from '../requested-changes.js';
7
8
  import { validateSlug } from '../slug.js';
8
9
  import { StaleLeaseError, } from '../store.js';
9
- import { declaredOrgs, foundryAppliedState, foundryAppliedVersions, foundryDeclaredPlatform, foundryMutationLeases, foundryRequestedChanges, } from './schema.js';
10
+ import { declaredOrgs, foundryAppliedState, foundryAppliedVersions, foundryDeclaredPlatform, foundryMutationLeases, foundryReleaseRequests, foundryRequestedChanges, } from './schema.js';
10
11
  const UNIQUE_VIOLATION = '23505';
11
12
  /**
12
13
  * #441: runs `write`, and with a `fence` only if its token is still the
@@ -718,6 +719,10 @@ function rowToPlatform(row) {
718
719
  enabled: row.archonEnabled,
719
720
  hostname: row.archonHostname,
720
721
  gitRef: row.archonGitRef,
722
+ // Omitted while off. Validated on read: a stored value is not trusted.
723
+ ...(row.archonBlueGreen && validateBlueGreen(row.archonBlueGreen, 'archon') === null
724
+ ? { blueGreen: row.archonBlueGreen }
725
+ : {}),
721
726
  },
722
727
  edge: {
723
728
  enabled: row.edgeEnabled,
@@ -771,6 +776,7 @@ function platformToRow(platform) {
771
776
  archonEnabled: platform.archon.enabled,
772
777
  archonHostname: platform.archon.hostname,
773
778
  archonGitRef: platform.archon.gitRef,
779
+ archonBlueGreen: platform.archon.blueGreen ?? null,
774
780
  edgeEnabled: platform.edge.enabled,
775
781
  edgeHostname: platform.edge.hostname,
776
782
  edgeGitRef: platform.edge.gitRef,
@@ -923,3 +929,50 @@ export function createPostgresAppliedVersionsStore(db) {
923
929
  },
924
930
  };
925
931
  }
932
+ /**
933
+ * `ReleaseStore` (`../release/state.ts`) on Postgres: the recorded `bg.*`
934
+ * keys come from an org's applied state, or the platform's for the orgId
935
+ * `platform`; requests are rows in `foundry_release_requests`. Reading leaves
936
+ * the open rows consumed only when the caller says its run succeeded (`consumeRequests`).
937
+ */
938
+ export function createPostgresReleaseStore(db) {
939
+ return {
940
+ async getScopeIds(orgId) {
941
+ if (orgId === 'platform') {
942
+ const [row] = await db
943
+ .select({ ids: foundryDeclaredPlatform.serviceIds })
944
+ .from(foundryDeclaredPlatform)
945
+ .limit(1);
946
+ return row?.ids ?? {};
947
+ }
948
+ const [row] = await db
949
+ .select({ ids: foundryAppliedState.serviceIds })
950
+ .from(foundryAppliedState)
951
+ .where(eq(foundryAppliedState.orgId, orgId))
952
+ .limit(1);
953
+ return row?.ids ?? {};
954
+ },
955
+ async addRequest({ orgId, entryKey, action, actor }) {
956
+ await db.insert(foundryReleaseRequests).values({ orgId, entryKey, action, actor });
957
+ },
958
+ async openRequests({ orgId, entryKey }) {
959
+ const rows = await db
960
+ .select()
961
+ .from(foundryReleaseRequests)
962
+ .where(and(eq(foundryReleaseRequests.orgId, orgId), eq(foundryReleaseRequests.entryKey, entryKey), isNull(foundryReleaseRequests.consumedAt)))
963
+ .orderBy(asc(foundryReleaseRequests.id));
964
+ return rows.flatMap((row) => {
965
+ const action = releaseActionSchema.safeParse(row.action);
966
+ return action.success ? [{ id: row.id, action: action.data, actor: row.actor }] : [];
967
+ });
968
+ },
969
+ async consumeRequests(ids) {
970
+ if (ids.length === 0)
971
+ return;
972
+ await db
973
+ .update(foundryReleaseRequests)
974
+ .set({ consumedAt: sql `now()` })
975
+ .where(and(inArray(foundryReleaseRequests.id, [...ids]), isNull(foundryReleaseRequests.consumedAt)));
976
+ },
977
+ };
978
+ }
@@ -2002,6 +2002,31 @@ export declare const foundryDeclaredPlatform: import("drizzle-orm/pg-core").PgTa
2002
2002
  identity: undefined;
2003
2003
  generated: undefined;
2004
2004
  }, {}, {}>;
2005
+ archonBlueGreen: import("drizzle-orm/pg-core").PgColumn<{
2006
+ name: "archon_blue_green";
2007
+ tableName: "foundry_declared_platform";
2008
+ dataType: "json";
2009
+ columnType: "PgJsonb";
2010
+ data: {
2011
+ steps?: number[];
2012
+ stepHoldMinutes?: number;
2013
+ } | null;
2014
+ driverParam: unknown;
2015
+ notNull: false;
2016
+ hasDefault: false;
2017
+ isPrimaryKey: false;
2018
+ isAutoincrement: false;
2019
+ hasRuntimeDefault: false;
2020
+ enumValues: undefined;
2021
+ baseColumn: never;
2022
+ identity: undefined;
2023
+ generated: undefined;
2024
+ }, {}, {
2025
+ $type: {
2026
+ steps?: number[];
2027
+ stepHoldMinutes?: number;
2028
+ } | null;
2029
+ }>;
2005
2030
  edgeEnabled: import("drizzle-orm/pg-core").PgColumn<{
2006
2031
  name: "edge_enabled";
2007
2032
  tableName: "foundry_declared_platform";
@@ -2414,6 +2439,135 @@ export declare const foundryRequestedChanges: import("drizzle-orm/pg-core").PgTa
2414
2439
  };
2415
2440
  dialect: "pg";
2416
2441
  }>;
2442
+ /** Mirrors `migrations/0055_release_requests.sql`: an operator's release action, consumed once by the runner. */
2443
+ export declare const foundryReleaseRequests: import("drizzle-orm/pg-core").PgTableWithColumns<{
2444
+ name: "foundry_release_requests";
2445
+ schema: undefined;
2446
+ columns: {
2447
+ id: import("drizzle-orm/pg-core").PgColumn<{
2448
+ name: "id";
2449
+ tableName: "foundry_release_requests";
2450
+ dataType: "number";
2451
+ columnType: "PgBigSerial53";
2452
+ data: number;
2453
+ driverParam: number;
2454
+ notNull: true;
2455
+ hasDefault: true;
2456
+ isPrimaryKey: true;
2457
+ isAutoincrement: false;
2458
+ hasRuntimeDefault: false;
2459
+ enumValues: undefined;
2460
+ baseColumn: never;
2461
+ identity: undefined;
2462
+ generated: undefined;
2463
+ }, {}, {}>;
2464
+ orgId: import("drizzle-orm/pg-core").PgColumn<{
2465
+ name: "org_id";
2466
+ tableName: "foundry_release_requests";
2467
+ dataType: "string";
2468
+ columnType: "PgText";
2469
+ data: string;
2470
+ driverParam: string;
2471
+ notNull: true;
2472
+ hasDefault: false;
2473
+ isPrimaryKey: false;
2474
+ isAutoincrement: false;
2475
+ hasRuntimeDefault: false;
2476
+ enumValues: [string, ...string[]];
2477
+ baseColumn: never;
2478
+ identity: undefined;
2479
+ generated: undefined;
2480
+ }, {}, {}>;
2481
+ entryKey: import("drizzle-orm/pg-core").PgColumn<{
2482
+ name: "entry_key";
2483
+ tableName: "foundry_release_requests";
2484
+ dataType: "string";
2485
+ columnType: "PgText";
2486
+ data: string;
2487
+ driverParam: string;
2488
+ notNull: true;
2489
+ hasDefault: false;
2490
+ isPrimaryKey: false;
2491
+ isAutoincrement: false;
2492
+ hasRuntimeDefault: false;
2493
+ enumValues: [string, ...string[]];
2494
+ baseColumn: never;
2495
+ identity: undefined;
2496
+ generated: undefined;
2497
+ }, {}, {}>;
2498
+ action: import("drizzle-orm/pg-core").PgColumn<{
2499
+ name: "action";
2500
+ tableName: "foundry_release_requests";
2501
+ dataType: "json";
2502
+ columnType: "PgJsonb";
2503
+ data: Record<string, unknown>;
2504
+ driverParam: unknown;
2505
+ notNull: true;
2506
+ hasDefault: false;
2507
+ isPrimaryKey: false;
2508
+ isAutoincrement: false;
2509
+ hasRuntimeDefault: false;
2510
+ enumValues: undefined;
2511
+ baseColumn: never;
2512
+ identity: undefined;
2513
+ generated: undefined;
2514
+ }, {}, {
2515
+ $type: Record<string, unknown>;
2516
+ }>;
2517
+ actor: import("drizzle-orm/pg-core").PgColumn<{
2518
+ name: "actor";
2519
+ tableName: "foundry_release_requests";
2520
+ dataType: "string";
2521
+ columnType: "PgText";
2522
+ data: string;
2523
+ driverParam: string;
2524
+ notNull: true;
2525
+ hasDefault: false;
2526
+ isPrimaryKey: false;
2527
+ isAutoincrement: false;
2528
+ hasRuntimeDefault: false;
2529
+ enumValues: [string, ...string[]];
2530
+ baseColumn: never;
2531
+ identity: undefined;
2532
+ generated: undefined;
2533
+ }, {}, {}>;
2534
+ createdAt: import("drizzle-orm/pg-core").PgColumn<{
2535
+ name: "created_at";
2536
+ tableName: "foundry_release_requests";
2537
+ dataType: "date";
2538
+ columnType: "PgTimestamp";
2539
+ data: Date;
2540
+ driverParam: string;
2541
+ notNull: true;
2542
+ hasDefault: true;
2543
+ isPrimaryKey: false;
2544
+ isAutoincrement: false;
2545
+ hasRuntimeDefault: false;
2546
+ enumValues: undefined;
2547
+ baseColumn: never;
2548
+ identity: undefined;
2549
+ generated: undefined;
2550
+ }, {}, {}>;
2551
+ consumedAt: import("drizzle-orm/pg-core").PgColumn<{
2552
+ name: "consumed_at";
2553
+ tableName: "foundry_release_requests";
2554
+ dataType: "date";
2555
+ columnType: "PgTimestamp";
2556
+ data: Date;
2557
+ driverParam: string;
2558
+ notNull: false;
2559
+ hasDefault: false;
2560
+ isPrimaryKey: false;
2561
+ isAutoincrement: false;
2562
+ hasRuntimeDefault: false;
2563
+ enumValues: undefined;
2564
+ baseColumn: never;
2565
+ identity: undefined;
2566
+ generated: undefined;
2567
+ }, {}, {}>;
2568
+ };
2569
+ dialect: "pg";
2570
+ }>;
2417
2571
  export type DeclaredOrgRow = typeof declaredOrgs.$inferSelect;
2418
2572
  export type NewDeclaredOrgRow = typeof declaredOrgs.$inferInsert;
2419
2573
  export type AppliedStateRow = typeof foundryAppliedState.$inferSelect;
@@ -191,6 +191,8 @@ export const foundryDeclaredPlatform = pgTable('foundry_declared_platform', {
191
191
  archonEnabled: boolean('archon_enabled').notNull().default(false),
192
192
  archonHostname: text('archon_hostname').notNull(),
193
193
  archonGitRef: text('archon_git_ref').notNull().default(''),
194
+ /** `migrations/0056_platform_archon_blue_green.sql`: `platform.archon.blueGreen`; null keeps one app. */
195
+ archonBlueGreen: jsonb('archon_blue_green').$type(),
194
196
  /** #317 (F7), `migrations/0043_platform_names.sql`, renamed from `names_*` by `0045_names_to_edge.sql`. */
195
197
  edgeEnabled: boolean('edge_enabled').notNull().default(false),
196
198
  edgeHostname: text('edge_hostname').notNull(),
@@ -230,3 +232,13 @@ export const foundryRequestedChanges = pgTable('foundry_requested_changes', {
230
232
  createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
231
233
  state: text('state').notNull().default('needs_approval'),
232
234
  });
235
+ /** Mirrors `migrations/0055_release_requests.sql`: an operator's release action, consumed once by the runner. */
236
+ export const foundryReleaseRequests = pgTable('foundry_release_requests', {
237
+ id: bigserial('id', { mode: 'number' }).primaryKey(),
238
+ orgId: text('org_id').notNull(),
239
+ entryKey: text('entry_key').notNull(),
240
+ action: jsonb('action').notNull().$type(),
241
+ actor: text('actor').notNull(),
242
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
243
+ consumedAt: timestamp('consumed_at', { withTimezone: true }),
244
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wtfalch/foundry",
3
- "version": "0.42.1",
3
+ "version": "0.43.0",
4
4
  "description": "Foundry's SDK: the declared org's types, the plan and apply contracts, and the state store's interface.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -8,5 +8,6 @@
8
8
  "hc": "sha256:aa08a61b0dcfd83f1332eab195d5cec1b595ae076eb8388cadf8a3d597114f9b",
9
9
  "alpine": "sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8c44de311d1",
10
10
  "busybox": "sha256:5cec3fc171c87218698e85a52af7087de727372aae264a787b8112901a5b0092",
11
- "postgres": "sha256:786dab398303b8ce7cb76b407bb21ef2e4dfbbbd4c6abcf3d29b3130467ffdbc"
11
+ "postgres": "sha256:786dab398303b8ce7cb76b407bb21ef2e4dfbbbd4c6abcf3d29b3130467ffdbc",
12
+ "traefik": "sha256:575fa15b135078fe5e50aa847987d96dbddd7b093c172429618404df73f3fa7c"
12
13
  }
package/versions.json CHANGED
@@ -8,5 +8,6 @@
8
8
  "hc": "v4.4",
9
9
  "alpine": "3.22.1",
10
10
  "busybox": "1.37.0-musl",
11
- "postgres": "16.14-alpine3.22"
11
+ "postgres": "16.14-alpine3.22",
12
+ "traefik": "v3.7.14"
12
13
  }