@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.
@@ -9,7 +9,7 @@ import { type AdoptCandidate, type AdoptInput, type AdoptPlan } from './adopt.js
9
9
  import { type AgoraHostPort } from './agora.js';
10
10
  import { type StepAction } from './diff.js';
11
11
  import { type LiveReaders, type LiveResource, type PlatformLiveReaders } from './live.js';
12
- import { type HeldKeyResolver } from './platform-org.js';
12
+ import { type EdgePortName, type HeldKeyResolver } from './platform-org.js';
13
13
  import type { CallsSfuConnectionPort } from './platform-sfu.js';
14
14
  /** Who did it, for `@wtfalch/audit`'s "who, when, the plan, the result" (#9, #19). */
15
15
  export interface AuditActor {
@@ -142,6 +142,8 @@ export interface StepExecutionContext {
142
142
  * scope key can never collide with the `system:kind:key` form.
143
143
  */
144
144
  recordScopeId(key: string, id: string): void;
145
+ /** Removes a recorded id (any key form). Optional: a context without it cannot forget one. */
146
+ forgetId?(key: string): void;
145
147
  /**
146
148
  * Writes every id recorded so far to the store now, not only at the end of
147
149
  * the run. For an id that must survive a crash mid-step: the ai key's id
@@ -161,6 +163,14 @@ export interface StepExecutor {
161
163
  execute(action: StepAction, context: StepExecutionContext): Promise<{
162
164
  resourceId?: string;
163
165
  } | undefined>;
166
+ /**
167
+ * #555: which of the edge-routed ports (`dns`, `zones`, `access`,
168
+ * `ratelimits`, `tunnels`) this executor was built with its own
169
+ * implementation of, which it uses before edge. The planner blocks a step
170
+ * that needs edge only if the port it needs is not listed here. Optional:
171
+ * an executor without it supplies none.
172
+ */
173
+ ownEdgePorts?(): Partial<Record<EdgePortName, boolean>>;
164
174
  }
165
175
  /**
166
176
  * #167: {@link StepExecutionContext}'s platform-scoped counterpart -- what
@@ -212,6 +222,10 @@ export interface PlatformStepExecutor {
212
222
  hc?: string;
213
223
  /** #540: see `PlanPlatformOptions.blocked.heartbeatsExecutorMissing` (`platform-diff.ts`). */
214
224
  heartbeatsExecutorMissing?: boolean;
225
+ /** See `PlanPlatformOptions.blocked.archonRunsInsideApp`. */
226
+ archonRunsInsideApp?: boolean;
227
+ /** See `PlanPlatformOptions.blocked.archonDeployRunning`. */
228
+ archonDeployRunning?: string;
215
229
  }>;
216
230
  /**
217
231
  * Steps whose live state differs from the declared one in a way the recorded
@@ -13,7 +13,7 @@ import { FILES_ROOT_UNDELIVERED_SCOPE_KEY, entryFilesUndeliveredScopeKey, } from
13
13
  import { readLivePlatformResources, readLiveResources, } from './live.js';
14
14
  import { lockedHostnames } from './locked-hostnames.js';
15
15
  import { PLATFORM_ARCHON_KEY, PLATFORM_FILES_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_FILES_KEY, appStepKey, planPlatform as computePlatformPlanDetail, toPlatformPlan, } from './platform-diff.js';
16
- import { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesFromPlatform, heldPlatformKeyRefs, orgPlatformReadinessFrom, } from './platform-org.js';
16
+ import { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesFromPlatform, dnsBlockedReason, heldPlatformKeyRefs, orgPlatformReadinessFrom, } from './platform-org.js';
17
17
  import { PLATFORM_AUTH_BROKER_MAIL_KEY_REF } from './platform-providers.js';
18
18
  import { liveWorkers, workerTraceScopeKeys } from './worker.js';
19
19
  /** #441: the lease scope that serializes every mutation of one company, and the one for the shared platform. */
