@treeseed/sdk 0.12.26 → 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 +86 -40
- 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,6 +56,7 @@ import {
|
|
|
56
56
|
deleteRailwayService,
|
|
57
57
|
deleteRailwayVolume,
|
|
58
58
|
deployRailwayServiceInstance,
|
|
59
|
+
inspectRailwayServiceDeploymentHealth,
|
|
59
60
|
listRailwayEnvironmentServices,
|
|
60
61
|
getRailwayServiceInstance,
|
|
61
62
|
getRailwayProject,
|
|
@@ -298,7 +299,7 @@ function buildPackageWorkflowAdapter() {
|
|
|
298
299
|
},
|
|
299
300
|
apply(input) {
|
|
300
301
|
const packageId = typeof input.unit.spec.packageId === "string" ? input.unit.spec.packageId : typeof input.unit.metadata.packageId === "string" ? input.unit.metadata.packageId : null;
|
|
301
|
-
if (packageId && input.context.
|
|
302
|
+
if (packageId && input.context.planOnly !== true && input.diff.action !== "noop") {
|
|
302
303
|
const sync = syncTreeseedPackageWorkflows({
|
|
303
304
|
root: input.context.tenantRoot,
|
|
304
305
|
packageId,
|
|
@@ -836,7 +837,7 @@ function buildLocalDockerComposeAdapter() {
|
|
|
836
837
|
const composeFiles = observedOrResolvedComposeFiles(input);
|
|
837
838
|
const cwd = String(input.observed.live.cwd ?? input.context.tenantRoot);
|
|
838
839
|
const projectName = String(input.unit.spec.projectName ?? "treeseed-capacity-provider");
|
|
839
|
-
if (input.context.
|
|
840
|
+
if (input.context.planOnly !== true && composeFiles.length > 0 && composeFiles.every((composeFile) => existsSync(composeFile))) {
|
|
840
841
|
const result = runDockerCompose({
|
|
841
842
|
composeFiles,
|
|
842
843
|
projectName,
|
|
@@ -1270,7 +1271,7 @@ function buildLocalProcessAdapter() {
|
|
|
1270
1271
|
},
|
|
1271
1272
|
async destroy(input) {
|
|
1272
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)];
|
|
1273
|
-
if (input.context.
|
|
1274
|
+
if (input.context.planOnly !== true) {
|
|
1274
1275
|
await runManagedDevAction({
|
|
1275
1276
|
tenantRoot: input.context.tenantRoot,
|
|
1276
1277
|
action: "stop",
|
|
@@ -1797,7 +1798,7 @@ function buildReleaseGateAdapter() {
|
|
|
1797
1798
|
parentContext: input.context,
|
|
1798
1799
|
selector,
|
|
1799
1800
|
target: { kind: "persistent", scope: environment },
|
|
1800
|
-
|
|
1801
|
+
planOnly: input.context.planOnly === true
|
|
1801
1802
|
});
|
|
1802
1803
|
return genericResult(input, { ...input.observed.live, nested, fingerprint: input.unit.spec.fingerprint });
|
|
1803
1804
|
}
|
|
@@ -1906,7 +1907,7 @@ function buildWorkflowMetaAdapter() {
|
|
|
1906
1907
|
parentContext: input.context,
|
|
1907
1908
|
selector,
|
|
1908
1909
|
target: input.context.target,
|
|
1909
|
-
|
|
1910
|
+
planOnly: input.context.planOnly === true
|
|
1910
1911
|
});
|
|
1911
1912
|
return genericResult(input, { ...input.observed.live, nested, fingerprint });
|
|
1912
1913
|
}
|
|
@@ -2000,7 +2001,7 @@ function buildCloudflareEnv(input) {
|
|
|
2000
2001
|
};
|
|
2001
2002
|
}
|
|
2002
2003
|
function hasLiveResourceId(value) {
|
|
2003
|
-
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");
|
|
2004
2005
|
}
|
|
2005
2006
|
function buildRailwayEnv(input, scope) {
|
|
2006
2007
|
const values = resolveReconcileEnvironmentValues(input, scope);
|
|
@@ -2421,6 +2422,12 @@ async function observeRailwayCustomDomainLive(input, domain) {
|
|
|
2421
2422
|
});
|
|
2422
2423
|
return normalized?.domain ? normalized : customDomain;
|
|
2423
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
|
+
}
|
|
2424
2431
|
function collectCloudflareEnvironmentSync(input) {
|
|
2425
2432
|
const target = toDeployTarget(input.context.target);
|
|
2426
2433
|
const scope = scopeFromTarget(target);
|
|
@@ -2481,7 +2488,7 @@ function verificationCheck(key, description, source, options) {
|
|
|
2481
2488
|
};
|
|
2482
2489
|
}
|
|
2483
2490
|
function summarizeVerification(unitId, checks, warnings = []) {
|
|
2484
|
-
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}`] : []);
|
|
2485
2492
|
const drifted = checks.flatMap(
|
|
2486
2493
|
(check) => check.exists && (!check.configured || !check.ready || !check.verified || check.issues.length > 0) ? [`${check.key}: ${check.issues.join("; ") || "verification failed"}`] : []
|
|
2487
2494
|
);
|
|
@@ -2518,7 +2525,7 @@ function unsupportedVerification(unitId, message) {
|
|
|
2518
2525
|
warnings: []
|
|
2519
2526
|
};
|
|
2520
2527
|
}
|
|
2521
|
-
function syncPagesEnvironmentVariablesForTarget(input, {
|
|
2528
|
+
function syncPagesEnvironmentVariablesForTarget(input, { planOnly = false } = {}) {
|
|
2522
2529
|
const target = toDeployTarget(input.context.target);
|
|
2523
2530
|
if (target.kind !== "persistent") {
|
|
2524
2531
|
return { vars: [], secrets: [] };
|
|
@@ -2540,7 +2547,7 @@ function syncPagesEnvironmentVariablesForTarget(input, { dryRun = false } = {})
|
|
|
2540
2547
|
...plainVars,
|
|
2541
2548
|
...secretVars
|
|
2542
2549
|
};
|
|
2543
|
-
if (
|
|
2550
|
+
if (planOnly || Object.keys(envVars).length === 0) {
|
|
2544
2551
|
return {
|
|
2545
2552
|
vars: Object.keys(plainVars),
|
|
2546
2553
|
secrets: Object.keys(secretVars)
|
|
@@ -2572,17 +2579,17 @@ function syncPagesEnvironmentVariablesForTarget(input, { dryRun = false } = {})
|
|
|
2572
2579
|
secrets: Object.keys(secretVars)
|
|
2573
2580
|
};
|
|
2574
2581
|
}
|
|
2575
|
-
function reconcileCloudflareTarget(input, {
|
|
2582
|
+
function reconcileCloudflareTarget(input, { planOnly = false } = {}) {
|
|
2576
2583
|
const target = toDeployTarget(input.context.target);
|
|
2577
2584
|
const deployConfig = input.context.deployConfig;
|
|
2578
2585
|
const state = loadDeployState(input.context.tenantRoot, deployConfig, { target });
|
|
2579
2586
|
const env = buildCloudflareEnv(input);
|
|
2580
|
-
const kvNamespaces =
|
|
2581
|
-
const d1Databases =
|
|
2582
|
-
const queues =
|
|
2583
|
-
const buckets =
|
|
2584
|
-
const pagesProjects =
|
|
2585
|
-
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);
|
|
2586
2593
|
const runStep = (label, fn) => {
|
|
2587
2594
|
try {
|
|
2588
2595
|
return fn();
|
|
@@ -2606,9 +2613,9 @@ function reconcileCloudflareTarget(input, { dryRun = false } = {}) {
|
|
|
2606
2613
|
state.kvNamespaces[binding].previewId = existing.id;
|
|
2607
2614
|
return;
|
|
2608
2615
|
}
|
|
2609
|
-
if (
|
|
2610
|
-
state.kvNamespaces[binding].id = `
|
|
2611
|
-
state.kvNamespaces[binding].previewId = `
|
|
2616
|
+
if (planOnly) {
|
|
2617
|
+
state.kvNamespaces[binding].id = `plan-${current.name}`;
|
|
2618
|
+
state.kvNamespaces[binding].previewId = `plan-${current.name}`;
|
|
2612
2619
|
return;
|
|
2613
2620
|
}
|
|
2614
2621
|
runWrangler(["kv", "namespace", "create", current.name], {
|
|
@@ -2638,9 +2645,9 @@ function reconcileCloudflareTarget(input, { dryRun = false } = {}) {
|
|
|
2638
2645
|
current.previewDatabaseId = existing.previewDatabaseUuid ?? existing.uuid;
|
|
2639
2646
|
return;
|
|
2640
2647
|
}
|
|
2641
|
-
if (
|
|
2642
|
-
current.databaseId = `
|
|
2643
|
-
current.previewDatabaseId = `
|
|
2648
|
+
if (planOnly) {
|
|
2649
|
+
current.databaseId = `plan-${current.databaseName}`;
|
|
2650
|
+
current.previewDatabaseId = `plan-${current.databaseName}`;
|
|
2644
2651
|
return;
|
|
2645
2652
|
}
|
|
2646
2653
|
try {
|
|
@@ -2676,7 +2683,7 @@ function reconcileCloudflareTarget(input, { dryRun = false } = {}) {
|
|
|
2676
2683
|
}
|
|
2677
2684
|
let refreshedBuckets = buckets;
|
|
2678
2685
|
const existing = refreshedBuckets.find((entry) => entry?.name === bucketName);
|
|
2679
|
-
if (existing ||
|
|
2686
|
+
if (existing || planOnly) {
|
|
2680
2687
|
return;
|
|
2681
2688
|
}
|
|
2682
2689
|
try {
|
|
@@ -2699,7 +2706,7 @@ function reconcileCloudflareTarget(input, { dryRun = false } = {}) {
|
|
|
2699
2706
|
}
|
|
2700
2707
|
const existing = pagesProjects.find((entry) => entry?.name === current.projectName);
|
|
2701
2708
|
if (existing) {
|
|
2702
|
-
if (!
|
|
2709
|
+
if (!planOnly && (existing.production_branch ?? "main") !== (current.productionBranch ?? "main")) {
|
|
2703
2710
|
cloudflareApiRequest(
|
|
2704
2711
|
`/accounts/${encodeURIComponent(env.CLOUDFLARE_ACCOUNT_ID)}/pages/projects/${encodeURIComponent(current.projectName)}`,
|
|
2705
2712
|
{
|
|
@@ -2714,7 +2721,7 @@ function reconcileCloudflareTarget(input, { dryRun = false } = {}) {
|
|
|
2714
2721
|
current.url = existing.subdomain ? `https://${existing.subdomain}` : current.url ?? `https://${current.projectName}.pages.dev`;
|
|
2715
2722
|
return;
|
|
2716
2723
|
}
|
|
2717
|
-
if (
|
|
2724
|
+
if (planOnly) {
|
|
2718
2725
|
current.url = `https://${current.projectName}.pages.dev`;
|
|
2719
2726
|
return;
|
|
2720
2727
|
}
|
|
@@ -2755,9 +2762,9 @@ function reconcileCloudflareTarget(input, { dryRun = false } = {}) {
|
|
|
2755
2762
|
current.mode = "managed";
|
|
2756
2763
|
current.managed = true;
|
|
2757
2764
|
const existing = findTurnstileWidget(turnstileWidgets, current, current.name);
|
|
2758
|
-
if (
|
|
2759
|
-
current.sitekey = current.sitekey ?? `
|
|
2760
|
-
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`;
|
|
2761
2768
|
current.lastSyncedAt = nowIso();
|
|
2762
2769
|
return;
|
|
2763
2770
|
}
|
|
@@ -2790,7 +2797,7 @@ function reconcileCloudflareTarget(input, { dryRun = false } = {}) {
|
|
|
2790
2797
|
runStep("r2", ensureR2Bucket);
|
|
2791
2798
|
runStep("pages", ensurePagesProject);
|
|
2792
2799
|
runStep("turnstile-widget", ensureTurnstileWidget);
|
|
2793
|
-
runStep("web-cache", () => reconcileCloudflareWebCacheRules(input.context.tenantRoot, deployConfig, state, target, {
|
|
2800
|
+
runStep("web-cache", () => reconcileCloudflareWebCacheRules(input.context.tenantRoot, deployConfig, state, target, { planOnly, env }));
|
|
2794
2801
|
state.readiness.configured = true;
|
|
2795
2802
|
state.readiness.provisioned = hasProvisionedCloudflareResources(state);
|
|
2796
2803
|
state.readiness.deployable = state.readiness.provisioned === true;
|
|
@@ -2807,7 +2814,7 @@ function reconcileCloudflareTarget(input, { dryRun = false } = {}) {
|
|
|
2807
2814
|
writeDeployState(input.context.tenantRoot, state, { target });
|
|
2808
2815
|
return { state, summary: buildProvisioningSummary(deployConfig, state, target) };
|
|
2809
2816
|
}
|
|
2810
|
-
function syncCloudflareSecretsForTarget(input, {
|
|
2817
|
+
function syncCloudflareSecretsForTarget(input, { planOnly = false } = {}) {
|
|
2811
2818
|
const target = toDeployTarget(input.context.target);
|
|
2812
2819
|
const deployConfig = input.context.deployConfig;
|
|
2813
2820
|
const state = loadDeployState(input.context.tenantRoot, deployConfig, { target });
|
|
@@ -3818,11 +3825,11 @@ async function reconcileStaleOperationsRunnerResourcesForProject(input, {
|
|
|
3818
3825
|
});
|
|
3819
3826
|
}
|
|
3820
3827
|
}
|
|
3821
|
-
async function syncRailwayEnvironmentForScope(input, {
|
|
3828
|
+
async function syncRailwayEnvironmentForScope(input, { planOnly = false, serviceKeys } = {}) {
|
|
3822
3829
|
const scope = input.context.target.kind === "persistent" ? input.context.target.scope : "staging";
|
|
3823
3830
|
const sync = collectRailwayEnvironmentSync(input);
|
|
3824
3831
|
const selectedServiceKeys = Array.isArray(serviceKeys) && serviceKeys.length > 0 ? [...new Set(serviceKeys.map((value) => String(value).trim()).filter(Boolean))] : void 0;
|
|
3825
|
-
traceRailwayReconcile(input.context.env, "sync:start", `scope=${scope}
|
|
3832
|
+
traceRailwayReconcile(input.context.env, "sync:start", `scope=${scope} planOnly=${planOnly ? "yes" : "no"}`);
|
|
3826
3833
|
const projectGroups = configuredRailwayProjectSyncGroups(input, scope, selectedServiceKeys);
|
|
3827
3834
|
if (projectGroups.length === 0) {
|
|
3828
3835
|
return {
|
|
@@ -3830,7 +3837,7 @@ async function syncRailwayEnvironmentForScope(input, { dryRun = false, serviceKe
|
|
|
3830
3837
|
services: [],
|
|
3831
3838
|
secrets: Object.keys(sync.secrets),
|
|
3832
3839
|
variables: Object.keys(sync.variables),
|
|
3833
|
-
|
|
3840
|
+
planOnly,
|
|
3834
3841
|
workspace: ""
|
|
3835
3842
|
};
|
|
3836
3843
|
}
|
|
@@ -3839,7 +3846,7 @@ async function syncRailwayEnvironmentForScope(input, { dryRun = false, serviceKe
|
|
|
3839
3846
|
for (const projectServices of projectGroups) {
|
|
3840
3847
|
const serviceKeySet = [...new Set(projectServices.map((service) => service.key))];
|
|
3841
3848
|
const topology = await resolveRailwayTopologyForScope(input, scope, {
|
|
3842
|
-
ensure: !
|
|
3849
|
+
ensure: !planOnly,
|
|
3843
3850
|
ensureServices: false,
|
|
3844
3851
|
serviceKeys: serviceKeySet,
|
|
3845
3852
|
includeInstances: false,
|
|
@@ -3854,7 +3861,7 @@ async function syncRailwayEnvironmentForScope(input, { dryRun = false, serviceKe
|
|
|
3854
3861
|
for (const service of projectServices) {
|
|
3855
3862
|
railwayIacServiceInput(input, sync, service, scope);
|
|
3856
3863
|
}
|
|
3857
|
-
if (
|
|
3864
|
+
if (planOnly) {
|
|
3858
3865
|
syncedServices.push(...projectServices);
|
|
3859
3866
|
continue;
|
|
3860
3867
|
}
|
|
@@ -3983,7 +3990,7 @@ async function syncRailwayEnvironmentForScope(input, { dryRun = false, serviceKe
|
|
|
3983
3990
|
services: syncedServices,
|
|
3984
3991
|
secrets: Object.keys(sync.secrets),
|
|
3985
3992
|
variables: Object.keys(sync.variables),
|
|
3986
|
-
|
|
3993
|
+
planOnly,
|
|
3987
3994
|
workspace
|
|
3988
3995
|
};
|
|
3989
3996
|
}
|
|
@@ -4750,7 +4757,8 @@ async function observeCustomDomainUnit(input) {
|
|
|
4750
4757
|
}
|
|
4751
4758
|
case "custom-domain:api": {
|
|
4752
4759
|
const domain = String(input.unit.spec.domain ?? "").trim();
|
|
4753
|
-
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;
|
|
4754
4762
|
if (live?.domain) {
|
|
4755
4763
|
storeCustomDomainState(input, "railway", domain, live);
|
|
4756
4764
|
}
|
|
@@ -4811,7 +4819,8 @@ async function verifyCustomDomainUnit(input) {
|
|
|
4811
4819
|
}
|
|
4812
4820
|
case "custom-domain:api": {
|
|
4813
4821
|
const domain = String(input.unit.spec.domain ?? "").trim();
|
|
4814
|
-
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;
|
|
4815
4824
|
const dnsRecords = Array.isArray(live?.dnsRecords) ? live.dnsRecords : [];
|
|
4816
4825
|
const certificateStatus = typeof live?.certificateStatus === "string" ? live.certificateStatus.trim().toUpperCase() : null;
|
|
4817
4826
|
const verified = live?.verified === true;
|
|
@@ -5081,13 +5090,50 @@ async function verifyRailwayUnit(input) {
|
|
|
5081
5090
|
}));
|
|
5082
5091
|
}
|
|
5083
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
|
+
};
|
|
5084
5097
|
if (desiredRootDirectory) {
|
|
5098
|
+
const observedRootDirectory = normalizeRailwayRootDirectory(entry.instance?.rootDirectory);
|
|
5099
|
+
const desiredNormalizedRootDirectory = normalizeRailwayRootDirectory(desiredRootDirectory);
|
|
5085
5100
|
checks.push(verificationCheck("railway.instance.root-directory", "Railway root directory matches desired config", "api", {
|
|
5086
5101
|
exists: Boolean(entry.instance?.id),
|
|
5087
|
-
configured:
|
|
5102
|
+
configured: observedRootDirectory === desiredNormalizedRootDirectory,
|
|
5088
5103
|
expected: desiredRootDirectory,
|
|
5089
5104
|
observed: entry.instance?.rootDirectory ?? null,
|
|
5090
|
-
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
|
|
5091
5137
|
}));
|
|
5092
5138
|
}
|
|
5093
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
|
});
|
|
@@ -7,7 +7,7 @@ import { deriveCloudflareWorkerName } from '../platform/deploy-config.js';
|
|
|
7
7
|
const tenantRoot = process.cwd();
|
|
8
8
|
function parseArgs(argv) {
|
|
9
9
|
const parsed = {
|
|
10
|
-
|
|
10
|
+
planOnly: false,
|
|
11
11
|
force: false,
|
|
12
12
|
skipConfirmation: false,
|
|
13
13
|
confirm: null,
|
|
@@ -20,8 +20,8 @@ function parseArgs(argv) {
|
|
|
20
20
|
const current = rest.shift();
|
|
21
21
|
if (!current)
|
|
22
22
|
continue;
|
|
23
|
-
if (current === '--
|
|
24
|
-
parsed.
|
|
23
|
+
if (current === '--plan') {
|
|
24
|
+
parsed.planOnly = true;
|
|
25
25
|
continue;
|
|
26
26
|
}
|
|
27
27
|
if (current === '--force') {
|
|
@@ -86,7 +86,7 @@ const scope = options.environment ?? 'prod';
|
|
|
86
86
|
const target = createPersistentDeployTarget(scope);
|
|
87
87
|
applyTreeseedEnvironmentToProcess({ tenantRoot, scope, override: true });
|
|
88
88
|
assertTreeseedCommandEnvironment({ tenantRoot, scope, purpose: 'destroy' });
|
|
89
|
-
const deployConfig = validateDestroyPrerequisites(tenantRoot, { requireRemote: !options.
|
|
89
|
+
const deployConfig = validateDestroyPrerequisites(tenantRoot, { requireRemote: !options.planOnly });
|
|
90
90
|
const state = loadDeployState(tenantRoot, deployConfig, { target });
|
|
91
91
|
const expectedConfirmation = getExpectedConfirmation(deployConfig);
|
|
92
92
|
if (!options.skipConfirmation) {
|
|
@@ -98,14 +98,14 @@ if (!options.skipConfirmation) {
|
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
const result = await destroyTreeseedEnvironmentResources(tenantRoot, {
|
|
101
|
-
|
|
101
|
+
planOnly: options.planOnly,
|
|
102
102
|
force: options.force,
|
|
103
103
|
deleteData: options.deleteData,
|
|
104
104
|
target,
|
|
105
105
|
});
|
|
106
106
|
printDestroySummary(result);
|
|
107
|
-
if (options.
|
|
108
|
-
console.log('
|
|
107
|
+
if (options.planOnly) {
|
|
108
|
+
console.log('Plan: no remote resources were deleted.');
|
|
109
109
|
process.exit(0);
|
|
110
110
|
}
|
|
111
111
|
cleanupDestroyedState(tenantRoot, { target, removeBuildArtifacts: options.removeBuildArtifacts });
|
|
@@ -10,7 +10,7 @@ function parseArgs(argv) {
|
|
|
10
10
|
environment: null,
|
|
11
11
|
projectId: null,
|
|
12
12
|
previewId: null,
|
|
13
|
-
|
|
13
|
+
planOnly: false,
|
|
14
14
|
};
|
|
15
15
|
const rest = [...argv];
|
|
16
16
|
while (rest.length) {
|
|
@@ -49,8 +49,8 @@ function parseArgs(argv) {
|
|
|
49
49
|
parsed.previewId = current.split('=', 2)[1] ?? null;
|
|
50
50
|
continue;
|
|
51
51
|
}
|
|
52
|
-
if (current === '--
|
|
53
|
-
parsed.
|
|
52
|
+
if (current === '--plan') {
|
|
53
|
+
parsed.planOnly = true;
|
|
54
54
|
continue;
|
|
55
55
|
}
|
|
56
56
|
throw new Error(`Unknown workflow action argument: ${current}`);
|
|
@@ -78,7 +78,7 @@ async function main() {
|
|
|
78
78
|
scope,
|
|
79
79
|
projectId: options.projectId ?? process.env.TREESEED_PROJECT_ID ?? null,
|
|
80
80
|
previewId: options.previewId,
|
|
81
|
-
|
|
81
|
+
planOnly: options.planOnly,
|
|
82
82
|
write: (line) => writeStatus(line),
|
|
83
83
|
});
|
|
84
84
|
if (result !== undefined) {
|
|
@@ -233,8 +233,8 @@ function runScaffoldChecks(siteRoot) {
|
|
|
233
233
|
});
|
|
234
234
|
}
|
|
235
235
|
if (scaffoldChecks.has('deploy')) {
|
|
236
|
-
withTiming('scaffold deploy
|
|
237
|
-
runStep('npm', ['run', 'deploy', '--', '--
|
|
236
|
+
withTiming('scaffold deploy plan', () => {
|
|
237
|
+
runStep('npm', ['run', 'deploy', '--', '--plan'], { cwd: siteRoot });
|
|
238
238
|
});
|
|
239
239
|
}
|
|
240
240
|
}
|
|
@@ -491,15 +491,15 @@ async function runLocalSuite() {
|
|
|
491
491
|
env: cacheEnv(),
|
|
492
492
|
});
|
|
493
493
|
});
|
|
494
|
-
await withStep('scaffold tenant deploy
|
|
495
|
-
return runCommand('scaffold-deploy-
|
|
494
|
+
await withStep('scaffold tenant deploy plan', async () => {
|
|
495
|
+
return runCommand('scaffold-deploy-plan', process.execPath, [packageScriptPath('treeseed'), 'deploy', '--plan'], {
|
|
496
496
|
cwd: siteRoot,
|
|
497
497
|
env: cacheEnv(),
|
|
498
498
|
});
|
|
499
499
|
});
|
|
500
|
-
await withStep('scaffold tenant destroy
|
|
500
|
+
await withStep('scaffold tenant destroy plan', async () => {
|
|
501
501
|
const deployConfig = readTenantConfig(siteRoot);
|
|
502
|
-
return runCommand('scaffold-destroy-
|
|
502
|
+
return runCommand('scaffold-destroy-plan', process.execPath, [packageScriptPath('treeseed'), 'destroy', '--plan', '--skip-confirmation', '--confirm', String(deployConfig.slug)], {
|
|
503
503
|
cwd: siteRoot,
|
|
504
504
|
env: cacheEnv(),
|
|
505
505
|
});
|