@wtfalch/foundry 0.19.0 → 0.19.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.
- package/dist/plan/agora.d.ts +11 -0
- package/dist/plan/agora.js +13 -0
- package/dist/plan/chat-matrix.d.ts +3 -0
- package/dist/plan/chat-matrix.js +2 -0
- package/dist/plan/diff.js +22 -1
- package/dist/plan/executor.d.ts +12 -0
- package/dist/plan/executor.js +99 -2
- package/package.json +1 -1
package/dist/plan/agora.d.ts
CHANGED
|
@@ -151,6 +151,17 @@ export declare const CALLS_SERVICE_TOKEN_ENV_SCOPE_KEY = "coolify_calls_service_
|
|
|
151
151
|
*/
|
|
152
152
|
export declare const CALLS_INTROSPECTION_SCOPE_KEY = "zitadel_calls_introspection_app";
|
|
153
153
|
export declare const CALLS_INTROSPECTION_UNDELIVERED_SCOPE_KEY = "zitadel_calls_introspection_undelivered";
|
|
154
|
+
/**
|
|
155
|
+
* The introspection API app minted for the org's chat service (once), in the
|
|
156
|
+
* messaging project, and the undelivered key recorded before its secret
|
|
157
|
+
* leaves the executor (platform rule 1). The first doubles as the marker
|
|
158
|
+
* `planChat` reads: recorded after the service's env carries the client. No
|
|
159
|
+
* `:` in either.
|
|
160
|
+
*/
|
|
161
|
+
export declare const CHAT_INTROSPECTION_SCOPE_KEY = "zitadel_chat_introspection_app";
|
|
162
|
+
export declare const CHAT_INTROSPECTION_UNDELIVERED_SCOPE_KEY = "zitadel_chat_introspection_undelivered";
|
|
163
|
+
/** The chat service's introspection API app name; one per org, exact match only. */
|
|
164
|
+
export declare function chatIntrospectionAppName(org: DeclaredOrg): string;
|
|
154
165
|
/** What the plan hands the calls service step: the two host values the token env needs. */
|
|
155
166
|
export interface CallsTokenHost {
|
|
156
167
|
issuer: string;
|
package/dist/plan/agora.js
CHANGED
|
@@ -163,6 +163,19 @@ export const CALLS_SERVICE_TOKEN_ENV_SCOPE_KEY = 'coolify_calls_service_token_en
|
|
|
163
163
|
*/
|
|
164
164
|
export const CALLS_INTROSPECTION_SCOPE_KEY = 'zitadel_calls_introspection_app';
|
|
165
165
|
export const CALLS_INTROSPECTION_UNDELIVERED_SCOPE_KEY = 'zitadel_calls_introspection_undelivered';
|
|
166
|
+
/**
|
|
167
|
+
* The introspection API app minted for the org's chat service (once), in the
|
|
168
|
+
* messaging project, and the undelivered key recorded before its secret
|
|
169
|
+
* leaves the executor (platform rule 1). The first doubles as the marker
|
|
170
|
+
* `planChat` reads: recorded after the service's env carries the client. No
|
|
171
|
+
* `:` in either.
|
|
172
|
+
*/
|
|
173
|
+
export const CHAT_INTROSPECTION_SCOPE_KEY = 'zitadel_chat_introspection_app';
|
|
174
|
+
export const CHAT_INTROSPECTION_UNDELIVERED_SCOPE_KEY = 'zitadel_chat_introspection_undelivered';
|
|
175
|
+
/** The chat service's introspection API app name; one per org, exact match only. */
|
|
176
|
+
export function chatIntrospectionAppName(org) {
|
|
177
|
+
return `chat-introspection-${org.slug}`;
|
|
178
|
+
}
|
|
166
179
|
/**
|
|
167
180
|
* The token env's inputs, or null while any is missing: the host's issuer and
|
|
168
181
|
* messaging project id (`AgoraHostValues`) and the org's recorded ZITADEL org
|
|
@@ -90,6 +90,9 @@ export interface ChatServiceEnvInput {
|
|
|
90
90
|
agoraHost: string;
|
|
91
91
|
masClientId: string;
|
|
92
92
|
masClientSecret: string;
|
|
93
|
+
/** The ZITADEL API app the service introspects Agora's opaque tokens as. */
|
|
94
|
+
introspectionClientId: string;
|
|
95
|
+
introspectionClientSecret: string;
|
|
93
96
|
}
|
|
94
97
|
/** Every variable `chat/apps/chat-service/src/config.ts` requires, plus the internal Synapse address and port. */
|
|
95
98
|
export declare function chatServiceEnvs(input: ChatServiceEnvInput): Array<{
|
package/dist/plan/chat-matrix.js
CHANGED
|
@@ -195,6 +195,8 @@ export function chatServiceEnvs(input) {
|
|
|
195
195
|
{ key: 'MAS_URL', value: upstreams.mas },
|
|
196
196
|
{ key: 'MAS_CLIENT_ID', value: input.masClientId },
|
|
197
197
|
{ key: 'MAS_CLIENT_SECRET', value: input.masClientSecret },
|
|
198
|
+
{ key: 'CHAT_INTROSPECTION_CLIENT_ID', value: input.introspectionClientId },
|
|
199
|
+
{ key: 'CHAT_INTROSPECTION_CLIENT_SECRET', value: input.introspectionClientSecret },
|
|
198
200
|
// DATABASE_URL_OWNER/DATABASE_URL are pushed by executor.ts's
|
|
199
201
|
// pushDatabaseUrl instead, not here (the same split coolifyApp and
|
|
200
202
|
// cmsSite follow, #239) -- that call mints a distinct <dbName>_rt
|
package/dist/plan/diff.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { VERSIONS, floatingTagBlocked } from '../bootstrap/versions.js';
|
|
2
2
|
import { APP_ENTRY_KEY, ownerMailboxRecorded, ownerMailboxes } from '../org.js';
|
|
3
|
-
import { AGORA_ACTIVATED_ENV_SCOPE_KEY, AGORA_APP_PORTS_EXPOSES, CALLS_INTROSPECTION_SCOPE_KEY, CALLS_INTROSPECTION_UNDELIVERED_SCOPE_KEY, NO_AGORA_HOST_VALUES, ZITADEL_ORG_SCOPE_KEY, agoraEnvDrift, agoraEnvRevision, agoraHostMissing, agoraHostnameFor, callsServiceHasName, callsServiceHasTokenEnv, callsTokenEnvMovedMessage, callsTokenHostFrom, desiredAgoraEnv, withRecordedMessagingProject, } from './agora.js';
|
|
3
|
+
import { AGORA_ACTIVATED_ENV_SCOPE_KEY, AGORA_APP_PORTS_EXPOSES, CALLS_INTROSPECTION_SCOPE_KEY, CALLS_INTROSPECTION_UNDELIVERED_SCOPE_KEY, CHAT_INTROSPECTION_SCOPE_KEY, NO_AGORA_HOST_VALUES, ZITADEL_ORG_SCOPE_KEY, agoraEnvDrift, agoraEnvRevision, agoraHostMissing, agoraHostnameFor, callsServiceHasName, callsServiceHasTokenEnv, callsTokenEnvMovedMessage, callsTokenHostFrom, desiredAgoraEnv, withRecordedMessagingProject, } from './agora.js';
|
|
4
4
|
import { BOULE_BROKER_ENTRY_KEY, bouleEnvDrift, desiredBouleEnv, wantedBouleBrokerEntry, } from './boule.js';
|
|
5
5
|
import { brokerEntryAppliedScopeKey, brokerEntryWarningsScopeKey, entryAuthEnv, entryAuthEnvAppliedScopeKey, wantedBrokerEntry, } from './broker-entries.js';
|
|
6
6
|
import { planEntryAiKeys } from './entry-ai-key.js';
|
|
@@ -964,6 +964,27 @@ function planChat(org, byKey, claimed, recordedIds, reading, issuerPlanned) {
|
|
|
964
964
|
blocked: null,
|
|
965
965
|
};
|
|
966
966
|
}
|
|
967
|
+
// A live chat made before its service had an introspection client
|
|
968
|
+
// answers every Agora sign-in with `token_invalid`. The marker is the
|
|
969
|
+
// client's app id, recorded after the service's env carries it.
|
|
970
|
+
if (!recordedIds[CHAT_INTROSPECTION_SCOPE_KEY]) {
|
|
971
|
+
return {
|
|
972
|
+
step: {
|
|
973
|
+
action: {
|
|
974
|
+
system: 'coolify',
|
|
975
|
+
kind: 'chat',
|
|
976
|
+
key: org.id,
|
|
977
|
+
desired: { usesDomain: liveUsesDomain, refreshIntrospection: true },
|
|
978
|
+
},
|
|
979
|
+
step: {
|
|
980
|
+
description: `give ${org.slug}'s chat service its token introspection client and redeploy it`,
|
|
981
|
+
destroys: false,
|
|
982
|
+
migrates: false,
|
|
983
|
+
},
|
|
984
|
+
},
|
|
985
|
+
blocked: null,
|
|
986
|
+
};
|
|
987
|
+
}
|
|
967
988
|
return none;
|
|
968
989
|
}
|
|
969
990
|
const domain = org.domain ?? 'its domain';
|
package/dist/plan/executor.d.ts
CHANGED
|
@@ -2228,6 +2228,18 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
|
|
|
2228
2228
|
* so an existing one is always re-written.
|
|
2229
2229
|
*/
|
|
2230
2230
|
private ensureCallsFileStorage;
|
|
2231
|
+
/**
|
|
2232
|
+
* The chat service's introspection client: an API app in the org's
|
|
2233
|
+
* messaging project (the audience Agora's token carries), so ZITADEL's
|
|
2234
|
+
* introspection accepts it. The chat service authenticates as it with
|
|
2235
|
+
* `client_secret_basic`. Minted once, the way {@link callsTokenEnvs} does:
|
|
2236
|
+
* the id is recorded under an undelivered key and persisted before the
|
|
2237
|
+
* secret leaves, the secret goes to `deliverSecret` and to the caller's env
|
|
2238
|
+
* write, never to a log, error or return beyond that. A re-run reads the id
|
|
2239
|
+
* and secret back from the service's own env and mints nothing. `changed`
|
|
2240
|
+
* is true only for a fresh mint.
|
|
2241
|
+
*/
|
|
2242
|
+
private ensureChatIntrospection;
|
|
2231
2243
|
/**
|
|
2232
2244
|
* Creates the `chat.admin` role on the org's messaging project and grants
|
|
2233
2245
|
* it to each owner whose invite is recorded (`zitadel:owner:<orgId>:<email>`,
|
package/dist/plan/executor.js
CHANGED
|
@@ -2,7 +2,7 @@ import { randomBytes } from 'node:crypto';
|
|
|
2
2
|
import { URL } from 'node:url';
|
|
3
3
|
import { CALLS_SFU_PRIVATE_URL, CALLS_SFU_PUBLIC_URL } from '../calls-sfu.js';
|
|
4
4
|
import { APP_ENTRY_KEY, appEntry, filesCollectionEnvName, ownerMailboxRecorded, ownerMailboxes, sharedSecretRef, } from '../org.js';
|
|
5
|
-
import { AGORA_ACTIVATED_ENV_SCOPE_KEY, AGORA_APP_PORTS_EXPOSES, AGORA_CLIENT_ID_SCOPE_KEY, AGORA_DEPLOYED_SCOPE_KEY, APP_CLIENT_ID_SCOPE_KEY, CALLS_INTROSPECTION_SCOPE_KEY, CALLS_INTROSPECTION_UNDELIVERED_SCOPE_KEY, CALLS_SERVICE_NAME_SCOPE_KEY, CALLS_SERVICE_PORT, CALLS_SERVICE_TOKEN_ENV_SCOPE_KEY, CHAT_SERVICE_URL_SCOPE_KEY, CMS_CLIENT_ID_SCOPE_KEY, ENTRY_CLIENT_ID_SCOPE_PREFIX, ZITADEL_MESSAGING_PROJECT_SCOPE_KEY, ZITADEL_ORG_SCOPE_KEY, agoraEnvDrift, agoraEnvRevision, agoraHostnameFor, callsIntrospectionAppName, callsServiceHasName, callsServiceInternalName, callsTokenEnvMarker, callsTokenEnvMovedMessage, desiredAgoraEnv, entryClientIdScopeKey, } from './agora.js';
|
|
5
|
+
import { AGORA_ACTIVATED_ENV_SCOPE_KEY, AGORA_APP_PORTS_EXPOSES, AGORA_CLIENT_ID_SCOPE_KEY, AGORA_DEPLOYED_SCOPE_KEY, APP_CLIENT_ID_SCOPE_KEY, CALLS_INTROSPECTION_SCOPE_KEY, CALLS_INTROSPECTION_UNDELIVERED_SCOPE_KEY, CALLS_SERVICE_NAME_SCOPE_KEY, CALLS_SERVICE_PORT, CALLS_SERVICE_TOKEN_ENV_SCOPE_KEY, CHAT_INTROSPECTION_SCOPE_KEY, CHAT_INTROSPECTION_UNDELIVERED_SCOPE_KEY, CHAT_SERVICE_URL_SCOPE_KEY, CMS_CLIENT_ID_SCOPE_KEY, ENTRY_CLIENT_ID_SCOPE_PREFIX, ZITADEL_MESSAGING_PROJECT_SCOPE_KEY, ZITADEL_ORG_SCOPE_KEY, agoraEnvDrift, agoraEnvRevision, agoraHostnameFor, callsIntrospectionAppName, callsServiceHasName, callsServiceInternalName, callsTokenEnvMarker, callsTokenEnvMovedMessage, chatIntrospectionAppName, desiredAgoraEnv, entryClientIdScopeKey, } from './agora.js';
|
|
6
6
|
import { BOULE_BROKER_ENTRY_KEY, BOULE_CLIENT_ID_SCOPE_KEY, BOULE_DATABASE_SCOPE_KEY, BOULE_DEPLOYED_SCOPE_KEY, bouleEnvDrift, bouleHostnameFor, desiredBouleEnv, wantedBouleBrokerEntry, } from './boule.js';
|
|
7
7
|
import { brokerEntryAppliedScopeKey, brokerEntryWarningsScopeKey, entryAuthEnv, entryAuthEnvAppliedScopeKey, wantedBrokerEntry, writeBrokerEntry, } from './broker-entries.js';
|
|
8
8
|
import { MAS_IMAGE, MAS_TAG, SYNAPSE_IMAGE, SYNAPSE_TAG, callsMasConfigYaml, callsServerName, callsSynapseHomeserverYaml, mintCallsMatrixSecrets, parseCallsDbUrl, } from './calls-matrix.js';
|
|
@@ -3079,6 +3079,74 @@ export class FoundryStepExecutor {
|
|
|
3079
3079
|
}
|
|
3080
3080
|
}
|
|
3081
3081
|
// ---- chat -----------------------------------------------------------------
|
|
3082
|
+
/**
|
|
3083
|
+
* The chat service's introspection client: an API app in the org's
|
|
3084
|
+
* messaging project (the audience Agora's token carries), so ZITADEL's
|
|
3085
|
+
* introspection accepts it. The chat service authenticates as it with
|
|
3086
|
+
* `client_secret_basic`. Minted once, the way {@link callsTokenEnvs} does:
|
|
3087
|
+
* the id is recorded under an undelivered key and persisted before the
|
|
3088
|
+
* secret leaves, the secret goes to `deliverSecret` and to the caller's env
|
|
3089
|
+
* write, never to a log, error or return beyond that. A re-run reads the id
|
|
3090
|
+
* and secret back from the service's own env and mints nothing. `changed`
|
|
3091
|
+
* is true only for a fresh mint.
|
|
3092
|
+
*/
|
|
3093
|
+
async ensureChatIntrospection(org, zitadelOrgId, projectId, serviceAppId, context) {
|
|
3094
|
+
const recorded = context.recordedIds();
|
|
3095
|
+
const done = recorded[CHAT_INTROSPECTION_SCOPE_KEY];
|
|
3096
|
+
const undelivered = recorded[CHAT_INTROSPECTION_UNDELIVERED_SCOPE_KEY];
|
|
3097
|
+
if (done) {
|
|
3098
|
+
const envs = await this.ports.coolify.listEnvs(serviceAppId);
|
|
3099
|
+
const read = (key) => envs.find((e) => e.key === key && !e.is_preview)?.value;
|
|
3100
|
+
const clientId = read('CHAT_INTROSPECTION_CLIENT_ID');
|
|
3101
|
+
const clientSecret = read('CHAT_INTROSPECTION_CLIENT_SECRET');
|
|
3102
|
+
if (!clientId || !clientSecret) {
|
|
3103
|
+
throw new Error(`coolify:chat for ${org.slug}: the introspection client (ZITADEL app ${done}) is recorded but its id or secret is no longer on the service; remove the recorded keys ${CHAT_INTROSPECTION_SCOPE_KEY} and ${CHAT_INTROSPECTION_UNDELIVERED_SCOPE_KEY}, and delete the ZITADEL client, to mint a new one`);
|
|
3104
|
+
}
|
|
3105
|
+
return { clientId, clientSecret, appId: done, changed: false };
|
|
3106
|
+
}
|
|
3107
|
+
if (undelivered) {
|
|
3108
|
+
// A minted client with no marker: a crash or failure after the mint.
|
|
3109
|
+
// Both values on the service mean the write landed and only the marker
|
|
3110
|
+
// was lost; otherwise nothing usable was delivered, and the app is
|
|
3111
|
+
// deleted below and made again.
|
|
3112
|
+
const envs = await this.ports.coolify.listEnvs(serviceAppId);
|
|
3113
|
+
const read = (key) => envs.find((e) => e.key === key && !e.is_preview)?.value;
|
|
3114
|
+
const clientId = read('CHAT_INTROSPECTION_CLIENT_ID');
|
|
3115
|
+
const clientSecret = read('CHAT_INTROSPECTION_CLIENT_SECRET');
|
|
3116
|
+
if (clientId && clientSecret) {
|
|
3117
|
+
return { clientId, clientSecret, appId: undelivered, changed: false };
|
|
3118
|
+
}
|
|
3119
|
+
}
|
|
3120
|
+
// An app by this name with no recorded secret is a crash between create
|
|
3121
|
+
// and record: nothing was delivered, so it is deleted and made again.
|
|
3122
|
+
const stale = (await this.ports.zitadel.listApps(zitadelOrgId, projectId)).apps.filter((app) => app.name === chatIntrospectionAppName(org) && app.state !== 'APP_STATE_REMOVED');
|
|
3123
|
+
const staleIds = new Set(stale.map((app) => app.id));
|
|
3124
|
+
if (undelivered)
|
|
3125
|
+
staleIds.add(undelivered);
|
|
3126
|
+
for (const id of staleIds) {
|
|
3127
|
+
try {
|
|
3128
|
+
await this.ports.zitadel.deleteApp(zitadelOrgId, projectId, id);
|
|
3129
|
+
}
|
|
3130
|
+
catch (error) {
|
|
3131
|
+
if (!isNotFoundError(error))
|
|
3132
|
+
throw error;
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
const client = await this.ports.zitadel.createApiApp(zitadelOrgId, projectId, {
|
|
3136
|
+
name: chatIntrospectionAppName(org),
|
|
3137
|
+
});
|
|
3138
|
+
context.recordScopeId(CHAT_INTROSPECTION_UNDELIVERED_SCOPE_KEY, client.appId);
|
|
3139
|
+
await context.persistScopeIds();
|
|
3140
|
+
if (!(await this.deliverWithRetry(`zitadel:chat:${org.slug}:introspection`, client.clientSecret))) {
|
|
3141
|
+
throw new Error(`coolify:chat for ${org.slug}: the introspection client secret (ZITADEL app ${client.appId}) could not be delivered after ${JOBS_DELIVERY_ATTEMPTS} attempts`);
|
|
3142
|
+
}
|
|
3143
|
+
return {
|
|
3144
|
+
clientId: client.clientId,
|
|
3145
|
+
clientSecret: client.clientSecret,
|
|
3146
|
+
appId: client.appId,
|
|
3147
|
+
changed: true,
|
|
3148
|
+
};
|
|
3149
|
+
}
|
|
3082
3150
|
/**
|
|
3083
3151
|
* Creates the `chat.admin` role on the org's messaging project and grants
|
|
3084
3152
|
* it to each owner whose invite is recorded (`zitadel:owner:<orgId>:<email>`,
|
|
@@ -3147,6 +3215,29 @@ export class FoundryStepExecutor {
|
|
|
3147
3215
|
context.recordScopeId(markerKey, '1');
|
|
3148
3216
|
return { resourceId: serviceAppId };
|
|
3149
3217
|
}
|
|
3218
|
+
// `planChat` plans this for a live chat whose service has no
|
|
3219
|
+
// introspection client: mint it once, push only its two env vars and
|
|
3220
|
+
// redeploy. Never the re-key paths or a fresh provision.
|
|
3221
|
+
if (action.desired.refreshIntrospection === true) {
|
|
3222
|
+
const serviceAppId = recorded[CHAT_SERVICE_SCOPE_KEY];
|
|
3223
|
+
const zitadelOrgId = recorded[ZITADEL_ORG_SCOPE_KEY];
|
|
3224
|
+
const projectId = recorded[ZITADEL_MESSAGING_PROJECT_SCOPE_KEY];
|
|
3225
|
+
if (!serviceAppId || !zitadelOrgId || !projectId) {
|
|
3226
|
+
throw new Error(`coolify:chat introspection for ${org.slug}: the chat service app, the ZITADEL org or the messaging project is not recorded yet`);
|
|
3227
|
+
}
|
|
3228
|
+
const hadMarker = Boolean(recorded[CHAT_INTROSPECTION_SCOPE_KEY]);
|
|
3229
|
+
const client = await this.ensureChatIntrospection(org, zitadelOrgId, projectId, serviceAppId, context);
|
|
3230
|
+
await this.ports.coolify.bulkUpsertEnvs(serviceAppId, [
|
|
3231
|
+
{ key: 'CHAT_INTROSPECTION_CLIENT_ID', value: client.clientId },
|
|
3232
|
+
{ key: 'CHAT_INTROSPECTION_CLIENT_SECRET', value: client.clientSecret },
|
|
3233
|
+
]);
|
|
3234
|
+
// Deploy first, marker last: a failed deploy leaves the marker unset,
|
|
3235
|
+
// so the next plan still carries this step and the container gets the env.
|
|
3236
|
+
if (client.changed || !hadMarker)
|
|
3237
|
+
await this.ports.coolify.deploy(serviceAppId);
|
|
3238
|
+
context.recordScopeId(CHAT_INTROSPECTION_SCOPE_KEY, client.appId);
|
|
3239
|
+
return { resourceId: serviceAppId };
|
|
3240
|
+
}
|
|
3150
3241
|
if (action.desired.usesDomain === false) {
|
|
3151
3242
|
throw new NotBuiltError(`coolify:chat reverse re-key for ${org.slug}`, 'moving its chat back onto the original server_name is not built (#7 amendment 3)');
|
|
3152
3243
|
}
|
|
@@ -3280,6 +3371,7 @@ export class FoundryStepExecutor {
|
|
|
3280
3371
|
rsaKeyPem: secrets.value.masRsaKeyPem,
|
|
3281
3372
|
ecKeyPem: secrets.value.masEcKeyPem,
|
|
3282
3373
|
}));
|
|
3374
|
+
const introspection = await this.ensureChatIntrospection(org, zitadelOrgId, project.id, service.appId, context);
|
|
3283
3375
|
await this.ports.coolify.bulkUpsertEnvs(service.appId, chatServiceEnvs({
|
|
3284
3376
|
orgSlug: org.slug,
|
|
3285
3377
|
companyName: org.displayName,
|
|
@@ -3289,6 +3381,8 @@ export class FoundryStepExecutor {
|
|
|
3289
3381
|
agoraHost,
|
|
3290
3382
|
masClientId: secrets.value.masClientId,
|
|
3291
3383
|
masClientSecret: secrets.value.masClientSecret,
|
|
3384
|
+
introspectionClientId: introspection.clientId,
|
|
3385
|
+
introspectionClientSecret: introspection.clientSecret,
|
|
3292
3386
|
}));
|
|
3293
3387
|
// Split the same way `coolifyApp`'s and `cmsSite`'s own databases are
|
|
3294
3388
|
// (#239, #456): `DATABASE_URL_OWNER` is the superuser string,
|
|
@@ -3306,11 +3400,14 @@ export class FoundryStepExecutor {
|
|
|
3306
3400
|
for (const app of [mas, synapse, service]) {
|
|
3307
3401
|
if (!app.existed ||
|
|
3308
3402
|
secrets.changed ||
|
|
3309
|
-
(app === service && serviceDbPush.changed) ||
|
|
3403
|
+
(app === service && (serviceDbPush.changed || introspection.changed)) ||
|
|
3310
3404
|
!isRunningOrStarting((await this.ports.coolify.getApplication(app.appId)).status)) {
|
|
3311
3405
|
await this.ports.coolify.deploy(app.appId);
|
|
3312
3406
|
}
|
|
3313
3407
|
}
|
|
3408
|
+
// After the write and the deploys, so a failure before them leaves the
|
|
3409
|
+
// marker unset and the next apply repeats the step.
|
|
3410
|
+
context.recordScopeId(CHAT_INTROSPECTION_SCOPE_KEY, introspection.appId);
|
|
3314
3411
|
// #456: recorded here too, not only on the `refreshDatabaseSplit` replan
|
|
3315
3412
|
// path above -- without it a fresh provision's very first apply never
|
|
3316
3413
|
// sets this marker, so `planChat` (`!recordedIds[serviceDbEnvAppliedScopeKey('chat')]`)
|