@wtfalch/foundry 0.21.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.
@@ -577,6 +577,12 @@ export declare const PLATFORM_AI_MIGRATE_BLOCKED_SCOPE_KEY = "ai_platform_migrat
577
577
  * that throws) is repaired by the next one.
578
578
  */
579
579
  export declare const PLATFORM_AI_DEPLOY_PENDING_SCOPE_KEY = "ai_platform_deploy_pending";
580
+ /**
581
+ * Set before the `coolify:jobs` step writes the scheduler's env and cleared
582
+ * once its deploy finished, so a run that dies in between is repaired by the
583
+ * next one. The planner plans the step while it is set.
584
+ */
585
+ export declare const JOBS_DEPLOY_PENDING_SCOPE_KEY = "jobs_deploy_pending";
580
586
  /**
581
587
  * #171: the resource key `planFiles` plans under and `FoundryPlatformStepExecutor`
582
588
  * dispatches on, the {@link PLATFORM_AUTH_KEY} shape for the files host.
@@ -653,6 +653,12 @@ export const PLATFORM_AI_MIGRATE_BLOCKED_SCOPE_KEY = 'ai_platform_migrate_blocke
653
653
  * that throws) is repaired by the next one.
654
654
  */
655
655
  export const PLATFORM_AI_DEPLOY_PENDING_SCOPE_KEY = 'ai_platform_deploy_pending';
656
+ /**
657
+ * Set before the `coolify:jobs` step writes the scheduler's env and cleared
658
+ * once its deploy finished, so a run that dies in between is repaired by the
659
+ * next one. The planner plans the step while it is set.
660
+ */
661
+ export const JOBS_DEPLOY_PENDING_SCOPE_KEY = 'jobs_deploy_pending';
656
662
  /**
657
663
  * #171: the resource key `planFiles` plans under and `FoundryPlatformStepExecutor`
658
664
  * dispatches on, the {@link PLATFORM_AUTH_KEY} shape for the files host.
@@ -900,7 +906,7 @@ export function planPlatform(platform, live, options = {}) {
900
906
  plannedSteps.push(zone);
901
907
  // #173: the jobs scheduler, second of the eight services this frame leaves
902
908
  // open (this file's own doc comment above).
903
- const jobs = planJobs(platform, byKey, claimed, jobsObservable);
909
+ const jobs = planJobs(platform, byKey, claimed, jobsObservable, recordedIds);
904
910
  if (jobs.step)
905
911
  plannedSteps.push(jobs.step);
906
912
  // A blocked SFU is reported in `unmanaged`, not planned: a step the
@@ -1114,7 +1120,7 @@ export const JOBS_SCHEDULER_KEY = 'jobs';
1114
1120
  * (`diff.ts`'s eventual `jobs:registration`) has nothing to register against
1115
1121
  * before this exists.
1116
1122
  */
