@treeseed/sdk 0.12.25 → 0.12.27
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/README.md +1 -1
- package/dist/guarantees/index.js +15 -1
- package/dist/hosting/builtins.js +4 -4
- package/dist/hosting/contracts.d.ts +3 -3
- package/dist/hosting/graph.d.ts +4 -4
- package/dist/hosting/graph.js +17 -17
- package/dist/operations/agent-tools.d.ts +1 -1
- package/dist/operations/agent-tools.js +1 -1
- package/dist/operations/providers/default.js +2 -2
- package/dist/operations/services/config-runtime.d.ts +9 -9
- package/dist/operations/services/config-runtime.js +10 -10
- package/dist/operations/services/deploy.d.ts +8 -8
- package/dist/operations/services/deploy.js +151 -151
- package/dist/operations/services/github-automation.d.ts +6 -6
- package/dist/operations/services/github-automation.js +8 -8
- package/dist/operations/services/hub-launch.d.ts +1 -1
- package/dist/operations/services/hub-launch.js +1 -1
- package/dist/operations/services/package-adapters.d.ts +2 -2
- package/dist/operations/services/package-adapters.js +1 -1
- package/dist/operations/services/project-host-operations.d.ts +1 -1
- package/dist/operations/services/project-host-operations.js +1 -1
- package/dist/operations/services/project-platform.d.ts +8 -8
- package/dist/operations/services/project-platform.js +31 -31
- package/dist/operations/services/project-web-monitor.d.ts +1 -1
- package/dist/operations/services/project-web-monitor.js +4 -4
- package/dist/operations/services/railway-deploy.d.ts +4 -4
- package/dist/operations/services/railway-deploy.js +7 -7
- package/dist/operations/services/repository-save-orchestrator.js +3 -3
- package/dist/operations/services/template-secret-sync.d.ts +1 -1
- package/dist/operations/services/template-secret-sync.js +3 -3
- package/dist/reconcile/builtin-adapters.js +137 -44
- package/dist/reconcile/contracts.d.ts +1 -1
- package/dist/reconcile/engine.d.ts +4 -4
- package/dist/reconcile/engine.js +12 -12
- package/dist/reconcile/providers/release-private.d.ts +1 -1
- package/dist/scripts/tenant-deploy.js +9 -9
- package/dist/scripts/tenant-destroy.js +7 -7
- package/dist/scripts/tenant-workflow-action.js +4 -4
- package/dist/scripts/test-scaffold.js +2 -2
- package/dist/scripts/workspace-command-e2e.js +4 -4
- package/dist/sdk-types.d.ts +2 -2
- package/dist/treedx/generated/openapi-types.d.ts +11 -11
- package/dist/treedx/types.d.ts +15 -15
- package/dist/workflow/operations.d.ts +6 -4
- package/dist/workflow/operations.js +46 -23
- package/dist/workflow.d.ts +0 -9
- package/package.json +1 -1
|
@@ -56,10 +56,12 @@ import {
|
|
|
56
56
|
deleteRailwayService,
|
|
57
57
|
deleteRailwayVolume,
|
|
58
58
|
deployRailwayServiceInstance,
|
|
59
|
+
inspectRailwayServiceDeploymentHealth,
|
|
59
60
|
listRailwayEnvironmentServices,
|
|
60
61
|
getRailwayServiceInstance,
|
|
61
62
|
getRailwayProject,
|
|
62
63
|
listRailwayCustomDomains,
|
|
64
|
+
listRailwayServiceDomains,
|
|
63
65
|
listRailwayEnvironments,
|
|
64
66
|
listRailwayProjects,
|
|
65
67
|
listRailwayServices,
|
|
@@ -297,7 +299,7 @@ function buildPackageWorkflowAdapter() {
|
|
|
297
299
|
},
|
|
298
300
|
apply(input) {
|
|
299
301
|
const packageId = typeof input.unit.spec.packageId === "string" ? input.unit.spec.packageId : typeof input.unit.metadata.packageId === "string" ? input.unit.metadata.packageId : null;
|
|
300
|
-
if (packageId && input.context.
|
|
302
|
+
if (packageId && input.context.planOnly !== true && input.diff.action !== "noop") {
|
|
301
303
|
const sync = syncTreeseedPackageWorkflows({
|
|
302
304
|
root: input.context.tenantRoot,
|
|
303
305
|
packageId,
|
|
@@ -835,7 +837,7 @@ function buildLocalDockerComposeAdapter() {
|
|
|
835
837
|
const composeFiles = observedOrResolvedComposeFiles(input);
|
|
836
838
|
const cwd = String(input.observed.live.cwd ?? input.context.tenantRoot);
|
|
837
839
|
const projectName = String(input.unit.spec.projectName ?? "treeseed-capacity-provider");
|
|
838
|
-
if (input.context.
|
|
840
|
+
if (input.context.planOnly !== true && composeFiles.length > 0 && composeFiles.every((composeFile) => existsSync(composeFile))) {
|
|
839
841
|
const result = runDockerCompose({
|
|
840
842
|
composeFiles,
|
|
841
843
|
projectName,
|
|
@@ -1269,7 +1271,7 @@ function buildLocalProcessAdapter() {
|
|
|
1269
1271
|
},
|
|
1270
1272
|
async destroy(input) {
|
|
1271
1273
|
const surfaces = Array.isArray(input.unit.spec.surfaces) ? input.unit.spec.surfaces.filter((entry) => typeof entry === "string") : [String(input.unit.spec.processId ?? input.unit.logicalName)];
|
|
1272
|
-
if (input.context.
|
|
1274
|
+
if (input.context.planOnly !== true) {
|
|
1273
1275
|
await runManagedDevAction({
|
|
1274
1276
|
tenantRoot: input.context.tenantRoot,
|
|
1275
1277
|
action: "stop",
|
|
@@ -1796,7 +1798,7 @@ function buildReleaseGateAdapter() {
|
|
|
1796
1798
|
parentContext: input.context,
|
|
1797
1799
|
selector,
|
|
1798
1800
|
target: { kind: "persistent", scope: environment },
|
|
1799
|
-
|
|
1801
|
+
planOnly: input.context.planOnly === true
|
|
1800
1802
|
});
|
|
1801
1803
|
return genericResult(input, { ...input.observed.live, nested, fingerprint: input.unit.spec.fingerprint });
|
|
1802
1804
|
}
|
|
@@ -1905,7 +1907,7 @@ function buildWorkflowMetaAdapter() {
|
|
|
1905
1907
|
parentContext: input.context,
|
|
1906
1908
|
selector,
|
|
1907
1909
|
target: input.context.target,
|
|
1908
|
-
|
|
1910
|
+
planOnly: input.context.planOnly === true
|
|
1909
1911
|
});
|
|
1910
1912
|
return genericResult(input, { ...input.observed.live, nested, fingerprint });
|
|
1911
1913
|
}
|
|
@@ -1999,7 +2001,7 @@ function buildCloudflareEnv(input) {
|
|
|
1999
2001
|
};
|
|
2000
2002
|
}
|
|
2001
2003
|
function hasLiveResourceId(value) {
|
|
2002
|
-
return typeof value === "string" && value.length > 0 && !value.startsWith("
|
|
2004
|
+
return typeof value === "string" && value.length > 0 && !value.startsWith("plan-") && !value.startsWith("local-") && !value.endsWith("-id") && !value.endsWith("-preview-id");
|
|
2003
2005
|
}
|
|
2004
2006
|
function buildRailwayEnv(input, scope) {
|
|
2005
2007
|
const values = resolveReconcileEnvironmentValues(input, scope);
|
|
@@ -2374,6 +2376,58 @@ async function ensureRailwayCustomDomain(input, service, domain, env, identifier
|
|
|
2374
2376
|
}
|
|
2375
2377
|
return ensured.domain;
|
|
2376
2378
|
}
|
|
2379
|
+
async function observeRailwayCustomDomainLive(input, domain) {
|
|
2380
|
+
if (!domain) {
|
|
2381
|
+
return null;
|
|
2382
|
+
}
|
|
2383
|
+
const scope = input.context.target.kind === "persistent" ? input.context.target.scope : "staging";
|
|
2384
|
+
const serviceKey = String(input.unit.metadata.serviceKey ?? "api").trim();
|
|
2385
|
+
const topology = await resolveRailwayTopologyForScope(input, scope, {
|
|
2386
|
+
refresh: true,
|
|
2387
|
+
serviceKeys: [serviceKey],
|
|
2388
|
+
includeInstances: false,
|
|
2389
|
+
includeVariables: false
|
|
2390
|
+
});
|
|
2391
|
+
const entry = topology.services.get(serviceKey) ?? null;
|
|
2392
|
+
const identifiers = {
|
|
2393
|
+
projectId: entry?.project?.id ?? null,
|
|
2394
|
+
environmentId: entry?.environment?.id ?? null,
|
|
2395
|
+
serviceId: entry?.service?.id ?? null
|
|
2396
|
+
};
|
|
2397
|
+
if (!identifiers.projectId || !identifiers.environmentId || !identifiers.serviceId) {
|
|
2398
|
+
return null;
|
|
2399
|
+
}
|
|
2400
|
+
const [customDomains, serviceDomains] = await Promise.all([
|
|
2401
|
+
listRailwayCustomDomains({
|
|
2402
|
+
projectId: identifiers.projectId,
|
|
2403
|
+
environmentId: identifiers.environmentId,
|
|
2404
|
+
serviceId: identifiers.serviceId,
|
|
2405
|
+
env: topology.env
|
|
2406
|
+
}),
|
|
2407
|
+
listRailwayServiceDomains({
|
|
2408
|
+
projectId: identifiers.projectId,
|
|
2409
|
+
environmentId: identifiers.environmentId,
|
|
2410
|
+
serviceId: identifiers.serviceId,
|
|
2411
|
+
env: topology.env
|
|
2412
|
+
})
|
|
2413
|
+
]);
|
|
2414
|
+
const customDomain = customDomains.find((entry2) => entry2.domain === domain) ?? null;
|
|
2415
|
+
if (!customDomain) {
|
|
2416
|
+
return null;
|
|
2417
|
+
}
|
|
2418
|
+
const serviceDomain = serviceDomains.find((entry2) => entry2.kind === "service") ?? null;
|
|
2419
|
+
const normalized = normalizeRailwayDomainPayload({
|
|
2420
|
+
...customDomain,
|
|
2421
|
+
serviceDomain: serviceDomain?.domain ?? null
|
|
2422
|
+
});
|
|
2423
|
+
return normalized?.domain ? normalized : customDomain;
|
|
2424
|
+
}
|
|
2425
|
+
function railwayCustomDomainHasDnsRequirements(live) {
|
|
2426
|
+
if (!live) {
|
|
2427
|
+
return false;
|
|
2428
|
+
}
|
|
2429
|
+
return Array.isArray(live.dnsRecords) && live.dnsRecords.length > 0 || typeof live.serviceDomain === "string" && live.serviceDomain.trim().length > 0 || typeof live.verificationDnsHost === "string" && live.verificationDnsHost.trim().length > 0 && typeof live.verificationToken === "string" && live.verificationToken.trim().length > 0;
|
|
2430
|
+
}
|
|
2377
2431
|
function collectCloudflareEnvironmentSync(input) {
|
|
2378
2432
|
const target = toDeployTarget(input.context.target);
|
|
2379
2433
|
const scope = scopeFromTarget(target);
|
|
@@ -2434,7 +2488,7 @@ function verificationCheck(key, description, source, options) {
|
|
|
2434
2488
|
};
|
|
2435
2489
|
}
|
|
2436
2490
|
function summarizeVerification(unitId, checks, warnings = []) {
|
|
2437
|
-
const missing = checks.flatMap((check) => !check.exists ? [`${check.key}: ${check.description}`] : []);
|
|
2491
|
+
const missing = checks.flatMap((check) => !check.exists ? [`${check.key}: ${check.issues.join("; ") || check.description}`] : []);
|
|
2438
2492
|
const drifted = checks.flatMap(
|
|
2439
2493
|
(check) => check.exists && (!check.configured || !check.ready || !check.verified || check.issues.length > 0) ? [`${check.key}: ${check.issues.join("; ") || "verification failed"}`] : []
|
|
2440
2494
|
);
|
|
@@ -2471,7 +2525,7 @@ function unsupportedVerification(unitId, message) {
|
|
|
2471
2525
|
warnings: []
|
|
2472
2526
|
};
|
|
2473
2527
|
}
|
|
2474
|
-
function syncPagesEnvironmentVariablesForTarget(input, {
|
|
2528
|
+
function syncPagesEnvironmentVariablesForTarget(input, { planOnly = false } = {}) {
|
|
2475
2529
|
const target = toDeployTarget(input.context.target);
|
|
2476
2530
|
if (target.kind !== "persistent") {
|
|
2477
2531
|
return { vars: [], secrets: [] };
|
|
@@ -2493,7 +2547,7 @@ function syncPagesEnvironmentVariablesForTarget(input, { dryRun = false } = {})
|
|
|
2493
2547
|
...plainVars,
|
|
2494
2548
|
...secretVars
|
|
2495
2549
|
};
|
|
2496
|
-
if (
|
|
2550
|
+
if (planOnly || Object.keys(envVars).length === 0) {
|
|
2497
2551
|
return {
|
|
2498
2552
|
vars: Object.keys(plainVars),
|
|
2499
2553
|
secrets: Object.keys(secretVars)
|
|
@@ -2525,17 +2579,17 @@ function syncPagesEnvironmentVariablesForTarget(input, { dryRun = false } = {})
|
|
|
2525
2579
|
secrets: Object.keys(secretVars)
|
|
2526
2580
|
};
|
|
2527
2581
|
}
|
|
2528
|
-
function reconcileCloudflareTarget(input, {
|
|
2582
|
+
function reconcileCloudflareTarget(input, { planOnly = false } = {}) {
|
|
2529
2583
|
const target = toDeployTarget(input.context.target);
|
|
2530
2584
|
const deployConfig = input.context.deployConfig;
|
|
2531
2585
|
const state = loadDeployState(input.context.tenantRoot, deployConfig, { target });
|
|
2532
2586
|
const env = buildCloudflareEnv(input);
|
|
2533
|
-
const kvNamespaces =
|
|
2534
|
-
const d1Databases =
|
|
2535
|
-
const queues =
|
|
2536
|
-
const buckets =
|
|
2537
|
-
const pagesProjects =
|
|
2538
|
-
const turnstileWidgets =
|
|
2587
|
+
const kvNamespaces = planOnly ? [] : listKvNamespaces(input.context.tenantRoot, env);
|
|
2588
|
+
const d1Databases = planOnly ? [] : listD1Databases(input.context.tenantRoot, env);
|
|
2589
|
+
const queues = planOnly ? [] : listQueues(input.context.tenantRoot, env);
|
|
2590
|
+
const buckets = planOnly ? [] : listR2Buckets(input.context.tenantRoot, env);
|
|
2591
|
+
const pagesProjects = planOnly ? [] : listPagesProjects(input.context.tenantRoot, env);
|
|
2592
|
+
const turnstileWidgets = planOnly ? [] : listTurnstileWidgets(input.context.tenantRoot, env);
|
|
2539
2593
|
const runStep = (label, fn) => {
|
|
2540
2594
|
try {
|
|
2541
2595
|
return fn();
|
|
@@ -2559,9 +2613,9 @@ function reconcileCloudflareTarget(input, { dryRun = false } = {}) {
|
|
|
2559
2613
|
state.kvNamespaces[binding].previewId = existing.id;
|
|
2560
2614
|
return;
|
|
2561
2615
|
}
|
|
2562
|
-
if (
|
|
2563
|
-
state.kvNamespaces[binding].id = `
|
|
2564
|
-
state.kvNamespaces[binding].previewId = `
|
|
2616
|
+
if (planOnly) {
|
|
2617
|
+
state.kvNamespaces[binding].id = `plan-${current.name}`;
|
|
2618
|
+
state.kvNamespaces[binding].previewId = `plan-${current.name}`;
|
|
2565
2619
|
return;
|
|
2566
2620
|
}
|
|
2567
2621
|
runWrangler(["kv", "namespace", "create", current.name], {
|
|
@@ -2591,9 +2645,9 @@ function reconcileCloudflareTarget(input, { dryRun = false } = {}) {
|
|
|
2591
2645
|
current.previewDatabaseId = existing.previewDatabaseUuid ?? existing.uuid;
|
|
2592
2646
|
return;
|
|
2593
2647
|
}
|
|
2594
|
-
if (
|
|
2595
|
-
current.databaseId = `
|
|
2596
|
-
current.previewDatabaseId = `
|
|
2648
|
+
if (planOnly) {
|
|
2649
|
+
current.databaseId = `plan-${current.databaseName}`;
|
|
2650
|
+
current.previewDatabaseId = `plan-${current.databaseName}`;
|
|
2597
2651
|
return;
|
|
2598
2652
|
}
|
|
2599
2653
|
try {
|
|
@@ -2629,7 +2683,7 @@ function reconcileCloudflareTarget(input, { dryRun = false } = {}) {
|
|
|
2629
2683
|
}
|
|
2630
2684
|
let refreshedBuckets = buckets;
|
|
2631
2685
|
const existing = refreshedBuckets.find((entry) => entry?.name === bucketName);
|
|
2632
|
-
if (existing ||
|
|
2686
|
+
if (existing || planOnly) {
|
|
2633
2687
|
return;
|
|
2634
2688
|
}
|
|
2635
2689
|
try {
|
|
@@ -2652,7 +2706,7 @@ function reconcileCloudflareTarget(input, { dryRun = false } = {}) {
|
|
|
2652
2706
|
}
|
|
2653
2707
|
const existing = pagesProjects.find((entry) => entry?.name === current.projectName);
|
|
2654
2708
|
if (existing) {
|
|
2655
|
-
if (!
|
|
2709
|
+
if (!planOnly && (existing.production_branch ?? "main") !== (current.productionBranch ?? "main")) {
|
|
2656
2710
|
cloudflareApiRequest(
|
|
2657
2711
|
`/accounts/${encodeURIComponent(env.CLOUDFLARE_ACCOUNT_ID)}/pages/projects/${encodeURIComponent(current.projectName)}`,
|
|
2658
2712
|
{
|
|
@@ -2667,7 +2721,7 @@ function reconcileCloudflareTarget(input, { dryRun = false } = {}) {
|
|
|
2667
2721
|
current.url = existing.subdomain ? `https://${existing.subdomain}` : current.url ?? `https://${current.projectName}.pages.dev`;
|
|
2668
2722
|
return;
|
|
2669
2723
|
}
|
|
2670
|
-
if (
|
|
2724
|
+
if (planOnly) {
|
|
2671
2725
|
current.url = `https://${current.projectName}.pages.dev`;
|
|
2672
2726
|
return;
|
|
2673
2727
|
}
|
|
@@ -2708,9 +2762,9 @@ function reconcileCloudflareTarget(input, { dryRun = false } = {}) {
|
|
|
2708
2762
|
current.mode = "managed";
|
|
2709
2763
|
current.managed = true;
|
|
2710
2764
|
const existing = findTurnstileWidget(turnstileWidgets, current, current.name);
|
|
2711
|
-
if (
|
|
2712
|
-
current.sitekey = current.sitekey ?? `
|
|
2713
|
-
current.secret = current.secret ?? `
|
|
2765
|
+
if (planOnly) {
|
|
2766
|
+
current.sitekey = current.sitekey ?? `plan-${current.name}-sitekey`;
|
|
2767
|
+
current.secret = current.secret ?? `plan-${current.name}-secret`;
|
|
2714
2768
|
current.lastSyncedAt = nowIso();
|
|
2715
2769
|
return;
|
|
2716
2770
|
}
|
|
@@ -2743,7 +2797,7 @@ function reconcileCloudflareTarget(input, { dryRun = false } = {}) {
|
|
|
2743
2797
|
runStep("r2", ensureR2Bucket);
|
|
2744
2798
|
runStep("pages", ensurePagesProject);
|
|
2745
2799
|
runStep("turnstile-widget", ensureTurnstileWidget);
|
|
2746
|
-
runStep("web-cache", () => reconcileCloudflareWebCacheRules(input.context.tenantRoot, deployConfig, state, target, {
|
|
2800
|
+
runStep("web-cache", () => reconcileCloudflareWebCacheRules(input.context.tenantRoot, deployConfig, state, target, { planOnly, env }));
|
|
2747
2801
|
state.readiness.configured = true;
|
|
2748
2802
|
state.readiness.provisioned = hasProvisionedCloudflareResources(state);
|
|
2749
2803
|
state.readiness.deployable = state.readiness.provisioned === true;
|
|
@@ -2760,7 +2814,7 @@ function reconcileCloudflareTarget(input, { dryRun = false } = {}) {
|
|
|
2760
2814
|
writeDeployState(input.context.tenantRoot, state, { target });
|
|
2761
2815
|
return { state, summary: buildProvisioningSummary(deployConfig, state, target) };
|
|
2762
2816
|
}
|
|
2763
|
-
function syncCloudflareSecretsForTarget(input, {
|
|
2817
|
+
function syncCloudflareSecretsForTarget(input, { planOnly = false } = {}) {
|
|
2764
2818
|
const target = toDeployTarget(input.context.target);
|
|
2765
2819
|
const deployConfig = input.context.deployConfig;
|
|
2766
2820
|
const state = loadDeployState(input.context.tenantRoot, deployConfig, { target });
|
|
@@ -3771,11 +3825,11 @@ async function reconcileStaleOperationsRunnerResourcesForProject(input, {
|
|
|
3771
3825
|
});
|
|
3772
3826
|
}
|
|
3773
3827
|
}
|
|
3774
|
-
async function syncRailwayEnvironmentForScope(input, {
|
|
3828
|
+
async function syncRailwayEnvironmentForScope(input, { planOnly = false, serviceKeys } = {}) {
|
|
3775
3829
|
const scope = input.context.target.kind === "persistent" ? input.context.target.scope : "staging";
|
|
3776
3830
|
const sync = collectRailwayEnvironmentSync(input);
|
|
3777
3831
|
const selectedServiceKeys = Array.isArray(serviceKeys) && serviceKeys.length > 0 ? [...new Set(serviceKeys.map((value) => String(value).trim()).filter(Boolean))] : void 0;
|
|
3778
|
-
traceRailwayReconcile(input.context.env, "sync:start", `scope=${scope}
|
|
3832
|
+
traceRailwayReconcile(input.context.env, "sync:start", `scope=${scope} planOnly=${planOnly ? "yes" : "no"}`);
|
|
3779
3833
|
const projectGroups = configuredRailwayProjectSyncGroups(input, scope, selectedServiceKeys);
|
|
3780
3834
|
if (projectGroups.length === 0) {
|
|
3781
3835
|
return {
|
|
@@ -3783,7 +3837,7 @@ async function syncRailwayEnvironmentForScope(input, { dryRun = false, serviceKe
|
|
|
3783
3837
|
services: [],
|
|
3784
3838
|
secrets: Object.keys(sync.secrets),
|
|
3785
3839
|
variables: Object.keys(sync.variables),
|
|
3786
|
-
|
|
3840
|
+
planOnly,
|
|
3787
3841
|
workspace: ""
|
|
3788
3842
|
};
|
|
3789
3843
|
}
|
|
@@ -3792,7 +3846,7 @@ async function syncRailwayEnvironmentForScope(input, { dryRun = false, serviceKe
|
|
|
3792
3846
|
for (const projectServices of projectGroups) {
|
|
3793
3847
|
const serviceKeySet = [...new Set(projectServices.map((service) => service.key))];
|
|
3794
3848
|
const topology = await resolveRailwayTopologyForScope(input, scope, {
|
|
3795
|
-
ensure: !
|
|
3849
|
+
ensure: !planOnly,
|
|
3796
3850
|
ensureServices: false,
|
|
3797
3851
|
serviceKeys: serviceKeySet,
|
|
3798
3852
|
includeInstances: false,
|
|
@@ -3807,7 +3861,7 @@ async function syncRailwayEnvironmentForScope(input, { dryRun = false, serviceKe
|
|
|
3807
3861
|
for (const service of projectServices) {
|
|
3808
3862
|
railwayIacServiceInput(input, sync, service, scope);
|
|
3809
3863
|
}
|
|
3810
|
-
if (
|
|
3864
|
+
if (planOnly) {
|
|
3811
3865
|
syncedServices.push(...projectServices);
|
|
3812
3866
|
continue;
|
|
3813
3867
|
}
|
|
@@ -3936,7 +3990,7 @@ async function syncRailwayEnvironmentForScope(input, { dryRun = false, serviceKe
|
|
|
3936
3990
|
services: syncedServices,
|
|
3937
3991
|
secrets: Object.keys(sync.secrets),
|
|
3938
3992
|
variables: Object.keys(sync.variables),
|
|
3939
|
-
|
|
3993
|
+
planOnly,
|
|
3940
3994
|
workspace
|
|
3941
3995
|
};
|
|
3942
3996
|
}
|
|
@@ -4680,7 +4734,7 @@ function resolveDesiredDnsRecords(input) {
|
|
|
4680
4734
|
}
|
|
4681
4735
|
return desiredRecords;
|
|
4682
4736
|
}
|
|
4683
|
-
function observeCustomDomainUnit(input) {
|
|
4737
|
+
async function observeCustomDomainUnit(input) {
|
|
4684
4738
|
switch (input.unit.unitType) {
|
|
4685
4739
|
case "custom-domain:web": {
|
|
4686
4740
|
const env = buildCloudflareEnv(input);
|
|
@@ -4703,7 +4757,8 @@ function observeCustomDomainUnit(input) {
|
|
|
4703
4757
|
}
|
|
4704
4758
|
case "custom-domain:api": {
|
|
4705
4759
|
const domain = String(input.unit.spec.domain ?? "").trim();
|
|
4706
|
-
const
|
|
4760
|
+
const cached = getCustomDomainState(input, "railway", domain);
|
|
4761
|
+
const live = (cached && railwayCustomDomainHasDnsRequirements(cached) ? cached : null) ?? await observeRailwayCustomDomainLive(input, domain) ?? cached ?? input.persistedState?.lastObservedState ?? input.persistedState?.lastReconciledState ?? null;
|
|
4707
4762
|
if (live?.domain) {
|
|
4708
4763
|
storeCustomDomainState(input, "railway", domain, live);
|
|
4709
4764
|
}
|
|
@@ -4746,7 +4801,7 @@ function observeDnsRecordUnit(input) {
|
|
|
4746
4801
|
warnings: desiredRecords.length === 0 ? ["No desired DNS records were available for verification."] : []
|
|
4747
4802
|
};
|
|
4748
4803
|
}
|
|
4749
|
-
function verifyCustomDomainUnit(input) {
|
|
4804
|
+
async function verifyCustomDomainUnit(input) {
|
|
4750
4805
|
switch (input.unit.unitType) {
|
|
4751
4806
|
case "custom-domain:web": {
|
|
4752
4807
|
const domain = String(input.unit.spec.domain ?? "").trim();
|
|
@@ -4764,7 +4819,8 @@ function verifyCustomDomainUnit(input) {
|
|
|
4764
4819
|
}
|
|
4765
4820
|
case "custom-domain:api": {
|
|
4766
4821
|
const domain = String(input.unit.spec.domain ?? "").trim();
|
|
4767
|
-
const
|
|
4822
|
+
const cached = getCustomDomainState(input, "railway", domain);
|
|
4823
|
+
const live = (cached && railwayCustomDomainHasDnsRequirements(cached) ? cached : null) ?? await observeRailwayCustomDomainLive(input, domain) ?? cached ?? getPersistedCustomDomainState(input, "railway", domain) ?? input.persistedState?.lastObservedState ?? input.persistedState?.lastReconciledState ?? null;
|
|
4768
4824
|
const dnsRecords = Array.isArray(live?.dnsRecords) ? live.dnsRecords : [];
|
|
4769
4825
|
const certificateStatus = typeof live?.certificateStatus === "string" ? live.certificateStatus.trim().toUpperCase() : null;
|
|
4770
4826
|
const verified = live?.verified === true;
|
|
@@ -4854,7 +4910,7 @@ async function reconcileCustomDomainUnit(input, diff) {
|
|
|
4854
4910
|
const projectName = String(input.unit.spec.projectName ?? "").trim();
|
|
4855
4911
|
const state = ensureCloudflarePagesDomain(env, projectName, domain) ?? { domain };
|
|
4856
4912
|
storeCustomDomainState(input, "cloudflare", domain, state);
|
|
4857
|
-
const observed = observeCustomDomainUnit(input);
|
|
4913
|
+
const observed = await observeCustomDomainUnit(input);
|
|
4858
4914
|
return {
|
|
4859
4915
|
unit: input.unit,
|
|
4860
4916
|
observed,
|
|
@@ -4891,7 +4947,7 @@ async function reconcileCustomDomainUnit(input, diff) {
|
|
|
4891
4947
|
}
|
|
4892
4948
|
);
|
|
4893
4949
|
storeCustomDomainState(input, "railway", String(input.unit.spec.domain ?? "").trim(), state);
|
|
4894
|
-
const observed = observeCustomDomainUnit(input);
|
|
4950
|
+
const observed = await observeCustomDomainUnit(input);
|
|
4895
4951
|
return {
|
|
4896
4952
|
unit: input.unit,
|
|
4897
4953
|
observed,
|
|
@@ -5034,13 +5090,50 @@ async function verifyRailwayUnit(input) {
|
|
|
5034
5090
|
}));
|
|
5035
5091
|
}
|
|
5036
5092
|
const desiredRootDirectory = service.imageRef || service.sourceMode === "image" ? null : railwayServiceRootDirectory(input.context.tenantRoot, service);
|
|
5093
|
+
const normalizeRailwayRootDirectory = (value) => {
|
|
5094
|
+
const trimmed = String(value ?? "").trim();
|
|
5095
|
+
return trimmed || ".";
|
|
5096
|
+
};
|
|
5037
5097
|
if (desiredRootDirectory) {
|
|
5098
|
+
const observedRootDirectory = normalizeRailwayRootDirectory(entry.instance?.rootDirectory);
|
|
5099
|
+
const desiredNormalizedRootDirectory = normalizeRailwayRootDirectory(desiredRootDirectory);
|
|
5038
5100
|
checks.push(verificationCheck("railway.instance.root-directory", "Railway root directory matches desired config", "api", {
|
|
5039
5101
|
exists: Boolean(entry.instance?.id),
|
|
5040
|
-
configured:
|
|
5102
|
+
configured: observedRootDirectory === desiredNormalizedRootDirectory,
|
|
5041
5103
|
expected: desiredRootDirectory,
|
|
5042
5104
|
observed: entry.instance?.rootDirectory ?? null,
|
|
5043
|
-
issues:
|
|
5105
|
+
issues: observedRootDirectory === desiredNormalizedRootDirectory ? [] : ["Railway root directory does not match the desired value."]
|
|
5106
|
+
}));
|
|
5107
|
+
}
|
|
5108
|
+
if (service.sourceMode === "git" && entry.service?.id && entry.environment?.id) {
|
|
5109
|
+
const deployment = await inspectRailwayServiceDeploymentHealth({
|
|
5110
|
+
serviceId: entry.service.id,
|
|
5111
|
+
environmentId: entry.environment.id,
|
|
5112
|
+
env: topology.env
|
|
5113
|
+
}).catch((error) => ({
|
|
5114
|
+
ok: false,
|
|
5115
|
+
repo: null,
|
|
5116
|
+
branch: null,
|
|
5117
|
+
rootDirectory: null,
|
|
5118
|
+
message: error instanceof Error ? error.message : String(error)
|
|
5119
|
+
}));
|
|
5120
|
+
const repoMatches = service.sourceRepo ? deployment.repo === service.sourceRepo : true;
|
|
5121
|
+
const branchMatches = service.sourceBranch ? deployment.branch === service.sourceBranch : true;
|
|
5122
|
+
const rootMatches = desiredRootDirectory ? normalizeRailwayRootDirectory(deployment.rootDirectory) === normalizeRailwayRootDirectory(desiredRootDirectory) : true;
|
|
5123
|
+
const deploymentIssues = [
|
|
5124
|
+
repoMatches ? null : `Expected Railway Git deployment repo ${service.sourceRepo}, observed ${deployment.repo ?? "(unset)"}.`,
|
|
5125
|
+
branchMatches ? null : `Expected Railway Git deployment branch ${service.sourceBranch}, observed ${deployment.branch ?? "(unset)"}.`,
|
|
5126
|
+
rootMatches ? null : `Expected Railway deployment root ${desiredRootDirectory}, observed ${deployment.rootDirectory ?? "(unset)"}.`,
|
|
5127
|
+
deployment.repo || deployment.branch ? null : "Railway latest deployment does not expose a Git repository/branch; staging may still be using a Docker image source."
|
|
5128
|
+
].filter((issue) => Boolean(issue));
|
|
5129
|
+
checks.push(verificationCheck("railway.service.source-mode", "Railway staging service deploys from Git source", "api", {
|
|
5130
|
+
exists: Boolean(entry.service?.id),
|
|
5131
|
+
configured: deploymentIssues.length === 0,
|
|
5132
|
+
ready: deploymentIssues.length === 0,
|
|
5133
|
+
verified: deploymentIssues.length === 0,
|
|
5134
|
+
expected: { sourceMode: "git", repo: service.sourceRepo ?? null, branch: service.sourceBranch ?? null, rootDirectory: desiredRootDirectory ?? null },
|
|
5135
|
+
observed: { repo: deployment.repo, branch: deployment.branch, rootDirectory: deployment.rootDirectory, message: deployment.message ?? null },
|
|
5136
|
+
issues: deploymentIssues
|
|
5044
5137
|
}));
|
|
5045
5138
|
}
|
|
5046
5139
|
if (service.key === "api") {
|
|
@@ -128,7 +128,7 @@ export interface TreeseedReconcileRunContext {
|
|
|
128
128
|
target: TreeseedReconcileTarget;
|
|
129
129
|
deployConfig: TreeseedDeployConfig;
|
|
130
130
|
launchEnv: NodeJS.ProcessEnv;
|
|
131
|
-
|
|
131
|
+
planOnly?: boolean;
|
|
132
132
|
write?: (line: string) => void;
|
|
133
133
|
session: Map<string, unknown>;
|
|
134
134
|
}
|
|
@@ -30,7 +30,7 @@ export declare function planTreeseedReconciliation({ tenantRoot, target, env, sy
|
|
|
30
30
|
state: TreeseedReconcileStateRecord;
|
|
31
31
|
deployConfig: import("../platform/contracts.js").TreeseedDeployConfig;
|
|
32
32
|
}>;
|
|
33
|
-
export declare function reconcileTreeseedTarget({ tenantRoot, target, env, systems, selector, units, write,
|
|
33
|
+
export declare function reconcileTreeseedTarget({ tenantRoot, target, env, systems, selector, units, write, planOnly, session, }: {
|
|
34
34
|
tenantRoot: string;
|
|
35
35
|
target: TreeseedReconcileTarget;
|
|
36
36
|
env?: NodeJS.ProcessEnv;
|
|
@@ -38,7 +38,7 @@ export declare function reconcileTreeseedTarget({ tenantRoot, target, env, syste
|
|
|
38
38
|
selector?: TreeseedReconcileSelector;
|
|
39
39
|
units?: TreeseedDesiredUnit[];
|
|
40
40
|
write?: (line: string) => void;
|
|
41
|
-
|
|
41
|
+
planOnly?: boolean;
|
|
42
42
|
session?: Map<string, unknown>;
|
|
43
43
|
}): Promise<{
|
|
44
44
|
target: TreeseedReconcileTarget;
|
|
@@ -83,11 +83,11 @@ export declare function collectTreeseedReconcileStatus({ tenantRoot, target, env
|
|
|
83
83
|
verification: TreeseedUnitVerificationResult | null;
|
|
84
84
|
}[];
|
|
85
85
|
}>;
|
|
86
|
-
export declare function reconcileTreeseedNestedTarget({ parentContext, selector, target,
|
|
86
|
+
export declare function reconcileTreeseedNestedTarget({ parentContext, selector, target, planOnly, }: {
|
|
87
87
|
parentContext: TreeseedReconcileRunContext;
|
|
88
88
|
selector: TreeseedReconcileSelector;
|
|
89
89
|
target: TreeseedReconcileTarget;
|
|
90
|
-
|
|
90
|
+
planOnly: boolean;
|
|
91
91
|
}): Promise<{
|
|
92
92
|
target: TreeseedReconcileTarget;
|
|
93
93
|
units: TreeseedDesiredUnit[];
|
package/dist/reconcile/engine.js
CHANGED
|
@@ -26,14 +26,14 @@ function formatVerificationFailure(verification) {
|
|
|
26
26
|
].filter(Boolean);
|
|
27
27
|
return details.length > 0 ? `Verification failed (${details.join(" | ")})` : "Verification failed.";
|
|
28
28
|
}
|
|
29
|
-
function createRunContext(tenantRoot, target, launchEnv, write,
|
|
29
|
+
function createRunContext(tenantRoot, target, launchEnv, write, planOnly = false, session) {
|
|
30
30
|
const { deployConfig } = deriveTreeseedDesiredUnits({ tenantRoot, target, env: launchEnv });
|
|
31
31
|
return {
|
|
32
32
|
tenantRoot,
|
|
33
33
|
target,
|
|
34
34
|
deployConfig,
|
|
35
35
|
launchEnv,
|
|
36
|
-
|
|
36
|
+
planOnly,
|
|
37
37
|
write,
|
|
38
38
|
session: session ?? /* @__PURE__ */ new Map()
|
|
39
39
|
};
|
|
@@ -278,12 +278,12 @@ async function reconcileTreeseedTarget({
|
|
|
278
278
|
selector,
|
|
279
279
|
units,
|
|
280
280
|
write,
|
|
281
|
-
|
|
281
|
+
planOnly = false,
|
|
282
282
|
session
|
|
283
283
|
}) {
|
|
284
284
|
const planned = await planTreeseedReconciliation({ tenantRoot, target, env, systems, selector, units, write });
|
|
285
285
|
const registry = createTreeseedReconcileRegistry(planned.deployConfig);
|
|
286
|
-
const context = createRunContext(tenantRoot, target, env, write,
|
|
286
|
+
const context = createRunContext(tenantRoot, target, env, write, planOnly, session);
|
|
287
287
|
const results = [];
|
|
288
288
|
const verificationMap = /* @__PURE__ */ new Map();
|
|
289
289
|
const timingEntries = [];
|
|
@@ -337,7 +337,7 @@ async function reconcileTreeseedTarget({
|
|
|
337
337
|
let result;
|
|
338
338
|
try {
|
|
339
339
|
const stageStartMs = performance.now();
|
|
340
|
-
if (
|
|
340
|
+
if (planOnly) {
|
|
341
341
|
result = {
|
|
342
342
|
unit: plan.unit,
|
|
343
343
|
observed: plan.observed,
|
|
@@ -360,7 +360,7 @@ async function reconcileTreeseedTarget({
|
|
|
360
360
|
unitTiming.children?.push({
|
|
361
361
|
name: `${unitTiming.name}:apply`,
|
|
362
362
|
durationMs: elapsedMs(stageStartMs),
|
|
363
|
-
status:
|
|
363
|
+
status: planOnly ? "skipped" : "success"
|
|
364
364
|
});
|
|
365
365
|
} catch (error) {
|
|
366
366
|
unitTiming.children?.push({
|
|
@@ -373,7 +373,7 @@ async function reconcileTreeseedTarget({
|
|
|
373
373
|
wrapAdapterFailure("apply", plan.unit.provider, plan.unit.unitType, plan.unit.unitId, error);
|
|
374
374
|
}
|
|
375
375
|
let refreshedObserved = result.observed;
|
|
376
|
-
if (!
|
|
376
|
+
if (!planOnly) {
|
|
377
377
|
try {
|
|
378
378
|
const stageStartMs = performance.now();
|
|
379
379
|
refreshedObserved = await Promise.resolve(adapter.refresh({
|
|
@@ -447,17 +447,17 @@ async function reconcileTreeseedTarget({
|
|
|
447
447
|
unitTiming.status = verification.verified ? "success" : "failed";
|
|
448
448
|
write?.(`Finished ${plan.unit.provider}:${plan.unit.unitType} (${plan.unit.logicalName}) in ${formatDurationMs(unitTiming.durationMs)}.`);
|
|
449
449
|
if (!verification.verified) {
|
|
450
|
-
if (!
|
|
450
|
+
if (!planOnly) {
|
|
451
451
|
await persistVerifiedResult(persisted, verifiedResult);
|
|
452
452
|
}
|
|
453
453
|
throw new Error(`Treeseed reconcile verification failed for ${plan.unit.provider}:${plan.unit.unitType} (${plan.unit.unitId}): ${formatVerificationFailure(verification)}`);
|
|
454
454
|
}
|
|
455
|
-
if (!
|
|
455
|
+
if (!planOnly) {
|
|
456
456
|
await persistVerifiedResult(persisted, verifiedResult);
|
|
457
457
|
}
|
|
458
458
|
results.push(verifiedResult);
|
|
459
459
|
});
|
|
460
|
-
if (!
|
|
460
|
+
if (!planOnly) {
|
|
461
461
|
writeTreeseedReconcileState(tenantRoot, planned.state, env);
|
|
462
462
|
}
|
|
463
463
|
return {
|
|
@@ -579,7 +579,7 @@ async function reconcileTreeseedNestedTarget({
|
|
|
579
579
|
parentContext,
|
|
580
580
|
selector,
|
|
581
581
|
target,
|
|
582
|
-
|
|
582
|
+
planOnly
|
|
583
583
|
}) {
|
|
584
584
|
const previousVerificationResults = parentContext.session.get("treeseed:verification-results");
|
|
585
585
|
const previousTimings = parentContext.session.get("treeseed:timings");
|
|
@@ -590,7 +590,7 @@ async function reconcileTreeseedNestedTarget({
|
|
|
590
590
|
env: parentContext.launchEnv,
|
|
591
591
|
selector,
|
|
592
592
|
write: parentContext.write,
|
|
593
|
-
|
|
593
|
+
planOnly,
|
|
594
594
|
session: parentContext.session
|
|
595
595
|
});
|
|
596
596
|
} finally {
|
|
@@ -61,7 +61,7 @@ export declare function runHostedReconcileGate(input: {
|
|
|
61
61
|
parentContext: TreeseedReconcileRunContext;
|
|
62
62
|
selector: TreeseedReconcileSelector;
|
|
63
63
|
target: TreeseedReconcileTarget;
|
|
64
|
-
|
|
64
|
+
planOnly: boolean;
|
|
65
65
|
}): Promise<{
|
|
66
66
|
target: TreeseedReconcileTarget;
|
|
67
67
|
units: import("../contracts.js").TreeseedDesiredUnit[];
|
|
@@ -21,7 +21,7 @@ function writeDeployReport(payload) {
|
|
|
21
21
|
}
|
|
22
22
|
function parseArgs(argv) {
|
|
23
23
|
const parsed = {
|
|
24
|
-
|
|
24
|
+
planOnly: false,
|
|
25
25
|
only: null,
|
|
26
26
|
name: null,
|
|
27
27
|
environment: null,
|
|
@@ -32,8 +32,8 @@ function parseArgs(argv) {
|
|
|
32
32
|
const current = rest.shift();
|
|
33
33
|
if (!current)
|
|
34
34
|
continue;
|
|
35
|
-
if (current === '--
|
|
36
|
-
parsed.
|
|
35
|
+
if (current === '--plan') {
|
|
36
|
+
parsed.planOnly = true;
|
|
37
37
|
continue;
|
|
38
38
|
}
|
|
39
39
|
if (current === '--only') {
|
|
@@ -166,8 +166,8 @@ async function main() {
|
|
|
166
166
|
}
|
|
167
167
|
if (scope === 'local') {
|
|
168
168
|
runTenantDeployPreflight({ cwd: tenantRoot, scope: 'local' });
|
|
169
|
-
await runTenantWebBuild({ tenantRoot, scope: 'local',
|
|
170
|
-
writeDeployReport({ ok: true, kind: 'success', scope,
|
|
169
|
+
await runTenantWebBuild({ tenantRoot, scope: 'local', planOnly: options.planOnly, env: process.env });
|
|
170
|
+
writeDeployReport({ ok: true, kind: 'success', scope, planOnly: options.planOnly, target: deployTargetLabel(target) });
|
|
171
171
|
console.log('Treeseed local deploy completed as a build-only publish target.');
|
|
172
172
|
return;
|
|
173
173
|
}
|
|
@@ -177,7 +177,7 @@ async function main() {
|
|
|
177
177
|
tenantRoot,
|
|
178
178
|
scope,
|
|
179
179
|
target,
|
|
180
|
-
|
|
180
|
+
planOnly: options.planOnly,
|
|
181
181
|
env: process.env,
|
|
182
182
|
});
|
|
183
183
|
}
|
|
@@ -192,21 +192,21 @@ async function main() {
|
|
|
192
192
|
}
|
|
193
193
|
if (shouldRun('migrate')) {
|
|
194
194
|
const result = await runTenantDataMigration(context);
|
|
195
|
-
console.log(`${options.
|
|
195
|
+
console.log(`${options.planOnly ? 'Planned' : 'Applied'} remote migrations for ${result.databaseName}.`);
|
|
196
196
|
}
|
|
197
197
|
if (shouldRun('build')) {
|
|
198
198
|
await runTenantWebBuild(context);
|
|
199
199
|
}
|
|
200
200
|
if (shouldRun('publish')) {
|
|
201
201
|
await runTenantWebPublish(context);
|
|
202
|
-
if (!options.
|
|
202
|
+
if (!options.planOnly) {
|
|
203
203
|
finalizeDeploymentState(tenantRoot, { target });
|
|
204
204
|
}
|
|
205
205
|
}
|
|
206
206
|
writeDeployReport({
|
|
207
207
|
ok: true,
|
|
208
208
|
kind: 'success',
|
|
209
|
-
|
|
209
|
+
planOnly: options.planOnly,
|
|
210
210
|
only: options.only,
|
|
211
211
|
target: deployTargetLabel(target),
|
|
212
212
|
});
|