@wtfalch/foundry 0.20.0 → 0.21.1

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.
@@ -134,6 +134,36 @@ async function withLogVolumeConfig(client, resource, recorded) {
134
134
  return resource;
135
135
  }
136
136
  }
137
+ /**
138
+ * #543: whether a recorded app's own Coolify health check is on, as
139
+ * `config.healthCheckEnabled`, so `diff.ts`'s `planApp` can notice a human
140
+ * turning it back off in the Coolify UI and plan the repair -- the same
141
+ * drift `platform-providers.ts`'s own `archonRollingUpdateDrifted` catches
142
+ * for Archon, now for a company's own hosting-entry app. Gated and failing
143
+ * exactly like {@link withLogVolumeConfig}: a failed read, or one that has
144
+ * not run at all (nothing recorded yet), leaves `healthCheckEnabled` absent,
145
+ * which `planApp` reads as compliant rather than drift.
146
+ */
147
+ async function withHealthCheckConfig(client, resource, recorded) {
148
+ if (resource.kind !== 'application' || !recorded[resource.key]?.startsWith('app:')) {
149
+ return resource;
150
+ }
151
+ try {
152
+ const app = await client.getApplication(resource.key);
153
+ // `@wtfalch/providers`' `CoolifyApplication` type does not declare
154
+ // `health_check_enabled` (as of 0.7.3), the same gap `withAgoraEnvConfig`
155
+ // above notes for `ports_exposes` -- `getApplication` returns the raw
156
+ // Coolify API response regardless.
157
+ const healthCheckEnabled = app.health_check_enabled;
158
+ return {
159
+ ...resource,
160
+ config: { ...resource.config, healthCheckEnabled },
161
+ };
162
+ }
163
+ catch {
164
+ return resource;
165
+ }
166
+ }
137
167
  /**
138
168
  * #239: whether a recorded database has an internal connection URL, as
139
169
  * `config.hasInternalUrl` (a boolean, never the URL), so `planApp` can report
@@ -256,7 +286,8 @@ export class ProvidersCoolifyReader {
256
286
  this.resolveRecordedIds(orgId),
257
287
  ]);
258
288
  const withLogConfig = await Promise.all(toFoundryResources('coolify', resources).map((r) => withLogVolumeConfig(this.client, r, recorded)));
259
- const withEnvNames = await Promise.all(withLogConfig.map((r) => withEnvNamesConfig(this.client, r, recorded)));
289
+ const withHealthCheck = await Promise.all(withLogConfig.map((r) => withHealthCheckConfig(this.client, r, recorded)));
290
+ const withEnvNames = await Promise.all(withHealthCheck.map((r) => withEnvNamesConfig(this.client, r, recorded)));
260
291
  const withDbUrl = await Promise.all(withEnvNames.map((r) => withDatabaseUrlConfig(this.client, r, recorded)));
261
292
  const withAgoraConfig = await Promise.all(withDbUrl.map((r) => withAgoraEnvConfig(this.client, r, recorded)));
262
293
  const withBouleConfig = await Promise.all(withAgoraConfig.map((r) => withBouleEnvConfig(this.client, r, recorded)));
@@ -10,14 +10,15 @@ import type { OrgId } from '../org.js';
10
10
  * file knows nothing about that shape, only whether it threw.
11
11
  *
12
12
  * "Stop at the first failed health check" (the issue's own words) is, today,
13
- * "stop at the first `apply` that throws" -- the shared health helper
14
- * (`deploy-health.ts`'s `waitForNewDeployment`) is not yet wired into the
15
- * org-level calls stack deploy (`executor.ts`'s `coolifyCalls`/
16
- * `coolifyCallsService` only ever fire-and-forget deploy, and never patch a
17
- * running app's image tag at all -- see `org.ts`'s `CallsConfig.versionPin`
18
- * doc comment). Once that step itself throws on a failed health check, a
19
- * caller's `apply` naturally does too, and this orchestration needs no
20
- * change.
13
+ * "stop at the first `apply` that throws" -- which is now also true of a
14
+ * failed health check specifically: #543 wired the shared health helper
15
+ * (`deploy-health.ts`'s `waitForNewDeployment`) into the org-level calls
16
+ * stack deploy too (`executor.ts`'s `coolifyCalls`/`coolifyCallsService`),
17
+ * alongside every other company-app step. Neither still patches a running
18
+ * app's image tag (see `org.ts`'s `CallsConfig.versionPin` doc comment,
19
+ * unrelated to #543). That step throwing on a failed health check is enough
20
+ * on its own: a caller's `apply` naturally does too, and this orchestration
21
+ * needs no change.
21
22
  */
