@wtfalch/foundry 0.20.0 → 0.21.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.d.ts +13 -0
- package/dist/bootstrap/declare.js +23 -1
- package/dist/bootstrap/versions.d.ts +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/migrations/0046_platform_hc.sql +19 -0
- package/dist/plan/deploy-health.js +5 -1
- package/dist/plan/diff.js +23 -0
- package/dist/plan/executor.d.ts +34 -0
- package/dist/plan/executor.js +181 -28
- package/dist/plan/health-check-settings.d.ts +47 -0
- package/dist/plan/health-check-settings.js +47 -0
- package/dist/plan/health-targets.d.ts +86 -0
- package/dist/plan/health-targets.js +344 -0
- package/dist/plan/index.d.ts +2 -0
- package/dist/plan/index.js +1 -0
- package/dist/plan/live.d.ts +12 -1
- package/dist/plan/plan-apply.d.ts +4 -0
- package/dist/plan/platform-diff.d.ts +118 -0
- package/dist/plan/platform-diff.js +272 -0
- package/dist/plan/platform-providers.d.ts +149 -1
- package/dist/plan/platform-providers.js +459 -36
- package/dist/plan/providers-readers.js +32 -1
- package/dist/plan/rollout.d.ts +9 -8
- package/dist/platform.d.ts +14 -1
- package/dist/platform.js +1 -0
- package/dist/store/postgres.js +8 -0
- package/dist/store/schema.d.ts +51 -0
- package/dist/store/schema.js +4 -0
- package/package.json +1 -1
- package/versions.json +2 -1
|
@@ -674,6 +674,104 @@ export declare const PLATFORM_EDGE_APPLIED_GIT_REF_SCOPE_KEY = "edge_applied_git
|
|
|
674
674
|
* (the {@link PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY} pattern). Not a secret.
|
|
675
675
|
*/
|
|
676
676
|
export declare const PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY = "edge_platform_internal_url";
|
|
677
|
+
/**
|
|
678
|
+
* #540: the resource key `planHc` plans under and `FoundryPlatformStepExecutor`
|
|
679
|
+
* dispatches on -- the {@link PLATFORM_EDGE_KEY} shape for the platform's own
|
|
680
|
+
* heartbeat server (self-hosted Healthchecks). `runPlatformSteps` records
|
|
681
|
+
* the app's uuid under `coolify:app:platform:hc` once the step succeeds,
|
|
682
|
+
* which is what stops `planHc` planning it again.
|
|
683
|
+
*/
|
|
684
|
+
export declare const PLATFORM_HC_KEY = "platform:hc";
|
|
685
|
+
/**
|
|
686
|
+
* #540: the hostname and `gitRef` (a Docker image tag, like `mail.gitRef`)
|
|
687
|
+
* the last successful `hc` run applied. `planHc` plans the step again when
|
|
688
|
+
* either differs from the declared value; a re-run patches the image tag
|
|
689
|
+
* and redeploys, and mints nothing new (`SECRET_KEY` is generated once and
|
|
690
|
+
* read back, never regenerated -- see `ensureGeneratedEnv`).
|
|
691
|
+
*/
|
|
692
|
+
export declare const PLATFORM_HC_APPLIED_HOSTNAME_SCOPE_KEY = "hc_applied_hostname";
|
|
693
|
+
export declare const PLATFORM_HC_APPLIED_GIT_REF_SCOPE_KEY = "hc_applied_git_ref";
|
|
694
|
+
/**
|
|
695
|
+
* #540 round 2: the held-key ref a human puts hc's first project API key
|
|
696
|
+
* under -- `deliverSecret`'s naming convention (`EDGE_URL`'s own pattern),
|
|
697
|
+
* `<service>:<scope>:<name>` like {@link PLATFORM_EDGE_KEY_REF}. Nothing in
|
|
698
|
+
* this package mints this key: `provisionPlatformHc` stands up an empty
|
|
699
|
+
* Healthchecks instance with no user, no project and so no key, and neither
|
|
700
|
+
* a `manage.py` command nor a documented API mints a project's own API key
|
|
701
|
+
* (confirmed against `healthchecks/healthchecks`'s own source, 2026-10) --
|
|
702
|
+
* only its web UI does, after a first superuser signs in by hand. Until a
|
|
703
|
+
* human does that and holds the key here, `FoundryPlatformStepExecutor
|
|
704
|
+
* .blockedSteps` (`platform-providers.ts`) reports the heartbeat checks
|
|
705
|
+
* blocked (`PlanPlatformOptions.blocked.heartbeatsApiKeyMissing`) rather
|
|
706
|
+
* than let `planHeartbeats` plan a step that would call hc's real
|
|
707
|
+
* Management API with no account and no key behind it -- see issue #540's
|
|
708
|
+
* own "Human steps" note. This package never reads the value back: whoever
|
|
709
|
+
* implements {@link HeartbeatsPort} against hc's real HTTP API is the host,
|
|
710
|
+
* and that implementation is where the key is actually used.
|
|
711
|
+
*/
|
|
712
|
+
export declare const PLATFORM_HC_API_KEY_REF = "hc:platform:apiKey";
|
|
713
|
+
/**
|
|
714
|
+
* #540: the heartbeat check named after the uptime Worker (foundry#542,
|
|
715
|
+
* which is what actually pings it -- blocked today on edge#26, so nothing
|
|
716
|
+
* pings this check yet; it still exists so the Worker has a URL to deliver
|
|
717
|
+
* to the moment it can). `platformBackupHeartbeatCheckName` is the matching
|
|
718
|
+
* name for each platform database backup (`PLATFORM_BACKUP_SERVICE_KEYS`).
|
|
719
|
+
* Both names are also the prefix `provisionPlatformHeartbeats`
|
|
720
|
+
* (`platform-providers.ts`) uses to recognise which live Healthchecks
|
|
721
|
+
* checks are its own to delete when they are no longer wanted -- it never
|
|
722
|
+
* touches a check outside this naming scheme.
|
|
723
|
+
*/
|
|
724
|
+
export declare const PLATFORM_UPTIME_HEARTBEAT_CHECK_NAME = "uptime-worker";
|
|
725
|
+
/** #540: see {@link PLATFORM_UPTIME_HEARTBEAT_CHECK_NAME}'s own doc comment. */
|
|
726
|
+
export declare function platformBackupHeartbeatCheckName(service: PlatformBackupServiceKey): string;
|
|
727
|
+
/**
|
|
728
|
+
* #540: the held-key ref each heartbeat check's own ping URL is recorded
|
|
729
|
+
* under, once created -- `deliverSecret`'s ref-naming convention
|
|
730
|
+
* (`EDGE_URL`'s own pattern), read back by whoever owns that piece of
|
|
731
|
+
* scheduled work. `platformBackupHeartbeatUrlRef` is the per-service
|
|
732
|
+
* counterpart. Unlike a minted credential (CLAUDE.md platform-step rule 1),
|
|
733
|
+
* a ping URL is not retried-and-guarded the same way: it is closer to a
|
|
734
|
+
* resource id, so `provisionPlatformHeartbeats` simply records it again
|
|
735
|
+
* every time the check it names is (re)created.
|
|
736
|
+
*/
|
|
737
|
+
export declare const PLATFORM_UPTIME_HEARTBEAT_URL_REF = "uptime:heartbeatUrl";
|
|
738
|
+
/** #540: see {@link PLATFORM_UPTIME_HEARTBEAT_URL_REF}'s own doc comment. */
|
|
739
|
+
export declare function platformBackupHeartbeatUrlRef(service: PlatformBackupServiceKey): string;
|
|
740
|
+
/**
|
|
741
|
+
* #540: every heartbeat check name foundry currently wants -- the uptime
|
|
742
|
+
* Worker's own, plus one per platform database backup that already has a
|
|
743
|
+
* recorded database id (`PLATFORM_BACKUP_SERVICE_KEYS`, the same "nothing to
|
|
744
|
+
* report yet" gate {@link planBackups} itself uses for a service with no
|
|
745
|
+
* database). Shared by {@link planHeartbeats} (to tell whether anything
|
|
746
|
+
* declared has changed since the last successful reconcile) and
|
|
747
|
+
* `provisionPlatformHeartbeats` (`platform-providers.ts`, to decide what to
|
|
748
|
+
* create or delete live) so the two can never compute a different desired
|
|
749
|
+
* set from each other.
|
|
750
|
+
*/
|
|
751
|
+
export declare function desiredHeartbeatCheckNames(recordedIds: Readonly<Record<string, string>>): readonly string[];
|
|
752
|
+
/**
|
|
753
|
+
* #540: the held-key ref a check named `name` (one {@link desiredHeartbeatCheckNames}
|
|
754
|
+
* produced) records its ping URL under -- `platform-providers.ts`'s
|
|
755
|
+
* `provisionPlatformHeartbeats` looks this up rather than re-deriving it
|
|
756
|
+
* inline, so the two name-to-ref mappings ({@link PLATFORM_UPTIME_HEARTBEAT_URL_REF}
|
|
757
|
+
* and {@link platformBackupHeartbeatUrlRef}) stay reachable from the name
|
|
758
|
+
* alone. Throws on a name this module did not itself produce -- a
|
|
759
|
+
* programmer error, never a live value.
|
|
760
|
+
*/
|
|
761
|
+
export declare function heartbeatCheckUrlRef(name: string): string;
|
|
762
|
+
/**
|
|
763
|
+
* #540: the sorted, comma-joined {@link desiredHeartbeatCheckNames} as of the
|
|
764
|
+
* last successful reconcile -- recorded by `provisionPlatformHeartbeats`
|
|
765
|
+
* once it has created every missing check and deleted every stale one.
|
|
766
|
+
* {@link planHeartbeats} plans the reconcile step again only when the
|
|
767
|
+
* declared desired set has since changed (a backup service's database
|
|
768
|
+
* arrived, say), not on every apply: `driveApply`'s bootstrap loop
|
|
769
|
+
* (`bootstrap/drive.ts`) only ever terminates once a plan has zero steps, so
|
|
770
|
+
* a step planned unconditionally on every round -- as this one once was --
|
|
771
|
+
* never lets a platform with `hc` enabled (the default, #540) finish
|
|
772
|
+
* bootstrapping at all.
|
|
773
|
+
*/
|
|
774
|
+
export declare const PLATFORM_HEARTBEATS_APPLIED_SCOPE_KEY = "heartbeats_applied_desired";
|
|
677
775
|
/**
|
|
678
776
|
* Options for {@link planPlatform}, replacing four positional arguments that
|
|
679
777
|
* shifted on every merge that added a platform service (#186, #187, #188) --
|
|
@@ -721,6 +819,26 @@ export interface PlanPlatformOptions {
|
|
|
721
819
|
* "executor missing" shape {@link authExecutorMissing} has for auth.
|
|
722
820
|
*/
|
|
723
821
|
backupExecutorMissing?: boolean;
|
|
822
|
+
/** #540: why `hc` is blocked, or absent when it is not. */
|
|
823
|
+
hc?: string;
|
|
824
|
+
/**
|
|
825
|
+
* #540: true once `hc` is applied but the host built the platform
|
|
826
|
+
* executor with no heartbeats dependencies -- the heartbeat-checks step
|
|
827
|
+
* is blocked, not planned, the same "executor missing" shape
|
|
828
|
+
* {@link backupExecutorMissing} has for a backup.
|
|
829
|
+
*/
|
|
830
|
+
heartbeatsExecutorMissing?: boolean;
|
|
831
|
+
/**
|
|
832
|
+
* #540 round 2: true while the heartbeats dependencies are wired but no
|
|
833
|
+
* value is held under {@link PLATFORM_HC_API_KEY_REF} -- there is no
|
|
834
|
+
* bootstrap that mints hc's first superuser and project API key yet
|
|
835
|
+
* (issue #540's own "Decide"), so a fresh platform's heartbeats step
|
|
836
|
+
* would otherwise call hc's real Management API with no account and no
|
|
837
|
+
* key behind it. Blocked, not planned, until a human creates both by
|
|
838
|
+
* hand and holds the key under that ref; see this flag's own "Human
|
|
839
|
+
* steps" note in `platform-providers.ts`'s `FoundryPlatformStepExecutor.blockedSteps`.
|
|
840
|
+
*/
|
|
841
|
+
heartbeatsApiKeyMissing?: boolean;
|
|
724
842
|
};
|
|
725
843
|
/**
|
|
726
844
|
* Steps whose live state differs from the declared one in a way the recorded
|
|
@@ -750,6 +750,124 @@ export const PLATFORM_EDGE_APPLIED_GIT_REF_SCOPE_KEY = 'edge_applied_git_ref';
|
|
|
750
750
|
* (the {@link PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY} pattern). Not a secret.
|
|
751
751
|
*/
|
|
752
752
|
export const PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY = 'edge_platform_internal_url';
|
|
753
|
+
/**
|
|
754
|
+
* #540: the resource key `planHc` plans under and `FoundryPlatformStepExecutor`
|
|
755
|
+
* dispatches on -- the {@link PLATFORM_EDGE_KEY} shape for the platform's own
|
|
756
|
+
* heartbeat server (self-hosted Healthchecks). `runPlatformSteps` records
|
|
757
|
+
* the app's uuid under `coolify:app:platform:hc` once the step succeeds,
|
|
758
|
+
* which is what stops `planHc` planning it again.
|
|
759
|
+
*/
|
|
760
|
+
export const PLATFORM_HC_KEY = 'platform:hc';
|
|
761
|
+
/**
|
|
762
|
+
* #540: the hostname and `gitRef` (a Docker image tag, like `mail.gitRef`)
|
|
763
|
+
* the last successful `hc` run applied. `planHc` plans the step again when
|
|
764
|
+
* either differs from the declared value; a re-run patches the image tag
|
|
765
|
+
* and redeploys, and mints nothing new (`SECRET_KEY` is generated once and
|
|
766
|
+
* read back, never regenerated -- see `ensureGeneratedEnv`).
|
|
767
|
+
*/
|
|
768
|
+
export const PLATFORM_HC_APPLIED_HOSTNAME_SCOPE_KEY = 'hc_applied_hostname';
|
|
769
|
+
export const PLATFORM_HC_APPLIED_GIT_REF_SCOPE_KEY = 'hc_applied_git_ref';
|
|
770
|
+
/**
|
|
771
|
+
* #540 round 2: the held-key ref a human puts hc's first project API key
|
|
772
|
+
* under -- `deliverSecret`'s naming convention (`EDGE_URL`'s own pattern),
|
|
773
|
+
* `<service>:<scope>:<name>` like {@link PLATFORM_EDGE_KEY_REF}. Nothing in
|
|
774
|
+
* this package mints this key: `provisionPlatformHc` stands up an empty
|
|
775
|
+
* Healthchecks instance with no user, no project and so no key, and neither
|
|
776
|
+
* a `manage.py` command nor a documented API mints a project's own API key
|
|
777
|
+
* (confirmed against `healthchecks/healthchecks`'s own source, 2026-10) --
|
|
778
|
+
* only its web UI does, after a first superuser signs in by hand. Until a
|
|
779
|
+
* human does that and holds the key here, `FoundryPlatformStepExecutor
|
|
780
|
+
* .blockedSteps` (`platform-providers.ts`) reports the heartbeat checks
|
|
781
|
+
* blocked (`PlanPlatformOptions.blocked.heartbeatsApiKeyMissing`) rather
|
|
782
|
+
* than let `planHeartbeats` plan a step that would call hc's real
|
|
783
|
+
* Management API with no account and no key behind it -- see issue #540's
|
|
784
|
+
* own "Human steps" note. This package never reads the value back: whoever
|
|
785
|
+
* implements {@link HeartbeatsPort} against hc's real HTTP API is the host,
|
|
786
|
+
* and that implementation is where the key is actually used.
|
|
787
|
+
*/
|
|
788
|
+
export const PLATFORM_HC_API_KEY_REF = 'hc:platform:apiKey';
|
|
789
|
+
/**
|
|
790
|
+
* #540: the heartbeat check named after the uptime Worker (foundry#542,
|
|
791
|
+
* which is what actually pings it -- blocked today on edge#26, so nothing
|
|
792
|
+
* pings this check yet; it still exists so the Worker has a URL to deliver
|
|
793
|
+
* to the moment it can). `platformBackupHeartbeatCheckName` is the matching
|
|
794
|
+
* name for each platform database backup (`PLATFORM_BACKUP_SERVICE_KEYS`).
|
|
795
|
+
* Both names are also the prefix `provisionPlatformHeartbeats`
|
|
796
|
+
* (`platform-providers.ts`) uses to recognise which live Healthchecks
|
|
797
|
+
* checks are its own to delete when they are no longer wanted -- it never
|
|
798
|
+
* touches a check outside this naming scheme.
|
|
799
|
+
*/
|
|
800
|
+
export const PLATFORM_UPTIME_HEARTBEAT_CHECK_NAME = 'uptime-worker';
|
|
801
|
+
/** #540: see {@link PLATFORM_UPTIME_HEARTBEAT_CHECK_NAME}'s own doc comment. */
|
|
802
|
+
export function platformBackupHeartbeatCheckName(service) {
|
|
803
|
+
return `backup-${service}`;
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* #540: the held-key ref each heartbeat check's own ping URL is recorded
|
|
807
|
+
* under, once created -- `deliverSecret`'s ref-naming convention
|
|
808
|
+
* (`EDGE_URL`'s own pattern), read back by whoever owns that piece of
|
|
809
|
+
* scheduled work. `platformBackupHeartbeatUrlRef` is the per-service
|
|
810
|
+
* counterpart. Unlike a minted credential (CLAUDE.md platform-step rule 1),
|
|
811
|
+
* a ping URL is not retried-and-guarded the same way: it is closer to a
|
|
812
|
+
* resource id, so `provisionPlatformHeartbeats` simply records it again
|
|
813
|
+
* every time the check it names is (re)created.
|
|
814
|
+
*/
|
|
815
|
+
export const PLATFORM_UPTIME_HEARTBEAT_URL_REF = 'uptime:heartbeatUrl';
|
|
816
|
+
/** #540: see {@link PLATFORM_UPTIME_HEARTBEAT_URL_REF}'s own doc comment. */
|
|
817
|
+
export function platformBackupHeartbeatUrlRef(service) {
|
|
818
|
+
return `backup:${service}:heartbeatUrl`;
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* #540: every heartbeat check name foundry currently wants -- the uptime
|
|
822
|
+
* Worker's own, plus one per platform database backup that already has a
|
|
823
|
+
* recorded database id (`PLATFORM_BACKUP_SERVICE_KEYS`, the same "nothing to
|
|
824
|
+
* report yet" gate {@link planBackups} itself uses for a service with no
|
|
825
|
+
* database). Shared by {@link planHeartbeats} (to tell whether anything
|
|
826
|
+
* declared has changed since the last successful reconcile) and
|
|
827
|
+
* `provisionPlatformHeartbeats` (`platform-providers.ts`, to decide what to
|
|
828
|
+
* create or delete live) so the two can never compute a different desired
|
|
829
|
+
* set from each other.
|
|
830
|
+
*/
|
|
831
|
+
export function desiredHeartbeatCheckNames(recordedIds) {
|
|
832
|
+
const names = [PLATFORM_UPTIME_HEARTBEAT_CHECK_NAME];
|
|
833
|
+
for (const service of PLATFORM_BACKUP_SERVICE_KEYS) {
|
|
834
|
+
if (recordedIds[PLATFORM_BACKUP_DATABASE_SCOPE_KEYS[service]]) {
|
|
835
|
+
names.push(platformBackupHeartbeatCheckName(service));
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
return names;
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* #540: the held-key ref a check named `name` (one {@link desiredHeartbeatCheckNames}
|
|
842
|
+
* produced) records its ping URL under -- `platform-providers.ts`'s
|
|
843
|
+
* `provisionPlatformHeartbeats` looks this up rather than re-deriving it
|
|
844
|
+
* inline, so the two name-to-ref mappings ({@link PLATFORM_UPTIME_HEARTBEAT_URL_REF}
|
|
845
|
+
* and {@link platformBackupHeartbeatUrlRef}) stay reachable from the name
|
|
846
|
+
* alone. Throws on a name this module did not itself produce -- a
|
|
847
|
+
* programmer error, never a live value.
|
|
848
|
+
*/
|
|
849
|
+
export function heartbeatCheckUrlRef(name) {
|
|
850
|
+
if (name === PLATFORM_UPTIME_HEARTBEAT_CHECK_NAME)
|
|
851
|
+
return PLATFORM_UPTIME_HEARTBEAT_URL_REF;
|
|
852
|
+
for (const service of PLATFORM_BACKUP_SERVICE_KEYS) {
|
|
853
|
+
if (name === platformBackupHeartbeatCheckName(service))
|
|
854
|
+
return platformBackupHeartbeatUrlRef(service);
|
|
855
|
+
}
|
|
856
|
+
throw new Error(`heartbeatCheckUrlRef: unrecognised heartbeat check name '${name}'`);
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* #540: the sorted, comma-joined {@link desiredHeartbeatCheckNames} as of the
|
|
860
|
+
* last successful reconcile -- recorded by `provisionPlatformHeartbeats`
|
|
861
|
+
* once it has created every missing check and deleted every stale one.
|
|
862
|
+
* {@link planHeartbeats} plans the reconcile step again only when the
|
|
863
|
+
* declared desired set has since changed (a backup service's database
|
|
864
|
+
* arrived, say), not on every apply: `driveApply`'s bootstrap loop
|
|
865
|
+
* (`bootstrap/drive.ts`) only ever terminates once a plan has zero steps, so
|
|
866
|
+
* a step planned unconditionally on every round -- as this one once was --
|
|
867
|
+
* never lets a platform with `hc` enabled (the default, #540) finish
|
|
868
|
+
* bootstrapping at all.
|
|
869
|
+
*/
|
|
870
|
+
export const PLATFORM_HEARTBEATS_APPLIED_SCOPE_KEY = 'heartbeats_applied_desired';
|
|
753
871
|
/**
|
|
754
872
|
* #167: the platform's own diff engine, mirroring `diff.ts`'s
|
|
755
873
|
* `computePlanDetail` for the one shared {@link DeclaredPlatform} record
|
|
@@ -808,6 +926,17 @@ export function planPlatform(platform, live, options = {}) {
|
|
|
808
926
|
: (service) => `the platform's ${service} is blocked, not planned: every DNS record is written through edge, and edge is neither applied nor planned (${platform.edge.enabled
|
|
809
927
|
? (edge.blocked ?? 'its step is not planned')
|
|
810
928
|
: 'the platform does not enable edge'})`;
|
|
929
|
+
// #540: hc needs only edge's DNS, the same as mail and archon -- no
|
|
930
|
+
// auth/ZITADEL client, so it is planned here rather than after `authReady`
|
|
931
|
+
// further down.
|
|
932
|
+
const hc = planHc(platform, recordedIds, blocked.hc ?? dnsBlocked?.('heartbeat server'));
|
|
933
|
+
if (hc.step)
|
|
934
|
+
plannedSteps.push(hc.step);
|
|
935
|
+
// #540: the `authReady` pattern, for hc -- true once hc is either already
|
|
936
|
+
// applied or about to run earlier in this very plan, so the heartbeats
|
|
937
|
+
// reconcile below can be planned in the *same* round hc first comes up,
|
|
938
|
+
// the same way files/mail/archon are planned in auth's own first round.
|
|
939
|
+
const hcReady = hc.step !== null || Boolean(recordedIds[resourceKey('coolify', 'app', PLATFORM_HC_KEY)]);
|
|
811
940
|
const auth = planAuth(platform, recordedIds, blocked.auth ?? dnsBlocked?.('sign-in'), blocked.authExecutorMissing ?? false, drifted.auth ?? false, blocked.authOtherInputsMissing ?? []);
|
|
812
941
|
if (auth.step)
|
|
813
942
|
plannedSteps.push(auth.step);
|
|
@@ -841,6 +970,12 @@ export function planPlatform(platform, live, options = {}) {
|
|
|
841
970
|
// recorded is already visible to `recordedIds` on this same plan.
|
|
842
971
|
const backups = planBackups(byKey, claimed, recordedIds, blocked.backupExecutorMissing ?? false, drifted.backupStorage ?? false, drifted.backupStorageBlocked);
|
|
843
972
|
plannedSteps.push(...backups.steps);
|
|
973
|
+
// #540: last of all, so the backup databases `planBackups` just saw above
|
|
974
|
+
// are the same set this reconcile sees too -- one check per platform
|
|
975
|
+
// database backup, plus the uptime Worker's own.
|
|
976
|
+
const heartbeats = planHeartbeats(platform, recordedIds, hcReady, blocked.heartbeatsExecutorMissing ?? false, blocked.heartbeatsApiKeyMissing ?? false);
|
|
977
|
+
if (heartbeats.step)
|
|
978
|
+
plannedSteps.push(heartbeats.step);
|
|
844
979
|
// #251: one line per service whose recorded applied ref no longer matches
|
|
845
980
|
// what the next apply would use (or, for zitadel, what is actually live).
|
|
846
981
|
// Never pushed into `plannedSteps` -- see `VersionChangeItem`'s own doc
|
|
@@ -867,6 +1002,9 @@ export function planPlatform(platform, live, options = {}) {
|
|
|
867
1002
|
edge.blocked
|
|
868
1003
|
? null
|
|
869
1004
|
: versionChangeLine('edge', recordedIds[PLATFORM_EDGE_APPLIED_GIT_REF_SCOPE_KEY], platform.edge.gitRef, 'git ref, redeploy'),
|
|
1005
|
+
hc.blocked
|
|
1006
|
+
? null
|
|
1007
|
+
: versionChangeLine('hc', recordedIds[PLATFORM_HC_APPLIED_GIT_REF_SCOPE_KEY], platform.hc.gitRef, 'image, redeploy'),
|
|
870
1008
|
auth.blocked
|
|
871
1009
|
? null
|
|
872
1010
|
: versionNotAppliedLine('zitadel', Boolean(recordedIds[resourceKey('coolify', 'app', PLATFORM_AUTH_KEY)]), recordedIds[PLATFORM_AUTH_ZITADEL_APPLIED_IMAGE_TAG_SCOPE_KEY], VERSIONS.zitadel),
|
|
@@ -900,6 +1038,8 @@ export function planPlatform(platform, live, options = {}) {
|
|
|
900
1038
|
unmanaged.push({ description: ai.blocked, key: 'ai' });
|
|
901
1039
|
if (edge.blocked)
|
|
902
1040
|
unmanaged.push({ description: edge.blocked, key: 'edge' });
|
|
1041
|
+
if (hc.blocked)
|
|
1042
|
+
unmanaged.push({ description: hc.blocked, key: 'hc' });
|
|
903
1043
|
if (archon.blocked)
|
|
904
1044
|
unmanaged.push({ description: archon.blocked, key: 'archon' });
|
|
905
1045
|
// #398: the old integrations key stays pinned until Archon holds the new
|
|
@@ -914,6 +1054,8 @@ export function planPlatform(platform, live, options = {}) {
|
|
|
914
1054
|
});
|
|
915
1055
|
}
|
|
916
1056
|
unmanaged.push(...backups.blocked);
|
|
1057
|
+
if (heartbeats.blocked)
|
|
1058
|
+
unmanaged.push({ description: heartbeats.blocked, key: 'heartbeats' });
|
|
917
1059
|
return { plannedSteps: plannedSteps.map(withStepSystem), unmanaged, versionChanges };
|
|
918
1060
|
}
|
|
919
1061
|
/**
|
|
@@ -1826,6 +1968,136 @@ function planEdge(platform, recordedIds, executorBlocked) {
|
|
|
1826
1968
|
blocked: null,
|
|
1827
1969
|
};
|
|
1828
1970
|
}
|
|
1971
|
+
/**
|
|
1972
|
+
* #540: a self-hosted heartbeat server (Healthchecks) for the platform, for
|
|
1973
|
+
* now -- a Coolify Postgres plus one Docker-image app
|
|
1974
|
+
* (`healthchecks/healthchecks`), reached through edge's tunnel and DNS at
|
|
1975
|
+
* `platform.hc.hostname`, the {@link planEdge} bundling shape minus any
|
|
1976
|
+
* auth/ZITADEL client: `hc` is an internal operational tool, not something
|
|
1977
|
+
* an organisation member signs into. Whether this stays the platform's
|
|
1978
|
+
* heartbeat server long-term, instead of or alongside the hand-set-up
|
|
1979
|
+
* `hc.wtfalch.dev` instance, and what happens to that instance's own data,
|
|
1980
|
+
* are both still open "Decide" items on issue #540's own thread -- not
|
|
1981
|
+
* settled by this code.
|
|
1982
|
+
*
|
|
1983
|
+
* Idempotent on the recorded app id plus the applied hostname and `gitRef`,
|
|
1984
|
+
* for {@link planAuth}'s own reason: no platform reader reports this
|
|
1985
|
+
* Coolify app. A changed hostname or `gitRef` plans the step again (a
|
|
1986
|
+
* redeploy, no new secret -- `SECRET_KEY` is generated once by
|
|
1987
|
+
* `provisionPlatformHc` and read back, never regenerated).
|
|
1988
|
+
*
|
|
1989
|
+
* Blocked, not planned: `executorBlocked`, from `dnsBlocked` (edge neither
|
|
1990
|
+
* applied nor planned) or the host building no `hc` dependencies.
|
|
1991
|
+
*/
|
|
1992
|
+
function planHc(platform, recordedIds, executorBlocked) {
|
|
1993
|
+
if (!platform.hc.enabled)
|
|
1994
|
+
return { step: null, blocked: null };
|
|
1995
|
+
const { hostname, gitRef } = platform.hc;
|
|
1996
|
+
const applied = Boolean(recordedIds[resourceKey('coolify', 'app', PLATFORM_HC_KEY)]) &&
|
|
1997
|
+
recordedIds[PLATFORM_HC_APPLIED_HOSTNAME_SCOPE_KEY] === hostname &&
|
|
1998
|
+
recordedIds[PLATFORM_HC_APPLIED_GIT_REF_SCOPE_KEY] === gitRef;
|
|
1999
|
+
if (applied)
|
|
2000
|
+
return { step: null, blocked: null };
|
|
2001
|
+
if (executorBlocked)
|
|
2002
|
+
return { step: null, blocked: executorBlocked };
|
|
2003
|
+
return {
|
|
2004
|
+
step: {
|
|
2005
|
+
action: {
|
|
2006
|
+
system: 'coolify',
|
|
2007
|
+
kind: 'app',
|
|
2008
|
+
key: PLATFORM_HC_KEY,
|
|
2009
|
+
desired: { hostname, gitRef },
|
|
2010
|
+
},
|
|
2011
|
+
step: {
|
|
2012
|
+
description: `provision the heartbeat server (Healthchecks) at ${hostname}, with its own database`,
|
|
2013
|
+
destroys: false,
|
|
2014
|
+
migrates: false,
|
|
2015
|
+
},
|
|
2016
|
+
},
|
|
2017
|
+
blocked: null,
|
|
2018
|
+
};
|
|
2019
|
+
}
|
|
2020
|
+
/**
|
|
2021
|
+
* #540: one reconcile step for every heartbeat check foundry owns -- the
|
|
2022
|
+
* uptime Worker's own check plus one per platform database backup
|
|
2023
|
+
* (`PLATFORM_BACKUP_SERVICE_KEYS`, filtered the same way {@link planBackups}
|
|
2024
|
+
* is to "has a recorded database", so a service with no database yet gets
|
|
2025
|
+
* no standing check either). The actual create-missing/delete-stale
|
|
2026
|
+
* reconciling against the live Healthchecks list (`HeartbeatsPort.listChecks`,
|
|
2027
|
+
* `platform-providers.ts`) happens inside `provisionPlatformHeartbeats`
|
|
2028
|
+
* itself, not here -- this function only decides *whether* that step runs
|
|
2029
|
+
* at all: never before `hc` is ready (`hcReady`, the {@link planPlatform}
|
|
2030
|
+
* own `authReady` pattern -- already applied, or about to be, earlier in
|
|
2031
|
+
* this same plan), never with no heartbeats dependencies wired, and never
|
|
2032
|
+
* again once {@link desiredHeartbeatCheckNames} already matches
|
|
2033
|
+
* {@link PLATFORM_HEARTBEATS_APPLIED_SCOPE_KEY}'s recorded value.
|
|
2034
|
+
*
|
|
2035
|
+
* That last check is the point: an earlier version of this function planned
|
|
2036
|
+
* the step unconditionally every round, on the theory that
|
|
2037
|
+
* `provisionPlatformHeartbeats` listing live checks first was idempotency
|
|
2038
|
+
* enough. It is, for what it creates -- but `bootstrap/drive.ts`'s
|
|
2039
|
+
* `driveApply` (the loop `bootstrapPlatform` runs to actually stand a
|
|
2040
|
+
* platform up) only stops once a round's plan has zero steps, and `hc` is
|
|
2041
|
+
* enabled by default (`bootstrap/declare.ts`), so a platform that never
|
|
2042
|
+
* recorded "nothing left to reconcile" could never finish bootstrapping at
|
|
2043
|
+
* all -- it would hit `driveApply`'s round cap on every single run. Compare
|
|
2044
|
+
* against the planner's own recorded desired-set marker, declared-to-declared,
|
|
2045
|
+
* so a plan with nothing left to do actually has nothing left to do.
|
|
2046
|
+
*
|
|
2047
|
+
* #540 round 2: also withheld while `apiKeyMissing` -- no human has held a
|
|
2048
|
+
* project API key under {@link PLATFORM_HC_API_KEY_REF} yet, so there is
|
|
2049
|
+
* nothing for the host's own `HeartbeatsPort` to authenticate a real call
|
|
2050
|
+
* with. Without this, a fresh platform's heartbeats step is ready
|
|
2051
|
+
* (`hcReady`) the same round `hc` is merely *planned*, long before anyone
|
|
2052
|
+
* has signed in to it to create that key.
|
|
2053
|
+
*/
|
|
2054
|
+
function planHeartbeats(platform, recordedIds, hcReady, executorMissing, apiKeyMissing) {
|
|
2055
|
+
if (!platform.hc.enabled) {
|
|
2056
|
+
// Removing checks is not planned. The recorded marker names the checks
|
|
2057
|
+
// foundry's last reconcile wanted, so they are foundry's own; report
|
|
2058
|
+
// them rather than leave them unseen.
|
|
2059
|
+
const left = recordedIds[PLATFORM_HEARTBEATS_APPLIED_SCOPE_KEY];
|
|
2060
|
+
return {
|
|
2061
|
+
step: null,
|
|
2062
|
+
blocked: left
|
|
2063
|
+
? `the heartbeat server is not enabled, but checks foundry created remain on it (${left}); removing them is not planned, delete them in Healthchecks by hand`
|
|
2064
|
+
: null,
|
|
2065
|
+
};
|
|
2066
|
+
}
|
|
2067
|
+
if (!hcReady) {
|
|
2068
|
+
return {
|
|
2069
|
+
step: null,
|
|
2070
|
+
blocked: 'the heartbeat checks are blocked, not planned: platform:hc has not been applied yet',
|
|
2071
|
+
};
|
|
2072
|
+
}
|
|
2073
|
+
if (executorMissing) {
|
|
2074
|
+
return {
|
|
2075
|
+
step: null,
|
|
2076
|
+
blocked: 'the heartbeat checks are blocked, not planned: the host built the platform executor without its heartbeats dependencies',
|
|
2077
|
+
};
|
|
2078
|
+
}
|
|
2079
|
+
if (apiKeyMissing) {
|
|
2080
|
+
return {
|
|
2081
|
+
step: null,
|
|
2082
|
+
blocked: `the heartbeat checks are blocked, not planned: no project API key is held yet for hc (${PLATFORM_HC_API_KEY_REF}); a human must sign in to hc, create the first superuser account and a project, mint that project's API key, and hold it under that ref (issue #540 "Human steps") before this step can call hc's check-creation API`,
|
|
2083
|
+
};
|
|
2084
|
+
}
|
|
2085
|
+
const desired = [...desiredHeartbeatCheckNames(recordedIds)].sort().join(',');
|
|
2086
|
+
if (recordedIds[PLATFORM_HEARTBEATS_APPLIED_SCOPE_KEY] === desired) {
|
|
2087
|
+
return { step: null, blocked: null };
|
|
2088
|
+
}
|
|
2089
|
+
return {
|
|
2090
|
+
step: {
|
|
2091
|
+
action: { system: 'heartbeats', kind: 'reconcile', key: 'platform', desired: {} },
|
|
2092
|
+
step: {
|
|
2093
|
+
description: 'reconcile heartbeat checks for the uptime Worker and every platform backup service',
|
|
2094
|
+
destroys: false,
|
|
2095
|
+
migrates: false,
|
|
2096
|
+
},
|
|
2097
|
+
},
|
|
2098
|
+
blocked: null,
|
|
2099
|
+
};
|
|
2100
|
+
}
|
|
1829
2101
|
/**
|
|
1830
2102
|
* #172: the platform's AI host (https://github.com/wtfalch/ai) -- a Coolify
|
|
1831
2103
|
* Postgres plus three apps built from that repo's root Dockerfile, one per
|
|
@@ -182,6 +182,18 @@ export interface FoundryPlatformStepExecutorOptions {
|
|
|
182
182
|
* planned.
|
|
183
183
|
*/
|
|
184
184
|
backup?: PlatformBackupDeps;
|
|
185
|
+
/**
|
|
186
|
+
* #540: dependencies for `coolify:app` keyed {@link PLATFORM_HC_KEY} --
|
|
187
|
+
* optional for the same reason as `auth`; a host that declares
|
|
188
|
+
* `platform.hc.enabled` without it gets the step reported as blocked.
|
|
189
|
+
*/
|
|
190
|
+
hc?: PlatformHcDeps;
|
|
191
|
+
/**
|
|
192
|
+
* #540: dependencies for the `heartbeats:reconcile` step -- optional for
|
|
193
|
+
* the same reason as `auth`; `hc` applied with this unset gets the
|
|
194
|
+
* heartbeat checks reported as blocked rather than planned.
|
|
195
|
+
*/
|
|
196
|
+
heartbeats?: PlatformHeartbeatsDeps;
|
|
185
197
|
}
|
|
186
198
|
/**
|
|
187
199
|
* The real {@link PlatformStepExecutor} for the two steps built so far:
|
|
@@ -261,6 +273,8 @@ export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor
|
|
|
261
273
|
private readonly edge?;
|
|
262
274
|
private readonly archon?;
|
|
263
275
|
private readonly backup?;
|
|
276
|
+
private readonly hc?;
|
|
277
|
+
private readonly heartbeats?;
|
|
264
278
|
constructor(cloudflare: Pick<CloudflarePort, 'findZoneByApex' | 'createZone'>, declaredOrgStore: Pick<DeclaredOrgStore, 'findOrgIdForZone' | 'list' | 'get'>, options?: FoundryPlatformStepExecutorOptions);
|
|
265
279
|
blockedSteps(platform: DeclaredPlatform, recordedIds?: Readonly<Record<string, string>>): Promise<{
|
|
266
280
|
auth?: string;
|
|
@@ -273,6 +287,9 @@ export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor
|
|
|
273
287
|
archon?: string;
|
|
274
288
|
mail?: string;
|
|
275
289
|
backupExecutorMissing?: boolean;
|
|
290
|
+
hc?: string;
|
|
291
|
+
heartbeatsExecutorMissing?: boolean;
|
|
292
|
+
heartbeatsApiKeyMissing?: boolean;
|
|
276
293
|
}>;
|
|
277
294
|
/**
|
|
278
295
|
* Whether Archon's live Access policy lets through anyone but the declared
|
|
@@ -676,6 +693,17 @@ export interface PlatformAiRuntime {
|
|
|
676
693
|
migrateIntervalMs?: number;
|
|
677
694
|
/** Tries at handing a secret to the host (default 3): the management key and the signing key both use this. */
|
|
678
695
|
deliverAttempts?: number;
|
|
696
|
+
/**
|
|
697
|
+
* #543: injected for tests, against a fake runner host -- the runner's own
|
|
698
|
+
* deploy wait now does a `'ready'` probe of its `/api/health`, the same
|
|
699
|
+
* `DeployHealthRuntime.fetch` field `provisionPlatformMail`/
|
|
700
|
+
* `provisionPlatformArchon` already take. Defaults to the global `fetch`.
|
|
701
|
+
*/
|
|
702
|
+
fetch?: typeof fetch;
|
|
703
|
+
/** How long to wait for the runner to answer ready (default 5 minutes). */
|
|
704
|
+
readyTimeoutMs?: number;
|
|
705
|
+
/** Pause between readiness polls (default 5 seconds). */
|
|
706
|
+
readyIntervalMs?: number;
|
|
679
707
|
}
|
|
680
708
|
/** The ref the AI management key is delivered under; the host's `AiOrganisationPort` adapter reads it back. */
|
|
681
709
|
export declare const PLATFORM_AI_MANAGEMENT_KEY_REF = "ai:platform:managementKey";
|
|
@@ -849,6 +877,17 @@ export interface PlatformFilesRuntime {
|
|
|
849
877
|
migrateIntervalMs?: number;
|
|
850
878
|
/** Tries at handing each secret to the host (default 3). */
|
|
851
879
|
deliverAttempts?: number;
|
|
880
|
+
/**
|
|
881
|
+
* #543: injected for tests, against a fake runner host -- the runner's own
|
|
882
|
+
* deploy wait now does a `'ready'` probe of its `/api/health`, same as
|
|
883
|
+
* `provisionPlatformMail`/`provisionPlatformArchon` already take. Defaults
|
|
884
|
+
* to the global `fetch`.
|
|
885
|
+
*/
|
|
886
|
+
fetch?: typeof fetch;
|
|
887
|
+
/** How long to wait for the runner to answer ready (default 5 minutes). */
|
|
888
|
+
readyTimeoutMs?: number;
|
|
889
|
+
/** Pause between readiness polls (default 5 seconds). */
|
|
890
|
+
readyIntervalMs?: number;
|
|
852
891
|
}
|
|
853
892
|
/**
|
|
854
893
|
* #171: how the host signs a files management request with the key this step
|
|
@@ -903,6 +942,30 @@ export interface PlatformEdgeDeps {
|
|
|
903
942
|
/** Clock, sleep and fetch for the readiness wait and delivery retries; real ones by default. */
|
|
904
943
|
runtime?: PlatformEdgeRuntime;
|
|
905
944
|
}
|
|
945
|
+
export interface PlatformHcRuntime {
|
|
946
|
+
fetch?: typeof fetch;
|
|
947
|
+
sleep?: (ms: number) => Promise<void>;
|
|
948
|
+
now?: () => number;
|
|
949
|
+
readyTimeoutMs?: number;
|
|
950
|
+
readyIntervalMs?: number;
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* #540: dependencies for `coolify:app` keyed {@link PLATFORM_HC_KEY} -- the
|
|
954
|
+
* {@link PlatformEdgeDeps} bundling shape minus a Cloudflare token of its
|
|
955
|
+
* own: `hc` writes its tunnel and DNS record through edge the same way
|
|
956
|
+
* mail and archon do, and needs no `heldKeys`/`deliverSecret` at all --
|
|
957
|
+
* `SECRET_KEY` (Django's session-signing key) is generated once by
|
|
958
|
+
* {@link provisionPlatformHc} itself and never leaves this Coolify app.
|
|
959
|
+
*/
|
|
960
|
+
export interface PlatformHcDeps {
|
|
961
|
+
coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'findApplicationByName' | 'getApplication' | 'createDockerImageApplication' | 'createDockerfileApplication' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'bulkUpsertEnvs' | 'listEnvs' | 'updateApplication' | 'deploy' | 'deployAndWait' | 'raw'>;
|
|
962
|
+
/** F4.dns: every DNS record call, through edge at its recorded address (see {@link PlatformDnsFactory}). */
|
|
963
|
+
dns: PlatformDnsFactory;
|
|
964
|
+
/** F4.tunnels: every tunnel call, through edge at its recorded address (see {@link PlatformTunnelsFactory}). */
|
|
965
|
+
tunnels: PlatformTunnelsFactory;
|
|
966
|
+
/** Clock, sleep and fetch for the readiness wait; real ones by default. */
|
|
967
|
+
runtime?: PlatformHcRuntime;
|
|
968
|
+
}
|
|
906
969
|
/**
|
|
907
970
|
* #174: everything {@link provisionPlatformIntegrations} needs beyond
|
|
908
971
|
* {@link PlatformStepExecutionContext} -- a bundle, like
|
|
@@ -977,6 +1040,91 @@ export interface PlatformBackupDeps {
|
|
|
977
1040
|
/** A cron string or one of Coolify's names (`daily`, `weekly`, ...). Default `'daily'`. */
|
|
978
1041
|
frequency?: string;
|
|
979
1042
|
}
|
|
1043
|
+
/** One check as self-hosted Healthchecks' own Management API reports it. */
|
|
1044
|
+
export interface HeartbeatCheck {
|
|
1045
|
+
id: string;
|
|
1046
|
+
name: string;
|
|
1047
|
+
pingUrl: string;
|
|
1048
|
+
}
|
|
1049
|
+
/**
|
|
1050
|
+
* #540: the heartbeat server's own Management API -- a project API key in a
|
|
1051
|
+
* header, `POST /api/v3/checks/` with `name`/`unique: ['name']` to upsert a
|
|
1052
|
+
* check idempotently by name, `GET /api/v3/checks/` to list every one,
|
|
1053
|
+
* `DELETE /api/v3/checks/<uuid>` to remove one -- restated here as the port
|
|
1054
|
+
* `provisionPlatformHeartbeats` plans and provisions against, never a real
|
|
1055
|
+
* HTTP client: this package never connects itself (`README.md`), so the
|
|
1056
|
+
* host implements this against its own running `hc` instance and its own
|
|
1057
|
+
* project API key, the same seam every other real I/O in this file crosses.
|
|
1058
|
+
*/
|
|
1059
|
+
export interface HeartbeatsPort {
|
|
1060
|
+
listChecks(): Promise<HeartbeatCheck[]>;
|
|
1061
|
+
/** Creates a check named `name`, or returns the existing one of that name -- the Management API's own upsert. */
|
|
1062
|
+
createCheck(name: string): Promise<HeartbeatCheck>;
|
|
1063
|
+
/**
|
|
1064
|
+
* #540 round 2: pauses a check (`POST /api/v3/checks/<uuid>/pause/`) so it
|
|
1065
|
+
* reads "paused" rather than "down" while nothing actually pings it yet --
|
|
1066
|
+
* both the uptime Worker's own check and every backup check (see this
|
|
1067
|
+
* file's own doc comment on `PlatformHeartbeatsDeps` for why). Sending a
|
|
1068
|
+
* check a ping resumes it automatically (Healthchecks' own documented
|
|
1069
|
+
* behaviour), so foundry never has to call the reverse of this: once a
|
|
1070
|
+
* real pinger exists, the check un-pauses itself on its first ping.
|
|
1071
|
+
* `provisionPlatformHeartbeats` calls this exactly once, right after
|
|
1072
|
+
* `createCheck`, and never again on a check it merely finds already live
|
|
1073
|
+
* -- a check already being pinged, or one an operator unpaused by hand,
|
|
1074
|
+
* must never be paused again by a reconcile run.
|
|
1075
|
+
*/
|
|
1076
|
+
pauseCheck(id: string): Promise<void>;
|
|
1077
|
+
deleteCheck(id: string): Promise<void>;
|
|
1078
|
+
}
|
|
1079
|
+
/**
|
|
1080
|
+
* #540: dependencies for the `heartbeats:reconcile` step
|
|
1081
|
+
* (`platform-diff.ts`'s `planHeartbeats`).
|
|
1082
|
+
*
|
|
1083
|
+
* #540 round 2: `resolve` is new. It guards only the *delete* half of
|
|
1084
|
+
* {@link provisionPlatformHeartbeats} -- a live check is only ever deleted
|
|
1085
|
+
* when foundry itself holds a ping URL for its exact name
|
|
1086
|
+
* (`heartbeatCheckUrlRef`, `platform-diff.ts`), proof this run or an
|
|
1087
|
+
* earlier one created it. Without that guard, a hand-made check sharing
|
|
1088
|
+
* foundry's naming scheme (an operator's own `uptime-worker`, say, made
|
|
1089
|
+
* before foundry ever ran) could be deleted the moment that name was not
|
|
1090
|
+
* yet desired -- a backup service with no recorded database yet is in
|
|
1091
|
+
* `provisionPlatformHeartbeats`'s own `ownedNames` but not `desiredSet`.
|
|
1092
|
+
* `resolve` is never consulted on the *create* half: a check already live
|
|
1093
|
+
* under a desired name is always treated as foundry's, the way it always
|
|
1094
|
+
* was, and (the round-2 fix for a second bug) its ping address is now
|
|
1095
|
+
* always re-saved rather than only on the create that first makes it live
|
|
1096
|
+
* -- see `put`'s own doc comment below for why that half carries no
|
|
1097
|
+
* adoption risk the way deleting does.
|
|
1098
|
+
*
|
|
1099
|
+
* `put` records a check's own ping URL -- on every create, and (new this
|
|
1100
|
+
* round) on every run that finds a desired check already live, not only
|
|
1101
|
+
* the run that created it. Before this fix, a save that failed once, or a
|
|
1102
|
+
* run that died between create and save, or a host that wired `heldKeys`
|
|
1103
|
+
* only after the original create, lost that address for good: the next run
|
|
1104
|
+
* found the check already live and never saved anything, despite this very
|
|
1105
|
+
* doc comment already claiming the step "can simply ask the live check
|
|
1106
|
+
* again." Required whenever an address actually needs saving -- optional
|
|
1107
|
+
* only so a host without one still compiles, in which case nothing here
|
|
1108
|
+
* can ever be created or re-confirmed, and the step throws rather than
|
|
1109
|
+
* silently drop an address: CLAUDE.md platform-step rule 1 is written for a
|
|
1110
|
+
* minted credential, not a ping URL (a lost one is simply re-asked from the
|
|
1111
|
+
* live check, never re-minted), but the rule's "never silently lose it"
|
|
1112
|
+
* half applies just as well here.
|
|
1113
|
+
*
|
|
1114
|
+
* Both fields are optional only so a host without either still compiles;
|
|
1115
|
+
* while `PlatformHeartbeatsDeps` is wired at all, `FoundryPlatformStepExecutor
|
|
1116
|
+
* .blockedSteps`'s own `heartbeatsApiKeyMissing` check blocks the step
|
|
1117
|
+
* until a project API key is held under {@link PLATFORM_HC_API_KEY_REF}, so
|
|
1118
|
+
* the live Healthchecks calls below are never reached with nothing behind
|
|
1119
|
+
* them.
|
|
1120
|
+
*/
|
|
1121
|
+
export interface PlatformHeartbeatsDeps {
|
|
1122
|
+
heartbeats: HeartbeatsPort;
|
|
1123
|
+
heldKeys?: {
|
|
1124
|
+
resolve(ref: string): Promise<string | null>;
|
|
1125
|
+
put?(ref: string, value: string): Promise<void>;
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
980
1128
|
/**
|
|
981
1129
|
* The real {@link PlatformCloudflareReader}: the platform's apex zone as
|
|
982
1130
|
* `planApexZone` expects to see it, and nothing else. The zone's DNS records
|
|
@@ -1006,7 +1154,7 @@ export declare class ProvidersPlatformCloudflareReader implements PlatformCloudf
|
|
|
1006
1154
|
export declare class ProvidersPlatformCoolifyReader implements PlatformCoolifyReader {
|
|
1007
1155
|
private readonly client;
|
|
1008
1156
|
private readonly platformStore;
|
|
1009
|
-
constructor(client: Pick<CoolifyPort, 'getApplication' | 'getDatabase' | 'listDatabaseBackups'>, platformStore: Pick<PlatformStore, 'getServiceIds'>);
|
|
1157
|
+
constructor(client: Pick<CoolifyPort, 'getApplication' | 'getDatabase' | 'listDatabaseBackups' | 'raw'>, platformStore: Pick<PlatformStore, 'getServiceIds'>);
|
|
1010
1158
|
listResources(): Promise<LiveResource[]>;
|
|
1011
1159
|
}
|
|
1012
1160
|
/** Scope id (no `:`): the share domains the runner's Coolify `domains` was last set to, comma-joined. */
|