@wtfalch/foundry 0.61.0 → 0.63.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/bootstrap/declare.js +4 -1
- package/dist/bootstrap/versions.d.ts +2 -0
- package/dist/index.d.ts +9 -1
- package/dist/index.js +3 -0
- package/dist/migrations/0065_native_build_runs.sql +32 -0
- package/dist/migrations/0066_org_kosmos.sql +10 -0
- package/dist/migrations/0067_requested_changes_kosmos.sql +6 -0
- package/dist/migrations/0068_platform_registry.sql +17 -0
- package/dist/modules/sign-in.js +1 -1
- package/dist/native-build.d.ts +50 -0
- package/dist/native-build.js +72 -0
- package/dist/org.d.ts +22 -3
- package/dist/org.js +25 -2
- package/dist/plan/agora.d.ts +1 -1
- package/dist/plan/agora.js +4 -0
- package/dist/plan/archon-blue-green.d.ts +5 -0
- package/dist/plan/archon-blue-green.js +2 -0
- package/dist/plan/blue-green-apply.d.ts +5 -0
- package/dist/plan/blue-green-apply.js +197 -22
- package/dist/plan/blue-green.d.ts +80 -9
- package/dist/plan/blue-green.js +115 -14
- package/dist/plan/boule.d.ts +1 -1
- package/dist/plan/boule.js +2 -0
- package/dist/plan/broker-entries.d.ts +9 -0
- package/dist/plan/broker-entries.js +29 -0
- package/dist/plan/deploy-health.js +9 -2
- package/dist/plan/desktop-callback.js +4 -1
- package/dist/plan/diff.d.ts +5 -0
- package/dist/plan/diff.js +184 -63
- package/dist/plan/edge-access.d.ts +2 -3
- package/dist/plan/edge-access.js +8 -0
- package/dist/plan/executor.d.ts +111 -19
- package/dist/plan/executor.js +578 -72
- package/dist/plan/index.d.ts +2 -0
- package/dist/plan/index.js +1 -0
- package/dist/plan/jobs-scheduler-http.d.ts +2 -0
- package/dist/plan/jobs-scheduler-http.js +11 -0
- package/dist/plan/kosmos.d.ts +56 -0
- package/dist/plan/kosmos.js +84 -0
- package/dist/plan/locked-hostnames.js +5 -2
- package/dist/plan/org-names.d.ts +3 -3
- package/dist/plan/org-names.js +3 -1
- package/dist/plan/org-postgres-migrate.js +1 -0
- package/dist/plan/own-limits.d.ts +12 -0
- package/dist/plan/own-limits.js +2 -0
- package/dist/plan/plan-apply.d.ts +51 -1
- package/dist/plan/plan-apply.js +298 -25
- package/dist/plan/platform-diff.d.ts +6 -0
- package/dist/plan/platform-diff.js +74 -0
- package/dist/plan/platform-org.d.ts +2 -0
- package/dist/plan/platform-providers.d.ts +46 -1
- package/dist/plan/platform-providers.js +162 -1
- package/dist/plan/platform-registry-keys.d.ts +58 -0
- package/dist/plan/platform-registry-keys.js +69 -0
- package/dist/plan/platform-registry.d.ts +67 -0
- package/dist/plan/platform-registry.js +298 -0
- package/dist/plan/post-logout-uris.d.ts +1 -1
- package/dist/plan/post-logout-uris.js +5 -2
- package/dist/plan/providers-readers.js +20 -1
- package/dist/plan/release-host.d.ts +31 -2
- package/dist/plan/release-host.js +114 -6
- package/dist/plan/stage-copy-host.d.ts +104 -0
- package/dist/plan/stage-copy-host.js +259 -0
- package/dist/plan/stage-copy.js +44 -0
- package/dist/plan/stage-env.d.ts +9 -0
- package/dist/plan/stage-env.js +38 -0
- package/dist/plan/stage-guards.d.ts +57 -1
- package/dist/plan/stage-guards.js +106 -0
- package/dist/plan/stage-isolation.js +56 -0
- package/dist/plan/stage-services.d.ts +93 -0
- package/dist/plan/stage-services.js +153 -0
- package/dist/plan/stage-signin.d.ts +152 -0
- package/dist/plan/stage-signin.js +133 -0
- package/dist/plan/stage-sink.d.ts +2 -0
- package/dist/plan/stage-sink.js +6 -1
- package/dist/platform.d.ts +7 -1
- package/dist/platform.js +1 -0
- package/dist/release/decide.d.ts +12 -0
- package/dist/release/decide.js +37 -2
- package/dist/release/migration-compatibility.d.ts +66 -0
- package/dist/release/migration-compatibility.js +340 -0
- package/dist/release/run.d.ts +71 -1
- package/dist/release/run.js +92 -8
- package/dist/release/state.d.ts +14 -1
- package/dist/release/state.js +43 -0
- package/dist/release/types.d.ts +20 -0
- package/dist/release/types.js +2 -0
- package/dist/requested-changes.d.ts +11 -2
- package/dist/requested-changes.js +13 -1
- package/dist/slug.d.ts +1 -1
- package/dist/slug.js +11 -2
- package/dist/store/postgres.js +21 -0
- package/dist/store/schema.d.ts +87 -0
- package/dist/store/schema.js +10 -0
- package/package.json +3 -2
- package/versions.digests.json +1 -0
- package/versions.json +1 -0
|
@@ -10,6 +10,8 @@ import { remoteDatabaseAddress, remoteDatabaseHost } from './remote-database.js'
|
|
|
10
10
|
import { STAGE_SIGN_IN_NAMES, buildStageEnv } from './stage-env.js';
|
|
11
11
|
import { buildStageProbes, runStageProbes, stageProbesRecord, } from './stage-guards.js';
|
|
12
12
|
import { assertStageIsolated, envClass, stageIsolationHash, } from './stage-isolation.js';
|
|
13
|
+
import { StageServiceError, ensureStageServices } from './stage-services.js';
|
|
14
|
+
import { STAGE_SIGNIN_NONE, StageSignInError, ensureStagePublicClient, ensureStageSignIn, } from './stage-signin.js';
|
|
13
15
|
import { STAGE_SINK_APPLIED_SCOPE_KEY, STAGE_SINK_APP_SCOPE_KEY, STAGE_SINK_HTTP_PORT, STAGE_SINK_IMAGE_TAG, STAGE_SINK_REPOSITORY, stageMailEnv, stageSinkAppName, } from './stage-sink.js';
|
|
14
16
|
import { readWorkspaceCommit, wsKey } from './workspace.js';
|
|
15
17
|
/** How long one Coolify call of the sink may take. */
|
|
@@ -95,7 +97,10 @@ export async function applyBlueGreenServer(deps, subject, server) {
|
|
|
95
97
|
await deps.persist();
|
|
96
98
|
}
|
|
97
99
|
}
|
|
98
|
-
deps.record(bgKey.applied(id, server.uuid), structureHash(subject, {
|
|
100
|
+
deps.record(bgKey.applied(id, server.uuid), structureHash(subject, {
|
|
101
|
+
primary: isPrimary,
|
|
102
|
+
...(deps.guardInputs ? { guards: deps.guardInputs } : {}),
|
|
103
|
+
}));
|
|
99
104
|
await deps.persist();
|
|
100
105
|
}
|
|
101
106
|
/**
|
|
@@ -481,30 +486,92 @@ export async function ensureStage(deps, subject, serverUuid) {
|
|
|
481
486
|
const stageHas = new Map((await coolify.listEnvs(appId)).filter((e) => !e.is_preview).map((e) => [e.key, e.value]));
|
|
482
487
|
const mailChanged = [...mail, ...signInEnv].some((e) => stageHas.get(e.key) !== e.value);
|
|
483
488
|
stageEnv.push(...mail);
|
|
484
|
-
const
|
|
489
|
+
const stageSurfaceOf = () => ({
|
|
485
490
|
env: stageEnv.map((e) => ({ name: e.key, class: envClass(e.key), value: e.value })),
|
|
486
491
|
resources: stageResources(subject),
|
|
492
|
+
});
|
|
493
|
+
const prodSurface = {
|
|
494
|
+
env: [...prodEnv, ...greenEnv]
|
|
495
|
+
.filter((e) => !e.is_preview && e.value !== undefined)
|
|
496
|
+
.map((e) => ({ name: e.key, class: envClass(e.key), value: e.value })),
|
|
497
|
+
resources: prodResources(subject),
|
|
498
|
+
internalHosts: Object.values(prodResources(subject)),
|
|
487
499
|
};
|
|
488
|
-
const
|
|
500
|
+
const sinkMail = Object.fromEntries(mail.map((e) => [e.key, e.value]));
|
|
501
|
+
const signInIsolation = deps.signIn
|
|
502
|
+
? { signIn: { customers: deps.signIn.customers, live: deps.signIn.live } }
|
|
503
|
+
: {};
|
|
504
|
+
const isolationViolations = (surface) => assertStageIsolated({
|
|
489
505
|
kind,
|
|
490
|
-
prod:
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
resources: prodResources(subject),
|
|
495
|
-
internalHosts: Object.values(prodResources(subject)),
|
|
496
|
-
},
|
|
497
|
-
stage: stageSurface,
|
|
498
|
-
sinkMail: Object.fromEntries(mail.map((e) => [e.key, e.value])),
|
|
499
|
-
...(deps.signIn
|
|
500
|
-
? { signIn: { customers: deps.signIn.customers, live: deps.signIn.live } }
|
|
501
|
-
: {}),
|
|
506
|
+
prod: prodSurface,
|
|
507
|
+
stage: surface,
|
|
508
|
+
sinkMail,
|
|
509
|
+
...signInIsolation,
|
|
502
510
|
});
|
|
511
|
+
// The env written this run, and the env stage already holds (put there by an earlier apply, an older
|
|
512
|
+
// builder or a hand): a forbidden name or a production value already on the live stage app is refused too.
|
|
513
|
+
// A name this run writes again is replaced, so only the other live names are checked here.
|
|
514
|
+
const written = new Set(stageEnv.map((e) => e.key));
|
|
515
|
+
const liveStage = {
|
|
516
|
+
env: [...stageHas]
|
|
517
|
+
.filter(([name, value]) => value !== undefined && !written.has(name))
|
|
518
|
+
.map(([name, value]) => ({ name, class: envClass(name), value })),
|
|
519
|
+
resources: {},
|
|
520
|
+
};
|
|
521
|
+
let stageSurface = stageSurfaceOf();
|
|
522
|
+
const violations = [
|
|
523
|
+
...new Set([
|
|
524
|
+
...isolationViolations(stageSurface),
|
|
525
|
+
...assertStageIsolated({
|
|
526
|
+
kind,
|
|
527
|
+
prod: prodSurface,
|
|
528
|
+
stage: liveStage,
|
|
529
|
+
sinkMail,
|
|
530
|
+
...signInIsolation,
|
|
531
|
+
}),
|
|
532
|
+
]),
|
|
533
|
+
].sort();
|
|
503
534
|
if (violations.length > 0) {
|
|
504
535
|
// Stage fails closed for itself: nothing is written to it, production's structure goes on.
|
|
505
536
|
await stageFailed(deps, subject, `blocked:${violations.join(',')}`);
|
|
506
537
|
return;
|
|
507
538
|
}
|
|
539
|
+
// Stage's own files, ai and jobs organisations and keys, for the services production's app uses. They are made
|
|
540
|
+
// before the probes, which need the keys; nothing is written to the stage app until the probes were rejected.
|
|
541
|
+
const prodValues = new Map(prodEnv
|
|
542
|
+
.filter((e) => !e.is_preview && e.value !== undefined)
|
|
543
|
+
.map((e) => [e.key, e.value]));
|
|
544
|
+
let servicesRecord;
|
|
545
|
+
try {
|
|
546
|
+
const made = await ensureStageServices({
|
|
547
|
+
id: subject.id,
|
|
548
|
+
kind,
|
|
549
|
+
prod: prodValues,
|
|
550
|
+
port: deps.stageGuards?.services,
|
|
551
|
+
recorded: deps.recorded,
|
|
552
|
+
record: deps.record,
|
|
553
|
+
persist: deps.persist,
|
|
554
|
+
secret: deps.secret,
|
|
555
|
+
log: (line) => console.info(`${deps.label}: ${line}`),
|
|
556
|
+
});
|
|
557
|
+
stageEnv.push(...made.env);
|
|
558
|
+
servicesRecord = made.record;
|
|
559
|
+
}
|
|
560
|
+
catch (error) {
|
|
561
|
+
// A StageServiceError holds ids and refs only; any other cause can echo a credential.
|
|
562
|
+
if (error instanceof StageServiceError)
|
|
563
|
+
console.error(`${deps.label}: ${error.message}`);
|
|
564
|
+
await stageFailed(deps, subject, 'blocked:services', error);
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
if (servicesRecord !== 'none') {
|
|
568
|
+
stageSurface = stageSurfaceOf();
|
|
569
|
+
const withServices = isolationViolations(stageSurface);
|
|
570
|
+
if (withServices.length > 0) {
|
|
571
|
+
await stageFailed(deps, subject, `blocked:${withServices.join(',')}`);
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
508
575
|
// Stage's credentials are tried against production before they are written; one that production accepts
|
|
509
576
|
// blocks stage (nothing is written to it), and what was tried is recorded by name so the copy slice sees it.
|
|
510
577
|
const stageValues = new Map((await coolify.listEnvs(appId))
|
|
@@ -512,7 +579,7 @@ export async function ensureStage(deps, subject, serverUuid) {
|
|
|
512
579
|
.map((e) => [e.key, e.value]));
|
|
513
580
|
for (const e of stageEnv)
|
|
514
581
|
stageValues.set(e.key, e.value);
|
|
515
|
-
|
|
582
|
+
let isolationHash = stageIsolationHash(stageSurface);
|
|
516
583
|
let probeResult;
|
|
517
584
|
try {
|
|
518
585
|
probeResult = await runStageProbes(buildStageProbes({
|
|
@@ -524,9 +591,12 @@ export async function ensureStage(deps, subject, serverUuid) {
|
|
|
524
591
|
? { liveCustomersOrgId: deps.signIn.live.customersOrgId }
|
|
525
592
|
: {}),
|
|
526
593
|
...(deps.signIn ? { signInRedirectUri: deps.signIn.redirectUri } : {}),
|
|
527
|
-
prod: new Map(
|
|
528
|
-
|
|
529
|
-
|
|
594
|
+
prod: new Map([
|
|
595
|
+
...prodValues,
|
|
596
|
+
...(deps.stageGuards?.services?.prodJobsProjectId
|
|
597
|
+
? [['JOBS_PROJECT_ID', deps.stageGuards.services.prodJobsProjectId]]
|
|
598
|
+
: []),
|
|
599
|
+
]),
|
|
530
600
|
}));
|
|
531
601
|
}
|
|
532
602
|
catch (error) {
|
|
@@ -534,7 +604,7 @@ export async function ensureStage(deps, subject, serverUuid) {
|
|
|
534
604
|
await stageFailed(deps, subject, 'blocked:database', error);
|
|
535
605
|
return;
|
|
536
606
|
}
|
|
537
|
-
|
|
607
|
+
let probes = stageProbesRecord(probeResult, isolationHash, stageValues);
|
|
538
608
|
if (probeResult.failed.length > 0) {
|
|
539
609
|
deps.record(bgKey.stageProbes(subject.id), probes.value);
|
|
540
610
|
await stageFailed(deps, subject, `blocked:${probeResult.failed.join(',')}`);
|
|
@@ -544,9 +614,108 @@ export async function ensureStage(deps, subject, serverUuid) {
|
|
|
544
614
|
await stageFailed(deps, subject, 'blocked:gate');
|
|
545
615
|
return;
|
|
546
616
|
}
|
|
617
|
+
// Stage's own sign-in: made only now, after the isolation check, the probes and the gate. Its credentials are
|
|
618
|
+
// added to the env and the isolation check runs once more over the whole of it.
|
|
619
|
+
const signIn = deps.stageGuards?.signIn;
|
|
620
|
+
let signInGuard;
|
|
621
|
+
if (signIn?.required === true && signIn.public === true) {
|
|
622
|
+
// A static SPA (Agora): a client and nothing secret. Its config is built from production's with the stage
|
|
623
|
+
// client id, then the isolation check runs once more over the whole env.
|
|
624
|
+
try {
|
|
625
|
+
const made = await ensureStagePublicClient({
|
|
626
|
+
id: subject.id,
|
|
627
|
+
port: signIn,
|
|
628
|
+
recorded: deps.recorded,
|
|
629
|
+
record: deps.record,
|
|
630
|
+
persist: deps.persist,
|
|
631
|
+
});
|
|
632
|
+
const rebuilt = buildStageEnv({
|
|
633
|
+
kind,
|
|
634
|
+
appUrl: stageAppUrl(subject),
|
|
635
|
+
logDir: deps.logDir,
|
|
636
|
+
database,
|
|
637
|
+
prod: prodEnv,
|
|
638
|
+
existing: [],
|
|
639
|
+
mint: () => '',
|
|
640
|
+
stageClientId: made.clientId,
|
|
641
|
+
});
|
|
642
|
+
const config = rebuilt.env.find((e) => e.key === 'AGORA_CONFIG');
|
|
643
|
+
if (config)
|
|
644
|
+
stageEnv.push(config);
|
|
645
|
+
signInGuard = made.guard;
|
|
646
|
+
}
|
|
647
|
+
catch (error) {
|
|
648
|
+
if (error instanceof StageSignInError)
|
|
649
|
+
console.error(`${deps.label}: ${error.message}`);
|
|
650
|
+
deps.record(bgKey.stageSignIn(subject.id), '');
|
|
651
|
+
await stageFailed(deps, subject, 'blocked:signin', error);
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
for (const e of stageEnv)
|
|
655
|
+
stageValues.set(e.key, e.value);
|
|
656
|
+
stageSurface = stageSurfaceOf();
|
|
657
|
+
const again = isolationViolations(stageSurface);
|
|
658
|
+
if (again.length > 0) {
|
|
659
|
+
deps.record(bgKey.stageSignIn(subject.id), '');
|
|
660
|
+
await stageFailed(deps, subject, `blocked:${again.join(',')}`);
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
isolationHash = stageIsolationHash(stageSurface);
|
|
664
|
+
probes = stageProbesRecord(probeResult, isolationHash, stageValues);
|
|
665
|
+
}
|
|
666
|
+
else if (signIn?.required === true) {
|
|
667
|
+
try {
|
|
668
|
+
const made = await ensureStageSignIn({
|
|
669
|
+
id: subject.id,
|
|
670
|
+
port: signIn,
|
|
671
|
+
recorded: deps.recorded,
|
|
672
|
+
record: deps.record,
|
|
673
|
+
persist: deps.persist,
|
|
674
|
+
secret: deps.secret,
|
|
675
|
+
generate: () => randomBytes(32).toString('base64url'),
|
|
676
|
+
sleep: deps.sleep,
|
|
677
|
+
});
|
|
678
|
+
stageEnv.push(...made.env);
|
|
679
|
+
signInGuard = made.guard;
|
|
680
|
+
}
|
|
681
|
+
catch (error) {
|
|
682
|
+
// A StageSignInError holds ids and refs only; any other cause can echo a credential.
|
|
683
|
+
if (error instanceof StageSignInError)
|
|
684
|
+
console.error(`${deps.label}: ${error.message}`);
|
|
685
|
+
deps.record(bgKey.stageSignIn(subject.id), '');
|
|
686
|
+
await stageFailed(deps, subject, 'blocked:signin', error);
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
if (!stageEnv.some((e) => e.key === 'AUTH_COOKIE_SECRET')) {
|
|
690
|
+
// A cookie secret of stage's own, kept across applies; never a copy of production's.
|
|
691
|
+
const have = stageHas.get('AUTH_COOKIE_SECRET') || randomBytes(32).toString('base64url');
|
|
692
|
+
deps.secret(have);
|
|
693
|
+
stageEnv.push({ key: 'AUTH_COOKIE_SECRET', value: have });
|
|
694
|
+
}
|
|
695
|
+
for (const e of stageEnv)
|
|
696
|
+
stageValues.set(e.key, e.value);
|
|
697
|
+
stageSurface = stageSurfaceOf();
|
|
698
|
+
const again = isolationViolations(stageSurface);
|
|
699
|
+
if (again.length > 0) {
|
|
700
|
+
deps.record(bgKey.stageSignIn(subject.id), '');
|
|
701
|
+
await stageFailed(deps, subject, `blocked:${again.join(',')}`);
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
isolationHash = stageIsolationHash(stageSurface);
|
|
705
|
+
probes = stageProbesRecord(probeResult, isolationHash, stageValues);
|
|
706
|
+
}
|
|
707
|
+
else if (signIn?.required === false) {
|
|
708
|
+
signInGuard = STAGE_SIGNIN_NONE;
|
|
709
|
+
}
|
|
710
|
+
else {
|
|
711
|
+
console.info(`${deps.label}: stage sign-in was not set up: the host gave no sign-in for this subject, so ${bgKey.stageSignIn(subject.id)} stays unrecorded`);
|
|
712
|
+
}
|
|
547
713
|
await upsertEnv(deps, appId, stageEnv);
|
|
714
|
+
if (signInGuard)
|
|
715
|
+
deps.record(bgKey.stageSignIn(subject.id), signInGuard);
|
|
548
716
|
if (mail.length > 0)
|
|
549
717
|
deps.record(bgKey.stageSink(subject.id), 'applied');
|
|
718
|
+
deps.record(bgKey.stageServices(subject.id), servicesRecord);
|
|
550
719
|
deps.record(bgKey.stageIsolation(subject.id), isolationHash);
|
|
551
720
|
deps.record(bgKey.stageProbes(subject.id), probes.value);
|
|
552
721
|
await deps.persist();
|
|
@@ -589,8 +758,11 @@ async function ensureStageGate(deps, subject) {
|
|
|
589
758
|
if (!gate || deps.recorded()[bgKey.stageGate(subject.id)])
|
|
590
759
|
return true;
|
|
591
760
|
let id;
|
|
761
|
+
let membersHash;
|
|
592
762
|
try {
|
|
593
|
-
|
|
763
|
+
const created = await gate.create();
|
|
764
|
+
id = created.id;
|
|
765
|
+
membersHash = created.membersHash;
|
|
594
766
|
}
|
|
595
767
|
catch (error) {
|
|
596
768
|
// An Access error can echo the request, which names the people allowed in: only its name is logged.
|
|
@@ -598,6 +770,8 @@ async function ensureStageGate(deps, subject) {
|
|
|
598
770
|
return false;
|
|
599
771
|
}
|
|
600
772
|
deps.record(bgKey.stageGate(subject.id), id);
|
|
773
|
+
if (membersHash)
|
|
774
|
+
deps.record(bgKey.stageGateMembers(subject.id), membersHash);
|
|
601
775
|
await deps.persist();
|
|
602
776
|
return true;
|
|
603
777
|
}
|
|
@@ -607,7 +781,8 @@ async function stageAppId(deps, subject, name, serverUuid) {
|
|
|
607
781
|
name,
|
|
608
782
|
server: serverUuid,
|
|
609
783
|
branch: subject.branch,
|
|
610
|
-
|
|
784
|
+
// Stage deploys only when a person promotes a commit to it (`deploy_stage`); a merge never reaches it.
|
|
785
|
+
autoDeploy: false,
|
|
611
786
|
});
|
|
612
787
|
}
|
|
613
788
|
function isNotFound(error) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type BlueGreenConfig, type DeclaredOrg, type HostingEntry } from '../org.js';
|
|
2
2
|
import { type StageKind } from './stage-isolation.js';
|
|
3
3
|
/**
|
|
4
4
|
* Blue/green releases (docs/adr/0001-blue-green-releases.md). A color is a property any app can have
|
|
@@ -29,6 +29,8 @@ export declare const bgKey: {
|
|
|
29
29
|
digest: (id: string, color: Color) => string;
|
|
30
30
|
weights: (id: string) => string;
|
|
31
31
|
stage: (id: string) => string;
|
|
32
|
+
/** The last `deploy_stage`: `<ISO time>|ok|<commit>` or `<ISO time>|failed|<step>`. */
|
|
33
|
+
stageDeploy: (id: string) => string;
|
|
32
34
|
window: (id: string) => string;
|
|
33
35
|
held: (id: string, digest: string) => string;
|
|
34
36
|
/** The weights the load balancer on `server` was last written with. */
|
|
@@ -55,8 +57,29 @@ export declare const bgKey: {
|
|
|
55
57
|
stageIsolation: (id: string) => string;
|
|
56
58
|
/** The id of the Access gate over the stage host: staff only, no bypass, no service-token rule. */
|
|
57
59
|
stageGate: (id: string) => string;
|
|
60
|
+
/** The hash of the sorted email list last written to the stage gate (owners plus the people holding `releases:preview`). */
|
|
61
|
+
stageGateMembers: (id: string) => string;
|
|
58
62
|
/** `ok:<hash>` once every apply-time probe was rejected; `notrun:<names>` or `failed:<names>` otherwise. */
|
|
59
63
|
stageProbes: (id: string) => string;
|
|
64
|
+
/** Stage's own sign-in client: its ZITADEL app id, and its client id (public, not a secret). */
|
|
65
|
+
stageClient: (id: string) => string;
|
|
66
|
+
stageClientId: (id: string) => string;
|
|
67
|
+
/** The held ref of stage's app key, recorded and persisted as soon as it is minted (no `:`: undelivered). */
|
|
68
|
+
stageSignInKey: (id: string) => string;
|
|
69
|
+
/** `{name, organizationId}` of stage's broker entry, so org destroy can remove it. No key. */
|
|
70
|
+
stageBroker: (id: string) => string;
|
|
71
|
+
/** `ok:<hash of the client, app and broker entry ids>` or `none`; the copy waits for it. */
|
|
72
|
+
stageSignIn: (id: string) => string;
|
|
73
|
+
/** `none` (production's app uses no files, ai or jobs) or `ok:<hash>` once every service it uses is held by stage or listed absent. */
|
|
74
|
+
stageServices: (id: string) => string;
|
|
75
|
+
/** `ok:<hash of the ids>` or `absent:<service>:<reason>` for one service (`files`, `ai` or `jobs`). */
|
|
76
|
+
stageService: (id: string, service: string) => string;
|
|
77
|
+
/** The id of stage's own organisation (files, ai) or project (jobs). An id, not a secret. */
|
|
78
|
+
stageServiceOrg: (id: string, service: string) => string;
|
|
79
|
+
/** The name stage's project or organisation was made under, when the service is deleted by name (jobs). */
|
|
80
|
+
stageServiceName: (id: string, service: string) => string;
|
|
81
|
+
/** `{ids, refs}` of stage's keys in that service: key ids and held refs, never a value. */
|
|
82
|
+
stageServiceKeys: (id: string, service: string) => string;
|
|
60
83
|
/** `applied` once the stage app holds the sink's mail env. The copy refuses without it. */
|
|
61
84
|
stageSink: (id: string) => string;
|
|
62
85
|
/** `<ISO time>|<dump execution id>|ok`, written by a finished copy of production into stage. */
|
|
@@ -164,11 +187,14 @@ export interface BlueGreenSubject {
|
|
|
164
187
|
* (address check, crash loop) and moves up by time.
|
|
165
188
|
*/
|
|
166
189
|
reporting: boolean;
|
|
167
|
-
/** Set for Boule and
|
|
190
|
+
/** Set for Boule, Agora and kosmos, which are not hosting entries. */
|
|
168
191
|
builtin?: BuiltinApp;
|
|
169
192
|
}
|
|
170
|
-
/**
|
|
171
|
-
|
|
193
|
+
/**
|
|
194
|
+
* The apps foundry runs for a company that can declare blue/green: Boule and Agora, every company's own, plus
|
|
195
|
+
* kosmos once the company turns it on (`org.boule`, `org.agora`, `org.kosmos`; docs/adr/0001, rule 1).
|
|
196
|
+
*/
|
|
197
|
+
export type BuiltinApp = 'boule' | 'agora' | 'kosmos';
|
|
172
198
|
export declare const BUILTIN_APPS: readonly BuiltinApp[];
|
|
173
199
|
/** What foundry builds each built-in app from; `executor.ts` creates the app from the same values. */
|
|
174
200
|
export declare const BUILTIN_APP: {
|
|
@@ -188,6 +214,14 @@ export declare const BUILTIN_APP: {
|
|
|
188
214
|
readonly database: false;
|
|
189
215
|
readonly reporting: false;
|
|
190
216
|
};
|
|
217
|
+
readonly kosmos: {
|
|
218
|
+
readonly repo: "wtfalch/kosmos";
|
|
219
|
+
readonly dockerfile: "/Dockerfile";
|
|
220
|
+
readonly port: 3000;
|
|
221
|
+
readonly healthPath: "/api/health";
|
|
222
|
+
readonly database: true;
|
|
223
|
+
readonly reporting: true;
|
|
224
|
+
};
|
|
191
225
|
};
|
|
192
226
|
/**
|
|
193
227
|
* A hosting entry that declares `blueGreen`, as a subject; `null` when it does not or cannot run.
|
|
@@ -195,11 +229,12 @@ export declare const BUILTIN_APP: {
|
|
|
195
229
|
*/
|
|
196
230
|
export declare function entrySubject(org: DeclaredOrg, entry: HostingEntry, recorded?: Readonly<Record<string, string>>): BlueGreenSubject | null;
|
|
197
231
|
/**
|
|
198
|
-
* The company's Boule or
|
|
199
|
-
* its name `<slug>-<app>` and is blue, like
|
|
232
|
+
* The company's Boule, Agora or kosmos as a subject; `null` when it does not declare blue/green (see
|
|
233
|
+
* {@link builtinDeclaresBlueGreen}). The running app keeps its name `<slug>-<app>` and is blue, like
|
|
234
|
+
* platform-archon. All three run on the primary server only.
|
|
200
235
|
*/
|
|
201
236
|
export declare function builtinSubject(org: DeclaredOrg, app: BuiltinApp, recorded?: Readonly<Record<string, string>>): BlueGreenSubject | null;
|
|
202
|
-
/** Every blue/green subject of an org: the hosting entries that declare it, then Boule and
|
|
237
|
+
/** Every blue/green subject of an org: the hosting entries that declare it, then Boule, Agora and kosmos. */
|
|
203
238
|
export declare function blueGreenSubjects(org: DeclaredOrg, recorded?: Readonly<Record<string, string>>): BlueGreenSubject[];
|
|
204
239
|
/** Whether the org declares blue/green for any of its apps. */
|
|
205
240
|
export declare const declaresBlueGreen: (org: DeclaredOrg) => boolean;
|
|
@@ -281,9 +316,38 @@ export interface BlueGreenPlanStep {
|
|
|
281
316
|
/** A short hash of what decides the shape of a server's apps, so a change replans the server. */
|
|
282
317
|
export declare function structureHash(subject: BlueGreenSubject, input: {
|
|
283
318
|
primary: boolean;
|
|
319
|
+
guards?: readonly string[];
|
|
284
320
|
}): string;
|
|
285
|
-
/**
|
|
286
|
-
|
|
321
|
+
/**
|
|
322
|
+
* The kind of stage app a subject has: Boule and Agora by `builtin` (a hosting entry that happens to be keyed
|
|
323
|
+
* `boule` is still a customer app), platform-archon by its id, else a customer app (kosmos included: it has no lists of its own).
|
|
324
|
+
*/
|
|
325
|
+
export declare const stageKindOf: (subject: Pick<BlueGreenSubject, "id" | "builtin">) => StageKind;
|
|
326
|
+
/** What decides which stage guards can be made. Each is a fact the planner and the executor both hold. */
|
|
327
|
+
export interface StageGuardFacts {
|
|
328
|
+
/** The host supplied the probe port. */
|
|
329
|
+
probesPort: boolean;
|
|
330
|
+
/** The host has an Access port. */
|
|
331
|
+
accessPort: boolean;
|
|
332
|
+
/** The platform's recorded Access provider. */
|
|
333
|
+
providerId?: string | null | undefined;
|
|
334
|
+
/** The domain the stage host is under. */
|
|
335
|
+
domain?: string | null | undefined;
|
|
336
|
+
/** The company's sign-in organisation, the claim the gate requires. */
|
|
337
|
+
claimValue?: string | null | undefined;
|
|
338
|
+
/** How many owner emails the gate admits. */
|
|
339
|
+
emails: number;
|
|
340
|
+
/** A `hosting.access` rule already names the stage host: no gate is made over it. */
|
|
341
|
+
hostRuled: boolean;
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* The stage guards that can be made now, by name. The names go into the applied structure hash, so a guard
|
|
345
|
+
* that becomes possible after the first apply changes the hash and plans the structure step once more.
|
|
346
|
+
*/
|
|
347
|
+
export declare function stageGuardInputs(facts: StageGuardFacts): string[];
|
|
348
|
+
/** The guard names as the plan's `desired.guards` carries them, and back. */
|
|
349
|
+
export declare const formatGuards: (guards: readonly string[]) => string;
|
|
350
|
+
export declare const parseGuards: (value: unknown) => string[];
|
|
287
351
|
/** The names stage can collide on. */
|
|
288
352
|
export declare function stageResources(subject: BlueGreenSubject): Record<string, string>;
|
|
289
353
|
/** The names of the subject's live parts: stage must equal none of them. */
|
|
@@ -310,6 +374,11 @@ export interface PlanBlueGreenInput {
|
|
|
310
374
|
accessProviderId?: string | null;
|
|
311
375
|
/** The company's stage mail sink is recorded and its live app is stopped or gone (read from Coolify at plan time). */
|
|
312
376
|
stageSinkDown?: boolean;
|
|
377
|
+
/** Which stage-guard ports the host has. Absent: neither, so no guard input is counted. */
|
|
378
|
+
stageGuardPorts?: {
|
|
379
|
+
probes: boolean;
|
|
380
|
+
access: boolean;
|
|
381
|
+
};
|
|
313
382
|
}
|
|
314
383
|
/**
|
|
315
384
|
* Plans the blue/green parts of an org: per usable server, one structure step (the apps, the balancer, the
|
|
@@ -359,6 +428,8 @@ export declare function planArchonBlueGreen(input: {
|
|
|
359
428
|
servers: readonly BlueGreenServer[] | undefined;
|
|
360
429
|
/** Why the executor cannot run it (no way to hold the health token), or absent. */
|
|
361
430
|
executorBlocked?: string | undefined;
|
|
431
|
+
/** The stage guards that can be made now ({@link stageGuardInputs}); they go into the applied hash. */
|
|
432
|
+
guards?: readonly string[];
|
|
362
433
|
}): {
|
|
363
434
|
step: BlueGreenPlanStep | null;
|
|
364
435
|
unmanaged: UnmanagedLine[];
|