@@ -773,6 +773,9 @@ export class FoundryPlanApply {
773
773
  recordScopeId: (key, id) => {
774
774
  serviceIds[key] = id;
775
775
  },
776
+ forgetId: (key) => {
777
+ delete serviceIds[key];
778
+ },
776
779
  persistScopeIds: async () => {
777
780
  await this.store.putAppliedState({
778
781
  orgId,
@@ -938,7 +941,13 @@ export class FoundryPlanApply {
938
941
  catch {
939
942
  // not held
940
943
  }
941
- return { ...readiness, mailKeyHeld, canHoldSecrets: typeof heldKeys?.put === 'function' };
944
+ return {
945
+ ...readiness,
946
+ mailKeyHeld,
947
+ canHoldSecrets: typeof heldKeys?.put === 'function',
948
+ dnsBlocked: dnsBlockedReason(platform, recorded),
949
+ ownEdgePorts: this.executor.ownEdgePorts?.(),
950
+ };
942
951
  }
943
952
  /**
944
953
  * #177: `AgoraHostValues` from the applied platform, the same
@@ -1171,7 +1180,7 @@ function destroyTargets(serviceIds, orgId) {
1171
1180
  continue;
1172
1181
  }
1173
1182
  for (const [resourceKey, id] of Object.entries(serviceIds)) {
1174
- if (!resourceKey.startsWith(prefix))
1183
+ if (!resourceKey.startsWith(prefix) || !id)
1175
1184
  continue;
1176
1185
  // Archon#271: foundry did not create an adopted app or database, so it never deletes it.
1177
1186
  if (adopted.has(resourceKey))
@@ -1224,7 +1233,7 @@ function destroyTargets(serviceIds, orgId) {
1224
1233
  targets.push({ system: 'edge', kind: 'key', key: entryKey, id });
1225
1234
  }
1226
1235
  for (const [resourceKey, id] of Object.entries(serviceIds)) {
1227
- if (!resourceKey.startsWith('zitadel:app:'))
1236
+ if (!resourceKey.startsWith('zitadel:app:') || !id)
1228
1237
  continue;
1229
1238
  targets.push({
1230
1239
  system: 'zitadel',
@@ -135,6 +135,15 @@ export declare const PLATFORM_ARCHON_INTEGRATIONS_KEY_SYNCED_SCOPE_KEY = "archon
135
135
  * values until someone redeploys by hand (#467).
136
136
  */
137
137
  export declare const PLATFORM_ARCHON_ENVS_DEPLOYED_SCOPE_KEY = "archon_envs_deployed_hash";
138
+ /**
139
+ * The uuid of the deployment Archon's step triggered without waiting
140
+ * (`PlatformArchonDeps.runsInsideApp`), `pending` between the trigger and the
141
+ * moment its uuid is known. The next plan reads that deployment's status:
142
+ * running blocks the step, failed or unknown plans it again, finished lets the
143
+ * next run of the step confirm it (the integrations key-synced marker) and
144
+ * clear this.
145
+ */
146
+ export declare const PLATFORM_ARCHON_DEPLOYMENT_SCOPE_KEY = "archon_deployment";
138
147
  /**
139
148
  * #211 round 4: the broker app id Archon last registered its entry with.
140
149
  * `provisionPlatformArchon` reads the broker to register with from
@@ -414,6 +423,16 @@ export declare function platformMailMxHost(apex: string): string;
414
423
  */
415
424
  export declare const ORG_MAIL_APPLIED_APEX_SCOPE_KEY = "mail_applied_apex";
416
425
  export declare const ORG_MAIL_APPLIED_HOST_SCOPE_KEY = "mail_applied_host";
426
+ /**
427
+ * #554: the mode and `usesDomain` the step last applied. No reader lists a
428
+ * live `coolify mail <org>` resource, so `planMail` decides "applied" from
429
+ * the record. Records written before these keys existed lack them; a
430
+ * missing mode vouches for `hosted` only, a missing `usesDomain` only when
431
+ * the declared domain equals the recorded `mail_domain`. A park records
432
+ * mode `off`.
433
+ */
434
+ export declare const ORG_MAIL_APPLIED_MODE_SCOPE_KEY = "mail_applied_mode";
435
+ export declare const ORG_MAIL_APPLIED_USES_DOMAIN_SCOPE_KEY = "mail_applied_uses_domain";
417
436
  /**
418
437
  * The key of a hosting entry in a `coolify:app` or `cloudflare:access` step
419
438
  * and in the ids recorded for it: `<orgId>:<entry key>`, so a second entry
@@ -577,6 +596,12 @@ export declare const PLATFORM_AI_MIGRATE_BLOCKED_SCOPE_KEY = "ai_platform_migrat
577
596
  * that throws) is repaired by the next one.
578
597
  */
579
598
  export declare const PLATFORM_AI_DEPLOY_PENDING_SCOPE_KEY = "ai_platform_deploy_pending";
599
+ /**
600
+ * Set before the `coolify:jobs` step writes the scheduler's env and cleared
601
+ * once its deploy finished, so a run that dies in between is repaired by the
602
+ * next one. The planner plans the step while it is set.
603
+ */
604
+ export declare const JOBS_DEPLOY_PENDING_SCOPE_KEY = "jobs_deploy_pending";
580
605
  /**
581
606
  * #171: the resource key `planFiles` plans under and `FoundryPlatformStepExecutor`
582
607
  * dispatches on, the {@link PLATFORM_AUTH_KEY} shape for the files host.
@@ -839,6 +864,15 @@ export interface PlanPlatformOptions {
839
864
  * steps" note in `platform-providers.ts`'s `FoundryPlatformStepExecutor.blockedSteps`.
840
865
  */
841
866
  heartbeatsApiKeyMissing?: boolean;
867
+ /**
868
+ * The host runs inside Archon's app (`PlatformArchonDeps.runsInsideApp`):
869
+ * Archon's step redeploys it, so it is ordered last in the plan, after
870
+ * every step that would otherwise be lost when the old container stops.
871
+ * Not a block.
872
+ */
873
+ archonRunsInsideApp?: boolean;
874
+ /** Why Archon's step must wait: the deployment it triggered is still running. Blocks even an applied step. */
875
+ archonDeployRunning?: string;
842
876
  };
843
877
  /**
844
878
  * Steps whose live state differs from the declared one in a way the recorded
@@ -156,6 +156,15 @@ export const PLATFORM_ARCHON_INTEGRATIONS_KEY_SYNCED_SCOPE_KEY = 'archon_integra
156
156
  * values until someone redeploys by hand (#467).
157
157
  */
158
158
  export const PLATFORM_ARCHON_ENVS_DEPLOYED_SCOPE_KEY = 'archon_envs_deployed_hash';
159
+ /**
160
+ * The uuid of the deployment Archon's step triggered without waiting
161
+ * (`PlatformArchonDeps.runsInsideApp`), `pending` between the trigger and the
162
+ * moment its uuid is known. The next plan reads that deployment's status:
163
+ * running blocks the step, failed or unknown plans it again, finished lets the
164
+ * next run of the step confirm it (the integrations key-synced marker) and
165
+ * clear this.
166
+ */
167
+ export const PLATFORM_ARCHON_DEPLOYMENT_SCOPE_KEY = 'archon_deployment';
159
168
  /**
160
169
  * #211 round 4: the broker app id Archon last registered its entry with.
161
170
  * `provisionPlatformArchon` reads the broker to register with from
@@ -451,6 +460,16 @@ export function platformMailMxHost(apex) {
451
460
  */
452
461
  export const ORG_MAIL_APPLIED_APEX_SCOPE_KEY = 'mail_applied_apex';
453
462
  export const ORG_MAIL_APPLIED_HOST_SCOPE_KEY = 'mail_applied_host';
463
+ /**
464
+ * #554: the mode and `usesDomain` the step last applied. No reader lists a
465
+ * live `coolify mail <org>` resource, so `planMail` decides "applied" from
466
+ * the record. Records written before these keys existed lack them; a
467
+ * missing mode vouches for `hosted` only, a missing `usesDomain` only when
468
+ * the declared domain equals the recorded `mail_domain`. A park records
469
+ * mode `off`.
470
+ */
471
+ export const ORG_MAIL_APPLIED_MODE_SCOPE_KEY = 'mail_applied_mode';
472
+ export const ORG_MAIL_APPLIED_USES_DOMAIN_SCOPE_KEY = 'mail_applied_uses_domain';
454
473
  /**
455
474
  * The key of a hosting entry in a `coolify:app` or `cloudflare:access` step
456
475
  * and in the ids recorded for it: `<orgId>:<entry key>`, so a second entry
@@ -653,6 +672,12 @@ export const PLATFORM_AI_MIGRATE_BLOCKED_SCOPE_KEY = 'ai_platform_migrate_blocke
653
672
  * that throws) is repaired by the next one.
654
673
  */
655
674
  export const PLATFORM_AI_DEPLOY_PENDING_SCOPE_KEY = 'ai_platform_deploy_pending';
675
+ /**
676
+ * Set before the `coolify:jobs` step writes the scheduler's env and cleared
677
+ * once its deploy finished, so a run that dies in between is repaired by the
678
+ * next one. The planner plans the step while it is set.
679
+ */
680
+ export const JOBS_DEPLOY_PENDING_SCOPE_KEY = 'jobs_deploy_pending';
656
681
  /**
657
682
  * #171: the resource key `planFiles` plans under and `FoundryPlatformStepExecutor`
658
683
  * dispatches on, the {@link PLATFORM_AUTH_KEY} shape for the files host.
@@ -900,7 +925,7 @@ export function planPlatform(platform, live, options = {}) {
900
925
  plannedSteps.push(zone);
901
926
  // #173: the jobs scheduler, second of the eight services this frame leaves
902
927
  // open (this file's own doc comment above).
903
- const jobs = planJobs(platform, byKey, claimed, jobsObservable);
928
+ const jobs = planJobs(platform, byKey, claimed, jobsObservable, recordedIds);
904
929
  if (jobs.step)
905
930
  plannedSteps.push(jobs.step);
906
931
  // A blocked SFU is reported in `unmanaged`, not planned: a step the
@@ -963,8 +988,10 @@ export function planPlatform(platform, live, options = {}) {
963
988
  // #175: last, on purpose. Archon's sign-in client lives in the sign-in
964
989
  // `planAuth` builds, and its env reads what every other service's step
965
990
  // records or delivers, so in one apply it runs after all of them.
966
- const archon = planArchon(platform, recordedIds, blocked.archon ?? dnsBlocked?.('Archon'), authReady, drifted.archon);
967
- if (archon.step)
991
+ const archon = planArchon(platform, recordedIds, blocked.archon ?? dnsBlocked?.('Archon'), authReady, drifted.archon, blocked.archonDeployRunning);
992
+ // The host runs inside this app: its step ends by redeploying the process
993
+ // that runs the plan, so it goes after everything else (pushed below).
994
+ if (archon.step && !blocked.archonRunsInsideApp)
968
995
  plannedSteps.push(archon.step);
969
996
  // #257: after every service above, so a database any of them just
970
997
  // recorded is already visible to `recordedIds` on this same plan.
@@ -976,6 +1003,8 @@ export function planPlatform(platform, live, options = {}) {
976
1003
  const heartbeats = planHeartbeats(platform, recordedIds, hcReady, blocked.heartbeatsExecutorMissing ?? false, blocked.heartbeatsApiKeyMissing ?? false);
977
1004
  if (heartbeats.step)
978
1005
  plannedSteps.push(heartbeats.step);
1006
+ if (archon.step && blocked.archonRunsInsideApp)
1007
+ plannedSteps.push(archon.step);
979
1008
  // #251: one line per service whose recorded applied ref no longer matches
980
1009
  // what the next apply would use (or, for zitadel, what is actually live).
981
1010
  // Never pushed into `plannedSteps` -- see `VersionChangeItem`'s own doc
@@ -1114,7 +1143,7 @@ export const JOBS_SCHEDULER_KEY = 'jobs';
1114
1143
  * (`diff.ts`'s eventual `jobs:registration`) has nothing to register against
1115
1144
  * before this exists.
1116
1145
  */
1117
- function planJobs(platform, byKey, claimed, observable) {
1146
+ function planJobs(platform, byKey, claimed, observable, recordedIds) {
1118
1147
  const key = resourceKey('coolify', 'jobs', JOBS_SCHEDULER_KEY);
1119
1148
  const live = byKey.get(key);
1120
1149
  if (live)
@@ -1125,8 +1154,14 @@ function planJobs(platform, byKey, claimed, observable) {
1125
1154
  // The step also starts the scheduler's database (0.16.3), so a live app
1126
1155
  // whose database the reader saw stopped plans it again. A status the
1127
1156
  // reader could not read is absent, and plans nothing.
1157
+ // An app without `DATABASE_URL_OWNER` also plans it again: the step
1158
+ // sets the owner and runtime URLs the scheduler's migrate stage needs.
1159
+ // So does a recorded pending deploy: the step wrote its env and died
1160
+ // before the deploy finished.
1128
1161
  const databaseStatus = live.config.databaseStatus;
1129
- if (typeof databaseStatus !== 'string' || databaseStatus.startsWith('running')) {
1162
+ if (live.config.ownerUrlMissing !== true &&
1163
+ !recordedIds[JOBS_DEPLOY_PENDING_SCOPE_KEY] &&
1164
+ (typeof databaseStatus !== 'string' || databaseStatus.startsWith('running'))) {
1130
1165
  return { step: null, blocked: null };
1131
1166
  }
1132
1167
  }
@@ -2215,9 +2250,12 @@ function planAi(platform, recordedIds, executorBlocked) {
2215
2250
  * ({@link PLATFORM_ARCHON_CONNECTOR_APP_SCOPE_KEY}) plans the step again, as
2216
2251
  * {@link planAuth} does.
2217
2252
  */
2218
- function planArchon(platform, recordedIds, executorBlocked, authReady, drifted) {
2253
+ function planArchon(platform, recordedIds, executorBlocked, authReady, drifted, deployRunning) {
2219
2254
  if (!platform.archon.enabled)
2220
2255
  return { step: null, blocked: null };
2256
+ // A deploy the step triggered is still running: planning it again would deploy twice.
2257
+ if (deployRunning)
2258
+ return { step: null, blocked: deployRunning };
2221
2259
  const key = resourceKey('coolify', 'app', PLATFORM_ARCHON_KEY);
2222
2260
  const applied = Boolean(recordedIds[key]) &&
2223
2261
  Boolean(recordedIds[PLATFORM_ARCHON_CONNECTOR_APP_SCOPE_KEY]) &&
@@ -35,6 +35,8 @@ export declare const NO_DOMAIN_MAIL_APEX = "oikos.host";
35
35
  * names the same "positional arguments that shifted on every merge" problem
36
36
  * this avoids).
37
37
  */
38
+ /** The edge-routed ports an executor may be given its own implementation of. */
39
+ export type EdgePortName = 'dns' | 'zones' | 'access' | 'ratelimits' | 'tunnels';
38
40
  export interface OrgPlatformReadiness {
39
41
  /**
40
42
  * `null` until the platform's own mail server (#169) is enabled and
@@ -96,6 +98,22 @@ export interface OrgPlatformReadiness {
96
98
  * used to say there was none; N1 is no longer deployed by hand).
97
99
  */
98
100
  edgeReady: boolean;
101
+ /**
102
+ * #555: why a company step that writes through edge cannot run yet, `null`
103
+ * or absent when none is blocked. DNS records, zones, Access apps, rate
104
+ * limit rules and tunnels all go through edge unless the host supplies its
105
+ * own port ({@link ownEdgePorts}); while edge is not applied, `diff.ts`
106
+ * reports each such step blocked instead of planning one that would fail
107
+ * half way.
108
+ */
109
+ dnsBlocked?: string | null;
110
+ /**
111
+ * #555: the edge-routed ports the host's executor supplies itself
112
+ * (`FoundryStepExecutorPorts.dns` / `zones` / `access` / `ratelimits` /
113
+ * `tunnels`). The executor uses these before edge, so a step that needs
114
+ * only these is not blocked by {@link dnsBlocked}.
115
+ */
116
+ ownEdgePorts?: Partial<Record<EdgePortName, boolean>>;
99
117
  /** `https://<platform.edge.hostname>`, which an entry with `uses.edge` gets as `EDGE_URL`. `null` until `edgeReady`. */
100
118
  edgeUrl?: string | null;
101
119
  /**
@@ -149,6 +167,16 @@ export interface HeldKeyResolver {
149
167
  * (which could carry provider detail) goes nowhere.
150
168
  */
151
169
  export declare function heldPlatformKeyRefs(heldKeys: HeldKeyResolver): Promise<Set<string>>;
170
+ /**
171
+ * #555: the reason no company step may write DNS yet, `null` when one may.
172
+ * Every DNS record, zone, Access app, rate limit rule and tunnel call goes
173
+ * through edge unless the host supplies its own port
174
+ * ({@link OrgPlatformReadiness.ownEdgePorts}), so edge must be enabled and
175
+ * applied (the same check as `edgeReady`, without the host-port half). {@link FoundryPlanApply} sets
176
+ * {@link OrgPlatformReadiness.dnsBlocked} from this; the pure
177
+ * {@link orgPlatformReadinessFrom} leaves it unset (nothing blocked).
178
+ */
179
+ export declare function dnsBlockedReason(platform: DeclaredPlatform, platformRecordedIds: Readonly<Record<string, string>>): string | null;
152
180
  /**
153
181
  * `FoundryPlanApply.computeFreshPlan`'s own precedent for turning a
154
182
  * `PlatformStore`'s `get()`/`getServiceIds()` into what an org's plan needs.
@@ -63,6 +63,23 @@ export async function heldPlatformKeyRefs(heldKeys) {
63
63
  function coolifyAppApplied(platformRecordedIds, key) {
64
64
  return Boolean(platformRecordedIds[`coolify:app:${key}`]);
65
65
  }
66
+ /**
67
+ * #555: the reason no company step may write DNS yet, `null` when one may.
68
+ * Every DNS record, zone, Access app, rate limit rule and tunnel call goes
69
+ * through edge unless the host supplies its own port
70
+ * ({@link OrgPlatformReadiness.ownEdgePorts}), so edge must be enabled and
71
+ * applied (the same check as `edgeReady`, without the host-port half). {@link FoundryPlanApply} sets
72
+ * {@link OrgPlatformReadiness.dnsBlocked} from this; the pure
73
+ * {@link orgPlatformReadinessFrom} leaves it unset (nothing blocked).
74
+ */
75
+ export function dnsBlockedReason(platform, platformRecordedIds) {
76
+ if (!platform.edge.enabled) {
77
+ return 'every DNS record is written through edge, and the platform does not enable edge';
78
+ }
79
+ return coolifyAppApplied(platformRecordedIds, PLATFORM_EDGE_KEY)
80
+ ? null
81
+ : 'every DNS record is written through edge, and edge is not applied yet';
82
+ }
66
83
  /**
67
84
  * `FoundryPlanApply.computeFreshPlan`'s own precedent for turning a
68
85
  * `PlatformStore`'s `get()`/`getServiceIds()` into what an org's plan needs.
@@ -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
@@ -290,6 +290,8 @@ export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor
290
290
  hc?: string;
291
291
  heartbeatsExecutorMissing?: boolean;
292
292
  heartbeatsApiKeyMissing?: boolean;
293
+ archonRunsInsideApp?: boolean;
294
+ archonDeployRunning?: string;
293
295
  }>;
294
296
  /**
295
297
  * Whether Archon's live Access policy lets through anyone but the declared
@@ -474,7 +476,7 @@ export interface PlatformMailRuntime {
474
476
  * {@link PlatformStepExecutionContext}, bundled like {@link PlatformAuthDeps}.
475
477
  */
476
478
  export interface PlatformArchonDeps {
477
- coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'findApplicationByName' | 'getApplication' | 'requireGithubApp' | 'createGithubAppApplication' | 'createDockerfileApplication' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'raw' | 'bulkUpsertEnvs' | 'listEnvs' | 'deleteEnv' | 'updateApplication' | 'deploy' | 'deployAndWait'>;
479
+ coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'findApplicationByName' | 'getApplication' | 'requireGithubApp' | 'createGithubAppApplication' | 'createDockerfileApplication' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'raw' | 'bulkUpsertEnvs' | 'listEnvs' | 'deleteEnv' | 'updateApplication' | 'deploy' | 'deployAndWait' | 'getDeployment'>;
478
480
  /** F4.dns: every DNS record call, through edge at its recorded address (see {@link PlatformDnsFactory}). */
479
481
  dns: PlatformDnsFactory;
480
482
  /** F4.tunnels: every tunnel call, through edge at its recorded address (see {@link PlatformTunnelsFactory}). */
@@ -493,6 +495,15 @@ export interface PlatformArchonDeps {
493
495
  * Access app nobody can pass.
494
496
  */
495
497
  accessEmails: readonly string[];
498
+ /**
499
+ * True when the host process runs inside the Coolify app this step
500
+ * redeploys (Archon's console is `platform:archon`). A deploy that waits
501
+ * would be killed with the old container, so the step then records every
502
+ * applied marker first, and triggers the deploy last, without waiting.
503
+ * The planner puts the step last in the plan. Next plan: the step runs
504
+ * again only if the app is not running (`archonRedeployFailed`).
505
+ */
506
+ runsInsideApp?: boolean;
496
507
  /**
497
508
  * Reads a secret an earlier step handed to the host through
498
509
  * `deliverSecret`, or `null` when none is held under `ref` yet -- the same
@@ -1154,7 +1165,7 @@ export declare class ProvidersPlatformCloudflareReader implements PlatformCloudf
1154
1165
  export declare class ProvidersPlatformCoolifyReader implements PlatformCoolifyReader {
1155
1166
  private readonly client;
1156
1167
  private readonly platformStore;
1157
- constructor(client: Pick<CoolifyPort, 'getApplication' | 'getDatabase' | 'listDatabaseBackups' | 'raw'>, platformStore: Pick<PlatformStore, 'getServiceIds'>);
1168
+ constructor(client: Pick<CoolifyPort, 'getApplication' | 'getDatabase' | 'listDatabaseBackups' | 'listEnvs' | 'raw'>, platformStore: Pick<PlatformStore, 'getServiceIds'>);
1158
1169
  listResources(): Promise<LiveResource[]>;
1159
1170
  }
1160
1171
  /** Scope id (no `:`): the share domains the runner's Coolify `domains` was last set to, comma-joined. */