22
23
  /** One org's outcome from {@link applyOrgsInOrder}. */
23
24
  export interface RolloutOrgResult {
@@ -30,7 +30,7 @@ export interface PlatformMailService extends PlatformService {
30
30
  * holds the Cloudflare token `<username>.<org domain>` claims use, so no
31
31
  * hosting entry ever holds that credential itself.
32
32
  */
33
- export type PlatformServiceKey = 'auth' | 'mail' | 'sfu' | 'files' | 'ai' | 'jobs' | 'integrations' | 'archon' | 'edge';
33
+ export type PlatformServiceKey = 'auth' | 'mail' | 'sfu' | 'files' | 'ai' | 'jobs' | 'integrations' | 'archon' | 'edge' | 'hc';
34
34
  export declare const PLATFORM_SERVICE_KEYS: readonly PlatformServiceKey[];
35
35
  /**
36
36
  * The one shared platform every org's services run on -- decided in #165 as
@@ -68,6 +68,19 @@ export interface DeclaredPlatform {
68
68
  archon: PlatformService;
69
69
  /** #317 (F7). No deploy step reads this service's own `gitRef` yet -- `N1` is deployed by hand until it exists. */
70
70
  edge: PlatformService;
71
+ /**
72
+ * #540: a self-hosted heartbeat server (Healthchecks) for the platform,
73
+ * for now -- whether it stays the platform's heartbeat server long-term,
74
+ * instead of or alongside the hand-set-up `hc.wtfalch.dev` instance, and
75
+ * what happens to that instance's own data, are both still open "Decide"
76
+ * items on issue #540's own thread, not settled by this field. `gitRef`
77
+ * is the Docker image tag it runs (the same overload `mail.gitRef`
78
+ * already carries for Stalwart's tag), not a git ref. Every piece of
79
+ * scheduled work foundry sets up (the uptime Worker, each platform
80
+ * database backup) gets one check here, named after it; see
81
+ * `plan/platform-diff.ts`'s `planHeartbeats`.
82
+ */
83
+ hc: PlatformService;
71
84
  }
72
85
  /** `<service>.<apex>` -- the default `hostname` for a platform service, before an operator overrides it. */
73
86
  export declare function defaultPlatformHostname(service: PlatformServiceKey, apex: string): string;
package/dist/platform.js CHANGED
@@ -10,6 +10,7 @@ export const PLATFORM_SERVICE_KEYS = [
10
10
  'integrations',
11
11
  'archon',
12
12
  'edge',
13
+ 'hc',
13
14
  ];
14
15
  /** `<service>.<apex>` -- the default `hostname` for a platform service, before an operator overrides it. */
15
16
  export function defaultPlatformHostname(service, apex) {
@@ -603,6 +603,11 @@ function rowToPlatform(row) {
603
603
  hostname: row.edgeHostname,
604
604
  gitRef: row.edgeGitRef,
605
605
  },
606
+ hc: {
607
+ enabled: row.hcEnabled,
608
+ hostname: row.hcHostname,
609
+ gitRef: row.hcGitRef,
610
+ },
606
611
  };
607
612
  }
608
613
  function platformToRow(platform) {
@@ -639,6 +644,9 @@ function platformToRow(platform) {
639
644
  edgeEnabled: platform.edge.enabled,
640
645
  edgeHostname: platform.edge.hostname,
641
646
  edgeGitRef: platform.edge.gitRef,
647
+ hcEnabled: platform.hc.enabled,
648
+ hcHostname: platform.hc.hostname,
649
+ hcGitRef: platform.hc.gitRef,
642
650
  };
643
651
  }