1117
- function planJobs(platform, byKey, claimed, observable) {
1123
+ function planJobs(platform, byKey, claimed, observable, recordedIds) {
1118
1124
  const key = resourceKey('coolify', 'jobs', JOBS_SCHEDULER_KEY);
1119
1125
  const live = byKey.get(key);
1120
1126
  if (live)
@@ -1125,8 +1131,14 @@ function planJobs(platform, byKey, claimed, observable) {
1125
1131
  // The step also starts the scheduler's database (0.16.3), so a live app
1126
1132
  // whose database the reader saw stopped plans it again. A status the
1127
1133
  // reader could not read is absent, and plans nothing.
1134
+ // An app without `DATABASE_URL_OWNER` also plans it again: the step
1135
+ // sets the owner and runtime URLs the scheduler's migrate stage needs.
1136
+ // So does a recorded pending deploy: the step wrote its env and died
1137
+ // before the deploy finished.
1128
1138
  const databaseStatus = live.config.databaseStatus;
1129
- if (typeof databaseStatus !== 'string' || databaseStatus.startsWith('running')) {
1139
+ if (live.config.ownerUrlMissing !== true &&
1140
+ !recordedIds[JOBS_DEPLOY_PENDING_SCOPE_KEY] &&
1141
+ (typeof databaseStatus !== 'string' || databaseStatus.startsWith('running'))) {
1130
1142
  return { step: null, blocked: null };
1131
1143
  }
1132
1144
  }
@@ -117,7 +117,7 @@ export declare const JOBS_ADMIN_TOKEN_REF = "jobs:scheduler:adminToken";
117
117
  * org's own app, reused here for the platform's one scheduler instead of once
118
118
  * per org.
119
119
  */
120
- type JobsCoolifyPort = Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'requireGithubApp' | 'findApplicationByName' | 'createGithubAppApplication' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'raw' | 'bulkUpsertEnvs' | 'getApplication' | 'deploy'>;
120
+ type JobsCoolifyPort = Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'requireGithubApp' | 'findApplicationByName' | 'createGithubAppApplication' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'raw' | 'bulkUpsertEnvs' | 'listEnvs' | 'getApplication' | 'deploy'>;
121
121
  /**
122
122
  * Options for {@link FoundryPlatformStepExecutor}'s constructor, replacing
123
123
  * four positional arguments that shifted on every merge that added a
@@ -1154,7 +1154,7 @@ export declare class ProvidersPlatformCloudflareReader implements PlatformCloudf
1154
1154
  export declare class ProvidersPlatformCoolifyReader implements PlatformCoolifyReader {
1155
1155
  private readonly client;
1156
1156
  private readonly platformStore;
1157
- constructor(client: Pick<CoolifyPort, 'getApplication' | 'getDatabase' | 'listDatabaseBackups' | 'raw'>, platformStore: Pick<PlatformStore, 'getServiceIds'>);
1157
+ constructor(client: Pick<CoolifyPort, 'getApplication' | 'getDatabase' | 'listDatabaseBackups' | 'listEnvs' | 'raw'>, platformStore: Pick<PlatformStore, 'getServiceIds'>);
1158
1158
  listResources(): Promise<LiveResource[]>;
1159
1159
  }
1160
1160
  /** Scope id (no `:`): the share domains the runner's Coolify `domains` was last set to, comma-joined. */
@@ -8,7 +8,7 @@ import { waitForDeploymentFinished, waitForHealthyVersion, waitForNewDeployment,
8
8
  import { ACCESS_POLICY_NAME, getDatabaseStarted, isNotFoundError, isRunningOrStarting, } from './executor.js';
9
9
  import { healthCheckSettings } from './health-check-settings.js';
10
10
  import { coolifyDomains, filesShareHostname } from './hostnames.js';
11
- import { AUTH_ZITADEL_RUN_OPTIONS, JOBS_SCHEDULER_KEY, PLATFORM_AI_APPLIED_GITREF_SCOPE_KEY, PLATFORM_AI_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_AI_DEPLOY_PENDING_SCOPE_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_KEY, PLATFORM_AI_MANAGEMENT_KEY_CONFLICT_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_AI_MIGRATE_BLOCKED_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_AI_SIGNING_KEY_SET_SCOPE_KEY, PLATFORM_AI_SIGNING_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_OPERATOR_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_ROLLING_UPDATE_SCOPE_KEY, PLATFORM_ARCHON_CONNECTOR_APP_SCOPE_KEY, PLATFORM_ARCHON_COOLIFY_READ_TOKEN_REF, PLATFORM_ARCHON_ENVS_DEPLOYED_SCOPE_KEY, PLATFORM_ARCHON_GATE_CLOSED_SCOPE_KEY, PLATFORM_ARCHON_INTEGRATIONS_KEY_SYNCED_SCOPE_KEY, PLATFORM_ARCHON_KEY, PLATFORM_ARCHON_MASTER_CLOUDFLARE_TOKEN_REF, PLATFORM_ARCHON_MASTER_COOLIFY_TOKEN_REF, PLATFORM_ARCHON_MASTER_COOLIFY_URL_REF, PLATFORM_AUTH_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY, PLATFORM_AUTH_BROKER_DEPLOYED_SCOPE_KEY, PLATFORM_AUTH_BROKER_SCOPE_KEY, PLATFORM_AUTH_CONNECTOR_APP_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_KEY, PLATFORM_AUTH_LOGIN_CLIENT_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY, PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY, PLATFORM_AUTH_SMTP_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_APPLIED_IMAGE_TAG_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY, PLATFORM_BACKUP_DATABASE_SCOPE_KEYS, PLATFORM_BACKUP_SERVICE_KEYS, PLATFORM_EDGE_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_EDGE_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF, PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY, PLATFORM_EDGE_KEY, PLATFORM_EDGE_KEY_REF, PLATFORM_EDGE_PLATFORM_KEY_SET_SCOPE_KEY, PLATFORM_EDGE_PLATFORM_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_FILES_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_FILES_MANAGEMENT_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_HC_API_KEY_REF, PLATFORM_HC_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_HC_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_HC_KEY, PLATFORM_HEARTBEATS_APPLIED_SCOPE_KEY, PLATFORM_INTEGRATIONS_APPLIED_GITREF_SCOPE_KEY, PLATFORM_INTEGRATIONS_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY, PLATFORM_INTEGRATIONS_KEY_ID_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_PREVIOUS_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_REQUESTED_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_UNDELIVERED_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_SET_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_INTEGRATIONS_RUNNER_KEYRING_DEPLOYED_SCOPE_KEY, PLATFORM_MAIL_ADMIN_KEY_SCOPE_KEY, PLATFORM_MAIL_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_MAIL_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_MAIL_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_MAIL_CONNECTOR_APP_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_UPTIME_HEARTBEAT_CHECK_NAME, archonBootstrapOperator, desiredHeartbeatCheckNames, heartbeatCheckUrlRef, parseBrokerDeployRecord, parseLoginClientRecord, platformBackupHeartbeatCheckName, platformMailMxHost, } from './platform-diff.js';
11
+ import { AUTH_ZITADEL_RUN_OPTIONS, JOBS_DEPLOY_PENDING_SCOPE_KEY, JOBS_SCHEDULER_KEY, PLATFORM_AI_APPLIED_GITREF_SCOPE_KEY, PLATFORM_AI_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_AI_DEPLOY_PENDING_SCOPE_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_KEY, PLATFORM_AI_MANAGEMENT_KEY_CONFLICT_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_AI_MIGRATE_BLOCKED_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_AI_SIGNING_KEY_SET_SCOPE_KEY, PLATFORM_AI_SIGNING_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_OPERATOR_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_ROLLING_UPDATE_SCOPE_KEY, PLATFORM_ARCHON_CONNECTOR_APP_SCOPE_KEY, PLATFORM_ARCHON_COOLIFY_READ_TOKEN_REF, PLATFORM_ARCHON_ENVS_DEPLOYED_SCOPE_KEY, PLATFORM_ARCHON_GATE_CLOSED_SCOPE_KEY, PLATFORM_ARCHON_INTEGRATIONS_KEY_SYNCED_SCOPE_KEY, PLATFORM_ARCHON_KEY, PLATFORM_ARCHON_MASTER_CLOUDFLARE_TOKEN_REF, PLATFORM_ARCHON_MASTER_COOLIFY_TOKEN_REF, PLATFORM_ARCHON_MASTER_COOLIFY_URL_REF, PLATFORM_AUTH_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY, PLATFORM_AUTH_BROKER_DEPLOYED_SCOPE_KEY, PLATFORM_AUTH_BROKER_SCOPE_KEY, PLATFORM_AUTH_CONNECTOR_APP_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_KEY, PLATFORM_AUTH_LOGIN_CLIENT_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY, PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY, PLATFORM_AUTH_SMTP_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_APPLIED_IMAGE_TAG_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY, PLATFORM_BACKUP_DATABASE_SCOPE_KEYS, PLATFORM_BACKUP_SERVICE_KEYS, PLATFORM_EDGE_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_EDGE_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF, PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY, PLATFORM_EDGE_KEY, PLATFORM_EDGE_KEY_REF, PLATFORM_EDGE_PLATFORM_KEY_SET_SCOPE_KEY, PLATFORM_EDGE_PLATFORM_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_FILES_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_FILES_MANAGEMENT_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_HC_API_KEY_REF, PLATFORM_HC_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_HC_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_HC_KEY, PLATFORM_HEARTBEATS_APPLIED_SCOPE_KEY, PLATFORM_INTEGRATIONS_APPLIED_GITREF_SCOPE_KEY, PLATFORM_INTEGRATIONS_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY, PLATFORM_INTEGRATIONS_KEY_ID_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_PREVIOUS_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_REQUESTED_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_UNDELIVERED_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_SET_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_INTEGRATIONS_RUNNER_KEYRING_DEPLOYED_SCOPE_KEY, PLATFORM_MAIL_ADMIN_KEY_SCOPE_KEY, PLATFORM_MAIL_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_MAIL_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_MAIL_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_MAIL_CONNECTOR_APP_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_UPTIME_HEARTBEAT_CHECK_NAME, archonBootstrapOperator, desiredHeartbeatCheckNames, heartbeatCheckUrlRef, parseBrokerDeployRecord, parseLoginClientRecord, platformBackupHeartbeatCheckName, platformMailMxHost, } from './platform-diff.js';
12
12
  import { applyCallsSfuPlatformStep } from './platform-sfu.js';
13
13
  import { CreateInstanceError, createZitadelInstance, generateSystemApiKeypair, instanceWithoutBearerMessage, systemApiUsersEnv, } from './zitadel-system-api.js';
14
14
  /** The key `runPlatformSteps` records the apex zone's id under (`system:kind:key`). */
@@ -596,7 +596,25 @@ export class FoundryPlatformStepExecutor {
596
596
  if (!db.internal_db_url) {
597
597
  throw new Error(`platform step coolify:jobs: database ${dbId} has no internal_db_url yet; not deploying the scheduler without a database`);
598
598
  }
599
- const envs = [{ key: 'DATABASE_URL', value: db.internal_db_url }];
599
+ // The scheduler migrates as `DATABASE_URL_OWNER` and creates its own
600
+ // `jobs_rt` role from `DATABASE_URL` (jobs README, "Deploying"). An app
601
+ // with no owner URL still holds the owner's URL in `DATABASE_URL`, so
602
+ // that is never reused: a runtime URL is minted, once, and kept after.
603
+ const hasOwnerUrl = (await coolify.listEnvs(appId)).some((e) => e.key === 'DATABASE_URL_OWNER' && !e.is_preview && e.value);
604
+ const runtimeUrl = await ensureAiRuntimeUrl(coolify, hasOwnerUrl ? [appId] : [], db.internal_db_url, 'platform step coolify:jobs');
605
+ const envs = [
606
+ { key: 'DATABASE_URL_OWNER', value: db.internal_db_url },
607
+ { key: 'DATABASE_URL', value: runtimeUrl },
608
+ ];
609
+ // Whether the scheduler runs on envs other than the ones written below.
610
+ // Marked and persisted before the first write, and cleared only after the
611
+ // deploy, so a run that dies in between leaves the mark and the next run
612
+ // deploys again (`CLAUDE.md` rule 4).
613
+ const pending = Boolean(recorded[JOBS_DEPLOY_PENDING_SCOPE_KEY]) || (await envsDiffer(coolify, appId, envs));
614
+ if (pending) {
615
+ context.recordScopeId(JOBS_DEPLOY_PENDING_SCOPE_KEY, '1');
616
+ await context.persist?.();
617
+ }
600
618
  // SCHEDULER_ADMIN_TOKEN gates the admin API the org registrations use
601
619
  // (jobs README, "The admin API"); unset, every admin route answers 401.
602
620
  // Delivered to the host before it is written to the app, so a failed
@@ -629,8 +647,12 @@ export class FoundryPlatformStepExecutor {
629
647
  // a redeploy, so deploy after a create or a new token. Also when it is not
630
648
  // running, so a deploy that failed on an earlier run is retried.
631
649
  const running = (await coolify.getApplication(appId)).status?.startsWith('running') ?? false;
632
- if (!existingApp || tokenJustSet || !running)
650
+ if (!existingApp || tokenJustSet || pending || !running)
633
651
  await coolify.deploy(appId);
652
+ if (pending) {
653
+ context.recordScopeId(JOBS_DEPLOY_PENDING_SCOPE_KEY, '');
654
+ await context.persist?.();
655
+ }
634
656
  return { resourceId: appId };
635
657
  }
636
658
  }
@@ -4185,7 +4207,7 @@ async function provisionPlatformAi(context, deps) {
4185
4207
  // ---- env -----------------------------------------------------------------
4186
4208
  // Both database URLs are live credentials: handed to `bulkUpsertEnvs` and
4187
4209
  // nowhere else, never returned, recorded, logged or put in an error.
4188
- const runtimeUrl = await ensureAiRuntimeUrl(coolify, [runner.appId, worker.appId, migrate.appId], ownerUrl);
4210
+ const runtimeUrl = await ensureAiRuntimeUrl(coolify, [runner.appId, worker.appId, migrate.appId], ownerUrl, 'platform:ai');
4189
4211
  const migrateEnvs = [
4190
4212
  { key: 'DATABASE_URL_OWNER', value: ownerUrl },
4191
4213
  { key: 'DATABASE_URL', value: runtimeUrl },
@@ -4556,7 +4578,7 @@ async function waitForAiMigration(coolify, appId, runtime, sleep) {
4556
4578
  * inside a try: Node's parse error carries the whole input, credential
4557
4579
  * included, so it is replaced with a fixed message.
4558
4580
  */
4559
- async function ensureAiRuntimeUrl(coolify, appIds, ownerUrl) {
4581
+ async function ensureAiRuntimeUrl(coolify, appIds, ownerUrl, step) {
4560
4582
  for (const appId of appIds) {
4561
4583
  const existing = (await coolify.listEnvs(appId)).find((e) => e.key === 'DATABASE_URL' && !e.is_preview && e.value);
4562
4584
  if (existing)
@@ -4569,11 +4591,11 @@ async function ensureAiRuntimeUrl(coolify, appIds, ownerUrl) {
4569
4591
  ({ host, pathname, search } = new URL(ownerUrl));
4570
4592
  }
4571
4593
  catch {
4572
- throw new Error("platform:ai: the database's internal_db_url could not be parsed");
4594
+ throw new Error(`${step}: the database's internal_db_url could not be parsed`);
4573
4595
  }
4574
4596
  const database = pathname.replace(/^\//, '');
4575
4597
  if (!/^[A-Za-z0-9_]+$/.test(database)) {
4576
- throw new Error("platform:ai: the database's name cannot form a runtime role name (letters, digits and _ only)");
4598
+ throw new Error(`${step}: the database's name cannot form a runtime role name (letters, digits and _ only)`);
4577
4599
  }
4578
4600
  const password = randomBytes(24).toString('base64url');
4579
4601
  return `postgres://${database}_rt:${password}@${host}${pathname}${search}`;
@@ -7098,6 +7120,16 @@ export class ProvidersPlatformCoolifyReader {
7098
7120
  databaseStatus = undefined;
7099
7121
  }
7100
7122
  }
7123
+ // An app with no `DATABASE_URL_OWNER` predates the owner/runtime
7124
+ // split, so `planJobs` plans the step again to set both URLs. Left
7125
+ // out when the envs cannot be read: unknown is not "missing".
7126
+ let ownerUrlMissing;
7127
+ try {
7128
+ ownerUrlMissing = !(await this.client.listEnvs(app.uuid)).some((e) => e.key === 'DATABASE_URL_OWNER' && !e.is_preview && e.value);
7129
+ }
7130
+ catch {
7131
+ ownerUrlMissing = undefined;
7132
+ }
7101
7133
  resources.push({
7102
7134
  system: 'coolify',
7103
7135
  kind: 'jobs',
@@ -7106,6 +7138,7 @@ export class ProvidersPlatformCoolifyReader {
7106
7138
  uuid: app.uuid,
7107
7139
  status: app.status,
7108
7140
  ...(databaseStatus !== undefined && { databaseStatus }),
7141
+ ...(ownerUrlMissing && { ownerUrlMissing }),
7109
7142
  },
7110
7143
  });
7111
7144
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wtfalch/foundry",
3
- "version": "0.21.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",