@wtfalch/foundry 0.21.0 → 0.22.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/plan/diff.js +176 -22
- package/dist/plan/executor.d.ts +21 -4
- package/dist/plan/executor.js +104 -36
- package/dist/plan/plan-apply.d.ts +15 -1
- package/dist/plan/plan-apply.js +13 -4
- package/dist/plan/platform-diff.d.ts +34 -0
- package/dist/plan/platform-diff.js +44 -6
- package/dist/plan/platform-org.d.ts +28 -0
- package/dist/plan/platform-org.js +17 -0
- package/dist/plan/platform-providers.d.ts +14 -3
- package/dist/plan/platform-providers.js +132 -12
- package/package.json +3 -2
|
@@ -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_DEPLOYMENT_SCOPE_KEY, 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`). */
|
|
@@ -318,6 +318,18 @@ export class FoundryPlatformStepExecutor {
|
|
|
318
318
|
if (!apiKey)
|
|
319
319
|
blocked.heartbeatsApiKeyMissing = true;
|
|
320
320
|
}
|
|
321
|
+
// Not a block: tells the planner to order Archon's step last (`PlanPlatformOptions.blocked.archonRunsInsideApp`).
|
|
322
|
+
if (this.archon?.runsInsideApp) {
|
|
323
|
+
blocked.archonRunsInsideApp = true;
|
|
324
|
+
// A deploy the step triggered and did not wait for: while it runs, a
|
|
325
|
+
// second deploy must not be planned.
|
|
326
|
+
const deployment = recordedIds[PLATFORM_ARCHON_DEPLOYMENT_SCOPE_KEY];
|
|
327
|
+
if (platform.archon.enabled &&
|
|
328
|
+
deployment &&
|
|
329
|
+
(await archonDeploymentState(this.archon.coolify, deployment)) === 'running') {
|
|
330
|
+
blocked.archonDeployRunning = `Archon is blocked, not planned: its deployment ${deployment} is still running; plan again when it has finished`;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
321
333
|
return blocked;
|
|
322
334
|
}
|
|
323
335
|
/**
|
|
@@ -347,7 +359,18 @@ export class FoundryPlatformStepExecutor {
|
|
|
347
359
|
!brokerDrifted &&
|
|
348
360
|
!bootstrapOperatorDrifted &&
|
|
349
361
|
(await archonRollingUpdateDrifted(this.archon.coolify, recordedIds[`coolify:app:${PLATFORM_ARCHON_KEY}`]));
|
|
350
|
-
|
|
362
|
+
// The deployment the step triggered failed or Coolify does not know it:
|
|
363
|
+
// plan the step again, which deploys again. A finished one is confirmed
|
|
364
|
+
// by the next run of the step (keys-synced), see `provisionPlatformArchon`.
|
|
365
|
+
const deployment = recordedIds[PLATFORM_ARCHON_DEPLOYMENT_SCOPE_KEY];
|
|
366
|
+
const redeployFailed = this.archon.runsInsideApp === true &&
|
|
367
|
+
Boolean(deployment) &&
|
|
368
|
+
(await archonDeploymentState(this.archon.coolify, deployment)) === 'failed';
|
|
369
|
+
if (accessDrifted ||
|
|
370
|
+
brokerDrifted ||
|
|
371
|
+
bootstrapOperatorDrifted ||
|
|
372
|
+
rollingUpdateDrifted ||
|
|
373
|
+
redeployFailed)
|
|
351
374
|
result.archon = true;
|
|
352
375
|
}
|
|
353
376
|
if (platform.auth.enabled && this.auth) {
|
|
@@ -596,7 +619,25 @@ export class FoundryPlatformStepExecutor {
|
|
|
596
619
|
if (!db.internal_db_url) {
|
|
597
620
|
throw new Error(`platform step coolify:jobs: database ${dbId} has no internal_db_url yet; not deploying the scheduler without a database`);
|
|
598
621
|
}
|
|
599
|
-
|
|
622
|
+
// The scheduler migrates as `DATABASE_URL_OWNER` and creates its own
|
|
623
|
+
// `jobs_rt` role from `DATABASE_URL` (jobs README, "Deploying"). An app
|
|
624
|
+
// with no owner URL still holds the owner's URL in `DATABASE_URL`, so
|
|
625
|
+
// that is never reused: a runtime URL is minted, once, and kept after.
|
|
626
|
+
const hasOwnerUrl = (await coolify.listEnvs(appId)).some((e) => e.key === 'DATABASE_URL_OWNER' && !e.is_preview && e.value);
|
|
627
|
+
const runtimeUrl = await ensureAiRuntimeUrl(coolify, hasOwnerUrl ? [appId] : [], db.internal_db_url, 'platform step coolify:jobs');
|
|
628
|
+
const envs = [
|
|
629
|
+
{ key: 'DATABASE_URL_OWNER', value: db.internal_db_url },
|
|
630
|
+
{ key: 'DATABASE_URL', value: runtimeUrl },
|
|
631
|
+
];
|
|
632
|
+
// Whether the scheduler runs on envs other than the ones written below.
|
|
633
|
+
// Marked and persisted before the first write, and cleared only after the
|
|
634
|
+
// deploy, so a run that dies in between leaves the mark and the next run
|
|
635
|
+
// deploys again (`CLAUDE.md` rule 4).
|
|
636
|
+
const pending = Boolean(recorded[JOBS_DEPLOY_PENDING_SCOPE_KEY]) || (await envsDiffer(coolify, appId, envs));
|
|
637
|
+
if (pending) {
|
|
638
|
+
context.recordScopeId(JOBS_DEPLOY_PENDING_SCOPE_KEY, '1');
|
|
639
|
+
await context.persist?.();
|
|
640
|
+
}
|
|
600
641
|
// SCHEDULER_ADMIN_TOKEN gates the admin API the org registrations use
|
|
601
642
|
// (jobs README, "The admin API"); unset, every admin route answers 401.
|
|
602
643
|
// Delivered to the host before it is written to the app, so a failed
|
|
@@ -629,8 +670,12 @@ export class FoundryPlatformStepExecutor {
|
|
|
629
670
|
// a redeploy, so deploy after a create or a new token. Also when it is not
|
|
630
671
|
// running, so a deploy that failed on an earlier run is retried.
|
|
631
672
|
const running = (await coolify.getApplication(appId)).status?.startsWith('running') ?? false;
|
|
632
|
-
if (!existingApp || tokenJustSet || !running)
|
|
673
|
+
if (!existingApp || tokenJustSet || pending || !running)
|
|
633
674
|
await coolify.deploy(appId);
|
|
675
|
+
if (pending) {
|
|
676
|
+
context.recordScopeId(JOBS_DEPLOY_PENDING_SCOPE_KEY, '');
|
|
677
|
+
await context.persist?.();
|
|
678
|
+
}
|
|
634
679
|
return { resourceId: appId };
|
|
635
680
|
}
|
|
636
681
|
}
|
|
@@ -2578,6 +2623,7 @@ async function provisionPlatformArchon(context, deps) {
|
|
|
2578
2623
|
const appliedGitRef = recorded[PLATFORM_ARCHON_APPLIED_GIT_REF_SCOPE_KEY];
|
|
2579
2624
|
const runtime = deps.runtime ?? {};
|
|
2580
2625
|
const sleep = runtime.sleep ?? defaultSleep;
|
|
2626
|
+
const selfRedeploy = deps.runsInsideApp === true;
|
|
2581
2627
|
if (deps.accessEmails.length === 0) {
|
|
2582
2628
|
throw new Error('platform:archon: no Access emails given; refusing to put Archon behind an Access app nobody can pass');
|
|
2583
2629
|
}
|
|
@@ -3008,7 +3054,17 @@ async function provisionPlatformArchon(context, deps) {
|
|
|
3008
3054
|
// patch above actually takes effect on the running container, not just on
|
|
3009
3055
|
// Coolify's stored config.
|
|
3010
3056
|
const rollingUpdateApplied = recorded[PLATFORM_ARCHON_APPLIED_ROLLING_UPDATE_SCOPE_KEY] === '1';
|
|
3011
|
-
|
|
3057
|
+
// The deployment an earlier run triggered without waiting: read before
|
|
3058
|
+
// deciding whether to deploy again.
|
|
3059
|
+
const priorDeployment = selfRedeploy ? recorded[PLATFORM_ARCHON_DEPLOYMENT_SCOPE_KEY] : undefined;
|
|
3060
|
+
const deployState = priorDeployment
|
|
3061
|
+
? await archonDeploymentState(deps.coolify, priorDeployment)
|
|
3062
|
+
: 'none';
|
|
3063
|
+
if (deployState === 'running' || deployState === 'unreadable') {
|
|
3064
|
+
throw new Error(`platform:archon: the deployment ${priorDeployment} cannot be confirmed yet (${deployState === 'running' ? 'still running' : 'status unreadable'}); plan again later`);
|
|
3065
|
+
}
|
|
3066
|
+
const archonDeployedNow = (keyUnsynced && deployState !== 'finished') ||
|
|
3067
|
+
deployState === 'failed' ||
|
|
3012
3068
|
app.justCreated ||
|
|
3013
3069
|
!app.running ||
|
|
3014
3070
|
changed ||
|
|
@@ -3017,7 +3073,7 @@ async function provisionPlatformArchon(context, deps) {
|
|
|
3017
3073
|
appliedGitRef !== gitRef ||
|
|
3018
3074
|
!rollingUpdateApplied ||
|
|
3019
3075
|
recorded[PLATFORM_ARCHON_APPLIED_HOSTNAME_SCOPE_KEY] !== hostname;
|
|
3020
|
-
if (archonDeployedNow) {
|
|
3076
|
+
if (archonDeployedNow && !selfRedeploy) {
|
|
3021
3077
|
// #467: cleared before the deploy below is even attempted, not after --
|
|
3022
3078
|
// a deploy that then fails, or a crash before `waitForHealthyVersion`
|
|
3023
3079
|
// passes, must leave this run's attempt unconfirmed. Recording it only
|
|
@@ -3089,13 +3145,14 @@ async function provisionPlatformArchon(context, deps) {
|
|
|
3089
3145
|
// Coolify's own deployment record finished, which is what actually closes
|
|
3090
3146
|
// the loophole named in #252 (an old container staying healthy while the
|
|
3091
3147
|
// deployment itself fails).
|
|
3092
|
-
if (archonDeployedNow) {
|
|
3148
|
+
if (archonDeployedNow && !selfRedeploy) {
|
|
3093
3149
|
await waitForHealthyVersion(appId, { kind: 'ready', url: `https://${hostname}/api/health` }, runtime, sleep, 'platform:archon', `archon stays at ${appliedGitRef ?? 'no recorded ref'} on ${recorded[PLATFORM_ARCHON_APPLIED_HOSTNAME_SCOPE_KEY] ?? 'no recorded hostname'}`);
|
|
3094
3150
|
}
|
|
3095
3151
|
// #398: the deployment finished and answers health, so Archon now runs with
|
|
3096
3152
|
// the key id its env carries. `provisionPlatformIntegrations` drops the
|
|
3097
3153
|
// previous public key only on this marker.
|
|
3098
|
-
|
|
3154
|
+
// With `runsInsideApp` only once Coolify's deployment record says it finished.
|
|
3155
|
+
if (keyUnsynced && integrationsKeyId && (!selfRedeploy || deployState === 'finished')) {
|
|
3099
3156
|
await record(PLATFORM_ARCHON_INTEGRATIONS_KEY_SYNCED_SCOPE_KEY, integrationsKeyId);
|
|
3100
3157
|
}
|
|
3101
3158
|
// #475: confirmed only now, after the health check above has passed on a
|
|
@@ -3118,6 +3175,36 @@ async function provisionPlatformArchon(context, deps) {
|
|
|
3118
3175
|
// it.
|
|
3119
3176
|
await record(PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY, brokerAppId);
|
|
3120
3177
|
await record(PLATFORM_ARCHON_APPLIED_OPERATOR_SCOPE_KEY, bootstrapOperator ?? 'none');
|
|
3178
|
+
// The host runs inside this app: every marker is recorded above, so the
|
|
3179
|
+
// deploy goes last and is not waited for -- the old container stops during
|
|
3180
|
+
// it, and the response, the lease and the audit row must be done first.
|
|
3181
|
+
if (selfRedeploy && !archonDeployedNow && deployState === 'finished') {
|
|
3182
|
+
await record(PLATFORM_ARCHON_DEPLOYMENT_SCOPE_KEY, ''); // confirmed above
|
|
3183
|
+
}
|
|
3184
|
+
if (selfRedeploy && archonDeployedNow) {
|
|
3185
|
+
await record(`coolify:app:${PLATFORM_ARCHON_KEY}`, appId);
|
|
3186
|
+
await record(PLATFORM_ARCHON_DEPLOYMENT_SCOPE_KEY, 'pending');
|
|
3187
|
+
let deploymentUuid;
|
|
3188
|
+
try {
|
|
3189
|
+
// `force` takes the branch that always creates a real deployment record
|
|
3190
|
+
// (a skipped one answers with a uuid Coolify never stored).
|
|
3191
|
+
const started = (await deps.coolify.deploy(appId, true));
|
|
3192
|
+
const uuid = Array.isArray(started) ? started[0]?.deployment_uuid : undefined;
|
|
3193
|
+
if (typeof uuid !== 'string' || !uuid) {
|
|
3194
|
+
throw new Error('platform:archon: Coolify answered the deploy with no deployment_uuid');
|
|
3195
|
+
}
|
|
3196
|
+
deploymentUuid = uuid;
|
|
3197
|
+
}
|
|
3198
|
+
catch (error) {
|
|
3199
|
+
// Not triggered: forget the applied ref so the next plan runs the step again.
|
|
3200
|
+
await record(PLATFORM_ARCHON_APPLIED_GIT_REF_SCOPE_KEY, '');
|
|
3201
|
+
await record(PLATFORM_ARCHON_DEPLOYMENT_SCOPE_KEY, '');
|
|
3202
|
+
throw error;
|
|
3203
|
+
}
|
|
3204
|
+
// The next plan reads this deployment's status; a kill before this write
|
|
3205
|
+
// leaves `pending`, which that plan reads as unconfirmed and plans again.
|
|
3206
|
+
await record(PLATFORM_ARCHON_DEPLOYMENT_SCOPE_KEY, deploymentUuid);
|
|
3207
|
+
}
|
|
3121
3208
|
return { resourceId: appId };
|
|
3122
3209
|
}
|
|
3123
3210
|
// #211 round 6: mirrors `wtfalch/auth/broker/src/apps.ts`'s own `isValidFrom`
|
|
@@ -3538,6 +3625,28 @@ async function archonBootstrapOperatorDrifted(coolify, appId, recordedIds) {
|
|
|
3538
3625
|
* version of this check never actually fired. `custom_network_aliases` and
|
|
3539
3626
|
* `health_check_enabled` are both top-level there.
|
|
3540
3627
|
*/
|
|
3628
|
+
/**
|
|
3629
|
+
* The state of the deployment Archon's step triggered without waiting
|
|
3630
|
+
* (`PlatformArchonDeps.runsInsideApp`), from Coolify's own deployment record:
|
|
3631
|
+
* `running` (queued or in progress), `finished`, `failed` (failed, cancelled,
|
|
3632
|
+
* its uuid never recorded, or unknown to Coolify -- a 404), or `unreadable`
|
|
3633
|
+
* for any other error. Status and a served request cannot tell a failed build
|
|
3634
|
+
* from a success (the old container keeps serving), so this record is the
|
|
3635
|
+
* only honest answer.
|
|
3636
|
+
*/
|
|
3637
|
+
async function archonDeploymentState(coolify, deploymentUuid) {
|
|
3638
|
+
if (deploymentUuid === 'pending')
|
|
3639
|
+
return 'failed';
|
|
3640
|
+
try {
|
|
3641
|
+
const { status } = await coolify.getDeployment(deploymentUuid);
|
|
3642
|
+
if (status === 'queued' || status === 'in_progress')
|
|
3643
|
+
return 'running';
|
|
3644
|
+
return status === 'finished' ? 'finished' : 'failed';
|
|
3645
|
+
}
|
|
3646
|
+
catch (error) {
|
|
3647
|
+
return isNotFoundError(error) ? 'failed' : 'unreadable';
|
|
3648
|
+
}
|
|
3649
|
+
}
|
|
3541
3650
|
async function archonRollingUpdateDrifted(coolify, appId) {
|
|
3542
3651
|
if (!appId)
|
|
3543
3652
|
return false;
|
|
@@ -4185,7 +4294,7 @@ async function provisionPlatformAi(context, deps) {
|
|
|
4185
4294
|
// ---- env -----------------------------------------------------------------
|
|
4186
4295
|
// Both database URLs are live credentials: handed to `bulkUpsertEnvs` and
|
|
4187
4296
|
// nowhere else, never returned, recorded, logged or put in an error.
|
|
4188
|
-
const runtimeUrl = await ensureAiRuntimeUrl(coolify, [runner.appId, worker.appId, migrate.appId], ownerUrl);
|
|
4297
|
+
const runtimeUrl = await ensureAiRuntimeUrl(coolify, [runner.appId, worker.appId, migrate.appId], ownerUrl, 'platform:ai');
|
|
4189
4298
|
const migrateEnvs = [
|
|
4190
4299
|
{ key: 'DATABASE_URL_OWNER', value: ownerUrl },
|
|
4191
4300
|
{ key: 'DATABASE_URL', value: runtimeUrl },
|
|
@@ -4556,7 +4665,7 @@ async function waitForAiMigration(coolify, appId, runtime, sleep) {
|
|
|
4556
4665
|
* inside a try: Node's parse error carries the whole input, credential
|
|
4557
4666
|
* included, so it is replaced with a fixed message.
|
|
4558
4667
|
*/
|
|
4559
|
-
async function ensureAiRuntimeUrl(coolify, appIds, ownerUrl) {
|
|
4668
|
+
async function ensureAiRuntimeUrl(coolify, appIds, ownerUrl, step) {
|
|
4560
4669
|
for (const appId of appIds) {
|
|
4561
4670
|
const existing = (await coolify.listEnvs(appId)).find((e) => e.key === 'DATABASE_URL' && !e.is_preview && e.value);
|
|
4562
4671
|
if (existing)
|
|
@@ -4569,11 +4678,11 @@ async function ensureAiRuntimeUrl(coolify, appIds, ownerUrl) {
|
|
|
4569
4678
|
({ host, pathname, search } = new URL(ownerUrl));
|
|
4570
4679
|
}
|
|
4571
4680
|
catch {
|
|
4572
|
-
throw new Error(
|
|
4681
|
+
throw new Error(`${step}: the database's internal_db_url could not be parsed`);
|
|
4573
4682
|
}
|
|
4574
4683
|
const database = pathname.replace(/^\//, '');
|
|
4575
4684
|
if (!/^[A-Za-z0-9_]+$/.test(database)) {
|
|
4576
|
-
throw new Error(
|
|
4685
|
+
throw new Error(`${step}: the database's name cannot form a runtime role name (letters, digits and _ only)`);
|
|
4577
4686
|
}
|
|
4578
4687
|
const password = randomBytes(24).toString('base64url');
|
|
4579
4688
|
return `postgres://${database}_rt:${password}@${host}${pathname}${search}`;
|
|
@@ -7098,6 +7207,16 @@ export class ProvidersPlatformCoolifyReader {
|
|
|
7098
7207
|
databaseStatus = undefined;
|
|
7099
7208
|
}
|
|
7100
7209
|
}
|
|
7210
|
+
// An app with no `DATABASE_URL_OWNER` predates the owner/runtime
|
|
7211
|
+
// split, so `planJobs` plans the step again to set both URLs. Left
|
|
7212
|
+
// out when the envs cannot be read: unknown is not "missing".
|
|
7213
|
+
let ownerUrlMissing;
|
|
7214
|
+
try {
|
|
7215
|
+
ownerUrlMissing = !(await this.client.listEnvs(app.uuid)).some((e) => e.key === 'DATABASE_URL_OWNER' && !e.is_preview && e.value);
|
|
7216
|
+
}
|
|
7217
|
+
catch {
|
|
7218
|
+
ownerUrlMissing = undefined;
|
|
7219
|
+
}
|
|
7101
7220
|
resources.push({
|
|
7102
7221
|
system: 'coolify',
|
|
7103
7222
|
kind: 'jobs',
|
|
@@ -7106,6 +7225,7 @@ export class ProvidersPlatformCoolifyReader {
|
|
|
7106
7225
|
uuid: app.uuid,
|
|
7107
7226
|
status: app.status,
|
|
7108
7227
|
...(databaseStatus !== undefined && { databaseStatus }),
|
|
7228
|
+
...(ownerUrlMissing && { ownerUrlMissing }),
|
|
7109
7229
|
},
|
|
7110
7230
|
});
|
|
7111
7231
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wtfalch/foundry",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.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",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"calls-sfu:deploy": "node scripts/calls-sfu-deploy.mjs"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
|
+
"@wtfalch/providers": ">=0.7.4",
|
|
40
41
|
"drizzle-orm": ">=0.45.2"
|
|
41
42
|
},
|
|
42
43
|
"dependencies": {
|
|
@@ -44,7 +45,7 @@
|
|
|
44
45
|
"@wtfalch/auth": "0.16.0",
|
|
45
46
|
"@wtfalch/edge": "0.9.0",
|
|
46
47
|
"@wtfalch/email": "^0.17.0",
|
|
47
|
-
"@wtfalch/providers": "0.7.
|
|
48
|
+
"@wtfalch/providers": "0.7.5",
|
|
48
49
|
"ssh2": "1.17.0",
|
|
49
50
|
"zod": "4.6.5"
|
|
50
51
|
},
|