@treeseed/sdk 0.10.15 → 0.10.17
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/api/config.js +10 -3
- package/dist/operations/services/config-runtime.js +6 -1
- package/dist/operations/services/deploy.d.ts +107 -27
- package/dist/operations/services/deploy.js +852 -28
- package/dist/operations/services/project-platform.d.ts +4 -2
- package/dist/operations/services/project-platform.js +56 -27
- package/dist/operations/services/railway-api.d.ts +20 -0
- package/dist/operations/services/railway-api.js +153 -11
- package/dist/operations/services/railway-deploy.d.ts +7 -4
- package/dist/operations/services/railway-deploy.js +12 -4
- package/dist/platform/environment.js +3 -0
- package/dist/reconcile/builtin-adapters.js +34 -20
- package/dist/scripts/tenant-destroy.js +8 -2
- package/dist/workflow/operations.d.ts +92 -0
- package/dist/workflow/operations.js +11 -3
- package/dist/workflow.d.ts +1 -0
- package/package.json +1 -1
- package/templates/github/deploy-web.workflow.yml +8 -1
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { dirname, relative, resolve } from "node:path";
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
5
|
import { createInterface } from "node:readline/promises";
|
|
6
6
|
import { resolveTreeseedWebCachePolicy } from "../../platform/deploy-config.js";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
deleteRailwayCustomDomain,
|
|
9
|
+
deleteRailwayEnvironment,
|
|
10
|
+
deleteRailwayProject,
|
|
11
|
+
deleteRailwayVolume,
|
|
12
|
+
getRailwayServiceInstance,
|
|
13
|
+
listRailwayCustomDomains,
|
|
14
|
+
listRailwayProjects,
|
|
15
|
+
listRailwayVariables,
|
|
16
|
+
listRailwayVolumes,
|
|
17
|
+
normalizeRailwayEnvironmentName,
|
|
18
|
+
resolveRailwayApiToken,
|
|
19
|
+
resolveRailwayWorkspace,
|
|
20
|
+
resolveRailwayWorkspaceContext
|
|
21
|
+
} from "./railway-api.js";
|
|
8
22
|
import { loadCliDeployConfig, resolveWranglerBin } from "./runtime-tools.js";
|
|
9
23
|
import { sdkD1MigrationsRoot } from "./runtime-paths.js";
|
|
10
24
|
const DEFAULT_COMPATIBILITY_DATE = "2026-04-05";
|
|
@@ -252,11 +266,22 @@ function targetWorkersDevUrl(workerName) {
|
|
|
252
266
|
function relativeFromGeneratedRoot(targetPath, generatedRoot) {
|
|
253
267
|
return relative(generatedRoot, targetPath).replaceAll("\\", "/");
|
|
254
268
|
}
|
|
255
|
-
function
|
|
269
|
+
function resolveContentServingMode(deployConfig, options = {}) {
|
|
270
|
+
const override = envOrNull("TREESEED_CONTENT_SERVING_MODE");
|
|
271
|
+
if (override) {
|
|
272
|
+
return override;
|
|
273
|
+
}
|
|
274
|
+
const target = options.target ? normalizeTarget(options.target) : null;
|
|
275
|
+
if (target?.kind === "persistent" && target.scope !== "local") {
|
|
276
|
+
return "published_runtime";
|
|
277
|
+
}
|
|
278
|
+
return deployConfig.providers?.content?.serving ?? "local_collections";
|
|
279
|
+
}
|
|
280
|
+
function buildPublicVars(deployConfig, options = {}) {
|
|
256
281
|
const identity = resolveTreeseedResourceIdentity(deployConfig, createPersistentDeployTarget("prod"));
|
|
257
282
|
const contentRuntimeProvider = deployConfig.providers?.content?.runtime ?? "team_scoped_r2_overlay";
|
|
258
283
|
const contentPublishProvider = deployConfig.providers?.content?.publish ?? contentRuntimeProvider;
|
|
259
|
-
const contentServingMode =
|
|
284
|
+
const contentServingMode = resolveContentServingMode(deployConfig, options);
|
|
260
285
|
const contentDefaultTeamId = identity.teamId;
|
|
261
286
|
const contentManifestKeyTemplate = deployConfig.cloudflare.r2?.manifestKeyTemplate ?? "teams/{teamId}/published/common.json";
|
|
262
287
|
const contentPreviewRootTemplate = deployConfig.cloudflare.r2?.previewRootTemplate ?? "teams/{teamId}/previews";
|
|
@@ -751,7 +776,7 @@ function buildWranglerConfigContents(tenantRoot, deployConfig, state, options =
|
|
|
751
776
|
const assetsDirectory = relativeFromGeneratedRoot(resolve(tenantRoot, "dist"), generatedRoot);
|
|
752
777
|
const migrationsDir = relativeFromGeneratedRoot(sdkD1MigrationsRoot, generatedRoot);
|
|
753
778
|
const vars = {
|
|
754
|
-
...buildPublicVars(deployConfig),
|
|
779
|
+
...buildPublicVars(deployConfig, { target }),
|
|
755
780
|
...buildLocalRuntimeVars(deployConfig, state, target, options.env)
|
|
756
781
|
};
|
|
757
782
|
const r2Config = deployConfig.cloudflare.r2;
|
|
@@ -1645,7 +1670,7 @@ function resolveExistingD1ByName(d1Databases, expectedName, current) {
|
|
|
1645
1670
|
};
|
|
1646
1671
|
}
|
|
1647
1672
|
function looksLikeMissingResource(output) {
|
|
1648
|
-
return /not found|does not exist|could not find|
|
|
1673
|
+
return /not found|does not exist|could(?: not|n't) find|couldnt find/i.test(output);
|
|
1649
1674
|
}
|
|
1650
1675
|
function deleteKvNamespace(tenantRoot, namespaceId, { env, dryRun, preview = false }) {
|
|
1651
1676
|
if (!namespaceId || isPlaceholderResourceId(namespaceId)) {
|
|
@@ -1716,7 +1741,661 @@ ${result.stderr ?? ""}`;
|
|
|
1716
1741
|
}
|
|
1717
1742
|
return { status: result.status === 0 ? "deleted" : "missing", name: workerName };
|
|
1718
1743
|
}
|
|
1719
|
-
function
|
|
1744
|
+
function resourceOperation(provider, type, name, status, extra = {}) {
|
|
1745
|
+
return {
|
|
1746
|
+
provider,
|
|
1747
|
+
type,
|
|
1748
|
+
name: name ?? null,
|
|
1749
|
+
status,
|
|
1750
|
+
...extra
|
|
1751
|
+
};
|
|
1752
|
+
}
|
|
1753
|
+
function deleteCloudflareApiResource(path, { env, dryRun, name, type }) {
|
|
1754
|
+
if (!path) {
|
|
1755
|
+
return resourceOperation("cloudflare", type, name, "missing");
|
|
1756
|
+
}
|
|
1757
|
+
if (dryRun) {
|
|
1758
|
+
return resourceOperation("cloudflare", type, name, "planned", { path });
|
|
1759
|
+
}
|
|
1760
|
+
const result = cloudflareApiRequest(path, { method: "DELETE", env, allowFailure: true });
|
|
1761
|
+
if (result?.success === false && !looksLikeMissingResource(formatCloudflareErrors(result))) {
|
|
1762
|
+
throw new Error(formatCloudflareErrors(result) || `Failed to delete Cloudflare ${type} ${name}.`);
|
|
1763
|
+
}
|
|
1764
|
+
return resourceOperation("cloudflare", type, name, result?.success === false ? "missing" : "deleted", { path });
|
|
1765
|
+
}
|
|
1766
|
+
function formatCloudflareErrors(payload) {
|
|
1767
|
+
return Array.isArray(payload?.errors) ? payload.errors.map((entry) => entry?.message ?? JSON.stringify(entry)).filter(Boolean).join("; ") : "";
|
|
1768
|
+
}
|
|
1769
|
+
function deleteQueueByName(tenantRoot, queue, { env, dryRun }) {
|
|
1770
|
+
const name = queueName(queue) ?? queue?.name ?? null;
|
|
1771
|
+
const id = queueId(queue);
|
|
1772
|
+
if (!name) {
|
|
1773
|
+
return resourceOperation("cloudflare", "queue", name, "missing");
|
|
1774
|
+
}
|
|
1775
|
+
if (dryRun) {
|
|
1776
|
+
return resourceOperation("cloudflare", "queue", name, "planned", { id });
|
|
1777
|
+
}
|
|
1778
|
+
const accountId = env?.CLOUDFLARE_ACCOUNT_ID ?? process.env.CLOUDFLARE_ACCOUNT_ID ?? "";
|
|
1779
|
+
const path = id ? `/accounts/${encodeURIComponent(accountId)}/queues/${encodeURIComponent(id)}` : null;
|
|
1780
|
+
if (path) {
|
|
1781
|
+
const deleted = deleteCloudflareApiResource(path, { env, dryRun: false, name, type: "queue" });
|
|
1782
|
+
if (deleted.status === "deleted" || deleted.status === "missing") {
|
|
1783
|
+
return { ...deleted, id };
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
const result = runWrangler(["queues", "delete", name], {
|
|
1787
|
+
cwd: tenantRoot,
|
|
1788
|
+
allowFailure: true,
|
|
1789
|
+
capture: true,
|
|
1790
|
+
env,
|
|
1791
|
+
input: "y\n"
|
|
1792
|
+
});
|
|
1793
|
+
const output = `${result.stdout ?? ""}
|
|
1794
|
+
${result.stderr ?? ""}`;
|
|
1795
|
+
if (result.status !== 0 && !looksLikeMissingResource(output)) {
|
|
1796
|
+
throw new Error(output.trim() || `Failed to delete queue ${name}.`);
|
|
1797
|
+
}
|
|
1798
|
+
return resourceOperation("cloudflare", "queue", name, result.status === 0 ? "deleted" : "missing", { id });
|
|
1799
|
+
}
|
|
1800
|
+
function isLegacyTreeseedQueueName(name, scope) {
|
|
1801
|
+
if (!name || !scope) {
|
|
1802
|
+
return false;
|
|
1803
|
+
}
|
|
1804
|
+
const environmentNames = scope === "prod" ? ["prod", "production"] : [scope];
|
|
1805
|
+
return environmentNames.some(
|
|
1806
|
+
(environmentName) => name === `agent-work-${environmentName}` || name === `agent-work-dlq-${environmentName}`
|
|
1807
|
+
);
|
|
1808
|
+
}
|
|
1809
|
+
function legacyQueueDestroyOperations(tenantRoot, queues, target, knownNames, { env, dryRun }) {
|
|
1810
|
+
const scope = target.kind === "persistent" ? target.scope : target.branchName;
|
|
1811
|
+
return queues.filter((queue) => {
|
|
1812
|
+
const name = queueName(queue);
|
|
1813
|
+
return name && !knownNames.has(name) && isLegacyTreeseedQueueName(name, scope);
|
|
1814
|
+
}).map((queue) => {
|
|
1815
|
+
const deleted = deleteQueueByName(tenantRoot, queue, { env, dryRun });
|
|
1816
|
+
return { ...deleted, legacy: true };
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
function deleteR2Bucket(tenantRoot, bucketName, { env, dryRun, deleteData }) {
|
|
1820
|
+
if (!bucketName) {
|
|
1821
|
+
return resourceOperation("cloudflare", "r2-bucket", bucketName, "missing");
|
|
1822
|
+
}
|
|
1823
|
+
if (!deleteData) {
|
|
1824
|
+
return resourceOperation("cloudflare", "r2-bucket", bucketName, "skipped", { reason: "data_preserved" });
|
|
1825
|
+
}
|
|
1826
|
+
if (dryRun) {
|
|
1827
|
+
return resourceOperation("cloudflare", "r2-bucket", bucketName, "planned");
|
|
1828
|
+
}
|
|
1829
|
+
const drained = drainR2Bucket(bucketName, { env });
|
|
1830
|
+
const result = runWrangler(["r2", "bucket", "delete", bucketName], {
|
|
1831
|
+
cwd: tenantRoot,
|
|
1832
|
+
allowFailure: true,
|
|
1833
|
+
capture: true,
|
|
1834
|
+
env,
|
|
1835
|
+
input: "y\n"
|
|
1836
|
+
});
|
|
1837
|
+
const output = `${result.stdout ?? ""}
|
|
1838
|
+
${result.stderr ?? ""}`;
|
|
1839
|
+
if (result.status !== 0 && !looksLikeMissingResource(output)) {
|
|
1840
|
+
throw new Error(output.trim() || `Failed to delete R2 bucket ${bucketName}.`);
|
|
1841
|
+
}
|
|
1842
|
+
return resourceOperation("cloudflare", "r2-bucket", bucketName, result.status === 0 ? "deleted" : "missing", drained);
|
|
1843
|
+
}
|
|
1844
|
+
function r2ObjectKey(entry) {
|
|
1845
|
+
return typeof entry?.key === "string" ? entry.key : typeof entry?.name === "string" ? entry.name : "";
|
|
1846
|
+
}
|
|
1847
|
+
function listR2Objects(bucketName, { env }) {
|
|
1848
|
+
const accountId = env?.CLOUDFLARE_ACCOUNT_ID ?? process.env.CLOUDFLARE_ACCOUNT_ID ?? "";
|
|
1849
|
+
if (!accountId || !bucketName) {
|
|
1850
|
+
return [];
|
|
1851
|
+
}
|
|
1852
|
+
const objects = [];
|
|
1853
|
+
let cursor = "";
|
|
1854
|
+
for (let page = 0; page < 100; page += 1) {
|
|
1855
|
+
const payload = cloudflareApiRequest(
|
|
1856
|
+
`/accounts/${encodeURIComponent(accountId)}/r2/buckets/${encodeURIComponent(bucketName)}/objects?per_page=1000${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""}`,
|
|
1857
|
+
{ env, allowFailure: true }
|
|
1858
|
+
);
|
|
1859
|
+
if (payload?.success === false) {
|
|
1860
|
+
break;
|
|
1861
|
+
}
|
|
1862
|
+
const pageObjects = Array.isArray(payload?.result) ? payload.result : Array.isArray(payload?.result?.objects) ? payload.result.objects : [];
|
|
1863
|
+
objects.push(...pageObjects);
|
|
1864
|
+
const nextCursor = typeof payload?.result_info?.cursor === "string" ? payload.result_info.cursor : typeof payload?.result?.cursor === "string" ? payload.result.cursor : "";
|
|
1865
|
+
if (!nextCursor || nextCursor === cursor || pageObjects.length === 0) {
|
|
1866
|
+
break;
|
|
1867
|
+
}
|
|
1868
|
+
cursor = nextCursor;
|
|
1869
|
+
}
|
|
1870
|
+
return objects;
|
|
1871
|
+
}
|
|
1872
|
+
function drainR2Bucket(bucketName, { env }) {
|
|
1873
|
+
const accountId = env?.CLOUDFLARE_ACCOUNT_ID ?? process.env.CLOUDFLARE_ACCOUNT_ID ?? "";
|
|
1874
|
+
if (!accountId || !bucketName) {
|
|
1875
|
+
return { objectsDeleted: 0 };
|
|
1876
|
+
}
|
|
1877
|
+
let objectsDeleted = 0;
|
|
1878
|
+
for (let batch = 0; batch < 100; batch += 1) {
|
|
1879
|
+
const objects = listR2Objects(bucketName, { env });
|
|
1880
|
+
if (objects.length === 0) {
|
|
1881
|
+
break;
|
|
1882
|
+
}
|
|
1883
|
+
let batchDeleted = 0;
|
|
1884
|
+
for (const object of objects) {
|
|
1885
|
+
const key = r2ObjectKey(object);
|
|
1886
|
+
if (!key) {
|
|
1887
|
+
continue;
|
|
1888
|
+
}
|
|
1889
|
+
const result = cloudflareApiRequest(
|
|
1890
|
+
`/accounts/${encodeURIComponent(accountId)}/r2/buckets/${encodeURIComponent(bucketName)}/objects/${encodeURIComponent(key)}`,
|
|
1891
|
+
{ method: "DELETE", env, allowFailure: true }
|
|
1892
|
+
);
|
|
1893
|
+
if (result?.success === false) {
|
|
1894
|
+
const message = formatCloudflareErrors(result);
|
|
1895
|
+
if (looksLikeMissingResource(message)) {
|
|
1896
|
+
continue;
|
|
1897
|
+
}
|
|
1898
|
+
throw new Error(message || `Failed to delete R2 object ${key}.`);
|
|
1899
|
+
}
|
|
1900
|
+
objectsDeleted += 1;
|
|
1901
|
+
batchDeleted += 1;
|
|
1902
|
+
}
|
|
1903
|
+
if (batchDeleted === 0) {
|
|
1904
|
+
break;
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
return { objectsDeleted };
|
|
1908
|
+
}
|
|
1909
|
+
function deleteD1DatabaseForDestroy(tenantRoot, databaseName, { env, dryRun, deleteData }) {
|
|
1910
|
+
if (!deleteData) {
|
|
1911
|
+
return resourceOperation("cloudflare", "d1-database", databaseName, "skipped", { reason: "data_preserved" });
|
|
1912
|
+
}
|
|
1913
|
+
const result = deleteD1Database(tenantRoot, databaseName, { env, dryRun });
|
|
1914
|
+
return resourceOperation("cloudflare", "d1-database", databaseName, result.status, result);
|
|
1915
|
+
}
|
|
1916
|
+
function pagesDomainName(domain) {
|
|
1917
|
+
return typeof domain?.name === "string" ? domain.name : typeof domain?.domain === "string" ? domain.domain : typeof domain?.hostname === "string" ? domain.hostname : "";
|
|
1918
|
+
}
|
|
1919
|
+
function listPagesCustomDomains(projectName, { env }) {
|
|
1920
|
+
const accountId = env?.CLOUDFLARE_ACCOUNT_ID ?? process.env.CLOUDFLARE_ACCOUNT_ID ?? "";
|
|
1921
|
+
if (!projectName || !accountId) {
|
|
1922
|
+
return [];
|
|
1923
|
+
}
|
|
1924
|
+
const domains = [];
|
|
1925
|
+
let page = 1;
|
|
1926
|
+
let totalPages = 1;
|
|
1927
|
+
while (page <= totalPages && page <= 50) {
|
|
1928
|
+
const payload = cloudflareApiRequest(
|
|
1929
|
+
`/accounts/${encodeURIComponent(accountId)}/pages/projects/${encodeURIComponent(projectName)}/domains?per_page=100&page=${page}`,
|
|
1930
|
+
{ env, allowFailure: true }
|
|
1931
|
+
);
|
|
1932
|
+
if (payload?.success === false) {
|
|
1933
|
+
break;
|
|
1934
|
+
}
|
|
1935
|
+
if (Array.isArray(payload?.result)) {
|
|
1936
|
+
domains.push(...payload.result);
|
|
1937
|
+
}
|
|
1938
|
+
const reportedTotal = Number(payload?.result_info?.total_pages);
|
|
1939
|
+
totalPages = Number.isFinite(reportedTotal) && reportedTotal > 0 ? reportedTotal : page;
|
|
1940
|
+
page += 1;
|
|
1941
|
+
}
|
|
1942
|
+
return domains;
|
|
1943
|
+
}
|
|
1944
|
+
function listPagesCustomDomainsWithWrangler(tenantRoot, projectName, { env }) {
|
|
1945
|
+
const result = runWrangler(["pages", "project", "list", "--json"], {
|
|
1946
|
+
cwd: tenantRoot,
|
|
1947
|
+
allowFailure: true,
|
|
1948
|
+
capture: true,
|
|
1949
|
+
env
|
|
1950
|
+
});
|
|
1951
|
+
if (result.status !== 0) {
|
|
1952
|
+
return [];
|
|
1953
|
+
}
|
|
1954
|
+
try {
|
|
1955
|
+
const projects = JSON.parse(result.stdout || "[]");
|
|
1956
|
+
const project = (Array.isArray(projects) ? projects : []).find((entry) => entry?.name === projectName || entry?.projectName === projectName || entry?.["Project Name"] === projectName);
|
|
1957
|
+
const domains = typeof project?.["Project Domains"] === "string" ? project["Project Domains"] : typeof project?.domains === "string" ? project.domains : "";
|
|
1958
|
+
return domains.split(",").map((entry) => entry.trim()).filter((entry) => entry && !entry.endsWith(".pages.dev"));
|
|
1959
|
+
} catch {
|
|
1960
|
+
return [];
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
function deletePagesCustomDomains(tenantRoot, projectName, knownNames, { env, dryRun }) {
|
|
1964
|
+
if (!projectName) {
|
|
1965
|
+
return [resourceOperation("cloudflare", "pages-custom-domain", projectName, "missing")];
|
|
1966
|
+
}
|
|
1967
|
+
const desiredNames = [...new Set((knownNames ?? []).filter(Boolean))];
|
|
1968
|
+
if (dryRun) {
|
|
1969
|
+
return desiredNames.length > 0 ? desiredNames.map((name) => resourceOperation("cloudflare", "pages-custom-domain", name, "planned", { projectName })) : [resourceOperation("cloudflare", "pages-custom-domain", projectName, "planned", { reason: "project_delete_prerequisite" })];
|
|
1970
|
+
}
|
|
1971
|
+
const accountId = env?.CLOUDFLARE_ACCOUNT_ID ?? process.env.CLOUDFLARE_ACCOUNT_ID ?? "";
|
|
1972
|
+
if (!accountId) {
|
|
1973
|
+
return desiredNames.length > 0 ? desiredNames.map((name) => resourceOperation("cloudflare", "pages-custom-domain", name, "blocked", { projectName, reason: "missing_cloudflare_account_id" })) : [resourceOperation("cloudflare", "pages-custom-domain", projectName, "blocked", { reason: "missing_cloudflare_account_id" })];
|
|
1974
|
+
}
|
|
1975
|
+
const listedNames = listPagesCustomDomains(projectName, { env }).map(pagesDomainName).filter(Boolean);
|
|
1976
|
+
const wranglerNames = listPagesCustomDomainsWithWrangler(tenantRoot, projectName, { env });
|
|
1977
|
+
const domainNames = [.../* @__PURE__ */ new Set([...desiredNames, ...listedNames, ...wranglerNames])];
|
|
1978
|
+
if (domainNames.length === 0) {
|
|
1979
|
+
return [resourceOperation("cloudflare", "pages-custom-domain", projectName, "missing", { projectName })];
|
|
1980
|
+
}
|
|
1981
|
+
return domainNames.map((name) => deleteCloudflareApiResource(
|
|
1982
|
+
`/accounts/${encodeURIComponent(accountId)}/pages/projects/${encodeURIComponent(projectName)}/domains/${encodeURIComponent(name)}`,
|
|
1983
|
+
{ env, dryRun: false, name, type: "pages-custom-domain" }
|
|
1984
|
+
));
|
|
1985
|
+
}
|
|
1986
|
+
function normalizePagesDeploymentId(deployment) {
|
|
1987
|
+
return typeof deployment?.id === "string" ? deployment.id : typeof deployment?.Id === "string" ? deployment.Id : "";
|
|
1988
|
+
}
|
|
1989
|
+
function normalizePagesDeployments(value) {
|
|
1990
|
+
return (Array.isArray(value) ? value : Array.isArray(value?.result) ? value.result : []).filter((entry) => normalizePagesDeploymentId(entry));
|
|
1991
|
+
}
|
|
1992
|
+
function listPagesDeploymentsWithApi(projectName, { env }) {
|
|
1993
|
+
const accountId = env?.CLOUDFLARE_ACCOUNT_ID ?? process.env.CLOUDFLARE_ACCOUNT_ID ?? "";
|
|
1994
|
+
if (!projectName || !accountId) {
|
|
1995
|
+
return [];
|
|
1996
|
+
}
|
|
1997
|
+
const deployments = [];
|
|
1998
|
+
for (const pagesEnvironment of ["preview", "production"]) {
|
|
1999
|
+
let page = 1;
|
|
2000
|
+
let totalPages = 1;
|
|
2001
|
+
while (page <= totalPages && page <= 50) {
|
|
2002
|
+
const payload = cloudflareApiRequest(
|
|
2003
|
+
`/accounts/${encodeURIComponent(accountId)}/pages/projects/${encodeURIComponent(projectName)}/deployments?per_page=100&page=${page}&env=${pagesEnvironment}`,
|
|
2004
|
+
{ env, allowFailure: true }
|
|
2005
|
+
);
|
|
2006
|
+
if (payload?.success === false) {
|
|
2007
|
+
break;
|
|
2008
|
+
}
|
|
2009
|
+
deployments.push(...normalizePagesDeployments(payload));
|
|
2010
|
+
const reportedTotal = Number(payload?.result_info?.total_pages);
|
|
2011
|
+
totalPages = Number.isFinite(reportedTotal) && reportedTotal > 0 ? reportedTotal : page;
|
|
2012
|
+
page += 1;
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
return deployments;
|
|
2016
|
+
}
|
|
2017
|
+
function listPagesDeployments(tenantRoot, projectName, { env }) {
|
|
2018
|
+
const deployments = [];
|
|
2019
|
+
for (const pagesEnvironment of ["preview", "production"]) {
|
|
2020
|
+
const result = runWrangler(["pages", "deployment", "list", "--project-name", projectName, "--environment", pagesEnvironment, "--json"], {
|
|
2021
|
+
cwd: tenantRoot,
|
|
2022
|
+
allowFailure: true,
|
|
2023
|
+
capture: true,
|
|
2024
|
+
env
|
|
2025
|
+
});
|
|
2026
|
+
if (result.status !== 0) {
|
|
2027
|
+
continue;
|
|
2028
|
+
}
|
|
2029
|
+
try {
|
|
2030
|
+
deployments.push(...normalizePagesDeployments(JSON.parse(result.stdout || "[]")));
|
|
2031
|
+
} catch {
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
if (deployments.length > 0) {
|
|
2035
|
+
const byId = new Map(deployments.map((deployment) => [normalizePagesDeploymentId(deployment), deployment]));
|
|
2036
|
+
return [...byId.values()];
|
|
2037
|
+
}
|
|
2038
|
+
return listPagesDeploymentsWithApi(projectName, { env });
|
|
2039
|
+
}
|
|
2040
|
+
function deletePagesDeployments(tenantRoot, projectName, { env, dryRun }) {
|
|
2041
|
+
if (!projectName) {
|
|
2042
|
+
return resourceOperation("cloudflare", "pages-deployments", projectName, "missing");
|
|
2043
|
+
}
|
|
2044
|
+
if (dryRun) {
|
|
2045
|
+
return resourceOperation("cloudflare", "pages-deployments", projectName, "planned", { reason: "project_delete_prerequisite" });
|
|
2046
|
+
}
|
|
2047
|
+
const accountId = env?.CLOUDFLARE_ACCOUNT_ID ?? process.env.CLOUDFLARE_ACCOUNT_ID ?? "";
|
|
2048
|
+
if (!accountId) {
|
|
2049
|
+
return resourceOperation("cloudflare", "pages-deployments", projectName, "blocked", { reason: "missing_cloudflare_account_id" });
|
|
2050
|
+
}
|
|
2051
|
+
let deleted = 0;
|
|
2052
|
+
let skipped = 0;
|
|
2053
|
+
let total = 0;
|
|
2054
|
+
for (let batch = 0; batch < 100; batch += 1) {
|
|
2055
|
+
const deployments = listPagesDeployments(tenantRoot, projectName, { env });
|
|
2056
|
+
if (deployments.length === 0) {
|
|
2057
|
+
return resourceOperation("cloudflare", "pages-deployments", projectName, deleted > 0 ? "deleted" : "missing", {
|
|
2058
|
+
deleted,
|
|
2059
|
+
skipped,
|
|
2060
|
+
total
|
|
2061
|
+
});
|
|
2062
|
+
}
|
|
2063
|
+
total += deployments.length;
|
|
2064
|
+
let batchDeleted = 0;
|
|
2065
|
+
let batchSkipped = 0;
|
|
2066
|
+
for (const deployment of deployments) {
|
|
2067
|
+
const deploymentId = normalizePagesDeploymentId(deployment);
|
|
2068
|
+
if (!deploymentId) {
|
|
2069
|
+
continue;
|
|
2070
|
+
}
|
|
2071
|
+
const result = cloudflareApiRequest(
|
|
2072
|
+
`/accounts/${encodeURIComponent(accountId)}/pages/projects/${encodeURIComponent(projectName)}/deployments/${encodeURIComponent(deploymentId)}`,
|
|
2073
|
+
{ method: "DELETE", env, allowFailure: true }
|
|
2074
|
+
);
|
|
2075
|
+
if (result?.success === false) {
|
|
2076
|
+
const message = formatCloudflareErrors(result);
|
|
2077
|
+
if (/active production deployment|production deployment|deployment is aliased|aliased deployment/iu.test(message)) {
|
|
2078
|
+
skipped += 1;
|
|
2079
|
+
batchSkipped += 1;
|
|
2080
|
+
continue;
|
|
2081
|
+
}
|
|
2082
|
+
if (looksLikeMissingResource(message)) {
|
|
2083
|
+
continue;
|
|
2084
|
+
}
|
|
2085
|
+
throw new Error(message || `Failed to delete Pages deployment ${deploymentId}.`);
|
|
2086
|
+
}
|
|
2087
|
+
deleted += 1;
|
|
2088
|
+
batchDeleted += 1;
|
|
2089
|
+
}
|
|
2090
|
+
if (batchDeleted === 0 && batchSkipped >= deployments.length) {
|
|
2091
|
+
break;
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
return resourceOperation("cloudflare", "pages-deployments", projectName, deleted > 0 ? "deleted" : "skipped", {
|
|
2095
|
+
deleted,
|
|
2096
|
+
skipped,
|
|
2097
|
+
total
|
|
2098
|
+
});
|
|
2099
|
+
}
|
|
2100
|
+
function deletePagesProject(projectName, { env, dryRun }) {
|
|
2101
|
+
const accountId = env?.CLOUDFLARE_ACCOUNT_ID ?? process.env.CLOUDFLARE_ACCOUNT_ID ?? "";
|
|
2102
|
+
if (!projectName || !accountId) {
|
|
2103
|
+
return resourceOperation("cloudflare", "pages-project", projectName, "missing");
|
|
2104
|
+
}
|
|
2105
|
+
return deleteCloudflareApiResource(
|
|
2106
|
+
`/accounts/${encodeURIComponent(accountId)}/pages/projects/${encodeURIComponent(projectName)}`,
|
|
2107
|
+
{ env, dryRun, name: projectName, type: "pages-project" }
|
|
2108
|
+
);
|
|
2109
|
+
}
|
|
2110
|
+
function listDnsRecordsForName(zoneId, name, env) {
|
|
2111
|
+
if (!zoneId || !name) {
|
|
2112
|
+
return [];
|
|
2113
|
+
}
|
|
2114
|
+
const result = cloudflareApiRequest(`/zones/${encodeURIComponent(zoneId)}/dns_records?name=${encodeURIComponent(name)}&per_page=100`, { env, allowFailure: true });
|
|
2115
|
+
return Array.isArray(result?.result) ? result.result : [];
|
|
2116
|
+
}
|
|
2117
|
+
function deleteDnsRecordsForName(deployConfig, name, { env, dryRun }) {
|
|
2118
|
+
if (!name) {
|
|
2119
|
+
return [resourceOperation("cloudflare", "dns-record", name, "missing")];
|
|
2120
|
+
}
|
|
2121
|
+
if (dryRun) {
|
|
2122
|
+
return [resourceOperation("cloudflare", "dns-record", name, "planned")];
|
|
2123
|
+
}
|
|
2124
|
+
const zoneId = resolveCloudflareZoneIdForHost(deployConfig, name, env);
|
|
2125
|
+
if (!zoneId) {
|
|
2126
|
+
return [resourceOperation("cloudflare", "dns-record", name, "blocked", { reason: "zone_unresolved" })];
|
|
2127
|
+
}
|
|
2128
|
+
const records = listDnsRecordsForName(zoneId, name, env);
|
|
2129
|
+
if (records.length === 0) {
|
|
2130
|
+
return [resourceOperation("cloudflare", "dns-record", name, "missing", { zoneId })];
|
|
2131
|
+
}
|
|
2132
|
+
return records.map((record) => {
|
|
2133
|
+
const recordName = record?.name ?? name;
|
|
2134
|
+
return deleteCloudflareApiResource(
|
|
2135
|
+
`/zones/${encodeURIComponent(zoneId)}/dns_records/${encodeURIComponent(record.id)}`,
|
|
2136
|
+
{ env, dryRun: false, name: recordName, type: "dns-record" }
|
|
2137
|
+
);
|
|
2138
|
+
});
|
|
2139
|
+
}
|
|
2140
|
+
function deleteTreeseedCacheRules(deployConfig, state, { env, dryRun }) {
|
|
2141
|
+
const targets = [
|
|
2142
|
+
{ role: "web", zoneId: state.webCache?.webZoneId, host: state.webCache?.webHost },
|
|
2143
|
+
{ role: "content", zoneId: state.webCache?.contentZoneId, host: state.webCache?.contentHost }
|
|
2144
|
+
].filter((entry) => entry.host || entry.zoneId);
|
|
2145
|
+
if (targets.length === 0) {
|
|
2146
|
+
return [resourceOperation("cloudflare", "cache-rules", null, "missing")];
|
|
2147
|
+
}
|
|
2148
|
+
return targets.map((target) => {
|
|
2149
|
+
const zoneId = target.zoneId ?? resolveCloudflareZoneIdForHost(deployConfig, target.host, env);
|
|
2150
|
+
if (!zoneId) {
|
|
2151
|
+
return resourceOperation("cloudflare", "cache-rules", target.host, "blocked", { reason: "zone_unresolved" });
|
|
2152
|
+
}
|
|
2153
|
+
if (dryRun) {
|
|
2154
|
+
return resourceOperation("cloudflare", "cache-rules", target.host, "planned", { zoneId });
|
|
2155
|
+
}
|
|
2156
|
+
const rulesets = listCloudflareZoneRulesets(zoneId, env);
|
|
2157
|
+
const ruleset = rulesets.find((entry) => entry?.phase === "http_request_cache_settings") ?? null;
|
|
2158
|
+
const rules = Array.isArray(ruleset?.rules) ? ruleset.rules : [];
|
|
2159
|
+
const kept = rules.filter((rule) => typeof rule?.description !== "string" || !rule.description.startsWith("treeseed-managed:"));
|
|
2160
|
+
if (!ruleset || kept.length === rules.length) {
|
|
2161
|
+
return resourceOperation("cloudflare", "cache-rules", target.host, "missing", { zoneId });
|
|
2162
|
+
}
|
|
2163
|
+
cloudflareApiRequest(`/zones/${encodeURIComponent(zoneId)}/rulesets/${encodeURIComponent(ruleset.id)}`, {
|
|
2164
|
+
method: "PUT",
|
|
2165
|
+
body: { rules: kept },
|
|
2166
|
+
env
|
|
2167
|
+
});
|
|
2168
|
+
return resourceOperation("cloudflare", "cache-rules", target.host, "deleted", { zoneId, rulesetId: ruleset.id, removed: rules.length - kept.length });
|
|
2169
|
+
});
|
|
2170
|
+
}
|
|
2171
|
+
function configuredRailwayDestroyTargets(tenantRoot, deployConfig, scope) {
|
|
2172
|
+
const normalizedScope = scope === "prod" ? "prod" : scope === "staging" ? "staging" : "local";
|
|
2173
|
+
if (normalizedScope === "local" || deployConfig.runtime?.mode !== "treeseed_managed") {
|
|
2174
|
+
return [];
|
|
2175
|
+
}
|
|
2176
|
+
let identity;
|
|
2177
|
+
try {
|
|
2178
|
+
identity = resolveTreeseedResourceIdentity(deployConfig, createPersistentDeployTarget(normalizedScope));
|
|
2179
|
+
} catch {
|
|
2180
|
+
identity = { deploymentKey: deployConfig.slug ?? deployConfig.name ?? "treeseed" };
|
|
2181
|
+
}
|
|
2182
|
+
const services = [];
|
|
2183
|
+
for (const serviceKey of ["api", "marketOperationsRunner"]) {
|
|
2184
|
+
const service = deployConfig.services?.[serviceKey];
|
|
2185
|
+
if (!service || service.enabled === false || (service.provider ?? "railway") !== "railway") {
|
|
2186
|
+
continue;
|
|
2187
|
+
}
|
|
2188
|
+
const baseServiceName = service.railway?.serviceName ?? `${identity.deploymentKey}-${serviceKey === "marketOperationsRunner" ? "market-operations-runner" : serviceKey}`;
|
|
2189
|
+
const runnerPool = serviceKey === "marketOperationsRunner" && service.railway?.runnerPool && typeof service.railway.runnerPool === "object" ? service.railway.runnerPool : null;
|
|
2190
|
+
const count = serviceKey === "marketOperationsRunner" ? Math.max(1, Number.parseInt(String(runnerPool?.bootstrapCount ?? 1), 10) || 1) : 1;
|
|
2191
|
+
for (let index = 1; index <= count; index += 1) {
|
|
2192
|
+
const serviceName = serviceKey === "marketOperationsRunner" ? `${String(baseServiceName).replace(/-\d+$/u, "")}-${String(index).padStart(2, "0")}` : baseServiceName;
|
|
2193
|
+
services.push({
|
|
2194
|
+
key: serviceKey,
|
|
2195
|
+
projectName: service.railway?.projectName ?? identity.deploymentKey,
|
|
2196
|
+
serviceName,
|
|
2197
|
+
railwayEnvironment: normalizeRailwayEnvironmentName(service.environments?.[normalizedScope]?.railwayEnvironment ?? normalizedScope),
|
|
2198
|
+
domain: service.environments?.[normalizedScope]?.domain ?? null,
|
|
2199
|
+
volumeMountPath: serviceKey === "marketOperationsRunner" ? service.railway?.volumeMountPath ?? runnerPool?.volumeMountPath ?? "/data" : null
|
|
2200
|
+
});
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
const marketDatabase = deployConfig.services?.marketDatabase;
|
|
2204
|
+
if (marketDatabase?.enabled !== false && marketDatabase?.provider === "railway" && marketDatabase?.railway?.resourceType === "postgres") {
|
|
2205
|
+
const baseName = typeof marketDatabase.railway?.serviceName === "string" && marketDatabase.railway.serviceName.trim() ? marketDatabase.railway.serviceName.trim() : `${deployConfig.slug ?? "treeseed-market"}-postgres`;
|
|
2206
|
+
services.push({
|
|
2207
|
+
key: "marketDatabase",
|
|
2208
|
+
projectName: deployConfig.services?.api?.railway?.projectName ?? identity.deploymentKey,
|
|
2209
|
+
serviceName: `${baseName.replace(/-(staging|prod|production)$/u, "")}-${normalizedScope === "prod" ? "prod" : normalizedScope}`,
|
|
2210
|
+
railwayEnvironment: normalizeRailwayEnvironmentName(normalizedScope),
|
|
2211
|
+
domain: null,
|
|
2212
|
+
volumeMountPath: null,
|
|
2213
|
+
dataStore: true
|
|
2214
|
+
});
|
|
2215
|
+
}
|
|
2216
|
+
return services;
|
|
2217
|
+
}
|
|
2218
|
+
async function destroyRailwayResources(tenantRoot, deployConfig, target, { dryRun = false, deleteData = false, env = process.env } = {}) {
|
|
2219
|
+
const scope = target.kind === "persistent" ? target.scope : target.branchName;
|
|
2220
|
+
const services = configuredRailwayDestroyTargets(tenantRoot, deployConfig, scope);
|
|
2221
|
+
if (services.length === 0) {
|
|
2222
|
+
return { operations: [resourceOperation("railway", "environment", scope, "skipped", { reason: "not_applicable" })] };
|
|
2223
|
+
}
|
|
2224
|
+
const operations = [];
|
|
2225
|
+
if (!resolveRailwayApiToken(env)) {
|
|
2226
|
+
return {
|
|
2227
|
+
operations: services.map((service) => resourceOperation("railway", "service", service.serviceName, "blocked", { reason: "missing_railway_api_token" }))
|
|
2228
|
+
};
|
|
2229
|
+
}
|
|
2230
|
+
const workspace = await resolveRailwayWorkspaceContext({ env, workspace: resolveRailwayWorkspace(env) });
|
|
2231
|
+
const projects = await listRailwayProjects({ env, workspaceId: workspace.id });
|
|
2232
|
+
const projectNames = [...new Set(services.map((service) => service.projectName).filter(Boolean))];
|
|
2233
|
+
for (const projectName of projectNames) {
|
|
2234
|
+
const project = projects.find((entry) => !entry.deletedAt && (entry.name === projectName || entry.id === projectName)) ?? null;
|
|
2235
|
+
if (!project) {
|
|
2236
|
+
operations.push(resourceOperation("railway", "project", projectName, "missing"));
|
|
2237
|
+
continue;
|
|
2238
|
+
}
|
|
2239
|
+
const serviceTargets = services.filter((service) => service.projectName === projectName);
|
|
2240
|
+
const environmentName = normalizeRailwayEnvironmentName(scope);
|
|
2241
|
+
const environment = project.environments.find((entry) => entry.name === environmentName || entry.id === environmentName) ?? null;
|
|
2242
|
+
if (!environment) {
|
|
2243
|
+
operations.push(resourceOperation("railway", "environment", environmentName, "missing", { projectId: project.id }));
|
|
2244
|
+
}
|
|
2245
|
+
for (const service of serviceTargets) {
|
|
2246
|
+
const railwayService = project.services.find((entry) => entry.name === service.serviceName || entry.id === service.serviceName) ?? null;
|
|
2247
|
+
const shouldDeleteData = service.dataStore ? deleteData : true;
|
|
2248
|
+
operations.push(resourceOperation("railway", service.dataStore ? "postgres-service" : "service", service.serviceName, railwayService ? dryRun ? "planned" : "planned" : "missing", {
|
|
2249
|
+
projectId: project.id,
|
|
2250
|
+
serviceId: railwayService?.id ?? null,
|
|
2251
|
+
...service.dataStore && !shouldDeleteData ? { status: "skipped", reason: "data_preserved" } : {}
|
|
2252
|
+
}));
|
|
2253
|
+
if (!railwayService || !environment) {
|
|
2254
|
+
continue;
|
|
2255
|
+
}
|
|
2256
|
+
if (shouldDeleteData) {
|
|
2257
|
+
const variables = await listRailwayVariables({
|
|
2258
|
+
projectId: project.id,
|
|
2259
|
+
environmentId: environment.id,
|
|
2260
|
+
serviceId: railwayService.id,
|
|
2261
|
+
env
|
|
2262
|
+
});
|
|
2263
|
+
for (const variableName of Object.keys(variables).sort()) {
|
|
2264
|
+
operations.push(resourceOperation("railway", "variable", `${service.serviceName}:${variableName}`, dryRun ? "planned" : "deleted", {
|
|
2265
|
+
projectId: project.id,
|
|
2266
|
+
serviceId: railwayService.id,
|
|
2267
|
+
environmentId: environment.id,
|
|
2268
|
+
reason: scope === "prod" && deleteData ? "project_delete" : "environment_delete"
|
|
2269
|
+
}));
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
const instance = await getRailwayServiceInstance({ serviceId: railwayService.id, environmentId: environment.id, env });
|
|
2273
|
+
if (instance.cronSchedule) {
|
|
2274
|
+
operations.push(resourceOperation("railway", "schedule", `${service.serviceName}:${instance.cronSchedule}`, dryRun ? "planned" : "deleted", {
|
|
2275
|
+
projectId: project.id,
|
|
2276
|
+
serviceId: railwayService.id,
|
|
2277
|
+
environmentId: environment.id,
|
|
2278
|
+
reason: scope === "prod" && deleteData ? "project_delete" : "environment_delete"
|
|
2279
|
+
}));
|
|
2280
|
+
}
|
|
2281
|
+
if (!service.dataStore && service.domain) {
|
|
2282
|
+
const domains = await listRailwayCustomDomains({ projectId: project.id, environmentId: environment.id, serviceId: railwayService.id, env });
|
|
2283
|
+
for (const domain of domains.filter((entry) => entry.domain === service.domain)) {
|
|
2284
|
+
if (dryRun) {
|
|
2285
|
+
operations.push(resourceOperation("railway", "custom-domain", domain.domain, "planned", { id: domain.id }));
|
|
2286
|
+
} else {
|
|
2287
|
+
const result = await deleteRailwayCustomDomain({ domainId: domain.id, env });
|
|
2288
|
+
operations.push(resourceOperation("railway", "custom-domain", domain.domain, result.status, { id: domain.id }));
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
const volumes = await listRailwayVolumes({ projectId: project.id, env });
|
|
2294
|
+
for (const volume of volumes) {
|
|
2295
|
+
const matchingInstance = volume.instances.find((instance) => instance.environmentId === environment?.id);
|
|
2296
|
+
if (!matchingInstance) {
|
|
2297
|
+
continue;
|
|
2298
|
+
}
|
|
2299
|
+
if (dryRun) {
|
|
2300
|
+
operations.push(resourceOperation("railway", "volume", volume.name, "planned", { id: volume.id, projectId: project.id }));
|
|
2301
|
+
} else {
|
|
2302
|
+
const result = await deleteRailwayVolume({ volumeId: volume.id, env });
|
|
2303
|
+
operations.push(resourceOperation("railway", "volume", volume.name, result.status, { id: volume.id, projectId: project.id }));
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
const shouldDeleteProject = shouldDeleteRailwayProjectAfterEnvironmentDestroy(project, scope, deleteData, environment?.id ?? null);
|
|
2307
|
+
if (scope === "prod" && deleteData || shouldDeleteProject) {
|
|
2308
|
+
if (dryRun) {
|
|
2309
|
+
operations.push(resourceOperation("railway", "project", project.name, "planned", {
|
|
2310
|
+
id: project.id,
|
|
2311
|
+
reason: scope === "prod" ? "prod_delete_data_cleanup" : "no_managed_persistent_environments"
|
|
2312
|
+
}));
|
|
2313
|
+
} else {
|
|
2314
|
+
const result = await deleteRailwayProject({ projectId: project.id, env });
|
|
2315
|
+
operations.push(resourceOperation("railway", "project", project.name, result.status, {
|
|
2316
|
+
id: project.id,
|
|
2317
|
+
reason: scope === "prod" ? "prod_delete_data_cleanup" : "no_managed_persistent_environments"
|
|
2318
|
+
}));
|
|
2319
|
+
}
|
|
2320
|
+
} else if (environment) {
|
|
2321
|
+
if (dryRun) {
|
|
2322
|
+
operations.push(resourceOperation("railway", "environment", environment.name, "planned", { id: environment.id, projectId: project.id }));
|
|
2323
|
+
} else {
|
|
2324
|
+
const result = await deleteRailwayEnvironment({ environmentId: environment.id, env });
|
|
2325
|
+
operations.push(resourceOperation("railway", "environment", environment.name, result.status, { id: environment.id, projectId: project.id }));
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
return { operations };
|
|
2330
|
+
}
|
|
2331
|
+
function shouldDeleteRailwayProjectAfterEnvironmentDestroy(project, scope, deleteData, deletedEnvironmentId = null) {
|
|
2332
|
+
if (!deleteData || scope === "prod") {
|
|
2333
|
+
return false;
|
|
2334
|
+
}
|
|
2335
|
+
const managedPersistentNames = /* @__PURE__ */ new Set(["staging", "production", "prod"]);
|
|
2336
|
+
const targetEnvironmentName = normalizeRailwayEnvironmentName(scope);
|
|
2337
|
+
const remainingManagedEnvironments = (project?.environments ?? []).filter((environment) => environment?.id !== deletedEnvironmentId).filter((environment) => environment?.name !== targetEnvironmentName).filter((environment) => managedPersistentNames.has(environment?.name));
|
|
2338
|
+
return remainingManagedEnvironments.length === 0;
|
|
2339
|
+
}
|
|
2340
|
+
function killPidFromFile(filePath, { dryRun }) {
|
|
2341
|
+
const pid = Number.parseInt(readFileSync(filePath, "utf8").trim(), 10);
|
|
2342
|
+
if (!Number.isFinite(pid) || pid <= 0) {
|
|
2343
|
+
return resourceOperation("local", "dev-process", filePath, "missing");
|
|
2344
|
+
}
|
|
2345
|
+
if (dryRun) {
|
|
2346
|
+
return resourceOperation("local", "dev-process", String(pid), "planned", { pidFile: filePath });
|
|
2347
|
+
}
|
|
2348
|
+
try {
|
|
2349
|
+
process.kill(-pid, "SIGTERM");
|
|
2350
|
+
} catch {
|
|
2351
|
+
try {
|
|
2352
|
+
process.kill(pid, "SIGTERM");
|
|
2353
|
+
} catch {
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
try {
|
|
2357
|
+
unlinkSync(filePath);
|
|
2358
|
+
} catch {
|
|
2359
|
+
}
|
|
2360
|
+
return resourceOperation("local", "dev-process", String(pid), "deleted", { pidFile: filePath });
|
|
2361
|
+
}
|
|
2362
|
+
function destroyLocalRuntimeResources(tenantRoot, { dryRun = false, deleteData = false } = {}) {
|
|
2363
|
+
const operations = [];
|
|
2364
|
+
const pidDir = resolve(tenantRoot, ".treeseed/dev-pids");
|
|
2365
|
+
if (existsSync(pidDir)) {
|
|
2366
|
+
for (const entry of readdirSync(pidDir)) {
|
|
2367
|
+
if (entry.endsWith(".pid")) {
|
|
2368
|
+
operations.push(killPidFromFile(resolve(pidDir, entry), { dryRun }));
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
} else {
|
|
2372
|
+
operations.push(resourceOperation("local", "dev-pids", pidDir, "missing"));
|
|
2373
|
+
}
|
|
2374
|
+
if (deleteData) {
|
|
2375
|
+
for (const relativePath of [
|
|
2376
|
+
".treeseed/generated/environments/local",
|
|
2377
|
+
".treeseed/generated/dev",
|
|
2378
|
+
".treeseed/market-operations-runner",
|
|
2379
|
+
".treeseed/local-capacity-provider/data"
|
|
2380
|
+
]) {
|
|
2381
|
+
const absolutePath = resolve(tenantRoot, relativePath);
|
|
2382
|
+
if (!existsSync(absolutePath)) {
|
|
2383
|
+
operations.push(resourceOperation("local", "data-path", relativePath, "missing"));
|
|
2384
|
+
continue;
|
|
2385
|
+
}
|
|
2386
|
+
if (dryRun) {
|
|
2387
|
+
operations.push(resourceOperation("local", "data-path", relativePath, "planned"));
|
|
2388
|
+
continue;
|
|
2389
|
+
}
|
|
2390
|
+
rmSync(absolutePath, { recursive: true, force: true });
|
|
2391
|
+
operations.push(resourceOperation("local", "data-path", relativePath, "deleted"));
|
|
2392
|
+
}
|
|
2393
|
+
} else {
|
|
2394
|
+
operations.push(resourceOperation("local", "data-path", ".treeseed/generated/environments/local", "skipped", { reason: "data_preserved" }));
|
|
2395
|
+
}
|
|
2396
|
+
return { operations };
|
|
2397
|
+
}
|
|
2398
|
+
async function destroyTreeseedEnvironmentResources(tenantRoot, options = {}) {
|
|
1720
2399
|
const target = normalizeTarget(options.scope ?? options.target ?? "prod");
|
|
1721
2400
|
const deployConfig = loadTenantDeployConfig(tenantRoot);
|
|
1722
2401
|
const state = loadDeployState(tenantRoot, deployConfig, { target });
|
|
@@ -1725,9 +2404,13 @@ function destroyCloudflareResources(tenantRoot, options = {}) {
|
|
|
1725
2404
|
CLOUDFLARE_ACCOUNT_ID: resolveConfiguredCloudflareAccountId(deployConfig)
|
|
1726
2405
|
};
|
|
1727
2406
|
const dryRun = options.dryRun ?? false;
|
|
2407
|
+
const deleteData = options.deleteData === true;
|
|
1728
2408
|
const force = options.force ?? false;
|
|
1729
2409
|
const kvNamespaces = dryRun ? [] : listKvNamespaces(tenantRoot, env);
|
|
1730
2410
|
const d1Databases = dryRun ? [] : listD1Databases(tenantRoot, env);
|
|
2411
|
+
const queues = listQueues(tenantRoot, env);
|
|
2412
|
+
const buckets = dryRun ? [] : listR2Buckets(tenantRoot, env);
|
|
2413
|
+
const pagesProjects = dryRun ? [] : listPagesProjects(tenantRoot, env);
|
|
1731
2414
|
state.kvNamespaces.FORM_GUARD_KV.id = resolveExistingKvIdByName(
|
|
1732
2415
|
kvNamespaces,
|
|
1733
2416
|
state.kvNamespaces.FORM_GUARD_KV.name,
|
|
@@ -1745,23 +2428,141 @@ function destroyCloudflareResources(tenantRoot, options = {}) {
|
|
|
1745
2428
|
state.d1Databases.SITE_DATA_DB.databaseName,
|
|
1746
2429
|
state.d1Databases.SITE_DATA_DB
|
|
1747
2430
|
);
|
|
1748
|
-
const
|
|
2431
|
+
const pagesProject = pagesProjects.find((entry) => entry?.name === state.pages?.projectName);
|
|
2432
|
+
const bucket = buckets.find((entry) => entry?.name === state.content?.bucketName);
|
|
2433
|
+
const queue = queues.find((entry) => queueName(entry) === state.queues?.agentWork?.name);
|
|
2434
|
+
const dlq = queues.find((entry) => queueName(entry) === state.queues?.agentWork?.dlqName);
|
|
2435
|
+
const workerResult = deleteWorker(tenantRoot, state.workerName, { env, dryRun, force });
|
|
1749
2436
|
const formGuard = deleteKvNamespace(tenantRoot, state.kvNamespaces.FORM_GUARD_KV.id, { env, dryRun });
|
|
1750
2437
|
const formGuardPreview = state.kvNamespaces.FORM_GUARD_KV.previewId && state.kvNamespaces.FORM_GUARD_KV.previewId !== state.kvNamespaces.FORM_GUARD_KV.id ? deleteKvNamespace(tenantRoot, state.kvNamespaces.FORM_GUARD_KV.previewId, { env, dryRun, preview: true }) : null;
|
|
1751
2438
|
const session = state.kvNamespaces.SESSION?.id ? deleteKvNamespace(tenantRoot, state.kvNamespaces.SESSION.id, { env, dryRun }) : null;
|
|
1752
2439
|
const sessionPreview = state.kvNamespaces.SESSION?.previewId && state.kvNamespaces.SESSION.previewId !== state.kvNamespaces.SESSION.id ? deleteKvNamespace(tenantRoot, state.kvNamespaces.SESSION.previewId, { env, dryRun, preview: true }) : null;
|
|
1753
|
-
const
|
|
2440
|
+
const knownKvIds = new Set([
|
|
2441
|
+
state.kvNamespaces.FORM_GUARD_KV.id,
|
|
2442
|
+
state.kvNamespaces.FORM_GUARD_KV.previewId,
|
|
2443
|
+
state.kvNamespaces.SESSION?.id,
|
|
2444
|
+
state.kvNamespaces.SESSION?.previewId
|
|
2445
|
+
].filter(Boolean));
|
|
2446
|
+
const legacyKvPrefix = state.identity?.deploymentKey ?? state.pages?.projectName ?? "";
|
|
2447
|
+
const legacyKvNamespaces = dryRun ? [] : kvNamespaces.filter((namespace) => {
|
|
2448
|
+
const title = typeof namespace?.title === "string" ? namespace.title : "";
|
|
2449
|
+
const id = typeof namespace?.id === "string" ? namespace.id : "";
|
|
2450
|
+
return title && id && !knownKvIds.has(id) && legacyKvPrefix && title.includes(legacyKvPrefix) && title.includes(target.scope);
|
|
2451
|
+
}).map((namespace) => {
|
|
2452
|
+
const result = deleteKvNamespace(tenantRoot, namespace.id, { env, dryRun: false });
|
|
2453
|
+
return resourceOperation("cloudflare", "kv-namespace", namespace.title, result.status, { ...result, legacy: true });
|
|
2454
|
+
});
|
|
2455
|
+
const database = deleteD1DatabaseForDestroy(tenantRoot, state.d1Databases.SITE_DATA_DB.databaseName, { env, dryRun, deleteData });
|
|
2456
|
+
const deletedQueue = deleteQueueByName(tenantRoot, queue ?? { name: state.queues?.agentWork?.name }, { env, dryRun });
|
|
2457
|
+
const deletedDlq = state.queues?.agentWork?.dlqName ? deleteQueueByName(tenantRoot, dlq ?? { name: state.queues.agentWork.dlqName }, { env, dryRun }) : null;
|
|
2458
|
+
const knownQueueNames = new Set([
|
|
2459
|
+
state.queues?.agentWork?.name,
|
|
2460
|
+
state.queues?.agentWork?.dlqName
|
|
2461
|
+
].filter(Boolean));
|
|
2462
|
+
const legacyQueues = legacyQueueDestroyOperations(tenantRoot, queues, target, knownQueueNames, { env, dryRun });
|
|
2463
|
+
const r2Bucket = bucket || dryRun ? deleteR2Bucket(tenantRoot, state.content?.bucketName, { env, dryRun, deleteData }) : resourceOperation("cloudflare", "r2-bucket", state.content?.bucketName, "missing");
|
|
2464
|
+
const pageDnsNames = [
|
|
2465
|
+
state.pages?.customDomain,
|
|
2466
|
+
deployConfig.surfaces?.web?.environments?.[target.scope]?.domain,
|
|
2467
|
+
target.scope === "prod" ? primaryHost(deployConfig.surfaces?.web?.publicBaseUrl ?? deployConfig.siteUrl) : null
|
|
2468
|
+
].filter(Boolean);
|
|
2469
|
+
const apiDnsNames = [
|
|
2470
|
+
deployConfig.services?.api?.environments?.[target.scope]?.domain,
|
|
2471
|
+
deployConfig.surfaces?.api?.environments?.[target.scope]?.domain
|
|
2472
|
+
].filter(Boolean);
|
|
2473
|
+
const dnsRecords = [.../* @__PURE__ */ new Set([...pageDnsNames, ...apiDnsNames])].flatMap((name) => deleteDnsRecordsForName(deployConfig, name, { env, dryRun }));
|
|
2474
|
+
const cacheRules = deleteTreeseedCacheRules(deployConfig, state, { env, dryRun });
|
|
2475
|
+
const pageCustomDomains = pagesProject || dryRun ? deletePagesCustomDomains(tenantRoot, state.pages?.projectName, pageDnsNames, { env, dryRun }) : [resourceOperation("cloudflare", "pages-custom-domain", state.pages?.projectName, "missing")];
|
|
2476
|
+
const pageDeployments = pagesProject || dryRun ? deletePagesDeployments(tenantRoot, state.pages?.projectName, { env, dryRun }) : resourceOperation("cloudflare", "pages-deployments", state.pages?.projectName, "missing");
|
|
2477
|
+
const pages = pagesProject || dryRun ? deletePagesProject(state.pages?.projectName, { env, dryRun }) : resourceOperation("cloudflare", "pages-project", state.pages?.projectName, "missing");
|
|
2478
|
+
const local = target.kind === "persistent" && target.scope === "local" ? destroyLocalRuntimeResources(tenantRoot, { dryRun, deleteData }) : { operations: [] };
|
|
2479
|
+
const railway = await destroyRailwayResources(tenantRoot, deployConfig, target, { dryRun, deleteData, env: process.env });
|
|
2480
|
+
const operations = {
|
|
2481
|
+
cloudflare: [
|
|
2482
|
+
resourceOperation("cloudflare", "worker", state.workerName, workerResult.status, workerResult),
|
|
2483
|
+
resourceOperation("cloudflare", "kv-namespace", state.kvNamespaces.FORM_GUARD_KV.name, formGuard.status, formGuard),
|
|
2484
|
+
...formGuardPreview ? [resourceOperation("cloudflare", "kv-namespace-preview", state.kvNamespaces.FORM_GUARD_KV.name, formGuardPreview.status, formGuardPreview)] : [],
|
|
2485
|
+
...session ? [resourceOperation("cloudflare", "kv-namespace", state.kvNamespaces.SESSION.name, session.status, session)] : [],
|
|
2486
|
+
...sessionPreview ? [resourceOperation("cloudflare", "kv-namespace-preview", state.kvNamespaces.SESSION.name, sessionPreview.status, sessionPreview)] : [],
|
|
2487
|
+
...legacyKvNamespaces,
|
|
2488
|
+
database,
|
|
2489
|
+
deletedQueue,
|
|
2490
|
+
...deletedDlq ? [deletedDlq] : [],
|
|
2491
|
+
...legacyQueues,
|
|
2492
|
+
r2Bucket,
|
|
2493
|
+
...pageCustomDomains,
|
|
2494
|
+
pageDeployments,
|
|
2495
|
+
pages,
|
|
2496
|
+
...dnsRecords,
|
|
2497
|
+
...cacheRules
|
|
2498
|
+
],
|
|
2499
|
+
railway: railway.operations,
|
|
2500
|
+
local: local.operations
|
|
2501
|
+
};
|
|
1754
2502
|
return {
|
|
1755
2503
|
target,
|
|
2504
|
+
deleteData,
|
|
1756
2505
|
summary: buildDestroySummary(deployConfig, state, target),
|
|
1757
|
-
operations
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
2506
|
+
operations
|
|
2507
|
+
};
|
|
2508
|
+
}
|
|
2509
|
+
function destroyCloudflareResources(tenantRoot, options = {}) {
|
|
2510
|
+
const target = normalizeTarget(options.scope ?? options.target ?? "prod");
|
|
2511
|
+
const deployConfig = loadTenantDeployConfig(tenantRoot);
|
|
2512
|
+
const state = loadDeployState(tenantRoot, deployConfig, { target });
|
|
2513
|
+
state.workerName = targetWorkerName(deployConfig, target);
|
|
2514
|
+
const env = {
|
|
2515
|
+
CLOUDFLARE_ACCOUNT_ID: resolveConfiguredCloudflareAccountId(deployConfig)
|
|
2516
|
+
};
|
|
2517
|
+
const dryRun = options.dryRun ?? false;
|
|
2518
|
+
const deleteData = options.deleteData === true;
|
|
2519
|
+
const force = options.force ?? false;
|
|
2520
|
+
const kvNamespaces = dryRun ? [] : listKvNamespaces(tenantRoot, env);
|
|
2521
|
+
const d1Databases = dryRun ? [] : listD1Databases(tenantRoot, env);
|
|
2522
|
+
const queues = listQueues(tenantRoot, env);
|
|
2523
|
+
const buckets = dryRun ? [] : listR2Buckets(tenantRoot, env);
|
|
2524
|
+
const pagesProjects = dryRun ? [] : listPagesProjects(tenantRoot, env);
|
|
2525
|
+
state.kvNamespaces.FORM_GUARD_KV.id = resolveExistingKvIdByName(
|
|
2526
|
+
kvNamespaces,
|
|
2527
|
+
state.kvNamespaces.FORM_GUARD_KV.name,
|
|
2528
|
+
state.kvNamespaces.FORM_GUARD_KV.id
|
|
2529
|
+
);
|
|
2530
|
+
state.d1Databases.SITE_DATA_DB = resolveExistingD1ByName(
|
|
2531
|
+
d1Databases,
|
|
2532
|
+
state.d1Databases.SITE_DATA_DB.databaseName,
|
|
2533
|
+
state.d1Databases.SITE_DATA_DB
|
|
2534
|
+
);
|
|
2535
|
+
const queue = queues.find((entry) => queueName(entry) === state.queues?.agentWork?.name);
|
|
2536
|
+
const dlq = queues.find((entry) => queueName(entry) === state.queues?.agentWork?.dlqName);
|
|
2537
|
+
const bucket = buckets.find((entry) => entry?.name === state.content?.bucketName);
|
|
2538
|
+
const pagesProject = pagesProjects.find((entry) => entry?.name === state.pages?.projectName);
|
|
2539
|
+
const worker = deleteWorker(tenantRoot, state.workerName, { env, dryRun, force });
|
|
2540
|
+
const formGuard = deleteKvNamespace(tenantRoot, state.kvNamespaces.FORM_GUARD_KV.id, { env, dryRun });
|
|
2541
|
+
const database = deleteD1DatabaseForDestroy(tenantRoot, state.d1Databases.SITE_DATA_DB.databaseName, { env, dryRun, deleteData });
|
|
2542
|
+
const deletedQueue = deleteQueueByName(tenantRoot, queue ?? { name: state.queues?.agentWork?.name }, { env, dryRun });
|
|
2543
|
+
const deletedDlq = state.queues?.agentWork?.dlqName ? deleteQueueByName(tenantRoot, dlq ?? { name: state.queues.agentWork.dlqName }, { env, dryRun }) : null;
|
|
2544
|
+
const knownQueueNames = new Set([
|
|
2545
|
+
state.queues?.agentWork?.name,
|
|
2546
|
+
state.queues?.agentWork?.dlqName
|
|
2547
|
+
].filter(Boolean));
|
|
2548
|
+
const legacyQueues = legacyQueueDestroyOperations(tenantRoot, queues, target, knownQueueNames, { env, dryRun });
|
|
2549
|
+
const r2Bucket = bucket || dryRun ? deleteR2Bucket(tenantRoot, state.content?.bucketName, { env, dryRun, deleteData }) : resourceOperation("cloudflare", "r2-bucket", state.content?.bucketName, "missing");
|
|
2550
|
+
const pages = pagesProject || dryRun ? deletePagesProject(state.pages?.projectName, { env, dryRun }) : resourceOperation("cloudflare", "pages-project", state.pages?.projectName, "missing");
|
|
2551
|
+
const operations = {
|
|
2552
|
+
worker,
|
|
2553
|
+
formGuard,
|
|
2554
|
+
database,
|
|
2555
|
+
queue: deletedQueue,
|
|
2556
|
+
dlq: deletedDlq,
|
|
2557
|
+
legacyQueues,
|
|
2558
|
+
r2Bucket,
|
|
2559
|
+
pages
|
|
2560
|
+
};
|
|
2561
|
+
return {
|
|
2562
|
+
target,
|
|
2563
|
+
deleteData,
|
|
2564
|
+
summary: buildDestroySummary(deployConfig, state, target),
|
|
2565
|
+
operations
|
|
1765
2566
|
};
|
|
1766
2567
|
}
|
|
1767
2568
|
function cleanupDestroyedState(tenantRoot, options = {}) {
|
|
@@ -1770,6 +2571,9 @@ function cleanupDestroyedState(tenantRoot, options = {}) {
|
|
|
1770
2571
|
const { statePath, generatedRoot } = resolveTargetPaths(tenantRoot, target);
|
|
1771
2572
|
rmSync(statePath, { force: true });
|
|
1772
2573
|
rmSync(generatedRoot, { recursive: true, force: true });
|
|
2574
|
+
if (options.removeBuildArtifacts) {
|
|
2575
|
+
rmSync(resolve(tenantRoot, "dist"), { recursive: true, force: true });
|
|
2576
|
+
}
|
|
1773
2577
|
return;
|
|
1774
2578
|
}
|
|
1775
2579
|
rmSync(resolve(tenantRoot, STATE_ROOT), { recursive: true, force: true });
|
|
@@ -2268,21 +3072,39 @@ function printDeploySummary(summary) {
|
|
|
2268
3072
|
}
|
|
2269
3073
|
function printDestroySummary(result) {
|
|
2270
3074
|
const { summary, operations } = result;
|
|
3075
|
+
const cloudflare = Array.isArray(operations?.cloudflare) ? operations.cloudflare : null;
|
|
3076
|
+
const legacy = cloudflare ? {
|
|
3077
|
+
worker: cloudflare.find((entry) => entry.type === "worker"),
|
|
3078
|
+
database: cloudflare.find((entry) => entry.type === "d1-database"),
|
|
3079
|
+
formGuard: cloudflare.find((entry) => entry.type === "kv-namespace"),
|
|
3080
|
+
formGuardPreview: cloudflare.find((entry) => entry.type === "kv-namespace-preview"),
|
|
3081
|
+
session: cloudflare.find((entry) => entry.type === "kv-namespace" && entry.name === summary.sessionKv?.name),
|
|
3082
|
+
sessionPreview: cloudflare.find((entry) => entry.type === "kv-namespace-preview" && entry.name === summary.sessionKv?.name)
|
|
3083
|
+
} : operations;
|
|
2271
3084
|
console.log("Treeseed destroy summary");
|
|
2272
3085
|
console.log(` Target: ${summary.target}`);
|
|
2273
|
-
console.log(` Worker: ${summary.workerName} -> ${
|
|
3086
|
+
console.log(` Worker: ${summary.workerName} -> ${legacy.worker?.status ?? "unknown"}`);
|
|
2274
3087
|
console.log(` Site URL: ${summary.siteUrl}`);
|
|
2275
3088
|
console.log(` Account ID: ${summary.accountId}`);
|
|
2276
|
-
console.log(` D1: ${summary.siteDataDb.databaseName} -> ${
|
|
2277
|
-
console.log(` KV FORM_GUARD_KV: ${summary.formGuardKv.name} -> ${
|
|
2278
|
-
if (
|
|
2279
|
-
console.log(` KV FORM_GUARD_KV preview -> ${
|
|
2280
|
-
}
|
|
2281
|
-
if (summary.sessionKv &&
|
|
2282
|
-
console.log(` KV SESSION (deprecated): ${summary.sessionKv.name} -> ${
|
|
2283
|
-
}
|
|
2284
|
-
if (
|
|
2285
|
-
console.log(` KV SESSION preview -> ${
|
|
3089
|
+
console.log(` D1: ${summary.siteDataDb.databaseName} -> ${legacy.database?.status ?? "unknown"}`);
|
|
3090
|
+
console.log(` KV FORM_GUARD_KV: ${summary.formGuardKv.name} -> ${legacy.formGuard?.status ?? "unknown"}`);
|
|
3091
|
+
if (legacy.formGuardPreview) {
|
|
3092
|
+
console.log(` KV FORM_GUARD_KV preview -> ${legacy.formGuardPreview.status}`);
|
|
3093
|
+
}
|
|
3094
|
+
if (summary.sessionKv && legacy.session) {
|
|
3095
|
+
console.log(` KV SESSION (deprecated): ${summary.sessionKv.name} -> ${legacy.session.status}`);
|
|
3096
|
+
}
|
|
3097
|
+
if (legacy.sessionPreview) {
|
|
3098
|
+
console.log(` KV SESSION preview -> ${legacy.sessionPreview.status}`);
|
|
3099
|
+
}
|
|
3100
|
+
if (cloudflare) {
|
|
3101
|
+
for (const entry of [
|
|
3102
|
+
...cloudflare.filter((item) => !["worker", "d1-database", "kv-namespace", "kv-namespace-preview"].includes(item.type)),
|
|
3103
|
+
...Array.isArray(operations?.railway) ? operations.railway : [],
|
|
3104
|
+
...Array.isArray(operations?.local) ? operations.local : []
|
|
3105
|
+
]) {
|
|
3106
|
+
console.log(` ${entry.provider} ${entry.type} ${entry.name ?? "(none)"} -> ${entry.status}`);
|
|
3107
|
+
}
|
|
2286
3108
|
}
|
|
2287
3109
|
}
|
|
2288
3110
|
export {
|
|
@@ -2300,6 +3122,7 @@ export {
|
|
|
2300
3122
|
deployTargetLabel,
|
|
2301
3123
|
deriveTreeseedStagingSurfaceDomain,
|
|
2302
3124
|
destroyCloudflareResources,
|
|
3125
|
+
destroyTreeseedEnvironmentResources,
|
|
2303
3126
|
ensureGeneratedWranglerConfig,
|
|
2304
3127
|
finalizeDeploymentState,
|
|
2305
3128
|
hasProvisionedCloudflareResources,
|
|
@@ -2333,6 +3156,7 @@ export {
|
|
|
2333
3156
|
runRemoteD1Migrations,
|
|
2334
3157
|
runWrangler,
|
|
2335
3158
|
scopeFromTarget,
|
|
3159
|
+
shouldDeleteRailwayProjectAfterEnvironmentDestroy,
|
|
2336
3160
|
syncCloudflareSecrets,
|
|
2337
3161
|
validateDeployPrerequisites,
|
|
2338
3162
|
validateDestroyPrerequisites,
|