644
652
  /**
@@ -1932,6 +1932,57 @@ export declare const foundryDeclaredPlatform: import("drizzle-orm/pg-core").PgTa
1932
1932
  identity: undefined;
1933
1933
  generated: undefined;
1934
1934
  }, {}, {}>;
1935
+ hcEnabled: import("drizzle-orm/pg-core").PgColumn<{
1936
+ name: "hc_enabled";
1937
+ tableName: "foundry_declared_platform";
1938
+ dataType: "boolean";
1939
+ columnType: "PgBoolean";
1940
+ data: boolean;
1941
+ driverParam: boolean;
1942
+ notNull: true;
1943
+ hasDefault: true;
1944
+ isPrimaryKey: false;
1945
+ isAutoincrement: false;
1946
+ hasRuntimeDefault: false;
1947
+ enumValues: undefined;
1948
+ baseColumn: never;
1949
+ identity: undefined;
1950
+ generated: undefined;
1951
+ }, {}, {}>;
1952
+ hcHostname: import("drizzle-orm/pg-core").PgColumn<{
1953
+ name: "hc_hostname";
1954
+ tableName: "foundry_declared_platform";
1955
+ dataType: "string";
1956
+ columnType: "PgText";
1957
+ data: string;
1958
+ driverParam: string;
1959
+ notNull: true;
1960
+ hasDefault: false;
1961
+ isPrimaryKey: false;
1962
+ isAutoincrement: false;
1963
+ hasRuntimeDefault: false;
1964
+ enumValues: [string, ...string[]];
1965
+ baseColumn: never;
1966
+ identity: undefined;
1967
+ generated: undefined;
1968
+ }, {}, {}>;
1969
+ hcGitRef: import("drizzle-orm/pg-core").PgColumn<{
1970
+ name: "hc_git_ref";
1971
+ tableName: "foundry_declared_platform";
1972
+ dataType: "string";
1973
+ columnType: "PgText";
1974
+ data: string;
1975
+ driverParam: string;
1976
+ notNull: true;
1977
+ hasDefault: true;
1978
+ isPrimaryKey: false;
1979
+ isAutoincrement: false;
1980
+ hasRuntimeDefault: false;
1981
+ enumValues: [string, ...string[]];
1982
+ baseColumn: never;
1983
+ identity: undefined;
1984
+ generated: undefined;
1985
+ }, {}, {}>;
1935
1986
  serviceIds: import("drizzle-orm/pg-core").PgColumn<{
1936
1987
  name: "service_ids";
1937
1988
  tableName: "foundry_declared_platform";
@@ -182,6 +182,10 @@ export const foundryDeclaredPlatform = pgTable('foundry_declared_platform', {
182
182
  edgeEnabled: boolean('edge_enabled').notNull().default(false),
183
183
  edgeHostname: text('edge_hostname').notNull(),
184
184
  edgeGitRef: text('edge_git_ref').notNull().default(''),
185
+ /** #540, `migrations/0046_platform_hc.sql`: the platform's own heartbeat server (self-hosted Healthchecks). */
186
+ hcEnabled: boolean('hc_enabled').notNull().default(false),
187
+ hcHostname: text('hc_hostname').notNull(),
188
+ hcGitRef: text('hc_git_ref').notNull().default(''),
185
189
  /** `migrations/0018_platform_apply.sql`: the platform's applied state, `system:kind:key` to provider id. */
186
190
  serviceIds: jsonb('service_ids').$type().notNull().default({}),
187
191
  createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wtfalch/foundry",
3
- "version": "0.20.0",
3
+ "version": "0.21.1",
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",
package/versions.json CHANGED
@@ -4,5 +4,6 @@
4
4
  "mas": "1.24.0",
5
5
  "livekit": "v1.13.7",
6
6
  "cloudflared": "2026.9.3",
7
- "stalwart": "v0.16.24"
7
+ "stalwart": "v0.16.24",
8
+ "hc": "v4.4"
8
9
  }