@wtfalch/foundry 0.72.3 → 0.74.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/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/migrations/0071_release_request_schedule.sql +12 -0
- package/dist/plan/plan-apply.js +3 -0
- package/dist/plan/platform-diff.d.ts +3 -2
- package/dist/plan/platform-diff.js +3 -2
- package/dist/plan/platform-providers.js +9 -8
- package/dist/plan/release-host.d.ts +5 -1
- package/dist/plan/release-host.js +99 -50
- package/dist/plan/signin-mail.d.ts +3 -11
- package/dist/plan/signin-mail.js +3 -4
- package/dist/release/decide.d.ts +9 -0
- package/dist/release/decide.js +1 -1
- package/dist/release/golive.d.ts +77 -0
- package/dist/release/golive.js +151 -0
- package/dist/release/run.d.ts +7 -1
- package/dist/release/run.js +205 -14
- package/dist/release/state.d.ts +82 -4
- package/dist/release/state.js +114 -2
- package/dist/release/types.d.ts +48 -2
- package/dist/store/postgres.js +97 -6
- package/dist/store/schema.d.ts +34 -0
- package/dist/store/schema.js +11 -2
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -22,9 +22,9 @@ export { StaleLeaseError } from './store.js';
|
|
|
22
22
|
export type { PlanApply, DestroyProgress } from './apply.js';
|
|
23
23
|
export { createPostgresAppliedVersionsStore, createPostgresDeclaredOrgStore, createPostgresPlatformStore, createPostgresRequestedChangeStore, DeclaredOrgError, PlatformError, } from './store/postgres.js';
|
|
24
24
|
export { createPostgresAuditLog } from './store/audit.js';
|
|
25
|
-
export type { Color, ReleaseAction, ReleaseEvent, ReleaseEventKind, ReleaseSeries, ReleaseSeriesColor, ReleaseSignal, ReleaseState, WindowState, } from './release/types.js';
|
|
25
|
+
export type { Color, ReleaseAction, ReleaseEvent, ReleaseEventKind, ReleaseSeries, ReleaseSeriesColor, ReleaseRequirement, ReleaseSignal, ReleaseState, ReleasedVersion, WindowState, } from './release/types.js';
|
|
26
26
|
export type { ReleaseStore } from './release/state.js';
|
|
27
|
-
export { readReleaseHistory, readReleaseState, requestReleaseAction } from './release/state.js';
|
|
27
|
+
export { readReleaseHistory, readReleasedVersions, readReleaseState, requestReleaseAction, rescheduleRelease, } from './release/state.js';
|
|
28
28
|
export type { BuiltinFanOut, FanOutApp, FanOutOrgState } from './release/fanout.js';
|
|
29
29
|
export { readBuiltinFanOut } from './release/fanout.js';
|
|
30
30
|
export type { ReleaseSeriesHost, ReleaseSeriesQuery } from './release/series.js';
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ export { NATIVE_BUILD_REPO, NATIVE_BUILD_WORKFLOW_FILE, NATIVE_BUILD_WORKFLOW_RE
|
|
|
14
14
|
export { StaleLeaseError } from './store.js';
|
|
15
15
|
export { createPostgresAppliedVersionsStore, createPostgresDeclaredOrgStore, createPostgresPlatformStore, createPostgresRequestedChangeStore, DeclaredOrgError, PlatformError, } from './store/postgres.js';
|
|
16
16
|
export { createPostgresAuditLog } from './store/audit.js';
|
|
17
|
-
export { readReleaseHistory, readReleaseState, requestReleaseAction } from './release/state.js';
|
|
17
|
+
export { readReleaseHistory, readReleasedVersions, readReleaseState, requestReleaseAction, rescheduleRelease, } from './release/state.js';
|
|
18
18
|
export { readBuiltinFanOut } from './release/fanout.js';
|
|
19
19
|
export { readReleaseSeries } from './release/series.js';
|
|
20
20
|
export { readEntryReleaseSeries } from './plan/release-series-reader.js';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
-- @wtfalch/foundry: a release request can wait for a time. `not_before` is null for every request made
|
|
2
|
+
-- until now, so each of them stays due at once. The release runner reads only the open rows whose time has come.
|
|
3
|
+
-- `claimed_at` is set when the runner starts a due go-live: from then on it can no longer be cancelled or moved.
|
|
4
|
+
-- Additive: two nullable columns and one partial index; no existing row is touched.
|
|
5
|
+
-- Mirrors src/store/schema.ts's `foundryReleaseRequests`.
|
|
6
|
+
|
|
7
|
+
alter table foundry_release_requests add column if not exists not_before timestamptz;
|
|
8
|
+
alter table foundry_release_requests add column if not exists claimed_at timestamptz;
|
|
9
|
+
|
|
10
|
+
create index if not exists foundry_release_requests_scheduled_idx
|
|
11
|
+
on foundry_release_requests (org_id, entry_key, not_before)
|
|
12
|
+
where consumed_at is null and not_before is not null;
|
package/dist/plan/plan-apply.js
CHANGED
|
@@ -1207,6 +1207,8 @@ export class FoundryPlanApply {
|
|
|
1207
1207
|
host,
|
|
1208
1208
|
context,
|
|
1209
1209
|
now: this.now(),
|
|
1210
|
+
nowFn: () => this.now(),
|
|
1211
|
+
...(ownerOrgId !== null ? { ownerOrgId } : {}),
|
|
1210
1212
|
primaryUuid,
|
|
1211
1213
|
...(copies === 'defer' ? { deferCopies: true } : {}),
|
|
1212
1214
|
});
|
|
@@ -1248,6 +1250,7 @@ export class FoundryPlanApply {
|
|
|
1248
1250
|
host,
|
|
1249
1251
|
context,
|
|
1250
1252
|
now: this.now(),
|
|
1253
|
+
nowFn: () => this.now(),
|
|
1251
1254
|
primaryUuid,
|
|
1252
1255
|
...(copies === 'defer' ? { deferCopies: true } : {}),
|
|
1253
1256
|
});
|
|
@@ -70,13 +70,14 @@ export declare const AUTH_TUNNEL_ROUTES_VERSION = "2";
|
|
|
70
70
|
export declare const PLATFORM_AUTH_TUNNEL_ROUTES_SCOPE_KEY = "auth_tunnel_routes_applied";
|
|
71
71
|
/**
|
|
72
72
|
* The version of the env `provisionAuthStack` writes to the login apps (`LOGIN_MAIL_FROM`,
|
|
73
|
-
* `LETTERMINT_API_TOKEN`, `LOGIN_PASSKEY_RP_ID`). Raise it when a key is added or its meaning changes, so an applied
|
|
73
|
+
* `LETTERMINT_API_TOKEN`, `LOGIN_PASSKEY_RP_ID`, `ZITADEL_DOMAIN`, `LOGIN_COMPANY_NAME`). Raise it when a key is added or its meaning changes, so an applied
|
|
74
74
|
* sign-in plans its step once more and the apps get the env. Never holds a value.
|
|
75
75
|
* 1: the login apps send their own invite and verification mail (auth 0.35.0, foundry 0.68.0).
|
|
76
76
|
* 2: a company's login apps get `LOGIN_PASSKEY_RP_ID` (foundry 0.70.2).
|
|
77
77
|
* 3: the login apps get `ZITADEL_DOMAIN` (foundry 0.72.2; written from 0.72.3).
|
|
78
|
+
* 4: the login apps get `LOGIN_COMPANY_NAME`, the sign-in page's wordmark (foundry 0.73.0).
|
|
78
79
|
*/
|
|
79
|
-
export declare const AUTH_LOGIN_ENV_VERSION = "
|
|
80
|
+
export declare const AUTH_LOGIN_ENV_VERSION = "4";
|
|
80
81
|
/**
|
|
81
82
|
* What the login apps' env is written for: the version, the sender address (`LOGIN_MAIL_FROM`) and the
|
|
82
83
|
* passkey RP ID (`LOGIN_PASSKEY_RP_ID`, none for the platform and a company without its own domain).
|
|
@@ -77,13 +77,14 @@ export const AUTH_TUNNEL_ROUTES_VERSION = '2';
|
|
|
77
77
|
export const PLATFORM_AUTH_TUNNEL_ROUTES_SCOPE_KEY = 'auth_tunnel_routes_applied';
|
|
78
78
|
/**
|
|
79
79
|
* The version of the env `provisionAuthStack` writes to the login apps (`LOGIN_MAIL_FROM`,
|
|
80
|
-
* `LETTERMINT_API_TOKEN`, `LOGIN_PASSKEY_RP_ID`). Raise it when a key is added or its meaning changes, so an applied
|
|
80
|
+
* `LETTERMINT_API_TOKEN`, `LOGIN_PASSKEY_RP_ID`, `ZITADEL_DOMAIN`, `LOGIN_COMPANY_NAME`). Raise it when a key is added or its meaning changes, so an applied
|
|
81
81
|
* sign-in plans its step once more and the apps get the env. Never holds a value.
|
|
82
82
|
* 1: the login apps send their own invite and verification mail (auth 0.35.0, foundry 0.68.0).
|
|
83
83
|
* 2: a company's login apps get `LOGIN_PASSKEY_RP_ID` (foundry 0.70.2).
|
|
84
84
|
* 3: the login apps get `ZITADEL_DOMAIN` (foundry 0.72.2; written from 0.72.3).
|
|
85
|
+
* 4: the login apps get `LOGIN_COMPANY_NAME`, the sign-in page's wordmark (foundry 0.73.0).
|
|
85
86
|
*/
|
|
86
|
-
export const AUTH_LOGIN_ENV_VERSION = '
|
|
87
|
+
export const AUTH_LOGIN_ENV_VERSION = '4';
|
|
87
88
|
/**
|
|
88
89
|
* What the login apps' env is written for: the version, the sender address (`LOGIN_MAIL_FROM`) and the
|
|
89
90
|
* passkey RP ID (`LOGIN_PASSKEY_RP_ID`, none for the platform and a company without its own domain).
|
|
@@ -1575,11 +1575,12 @@ const PLATFORM_AUTH_RESERVED_USERNAME = 'admin';
|
|
|
1575
1575
|
/** #216 (building on #209/#215): the narrower `IAM_LOGIN_CLIENT` machine user for the login app and the broker. */
|
|
1576
1576
|
const PLATFORM_AUTH_LOGIN_CLIENT_USER_NAME = 'foundry-login-client';
|
|
1577
1577
|
export const PLATFORM_AUTH_ORG_NAME = 'foundry';
|
|
1578
|
+
const LOGIN_COMPANY_NAME_ENV = 'LOGIN_COMPANY_NAME';
|
|
1578
1579
|
/**
|
|
1579
1580
|
* The instance's first organisation (the default one, whose name the login shows as its wordmark).
|
|
1580
1581
|
* A company's own instance passes the recorded id (the platform passes none and keeps the name `foundry`);
|
|
1581
|
-
* found by that id when there is one, because a company's own instance
|
|
1582
|
-
*
|
|
1582
|
+
* found by that id when there is one, because a company's own instance may have it renamed (foundry
|
|
1583
|
+
* before 0.73.0 did), and a lookup by name would then make a second `foundry` org. The
|
|
1583
1584
|
* name is the way in only before the id is recorded. A recorded id ZITADEL no longer has is a failure,
|
|
1584
1585
|
* never a reason to make another org.
|
|
1585
1586
|
*/
|
|
@@ -2228,6 +2229,11 @@ export async function provisionAuthStack(scope, deps, target, input) {
|
|
|
2228
2229
|
},
|
|
2229
2230
|
// #1086: the login builds redirects for an unbound host from this, never from a header.
|
|
2230
2231
|
{ key: 'ZITADEL_DOMAIN', value: hostname },
|
|
2232
|
+
// The wordmark on the sign-in page: a company's own name, `Archon` for the platform's login.
|
|
2233
|
+
{
|
|
2234
|
+
key: LOGIN_COMPANY_NAME_ENV,
|
|
2235
|
+
value: input.mailBranding ? signinMailName(input.mailBranding.companyName) : 'Archon',
|
|
2236
|
+
},
|
|
2231
2237
|
{ key: 'OTEL_NODE_RESOURCE_DETECTORS', value: 'none' },
|
|
2232
2238
|
{ key: 'API_CACHE_CONFIG', value: '{"defaultMinutes":2,"longMinutes":5}' },
|
|
2233
2239
|
// #405: the login app's own requestId-free fallback (`resolveRedirectUri`
|
|
@@ -2856,17 +2862,12 @@ export async function provisionAuthStack(scope, deps, target, input) {
|
|
|
2856
2862
|
(deps.signinMailWritable !== true ||
|
|
2857
2863
|
typeof mailZitadel.ensureEmailOtpMessageText !== 'function' ||
|
|
2858
2864
|
typeof mailZitadel.ensureLabelPolicy !== 'function' ||
|
|
2859
|
-
typeof mailZitadel.ensureOrgName !== 'function' ||
|
|
2860
2865
|
typeof mailZitadel.ensureEmailOtpGenerator !== 'function')) {
|
|
2861
2866
|
console.info(`${label}: the host cannot write message texts, so the sign-in mail keeps ZITADEL's default text`);
|
|
2862
2867
|
}
|
|
2863
2868
|
else if (input.mailBranding) {
|
|
2864
|
-
const firstOrgId = scope.recordedIds()[PLATFORM_ZITADEL_ORG_SCOPE_KEY];
|
|
2865
|
-
if (!firstOrgId) {
|
|
2866
|
-
throw new Error(`${label}: the instance's first organisation is not recorded`);
|
|
2867
|
-
}
|
|
2868
2869
|
try {
|
|
2869
|
-
await applySigninMail(mailZitadel, input.mailBranding.companyName
|
|
2870
|
+
await applySigninMail(mailZitadel, input.mailBranding.companyName);
|
|
2870
2871
|
}
|
|
2871
2872
|
catch (error) {
|
|
2872
2873
|
const cause = redactSecrets(error instanceof Error ? error.message : String(error), [
|
|
@@ -84,7 +84,11 @@ export declare function createReleaseHost(input: ReleaseHostInput): ReleaseHost;
|
|
|
84
84
|
* (like the next apply, through its `bluegreen-lb` step) writes it again.
|
|
85
85
|
*/
|
|
86
86
|
export declare function runSubjectRelease(input: {
|
|
87
|
-
store: Pick<ReleaseStore, 'openRequests' | 'consumeRequests'> & Partial<Pick<ReleaseStore, 'appendEvents'>>;
|
|
87
|
+
store: Pick<ReleaseStore, 'openRequests' | 'consumeRequests' | 'claimRequests'> & Partial<Pick<ReleaseStore, 'appendEvents'>>;
|
|
88
|
+
/** `DeclaredPlatform.ownerOrgId`: without it no go_live or set_required request runs. */
|
|
89
|
+
ownerOrgId?: string;
|
|
90
|
+
/** The clock for the repeated passes of a go-live; absent: `now` for every pass. */
|
|
91
|
+
nowFn?: () => Date;
|
|
88
92
|
scopeOwnerId: string;
|
|
89
93
|
subject: BlueGreenSubject;
|
|
90
94
|
host: ReleaseHost;
|
|
@@ -9,6 +9,9 @@ import { waitForDeploymentFinished, waitForNewDeployment, } from './deploy-healt
|
|
|
9
9
|
import { isNotFoundError } from './executor.js';
|
|
10
10
|
import { runStageCopyRequest } from './stage-copy-host.js';
|
|
11
11
|
const COMMIT = /^[0-9a-f]{40}$/;
|
|
12
|
+
const APP_RELEASE_ENV = ['APP_TAG', 'APP_RELEASE', 'APP_MINIMUM_RELEASE'];
|
|
13
|
+
/** One tick runs a pending go-live at most this many times (stage, idle, staff, open). */
|
|
14
|
+
const GO_LIVE_RUNS = 4;
|
|
12
15
|
/** A stage deploy waits at most this many polls of this long (10 minutes) for Coolify's deployment to finish. */
|
|
13
16
|
export const STAGE_DEPLOY_ATTEMPTS = 60;
|
|
14
17
|
export const STAGE_DEPLOY_INTERVAL_MS = 10_000;
|
|
@@ -109,7 +112,7 @@ export function createReleaseHost(input) {
|
|
|
109
112
|
const pinInput = () => pinInputOf(input, recorded);
|
|
110
113
|
const addressOf = (color) => colorAddress(input, recorded, color);
|
|
111
114
|
const healthColor = () => healthColorOf(recorded(), id);
|
|
112
|
-
const deployTo = async (color, digest) => {
|
|
115
|
+
const deployTo = async (color, digest, appEnv) => {
|
|
113
116
|
if (!COMMIT.test(digest))
|
|
114
117
|
throw new Error(`${input.label}: the release is not a built commit`);
|
|
115
118
|
const token = subject.reporting ? await input.healthToken() : null;
|
|
@@ -135,22 +138,40 @@ export function createReleaseHost(input) {
|
|
|
135
138
|
throw new Error(`${input.label}: reading the env of the blue app failed`);
|
|
136
139
|
}
|
|
137
140
|
}
|
|
141
|
+
// A go-live's own values replace blue's: the color boots with the tag's tag, release and minimum.
|
|
138
142
|
const refreshed = copiedEnv({ label: input.label, secret: input.secret }, subject, source, {
|
|
139
143
|
uuid: server,
|
|
140
144
|
primaryUuid: input.primaryUuid,
|
|
141
|
-
});
|
|
145
|
+
}).filter((e) => !appEnv || !APP_RELEASE_ENV.includes(e.key));
|
|
146
|
+
const release = appEnv
|
|
147
|
+
? [
|
|
148
|
+
{ key: 'APP_TAG', value: appEnv.tag },
|
|
149
|
+
{ key: 'APP_RELEASE', value: String(appEnv.release) },
|
|
150
|
+
...(appEnv.minimum === null
|
|
151
|
+
? []
|
|
152
|
+
: [{ key: 'APP_MINIMUM_RELEASE', value: String(appEnv.minimum) }]),
|
|
153
|
+
]
|
|
154
|
+
: [];
|
|
142
155
|
const reporting = token ? reportingEnv({ color, release: digest, healthToken: token }) : [];
|
|
143
156
|
try {
|
|
144
|
-
const envs = [...refreshed, ...reporting];
|
|
157
|
+
const envs = [...refreshed, ...reporting, ...release];
|
|
145
158
|
if (envs.length > 0) {
|
|
146
159
|
await coolify.bulkUpsertEnvs(appId, envs.map((e) => ({ ...e, literal: true })));
|
|
147
160
|
}
|
|
148
161
|
// A key removed from blue (a rotated-out credential) must not stay on the slot.
|
|
149
|
-
if (appId !== input.primaryBlueAppId) {
|
|
150
|
-
const keep = new Set([
|
|
162
|
+
if (appId !== input.primaryBlueAppId || (appEnv && appEnv.minimum === null)) {
|
|
163
|
+
const keep = new Set([
|
|
164
|
+
...(appId === input.primaryBlueAppId
|
|
165
|
+
? []
|
|
166
|
+
: source
|
|
167
|
+
.map((e) => e.key)
|
|
168
|
+
.filter((key) => !appEnv || !APP_RELEASE_ENV.includes(key))),
|
|
169
|
+
...envs.map((e) => e.key),
|
|
170
|
+
]);
|
|
151
171
|
for (const env of await coolify.listEnvs(appId)) {
|
|
152
172
|
if (env.uuid &&
|
|
153
173
|
!keep.has(env.key) &&
|
|
174
|
+
(appId !== input.primaryBlueAppId || env.key === 'APP_MINIMUM_RELEASE') &&
|
|
154
175
|
!env.key.startsWith('REPORTING_') &&
|
|
155
176
|
env.key !== ACTIONS_KEY_ENV) {
|
|
156
177
|
await coolify.deleteEnv(appId, env.uuid);
|
|
@@ -272,7 +293,7 @@ export function createReleaseHost(input) {
|
|
|
272
293
|
stageCopy: (request) => runStageCopyRequest({ subject, context, ports: input.stageCopy, renew: input.renew }, request),
|
|
273
294
|
async effect(effect) {
|
|
274
295
|
if (effect.kind === 'deploy')
|
|
275
|
-
await deployTo(effect.color, effect.digest);
|
|
296
|
+
await deployTo(effect.color, effect.digest, effect.appEnv);
|
|
276
297
|
else if (effect.kind === 'stop')
|
|
277
298
|
await stop(effect.color);
|
|
278
299
|
else if (effect.kind === 'deploy_stage')
|
|
@@ -355,53 +376,81 @@ export async function runSubjectRelease(input) {
|
|
|
355
376
|
context.recordScopeId(bgKey.weights(id), DEFAULT_WEIGHTS);
|
|
356
377
|
await context.persist();
|
|
357
378
|
}
|
|
358
|
-
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
},
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
379
|
+
/** Where a go-live stands: it moves when stage, a color's digest or the window changes. */
|
|
380
|
+
const stand = () => [
|
|
381
|
+
ids()[bgKey.stage(id)] ?? '',
|
|
382
|
+
ids()[bgKey.digest(id, 'blue')] ?? '',
|
|
383
|
+
ids()[bgKey.digest(id, 'green')] ?? '',
|
|
384
|
+
// A window never written reads as idle, so the first run's own write is not a change.
|
|
385
|
+
ids()[bgKey.window(id)] ?? '{"kind":"idle"}',
|
|
386
|
+
].join('|');
|
|
387
|
+
const pass = async (now) => {
|
|
388
|
+
await input.host.refreshStage();
|
|
389
|
+
const before = ids()[bgKey.weights(id)];
|
|
390
|
+
const result = await runRelease({
|
|
391
|
+
store: {
|
|
392
|
+
getScopeIds: async () => ({ ...ids() }),
|
|
393
|
+
openRequests: (query) => input.store.openRequests(query),
|
|
394
|
+
claimRequests: (claim) => input.store.claimRequests(claim),
|
|
395
|
+
},
|
|
396
|
+
orgId: input.scopeOwnerId,
|
|
397
|
+
...(input.ownerOrgId !== undefined ? { ownerOrgId: input.ownerOrgId } : {}),
|
|
398
|
+
entryKey: id,
|
|
399
|
+
entry: subject.config,
|
|
400
|
+
now,
|
|
401
|
+
deps: input.host.deps,
|
|
402
|
+
...(subject.reporting ? {} : { hardOnly: true }),
|
|
403
|
+
...(input.deferCopies ? { deferCopies: true } : {}),
|
|
404
|
+
});
|
|
405
|
+
for (const [key, value] of Object.entries(result.writes)) {
|
|
406
|
+
if (value === null)
|
|
407
|
+
context.forgetId(key);
|
|
408
|
+
else
|
|
409
|
+
context.recordScopeId(key, value);
|
|
387
410
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
411
|
+
await context.persist();
|
|
412
|
+
if (input.store.appendEvents && result.events.length > 0) {
|
|
413
|
+
try {
|
|
414
|
+
await input.store.appendEvents({
|
|
415
|
+
orgId: input.scopeOwnerId,
|
|
416
|
+
entryKey: id,
|
|
417
|
+
events: result.events,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
// The history is a record, not a gate: losing a line must not leave the run's requests open to replay.
|
|
422
|
+
console.error(`release ${id}: recording ${result.events.length} history lines failed`);
|
|
423
|
+
}
|
|
391
424
|
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
blocked: result.blocked,
|
|
401
|
-
refused: result.refused,
|
|
402
|
-
deferredCopy: result.deferredCopy,
|
|
403
|
-
copy: result.copy,
|
|
425
|
+
await input.store.consumeRequests(result.requestIds);
|
|
426
|
+
const after = parseWeights(ids()[bgKey.weights(id)]);
|
|
427
|
+
// Also when a balancer lags the weights (an earlier write failed), so the next tick retries it.
|
|
428
|
+
if (after &&
|
|
429
|
+
(formatWeights(after) !== before || balancerLags(ids(), id, formatWeights(after)))) {
|
|
430
|
+
await input.host.writeWeights();
|
|
431
|
+
}
|
|
432
|
+
return result;
|
|
404
433
|
};
|
|
434
|
+
let was = stand();
|
|
435
|
+
let result = await pass(input.now);
|
|
436
|
+
const seen = { blocked: new Set(result.blocked), refused: new Set(result.refused) };
|
|
437
|
+
let copy = result.copy;
|
|
438
|
+
let deferredCopy = result.deferredCopy;
|
|
439
|
+
// A go-live is stage, then idle, then staff, then open: each step is one run, and a step that worked makes the
|
|
440
|
+
// next one possible, so the tag goes live in one tick when nothing fails. A run that changed nothing stops it.
|
|
441
|
+
for (let more = 1; more < GO_LIVE_RUNS && result.goLiveOpen; more += 1) {
|
|
442
|
+
if (stand() === was)
|
|
443
|
+
break;
|
|
444
|
+
was = stand();
|
|
445
|
+
result = await pass(input.nowFn?.() ?? input.now);
|
|
446
|
+
for (const line of result.blocked)
|
|
447
|
+
seen.blocked.add(line);
|
|
448
|
+
for (const line of result.refused)
|
|
449
|
+
seen.refused.add(line);
|
|
450
|
+
copy ??= result.copy;
|
|
451
|
+
deferredCopy ||= result.deferredCopy;
|
|
452
|
+
}
|
|
453
|
+
return { blocked: [...seen.blocked], refused: [...seen.refused], deferredCopy, copy };
|
|
405
454
|
}
|
|
406
455
|
/**
|
|
407
456
|
* Runs the stage copy a release run decided on and records what came of it. Call it after
|
|
@@ -18,16 +18,8 @@ export declare const SIGNIN_MAIL_CODE: {
|
|
|
18
18
|
readonly length: 6;
|
|
19
19
|
readonly expirySeconds: 300;
|
|
20
20
|
};
|
|
21
|
-
/** The
|
|
21
|
+
/** The three ZITADEL calls the sign-in mail needs. */
|
|
22
22
|
export interface PlatformZitadelMailBranding {
|
|
23
|
-
/**
|
|
24
|
-
* Renames an organisation, only when its name differs (`PUT /management/v1/orgs/me` as that
|
|
25
|
-
* organisation). The login shows the instance's default organisation's name as its wordmark when the
|
|
26
|
-
* host is bound to no company, so a company's own instance names it after the company.
|
|
27
|
-
*/
|
|
28
|
-
ensureOrgName(orgId: string, name: string): Promise<{
|
|
29
|
-
changed: boolean;
|
|
30
|
-
}>;
|
|
31
23
|
ensureEmailOtpMessageText(language: string, text: {
|
|
32
24
|
title: string;
|
|
33
25
|
preHeader: string;
|
|
@@ -104,7 +96,7 @@ export declare const SIGNIN_MAIL_LABEL_POLICY: {
|
|
|
104
96
|
readonly warnColorDark: "#FF9C8C";
|
|
105
97
|
readonly disableWatermark: true;
|
|
106
98
|
};
|
|
107
|
-
/**
|
|
108
|
-
export declare function applySigninMail(zitadel: PlatformZitadelMailBranding, companyName: string
|
|
99
|
+
/** Writes the text, the look and the code, each only when it differs. Throws the client's fixed-text error. The wordmark is the login's `LOGIN_COMPANY_NAME`, not an organisation name. */
|
|
100
|
+
export declare function applySigninMail(zitadel: PlatformZitadelMailBranding, companyName: string): Promise<{
|
|
109
101
|
changed: boolean;
|
|
110
102
|
}>;
|
package/dist/plan/signin-mail.js
CHANGED
|
@@ -71,13 +71,12 @@ export const SIGNIN_MAIL_LABEL_POLICY = {
|
|
|
71
71
|
warnColorDark: '#FF9C8C',
|
|
72
72
|
disableWatermark: true,
|
|
73
73
|
};
|
|
74
|
-
/**
|
|
75
|
-
export async function applySigninMail(zitadel, companyName
|
|
76
|
-
const name = await zitadel.ensureOrgName(defaultOrgId, signinMailName(companyName));
|
|
74
|
+
/** Writes the text, the look and the code, each only when it differs. Throws the client's fixed-text error. The wordmark is the login's `LOGIN_COMPANY_NAME`, not an organisation name. */
|
|
75
|
+
export async function applySigninMail(zitadel, companyName) {
|
|
77
76
|
const text = await zitadel.ensureEmailOtpMessageText(SIGNIN_MAIL_LANGUAGE, signinMailText(companyName));
|
|
78
77
|
const look = await zitadel.ensureLabelPolicy({ ...SIGNIN_MAIL_LABEL_POLICY });
|
|
79
78
|
const code = await zitadel.ensureEmailOtpGenerator({ ...SIGNIN_MAIL_CODE });
|
|
80
79
|
return {
|
|
81
|
-
changed:
|
|
80
|
+
changed: text.changed || look.changed || code.changed,
|
|
82
81
|
};
|
|
83
82
|
}
|
package/dist/release/decide.d.ts
CHANGED
|
@@ -79,6 +79,15 @@ export type ReleaseEffect = {
|
|
|
79
79
|
kind: 'deploy';
|
|
80
80
|
color: Color;
|
|
81
81
|
digest: string;
|
|
82
|
+
/**
|
|
83
|
+
* A go-live's `APP_TAG`, `APP_RELEASE` and `APP_MINIMUM_RELEASE` (removed when `minimum` is null). The host
|
|
84
|
+
* writes them to this color with the deploy, so the color boots with them; the serving color is never touched.
|
|
85
|
+
*/
|
|
86
|
+
appEnv?: {
|
|
87
|
+
tag: string;
|
|
88
|
+
release: number;
|
|
89
|
+
minimum: number | null;
|
|
90
|
+
};
|
|
82
91
|
} | {
|
|
83
92
|
kind: 'stop';
|
|
84
93
|
color: Color;
|
package/dist/release/decide.js
CHANGED
|
@@ -161,7 +161,7 @@ function applyActions(r, actions, now, out, kind) {
|
|
|
161
161
|
out.refused.push('promote refused: stage has no digest');
|
|
162
162
|
else if (cur.held.includes(digest))
|
|
163
163
|
out.refused.push('promote refused: digest is held');
|
|
164
|
-
else if (digest === cur.digests[cur.live])
|
|
164
|
+
else if (digest === cur.digests[cur.live] && !a.redeploy)
|
|
165
165
|
out.refused.push('promote refused: digest is already live');
|
|
166
166
|
else {
|
|
167
167
|
const to = otherColor(cur.live);
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { Recorded } from './decide.js';
|
|
2
|
+
import type { ReleaseAction, ReleaseEvent, ReleasedVersion } from './types.js';
|
|
3
|
+
export type GoLiveAction = Extract<ReleaseAction, {
|
|
4
|
+
kind: 'go_live';
|
|
5
|
+
}>;
|
|
6
|
+
export type SetRequiredAction = Extract<ReleaseAction, {
|
|
7
|
+
kind: 'set_required';
|
|
8
|
+
}>;
|
|
9
|
+
export type App = 'boule' | 'agora';
|
|
10
|
+
/** A go-live tries each deploy step this many ticks before it is refused. */
|
|
11
|
+
export declare const GO_LIVE_TRIES = 3;
|
|
12
|
+
/** `bg.<entry>.golive`: `<request id>|<stage deploys tried>|<release reserved by the idle deploy>|<idle deploys failed>`. */
|
|
13
|
+
export interface GoLiveState {
|
|
14
|
+
requestId: number;
|
|
15
|
+
tries: number;
|
|
16
|
+
release: number | null;
|
|
17
|
+
idle: number;
|
|
18
|
+
}
|
|
19
|
+
export declare function parseGoLive(value: string | undefined): GoLiveState | null;
|
|
20
|
+
export declare function formatGoLive(s: GoLiveState): string;
|
|
21
|
+
/** What one due `go_live` request comes to in this run. */
|
|
22
|
+
export interface GoLivePlan {
|
|
23
|
+
/** For `decide`: `deploy_stage`, `promote` or `open` (never more than one). */
|
|
24
|
+
actions: ReleaseAction[];
|
|
25
|
+
/** The `promote` among `actions`, when there is one: its deploy carries the tag's env. */
|
|
26
|
+
promote?: ReleaseAction;
|
|
27
|
+
/** The tag completed in this run: record it. */
|
|
28
|
+
record: boolean;
|
|
29
|
+
/** The request is consumed. False: it stays open and the next run judges it again. */
|
|
30
|
+
consume: boolean;
|
|
31
|
+
/** The runner starts this go-live: the request is claimed before any effect, so it can no longer be cancelled or moved. */
|
|
32
|
+
claim: boolean;
|
|
33
|
+
/** The env the idle color is deployed with (only with `promote`). */
|
|
34
|
+
appEnv?: {
|
|
35
|
+
tag: string;
|
|
36
|
+
release: number;
|
|
37
|
+
minimum: number | null;
|
|
38
|
+
};
|
|
39
|
+
/** The tries to carry into `bg.<entry>.golive` when the idle deploy worked or failed (only with `promote`). */
|
|
40
|
+
carry?: {
|
|
41
|
+
tries: number;
|
|
42
|
+
idle: number;
|
|
43
|
+
};
|
|
44
|
+
/** `bg.<entry>.golive` after this run (null removes it); not used with `promote`. */
|
|
45
|
+
state?: GoLiveState | null;
|
|
46
|
+
refused?: string;
|
|
47
|
+
blocked?: string;
|
|
48
|
+
events: Array<Pick<ReleaseEvent, 'kind' | 'reason'>>;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Decides one `go_live`. Stage is deployed at the tag's commit when it differs, then the idle color is deployed
|
|
52
|
+
* with the tag's env (`promote`, also for a commit the live color already runs), then it is opened (the staff hold
|
|
53
|
+
* is passed here: stage was the review) only when the idle color runs and answers, and the request waits until
|
|
54
|
+
* the release completes, one color at 100 % with the window closed. Only then is the version recorded, and only
|
|
55
|
+
* when `bg.<entry>.golive` holds the release number this request's own idle deploy reserved: a tag no color
|
|
56
|
+
* carries is never recorded. Each deploy step is tried `GO_LIVE_TRIES` ticks, a history line for each retry.
|
|
57
|
+
*/
|
|
58
|
+
export declare function planGoLive(input: {
|
|
59
|
+
app: App;
|
|
60
|
+
action: GoLiveAction;
|
|
61
|
+
requestId: number;
|
|
62
|
+
recorded: Recorded;
|
|
63
|
+
ids: Readonly<Record<string, string>>;
|
|
64
|
+
released: readonly ReleasedVersion[];
|
|
65
|
+
now: Date;
|
|
66
|
+
/** The new color's observation (only meaningful while a release runs). */
|
|
67
|
+
observation: {
|
|
68
|
+
status: 'running' | 'crash_loop' | 'other';
|
|
69
|
+
address: 'ok' | 'failed' | 'unchecked';
|
|
70
|
+
};
|
|
71
|
+
}): GoLivePlan;
|
|
72
|
+
/** The highest release at or below `release` that is required; null when none. */
|
|
73
|
+
export declare function minimumFor(versions: readonly ReleasedVersion[], release: number): number | null;
|
|
74
|
+
/** A free release number: the unix second of `now`, or the next free one when a version already took it. */
|
|
75
|
+
export declare function releaseNumber(now: Date, versions: readonly ReleasedVersion[]): number;
|
|
76
|
+
/** The scope writes that record `v`. */
|
|
77
|
+
export declare function recordWrites(v: ReleasedVersion): Record<string, string>;
|