@treeseed/sdk 0.5.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2 -0
- package/dist/index.js +46 -0
- package/dist/operations/providers/default.js +1 -1
- package/dist/operations/services/config-runtime.d.ts +49 -42
- package/dist/operations/services/config-runtime.js +465 -142
- package/dist/operations/services/deploy.d.ts +298 -0
- package/dist/operations/services/deploy.js +381 -137
- package/dist/operations/services/git-workflow.d.ts +9 -0
- package/dist/operations/services/git-workflow.js +32 -0
- package/dist/operations/services/github-api.d.ts +115 -0
- package/dist/operations/services/github-api.js +455 -0
- package/dist/operations/services/github-automation.d.ts +19 -33
- package/dist/operations/services/github-automation.js +44 -131
- package/dist/operations/services/key-agent.d.ts +20 -1
- package/dist/operations/services/key-agent.js +267 -102
- package/dist/operations/services/knowledge-coop-launch.d.ts +2 -3
- package/dist/operations/services/knowledge-coop-launch.js +26 -12
- package/dist/operations/services/project-platform.d.ts +157 -150
- package/dist/operations/services/project-platform.js +129 -26
- package/dist/operations/services/railway-api.d.ts +244 -0
- package/dist/operations/services/railway-api.js +882 -0
- package/dist/operations/services/railway-deploy.d.ts +171 -27
- package/dist/operations/services/railway-deploy.js +672 -172
- package/dist/operations/services/runtime-tools.d.ts +18 -0
- package/dist/operations/services/runtime-tools.js +19 -6
- package/dist/operations/services/workspace-preflight.js +2 -2
- package/dist/platform/contracts.d.ts +7 -0
- package/dist/platform/deploy-config.js +23 -0
- package/dist/platform/deploy-runtime.d.ts +1 -0
- package/dist/platform/deploy-runtime.js +7 -9
- package/dist/platform/env.yaml +10 -9
- package/dist/platform/environment.js +4 -0
- package/dist/platform/plugin.d.ts +6 -0
- package/dist/platform/plugins/constants.d.ts +1 -0
- package/dist/platform/plugins/constants.js +1 -0
- package/dist/platform/plugins/runtime.d.ts +4 -0
- package/dist/platform/plugins/runtime.js +8 -1
- package/dist/platform/published-content.js +27 -4
- package/dist/platform/tenant/runtime-config.js +33 -24
- package/dist/plugin-default.d.ts +1 -0
- package/dist/plugin-default.js +1 -0
- package/dist/reconcile/builtin-adapters.d.ts +3 -0
- package/dist/reconcile/builtin-adapters.js +2093 -0
- package/dist/reconcile/contracts.d.ts +155 -0
- package/dist/reconcile/contracts.js +0 -0
- package/dist/reconcile/desired-state.d.ts +179 -0
- package/dist/reconcile/desired-state.js +319 -0
- package/dist/reconcile/engine.d.ts +405 -0
- package/dist/reconcile/engine.js +356 -0
- package/dist/reconcile/errors.d.ts +5 -0
- package/dist/reconcile/errors.js +13 -0
- package/dist/reconcile/index.d.ts +7 -0
- package/dist/reconcile/index.js +7 -0
- package/dist/reconcile/registry.d.ts +7 -0
- package/dist/reconcile/registry.js +64 -0
- package/dist/reconcile/state.d.ts +7 -0
- package/dist/reconcile/state.js +303 -0
- package/dist/reconcile/units.d.ts +6 -0
- package/dist/reconcile/units.js +68 -0
- package/dist/scripts/config-treeseed.js +27 -19
- package/dist/scripts/tenant-deploy.js +35 -14
- package/dist/workflow/operations.js +127 -22
- package/dist/workflow-support.d.ts +3 -1
- package/dist/workflow-support.js +50 -0
- package/dist/workflow.d.ts +2 -0
- package/package.json +7 -1
|
@@ -13,20 +13,36 @@ import {
|
|
|
13
13
|
} from "../../platform/environment.js";
|
|
14
14
|
import { loadTreeseedManifest } from "../../platform/tenant-config.js";
|
|
15
15
|
import {
|
|
16
|
+
buildProvisioningSummary,
|
|
16
17
|
createPersistentDeployTarget,
|
|
17
18
|
ensureGeneratedWranglerConfig,
|
|
18
19
|
loadDeployState,
|
|
19
|
-
|
|
20
|
-
provisionCloudflareResources,
|
|
21
|
-
syncCloudflareSecrets,
|
|
22
|
-
verifyProvisionedCloudflareResources
|
|
20
|
+
syncCloudflareSecrets
|
|
23
21
|
} from "./deploy.js";
|
|
22
|
+
import { collectTreeseedReconcileStatus, reconcileTreeseedTarget } from "../../reconcile/index.js";
|
|
24
23
|
import { maybeResolveGitHubRepositorySlug } from "./github-automation.js";
|
|
25
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
buildRailwayCommandEnv
|
|
26
|
+
} from "./railway-deploy.js";
|
|
27
|
+
import {
|
|
28
|
+
normalizeRailwayEnvironmentName,
|
|
29
|
+
resolveRailwayWorkspace
|
|
30
|
+
} from "./railway-api.js";
|
|
31
|
+
import {
|
|
32
|
+
createGitHubApiClient,
|
|
33
|
+
ensureGitHubBranchFromBase,
|
|
34
|
+
listGitHubRepositorySecretNames,
|
|
35
|
+
listGitHubRepositoryVariableNames,
|
|
36
|
+
upsertGitHubRepositorySecret,
|
|
37
|
+
upsertGitHubRepositoryVariable,
|
|
38
|
+
upsertGitHubRepositoryVariableWithGhCli
|
|
39
|
+
} from "./github-api.js";
|
|
26
40
|
import { loadCliDeployConfig, packageScriptPath, resolveWranglerBin, withProcessCwd } from "./runtime-tools.js";
|
|
41
|
+
import { PRODUCTION_BRANCH, STAGING_BRANCH } from "./git-workflow.js";
|
|
27
42
|
import {
|
|
28
43
|
assertTreeseedKeyAgentResponse,
|
|
29
44
|
getTreeseedKeyAgentPaths,
|
|
45
|
+
inspectTreeseedKeyAgentDiagnostics,
|
|
30
46
|
readWrappedMachineKeyFile,
|
|
31
47
|
replaceWrappedMachineKey,
|
|
32
48
|
rotateWrappedMachineKeyPassphrase,
|
|
@@ -55,6 +71,30 @@ const CLI_CHECK_TIMEOUT_MS = 5e3;
|
|
|
55
71
|
const DEPRECATED_LOCAL_ENV_FILES = [".env.local", ".dev.vars"];
|
|
56
72
|
const warnedDeprecatedLocalEnvRoots = /* @__PURE__ */ new Set();
|
|
57
73
|
const inlineTreeseedSecretSessions = /* @__PURE__ */ new Map();
|
|
74
|
+
const railwayConnectionCheckCache = /* @__PURE__ */ new Map();
|
|
75
|
+
function filterEnvironmentValuesByRegistry(values, registry) {
|
|
76
|
+
const registeredKeys = new Set(registry.entries.map((entry) => entry.id));
|
|
77
|
+
return Object.fromEntries(
|
|
78
|
+
Object.entries(values).filter(([key]) => registeredKeys.has(key))
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
function inspectTreeseedPassphraseEnvDiagnostic(env = process.env) {
|
|
82
|
+
const configured = typeof env[TREESEED_MACHINE_KEY_PASSPHRASE_ENV] === "string" && env[TREESEED_MACHINE_KEY_PASSPHRASE_ENV].trim().length > 0;
|
|
83
|
+
return {
|
|
84
|
+
envVar: TREESEED_MACHINE_KEY_PASSPHRASE_ENV,
|
|
85
|
+
configured,
|
|
86
|
+
recommendedLaunch: `Export ${TREESEED_MACHINE_KEY_PASSPHRASE_ENV} in a shell and launch \`code .\` from that shell before starting the Codex session.`
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
async function inspectTreeseedKeyAgentTransportDiagnostic() {
|
|
90
|
+
const { socketPath, pidPath } = getTreeseedKeyAgentPaths();
|
|
91
|
+
const diagnostics = await inspectTreeseedKeyAgentDiagnostics(socketPath);
|
|
92
|
+
return {
|
|
93
|
+
socketPath,
|
|
94
|
+
pidPath,
|
|
95
|
+
...diagnostics
|
|
96
|
+
};
|
|
97
|
+
}
|
|
58
98
|
function createDefaultRemoteHost() {
|
|
59
99
|
return {
|
|
60
100
|
id: "official",
|
|
@@ -419,21 +459,26 @@ function unlockTreeseedSecretSessionFromEnv(tenantRoot, options = {}) {
|
|
|
419
459
|
startTreeseedKeyAgentDaemon(tenantRoot);
|
|
420
460
|
let response = { ok: false, code: "daemon_unavailable", message: "Treeseed key agent is unavailable." };
|
|
421
461
|
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
462
|
+
try {
|
|
463
|
+
const parsed = runTreeseedKeyAgentCommand([
|
|
464
|
+
"unlock-from-env",
|
|
465
|
+
"--key-path",
|
|
466
|
+
keyPath,
|
|
467
|
+
"--socket-path",
|
|
468
|
+
socketPath,
|
|
469
|
+
"--idle-timeout-ms",
|
|
470
|
+
String(TREESEED_KEY_AGENT_IDLE_TIMEOUT_MS),
|
|
471
|
+
...options.allowMigration === false ? [] : ["--allow-migration"],
|
|
472
|
+
...options.createIfMissing === false ? [] : ["--create-if-missing"]
|
|
473
|
+
]);
|
|
474
|
+
assertTreeseedKeyAgentResponse(parsed, `Unable to unlock the Treeseed secret session from ${TREESEED_MACHINE_KEY_PASSPHRASE_ENV}.`);
|
|
475
|
+
return parsed.status;
|
|
476
|
+
} catch (error) {
|
|
477
|
+
if (attempt === 19) {
|
|
478
|
+
throw error;
|
|
479
|
+
}
|
|
480
|
+
sleepMs(25);
|
|
435
481
|
}
|
|
436
|
-
sleepMs(25);
|
|
437
482
|
}
|
|
438
483
|
assertTreeseedKeyAgentResponse(
|
|
439
484
|
response,
|
|
@@ -1298,11 +1343,7 @@ function resolveTreeseedMachineEnvironmentValues(tenantRoot, scope) {
|
|
|
1298
1343
|
};
|
|
1299
1344
|
const entryById = new Map(registry.entries.map((entry) => [entry.id, entry]));
|
|
1300
1345
|
const values = {};
|
|
1301
|
-
const knownKeys =
|
|
1302
|
-
...Object.keys(bucketValuesByScope.shared ?? {}),
|
|
1303
|
-
...Object.keys(bucketValuesByScope[scope] ?? {}),
|
|
1304
|
-
...registry.entries.map((entry) => entry.id)
|
|
1305
|
-
]);
|
|
1346
|
+
const knownKeys = new Set(registry.entries.map((entry) => entry.id));
|
|
1306
1347
|
for (const entryId of knownKeys) {
|
|
1307
1348
|
const resolved = resolveEntryValueFromBuckets(entryById.get(entryId), entryId, scope, bucketValuesByScope);
|
|
1308
1349
|
if (typeof resolved === "string" && resolved.length > 0) {
|
|
@@ -1349,6 +1390,7 @@ function collectTreeseedEnvironmentContext(tenantRoot) {
|
|
|
1349
1390
|
}
|
|
1350
1391
|
function collectTreeseedConfigSeedValues(tenantRoot, scope, env = process.env) {
|
|
1351
1392
|
warnDeprecatedTreeseedLocalEnvFiles(tenantRoot);
|
|
1393
|
+
const registry = collectTreeseedEnvironmentContext(tenantRoot);
|
|
1352
1394
|
let machineValues = {};
|
|
1353
1395
|
try {
|
|
1354
1396
|
machineValues = resolveTreeseedMachineEnvironmentValues(tenantRoot, scope);
|
|
@@ -1363,17 +1405,22 @@ function collectTreeseedConfigSeedValues(tenantRoot, scope, env = process.env) {
|
|
|
1363
1405
|
normalizedEnv[canonicalKey] = normalizedEnv[legacyKey];
|
|
1364
1406
|
}
|
|
1365
1407
|
}
|
|
1366
|
-
return {
|
|
1408
|
+
return filterEnvironmentValuesByRegistry({
|
|
1367
1409
|
...machineValues,
|
|
1368
1410
|
...Object.fromEntries(Object.entries(normalizedEnv).map(([key, value]) => [key, value ?? void 0]))
|
|
1369
|
-
};
|
|
1411
|
+
}, registry);
|
|
1370
1412
|
}
|
|
1371
1413
|
function collectTreeseedConfigSeedValueSources(tenantRoot, scope, env = process.env) {
|
|
1372
1414
|
warnDeprecatedTreeseedLocalEnvFiles(tenantRoot);
|
|
1415
|
+
const registry = collectTreeseedEnvironmentContext(tenantRoot);
|
|
1416
|
+
const registeredKeys = new Set(registry.entries.map((entry) => entry.id));
|
|
1373
1417
|
const values = {};
|
|
1374
1418
|
const sources = {};
|
|
1375
1419
|
const merge = (source, entries) => {
|
|
1376
1420
|
for (const [key, value] of Object.entries(entries)) {
|
|
1421
|
+
if (!registeredKeys.has(key)) {
|
|
1422
|
+
continue;
|
|
1423
|
+
}
|
|
1377
1424
|
if (typeof value !== "string" || value.length === 0) {
|
|
1378
1425
|
continue;
|
|
1379
1426
|
}
|
|
@@ -1470,7 +1517,7 @@ function assertTreeseedCommandEnvironment({ tenantRoot, scope, purpose }) {
|
|
|
1470
1517
|
error.details = report.validation;
|
|
1471
1518
|
throw error;
|
|
1472
1519
|
}
|
|
1473
|
-
function runGh(args, { cwd, dryRun = false, input } = {}) {
|
|
1520
|
+
function runGh(args, { cwd, dryRun = false, input, env } = {}) {
|
|
1474
1521
|
if (dryRun) {
|
|
1475
1522
|
return { status: 0, stdout: "", stderr: "" };
|
|
1476
1523
|
}
|
|
@@ -1478,8 +1525,18 @@ function runGh(args, { cwd, dryRun = false, input } = {}) {
|
|
|
1478
1525
|
cwd,
|
|
1479
1526
|
stdio: input !== void 0 ? ["pipe", "pipe", "pipe"] : ["ignore", "pipe", "pipe"],
|
|
1480
1527
|
encoding: "utf8",
|
|
1481
|
-
input
|
|
1528
|
+
input,
|
|
1529
|
+
timeout: 15e3,
|
|
1530
|
+
env: {
|
|
1531
|
+
...process.env,
|
|
1532
|
+
...env ?? {},
|
|
1533
|
+
GH_PROMPT_DISABLED: "1",
|
|
1534
|
+
GH_NO_UPDATE_NOTIFIER: "1"
|
|
1535
|
+
}
|
|
1482
1536
|
});
|
|
1537
|
+
if (result.error?.code === "ETIMEDOUT") {
|
|
1538
|
+
throw new Error(`gh ${args.join(" ")} timed out`);
|
|
1539
|
+
}
|
|
1483
1540
|
if (result.status !== 0) {
|
|
1484
1541
|
throw new Error(result.stderr?.trim() || result.stdout?.trim() || `gh ${args.join(" ")} failed`);
|
|
1485
1542
|
}
|
|
@@ -1543,12 +1600,22 @@ function ensureTreeseedActVerificationTooling({ tenantRoot = process.cwd(), inst
|
|
|
1543
1600
|
attemptedInstall: false,
|
|
1544
1601
|
installedDuringConfig: false
|
|
1545
1602
|
});
|
|
1546
|
-
const
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1603
|
+
const wranglerCli = (() => {
|
|
1604
|
+
try {
|
|
1605
|
+
const wranglerCheck = checkCommand(process.execPath, [resolveWranglerBin(), "--version"], { cwd: tenantRoot, env });
|
|
1606
|
+
return toolStatus(
|
|
1607
|
+
"wranglerCli",
|
|
1608
|
+
wranglerCheck.ok,
|
|
1609
|
+
wranglerCheck.ok ? wranglerCheck.stdout.split("\n")[0] ?? "Wrangler CLI detected." : wranglerCheck.detail || "Wrangler CLI is unavailable."
|
|
1610
|
+
);
|
|
1611
|
+
} catch (error) {
|
|
1612
|
+
return toolStatus(
|
|
1613
|
+
"wranglerCli",
|
|
1614
|
+
false,
|
|
1615
|
+
error instanceof Error && error.message ? error.message : "Wrangler CLI is unavailable."
|
|
1616
|
+
);
|
|
1617
|
+
}
|
|
1618
|
+
})();
|
|
1552
1619
|
const railwayCheck = checkCommand("railway", ["--version"], { cwd: tenantRoot, env });
|
|
1553
1620
|
const railwayCli = toolStatus(
|
|
1554
1621
|
"railwayCli",
|
|
@@ -1627,6 +1694,9 @@ function providerConnectionResult(provider, ready, detail, extra = {}) {
|
|
|
1627
1694
|
...extra
|
|
1628
1695
|
};
|
|
1629
1696
|
}
|
|
1697
|
+
function isTransientProviderConnectionError(detail) {
|
|
1698
|
+
return /fetch failed|failed to fetch|timed out|etimedout|econnreset|enetunreach|temporarily unavailable|aborted|api check failed|rate.?limit|too many requests|429/iu.test(detail || "");
|
|
1699
|
+
}
|
|
1630
1700
|
function checkGitHubConnection({ tenantRoot, env }) {
|
|
1631
1701
|
if (!env.GH_TOKEN) {
|
|
1632
1702
|
return providerConnectionResult("github", false, "GH_TOKEN is not configured.", { skipped: true });
|
|
@@ -1636,76 +1706,127 @@ function checkGitHubConnection({ tenantRoot, env }) {
|
|
|
1636
1706
|
}
|
|
1637
1707
|
const repository = maybeResolveGitHubRepositorySlug(tenantRoot);
|
|
1638
1708
|
const args = repository ? ["repo", "view", repository, "--json", "nameWithOwner", "--jq", ".nameWithOwner"] : ["api", "user", "--jq", ".login"];
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
stdio: "pipe",
|
|
1642
|
-
encoding: "utf8",
|
|
1643
|
-
env: { ...process.env, ...env },
|
|
1644
|
-
timeout: CLI_CHECK_TIMEOUT_MS
|
|
1645
|
-
});
|
|
1646
|
-
if (result.status !== 0) {
|
|
1647
|
-
return providerConnectionResult("github", false, formatCheckOutput(result) || "GitHub API check failed.");
|
|
1648
|
-
}
|
|
1649
|
-
const resolved = result.stdout.trim();
|
|
1650
|
-
return providerConnectionResult(
|
|
1651
|
-
"github",
|
|
1652
|
-
true,
|
|
1653
|
-
repository ? `GitHub token can access ${resolved || repository}.` : resolved ? `Authenticated as ${resolved}.` : "GitHub API check succeeded."
|
|
1654
|
-
);
|
|
1655
|
-
}
|
|
1656
|
-
function checkCloudflareConnection({ tenantRoot, env }) {
|
|
1657
|
-
if (!env.CLOUDFLARE_API_TOKEN) {
|
|
1658
|
-
return providerConnectionResult("cloudflare", false, "CLOUDFLARE_API_TOKEN is not configured.", { skipped: true });
|
|
1659
|
-
}
|
|
1660
|
-
try {
|
|
1661
|
-
const result = spawnSync(process.execPath, [resolveWranglerBin(), "whoami"], {
|
|
1709
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
1710
|
+
const result = spawnSync("gh", args, {
|
|
1662
1711
|
cwd: tenantRoot,
|
|
1663
1712
|
stdio: "pipe",
|
|
1664
1713
|
encoding: "utf8",
|
|
1665
1714
|
env: { ...process.env, ...env },
|
|
1666
1715
|
timeout: CLI_CHECK_TIMEOUT_MS
|
|
1667
1716
|
});
|
|
1668
|
-
if (result.status
|
|
1669
|
-
|
|
1717
|
+
if (result.status === 0) {
|
|
1718
|
+
const resolved = result.stdout.trim();
|
|
1719
|
+
return providerConnectionResult(
|
|
1720
|
+
"github",
|
|
1721
|
+
true,
|
|
1722
|
+
repository ? `GitHub token can access ${resolved || repository}.` : resolved ? `Authenticated as ${resolved}.` : "GitHub API check succeeded."
|
|
1723
|
+
);
|
|
1724
|
+
}
|
|
1725
|
+
const detail = formatCheckOutput(result) || "GitHub API check failed.";
|
|
1726
|
+
if (attempt >= 2 || !isTransientProviderConnectionError(detail)) {
|
|
1727
|
+
return providerConnectionResult("github", false, detail);
|
|
1670
1728
|
}
|
|
1671
|
-
return providerConnectionResult("cloudflare", true, "Wrangler authenticated with CLOUDFLARE_API_TOKEN.");
|
|
1672
|
-
} catch (error) {
|
|
1673
|
-
return providerConnectionResult("cloudflare", false, error instanceof Error ? error.message : "Cloudflare Wrangler check failed.");
|
|
1674
1729
|
}
|
|
1730
|
+
return providerConnectionResult("github", false, "GitHub API check failed.");
|
|
1675
1731
|
}
|
|
1676
|
-
function
|
|
1677
|
-
if (!env.
|
|
1678
|
-
return providerConnectionResult("
|
|
1732
|
+
function checkCloudflareConnection({ tenantRoot, env }) {
|
|
1733
|
+
if (!env.CLOUDFLARE_API_TOKEN) {
|
|
1734
|
+
return providerConnectionResult("cloudflare", false, "CLOUDFLARE_API_TOKEN is not configured.", { skipped: true });
|
|
1679
1735
|
}
|
|
1680
|
-
|
|
1681
|
-
|
|
1736
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
1737
|
+
try {
|
|
1738
|
+
const result = spawnSync(process.execPath, [resolveWranglerBin(), "whoami"], {
|
|
1739
|
+
cwd: tenantRoot,
|
|
1740
|
+
stdio: "pipe",
|
|
1741
|
+
encoding: "utf8",
|
|
1742
|
+
env: { ...process.env, ...env },
|
|
1743
|
+
timeout: CLI_CHECK_TIMEOUT_MS
|
|
1744
|
+
});
|
|
1745
|
+
if (result.status === 0) {
|
|
1746
|
+
return providerConnectionResult("cloudflare", true, "Wrangler authenticated with CLOUDFLARE_API_TOKEN.");
|
|
1747
|
+
}
|
|
1748
|
+
const detail = formatCheckOutput(result) || "Cloudflare Wrangler check failed.";
|
|
1749
|
+
if (attempt >= 2 || !isTransientProviderConnectionError(detail)) {
|
|
1750
|
+
return providerConnectionResult("cloudflare", false, detail);
|
|
1751
|
+
}
|
|
1752
|
+
} catch (error) {
|
|
1753
|
+
const detail = error instanceof Error ? error.message : "Cloudflare Wrangler check failed.";
|
|
1754
|
+
if (attempt >= 2 || !isTransientProviderConnectionError(detail)) {
|
|
1755
|
+
return providerConnectionResult("cloudflare", false, detail);
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1682
1758
|
}
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1759
|
+
return providerConnectionResult(
|
|
1760
|
+
"cloudflare",
|
|
1761
|
+
false,
|
|
1762
|
+
"Cloudflare connectivity preflight hit transient fetch failures; bootstrap will continue and rely on live reconcile verification.",
|
|
1763
|
+
{ skipped: true, warning: true, transient: true }
|
|
1764
|
+
);
|
|
1765
|
+
}
|
|
1766
|
+
async function checkRailwayConnection({ tenantRoot, env }) {
|
|
1767
|
+
if (!env.RAILWAY_API_TOKEN) {
|
|
1768
|
+
return providerConnectionResult("railway", false, "RAILWAY_API_TOKEN is not configured.", { skipped: true });
|
|
1769
|
+
}
|
|
1770
|
+
const workspaceName = env.TREESEED_RAILWAY_WORKSPACE || resolveRailwayWorkspace(env);
|
|
1771
|
+
const cacheKey = JSON.stringify({
|
|
1772
|
+
tenantRoot,
|
|
1773
|
+
token: env.RAILWAY_API_TOKEN,
|
|
1774
|
+
workspaceName
|
|
1689
1775
|
});
|
|
1690
|
-
|
|
1691
|
-
|
|
1776
|
+
const cached = railwayConnectionCheckCache.get(cacheKey);
|
|
1777
|
+
if (cached) {
|
|
1778
|
+
return await cached;
|
|
1779
|
+
}
|
|
1780
|
+
const checkPromise = (async () => {
|
|
1781
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
1782
|
+
try {
|
|
1783
|
+
const whoami = checkCommand("railway", ["whoami"], { cwd: tenantRoot, env });
|
|
1784
|
+
if (!whoami.ok) {
|
|
1785
|
+
if (/rate.?limit|too many requests|429/iu.test(whoami.detail || "")) {
|
|
1786
|
+
return providerConnectionResult(
|
|
1787
|
+
"railway",
|
|
1788
|
+
false,
|
|
1789
|
+
"Railway connectivity preflight was rate-limited; bootstrap will continue and rely on API-backed reconcile verification.",
|
|
1790
|
+
{ skipped: true, warning: true, rateLimited: true }
|
|
1791
|
+
);
|
|
1792
|
+
}
|
|
1793
|
+
throw new Error(whoami.detail || "Railway CLI authentication check failed.");
|
|
1794
|
+
}
|
|
1795
|
+
const identity = whoami.stdout.replace(/^logged in as\s+/iu, "").replace(/\s*👋\s*$/u, "").trim() || "an account";
|
|
1796
|
+
return providerConnectionResult("railway", true, `Railway authenticated as ${identity} in workspace ${workspaceName}. Project and service existence will be reconciled during bootstrap.`);
|
|
1797
|
+
} catch (error) {
|
|
1798
|
+
const detail = error instanceof Error ? error.message : "Railway API check failed.";
|
|
1799
|
+
if (attempt >= 2 || !isTransientProviderConnectionError(detail)) {
|
|
1800
|
+
return providerConnectionResult("railway", false, detail);
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
return providerConnectionResult("railway", false, "Railway API check failed.");
|
|
1805
|
+
})();
|
|
1806
|
+
railwayConnectionCheckCache.set(cacheKey, checkPromise);
|
|
1807
|
+
try {
|
|
1808
|
+
return await checkPromise;
|
|
1809
|
+
} catch (error) {
|
|
1810
|
+
railwayConnectionCheckCache.delete(cacheKey);
|
|
1811
|
+
throw error;
|
|
1692
1812
|
}
|
|
1693
|
-
return providerConnectionResult("railway", true, result.stdout.trim() || "Railway CLI check succeeded.");
|
|
1694
1813
|
}
|
|
1695
|
-
function checkTreeseedProviderConnections({ tenantRoot, scope = "prod", env = process.env } = {}) {
|
|
1814
|
+
async function checkTreeseedProviderConnections({ tenantRoot, scope = "prod", env = process.env } = {}) {
|
|
1696
1815
|
const values = collectTreeseedConfigSeedValues(tenantRoot, scope, env);
|
|
1697
|
-
const
|
|
1816
|
+
const rawCommandEnv = {
|
|
1698
1817
|
GH_TOKEN: values.GH_TOKEN,
|
|
1699
1818
|
CLOUDFLARE_API_TOKEN: values.CLOUDFLARE_API_TOKEN,
|
|
1700
1819
|
CLOUDFLARE_ACCOUNT_ID: values.CLOUDFLARE_ACCOUNT_ID,
|
|
1701
1820
|
RAILWAY_API_TOKEN: values.RAILWAY_API_TOKEN,
|
|
1702
|
-
|
|
1821
|
+
TREESEED_RAILWAY_WORKSPACE: values.TREESEED_RAILWAY_WORKSPACE || resolveRailwayWorkspace(values)
|
|
1703
1822
|
};
|
|
1823
|
+
const commandEnv = buildRailwayCommandEnv(rawCommandEnv);
|
|
1704
1824
|
const checks = [
|
|
1705
1825
|
checkGitHubConnection({ tenantRoot, env: commandEnv }),
|
|
1706
|
-
checkCloudflareConnection({ tenantRoot, env: commandEnv })
|
|
1707
|
-
checkRailwayConnection({ tenantRoot, env: commandEnv })
|
|
1826
|
+
checkCloudflareConnection({ tenantRoot, env: commandEnv })
|
|
1708
1827
|
];
|
|
1828
|
+
const railwayCheck = await checkRailwayConnection({ tenantRoot, env: commandEnv });
|
|
1829
|
+
checks.push(railwayCheck);
|
|
1709
1830
|
return {
|
|
1710
1831
|
scope,
|
|
1711
1832
|
ok: checks.every((check) => check.ready || check.skipped),
|
|
@@ -1722,14 +1843,20 @@ function formatTreeseedProviderConnectionReport(report) {
|
|
|
1722
1843
|
}
|
|
1723
1844
|
return lines.join("\n");
|
|
1724
1845
|
}
|
|
1846
|
+
function formatTreeseedProviderConnectionFailures(reports) {
|
|
1847
|
+
const failing = reports.filter((report) => report.checks.some((check) => !check.ready && !check.skipped));
|
|
1848
|
+
if (failing.length === 0) {
|
|
1849
|
+
return "";
|
|
1850
|
+
}
|
|
1851
|
+
return [
|
|
1852
|
+
"Treeseed provider connection checks failed.",
|
|
1853
|
+
...failing.map((report) => formatTreeseedProviderConnectionReport(report))
|
|
1854
|
+
].join("\n");
|
|
1855
|
+
}
|
|
1725
1856
|
function writeProviderConnectionReport(write, report) {
|
|
1726
1857
|
write(formatTreeseedProviderConnectionReport(report));
|
|
1727
1858
|
}
|
|
1728
|
-
function
|
|
1729
|
-
const result = runGh([command, "list", "--repo", repository, "--json", "name"], { cwd: tenantRoot });
|
|
1730
|
-
return new Set(JSON.parse(result.stdout || "[]").map((entry) => entry?.name).filter(Boolean));
|
|
1731
|
-
}
|
|
1732
|
-
function syncTreeseedGitHubEnvironment({ tenantRoot, scope = "prod", dryRun = false } = {}) {
|
|
1859
|
+
async function syncTreeseedGitHubEnvironment({ tenantRoot, scope = "prod", dryRun = false } = {}) {
|
|
1733
1860
|
const repository = maybeResolveGitHubRepositorySlug(tenantRoot);
|
|
1734
1861
|
if (!repository) {
|
|
1735
1862
|
throw new Error("Unable to determine the GitHub repository from the origin remote.");
|
|
@@ -1737,8 +1864,14 @@ function syncTreeseedGitHubEnvironment({ tenantRoot, scope = "prod", dryRun = fa
|
|
|
1737
1864
|
const registry = collectTreeseedEnvironmentContext(tenantRoot);
|
|
1738
1865
|
const values = resolveTreeseedMachineEnvironmentValues(tenantRoot, scope);
|
|
1739
1866
|
const relevant = registry.entries.filter((entry) => entry.scopes.includes(scope));
|
|
1740
|
-
const
|
|
1741
|
-
const
|
|
1867
|
+
const ghToken = values.GH_TOKEN || process.env.GH_TOKEN || process.env.GITHUB_TOKEN || "";
|
|
1868
|
+
const ghEnv = ghToken ? {
|
|
1869
|
+
GH_TOKEN: ghToken,
|
|
1870
|
+
GITHUB_TOKEN: ghToken
|
|
1871
|
+
} : {};
|
|
1872
|
+
const githubClient = createGitHubApiClient({ env: ghEnv });
|
|
1873
|
+
const secretNames = await listGitHubRepositorySecretNames(repository, { client: githubClient });
|
|
1874
|
+
const variableNames = await listGitHubRepositoryVariableNames(repository, { client: githubClient });
|
|
1742
1875
|
const synced = {
|
|
1743
1876
|
secrets: [],
|
|
1744
1877
|
variables: []
|
|
@@ -1749,11 +1882,23 @@ function syncTreeseedGitHubEnvironment({ tenantRoot, scope = "prod", dryRun = fa
|
|
|
1749
1882
|
continue;
|
|
1750
1883
|
}
|
|
1751
1884
|
if (entry.targets.includes("github-secret")) {
|
|
1752
|
-
|
|
1885
|
+
if (!dryRun) {
|
|
1886
|
+
await upsertGitHubRepositorySecret(repository, entry.id, value, { client: githubClient });
|
|
1887
|
+
}
|
|
1753
1888
|
synced.secrets.push({ name: entry.id, existed: secretNames.has(entry.id) });
|
|
1754
1889
|
}
|
|
1755
1890
|
if (entry.targets.includes("github-variable")) {
|
|
1756
|
-
|
|
1891
|
+
if (!dryRun) {
|
|
1892
|
+
try {
|
|
1893
|
+
upsertGitHubRepositoryVariableWithGhCli(repository, entry.id, value, { env: ghEnv });
|
|
1894
|
+
} catch (error) {
|
|
1895
|
+
const message = error instanceof Error ? error.message : String(error ?? "");
|
|
1896
|
+
if (!/not found|ENOENT|timed out|timeout|aborted|gh api exited/iu.test(message)) {
|
|
1897
|
+
throw error;
|
|
1898
|
+
}
|
|
1899
|
+
await upsertGitHubRepositoryVariable(repository, entry.id, value, { client: githubClient });
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1757
1902
|
synced.variables.push({ name: entry.id, existed: variableNames.has(entry.id) });
|
|
1758
1903
|
}
|
|
1759
1904
|
}
|
|
@@ -1805,7 +1950,7 @@ function syncTreeseedRailwayEnvironment({ tenantRoot, scope = "prod", dryRun = f
|
|
|
1805
1950
|
serviceId: service.railway?.serviceId ?? "",
|
|
1806
1951
|
rootDir: resolve(tenantRoot, service.railway?.rootDir ?? service.rootDir ?? defaultRootDir),
|
|
1807
1952
|
baseUrl: environment?.baseUrl ?? service.publicBaseUrl ?? "(unset)",
|
|
1808
|
-
environmentName: environment?.railwayEnvironment ?? scope,
|
|
1953
|
+
environmentName: normalizeRailwayEnvironmentName(environment?.railwayEnvironment ?? scope),
|
|
1809
1954
|
secrets: railwaySecretNames,
|
|
1810
1955
|
variables: railwayVariableNames,
|
|
1811
1956
|
dryRun
|
|
@@ -1830,23 +1975,22 @@ function syncTreeseedRailwayEnvironment({ tenantRoot, scope = "prod", dryRun = f
|
|
|
1830
1975
|
services
|
|
1831
1976
|
};
|
|
1832
1977
|
}
|
|
1833
|
-
function initializeTreeseedPersistentEnvironment({ tenantRoot, scope = "prod", dryRun = false } = {}) {
|
|
1978
|
+
async function initializeTreeseedPersistentEnvironment({ tenantRoot, scope = "prod", dryRun = false } = {}) {
|
|
1834
1979
|
const normalizedScope = scope === "prod" ? "prod" : scope;
|
|
1835
1980
|
const target = createPersistentDeployTarget(normalizedScope);
|
|
1836
|
-
const summary =
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
}
|
|
1981
|
+
const summary = await reconcileTreeseedTarget({
|
|
1982
|
+
tenantRoot,
|
|
1983
|
+
target,
|
|
1984
|
+
env: process.env
|
|
1985
|
+
});
|
|
1842
1986
|
return {
|
|
1843
1987
|
scope: normalizedScope,
|
|
1844
1988
|
target,
|
|
1845
1989
|
summary,
|
|
1846
|
-
secrets:
|
|
1990
|
+
secrets: summary.results.filter((result) => result.unit.provider === "cloudflare").flatMap((result) => Object.keys(result.resourceLocators ?? {}))
|
|
1847
1991
|
};
|
|
1848
1992
|
}
|
|
1849
|
-
function summarizePersistentReadiness(tenantRoot, scope, validation, connectionChecks) {
|
|
1993
|
+
async function summarizePersistentReadiness(tenantRoot, scope, validation, connectionChecks, env = process.env, { includeReconcileStatus = true } = {}) {
|
|
1850
1994
|
const validationProblems = [...validation.missing, ...validation.invalid];
|
|
1851
1995
|
const validationBlockers = validationProblems.map((problem) => problem.message);
|
|
1852
1996
|
const connectionReady = connectionChecks.every((check) => check.ready || check.skipped);
|
|
@@ -1888,37 +2032,105 @@ function summarizePersistentReadiness(tenantRoot, scope, validation, connectionC
|
|
|
1888
2032
|
}
|
|
1889
2033
|
};
|
|
1890
2034
|
}
|
|
1891
|
-
const cloudflare = verifyProvisionedCloudflareResources(tenantRoot, { scope });
|
|
1892
|
-
let railwayReady = true;
|
|
1893
|
-
let railwayIssue = null;
|
|
1894
|
-
try {
|
|
1895
|
-
validateRailwayDeployPrerequisites(tenantRoot, scope);
|
|
1896
|
-
} catch (error) {
|
|
1897
|
-
railwayReady = false;
|
|
1898
|
-
railwayIssue = error instanceof Error ? error.message : String(error);
|
|
1899
|
-
}
|
|
1900
2035
|
const configured = validation.ok;
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
2036
|
+
if (!includeReconcileStatus) {
|
|
2037
|
+
return {
|
|
2038
|
+
configured,
|
|
2039
|
+
provisioned: false,
|
|
2040
|
+
deployable: false,
|
|
2041
|
+
phase: "config_complete",
|
|
2042
|
+
blockers: [...connectionIssues],
|
|
2043
|
+
warnings: connectionWarnings,
|
|
2044
|
+
checks: {
|
|
2045
|
+
validation: validation.ok,
|
|
2046
|
+
connections: connectionReady,
|
|
2047
|
+
reconcile: "deferred"
|
|
2048
|
+
}
|
|
2049
|
+
};
|
|
1909
2050
|
}
|
|
2051
|
+
const reconcile = await collectTreeseedReconcileStatus({
|
|
2052
|
+
tenantRoot,
|
|
2053
|
+
target: createPersistentDeployTarget(scope),
|
|
2054
|
+
env
|
|
2055
|
+
});
|
|
2056
|
+
const provisioned = reconcile.ready;
|
|
2057
|
+
const deployable = configured && provisioned && connectionReady;
|
|
2058
|
+
const blockers = [...connectionIssues, ...reconcile.blockers];
|
|
1910
2059
|
return {
|
|
1911
2060
|
configured,
|
|
1912
2061
|
provisioned,
|
|
1913
2062
|
deployable,
|
|
1914
2063
|
phase: provisioned ? "provisioned" : "config_complete",
|
|
1915
2064
|
blockers,
|
|
1916
|
-
warnings: connectionWarnings,
|
|
2065
|
+
warnings: [...connectionWarnings, ...reconcile.warnings],
|
|
1917
2066
|
checks: {
|
|
1918
2067
|
validation: validation.ok,
|
|
1919
2068
|
connections: connectionReady,
|
|
1920
|
-
|
|
1921
|
-
|
|
2069
|
+
reconcile: reconcile.units
|
|
2070
|
+
}
|
|
2071
|
+
};
|
|
2072
|
+
}
|
|
2073
|
+
function summarizeReconciledPersistentReadiness(scope, validation, connectionChecks, reconciled) {
|
|
2074
|
+
const validationProblems = [...validation.missing, ...validation.invalid];
|
|
2075
|
+
const validationBlockers = validationProblems.map((problem) => problem.message);
|
|
2076
|
+
const connectionReady = connectionChecks.every((check) => check.ready || check.skipped);
|
|
2077
|
+
const connectionIssues = connectionChecks.filter((check) => !check.ready && !check.skipped).map((check) => `${check.provider}: ${check.detail}`);
|
|
2078
|
+
const connectionWarnings = connectionChecks.filter((check) => check.skipped).map((check) => `${check.provider}: ${check.detail}`);
|
|
2079
|
+
if (scope === "local") {
|
|
2080
|
+
return {
|
|
2081
|
+
configured: validation.ok,
|
|
2082
|
+
provisioned: true,
|
|
2083
|
+
deployable: validation.ok && connectionReady,
|
|
2084
|
+
phase: validation.ok ? "code_ready" : "config_incomplete",
|
|
2085
|
+
blockers: [
|
|
2086
|
+
...validationBlockers,
|
|
2087
|
+
...connectionIssues
|
|
2088
|
+
],
|
|
2089
|
+
warnings: connectionWarnings,
|
|
2090
|
+
checks: {
|
|
2091
|
+
validation: validation.ok,
|
|
2092
|
+
connections: connectionReady
|
|
2093
|
+
}
|
|
2094
|
+
};
|
|
2095
|
+
}
|
|
2096
|
+
if (!validation.ok) {
|
|
2097
|
+
return {
|
|
2098
|
+
configured: false,
|
|
2099
|
+
provisioned: false,
|
|
2100
|
+
deployable: false,
|
|
2101
|
+
phase: "config_incomplete",
|
|
2102
|
+
blockers: [
|
|
2103
|
+
...validationBlockers,
|
|
2104
|
+
...connectionIssues
|
|
2105
|
+
],
|
|
2106
|
+
warnings: connectionWarnings,
|
|
2107
|
+
checks: {
|
|
2108
|
+
validation: false,
|
|
2109
|
+
connections: connectionReady,
|
|
2110
|
+
reconcile: []
|
|
2111
|
+
}
|
|
2112
|
+
};
|
|
2113
|
+
}
|
|
2114
|
+
const actions = reconciled?.actions ?? [];
|
|
2115
|
+
const blockers = actions.filter((action) => action.verified !== true).flatMap((action) => [
|
|
2116
|
+
...action.missing.map((entry) => `${action.provider}:${action.unitType}: ${entry}`),
|
|
2117
|
+
...action.drifted.map((entry) => `${action.provider}:${action.unitType}: ${entry}`)
|
|
2118
|
+
]);
|
|
2119
|
+
const provisioned = blockers.length === 0 && actions.length > 0;
|
|
2120
|
+
return {
|
|
2121
|
+
configured: true,
|
|
2122
|
+
provisioned,
|
|
2123
|
+
deployable: provisioned && connectionReady,
|
|
2124
|
+
phase: provisioned ? "provisioned" : "config_complete",
|
|
2125
|
+
blockers: [
|
|
2126
|
+
...connectionIssues,
|
|
2127
|
+
...blockers
|
|
2128
|
+
],
|
|
2129
|
+
warnings: connectionWarnings,
|
|
2130
|
+
checks: {
|
|
2131
|
+
validation: true,
|
|
2132
|
+
connections: connectionReady,
|
|
2133
|
+
reconcile: actions
|
|
1922
2134
|
}
|
|
1923
2135
|
};
|
|
1924
2136
|
}
|
|
@@ -2151,7 +2363,7 @@ function applyTreeseedConfigValues({
|
|
|
2151
2363
|
sharedStorageMigrations
|
|
2152
2364
|
};
|
|
2153
2365
|
}
|
|
2154
|
-
function finalizeTreeseedConfig({
|
|
2366
|
+
async function finalizeTreeseedConfig({
|
|
2155
2367
|
tenantRoot,
|
|
2156
2368
|
scopes = [...TREESEED_ENVIRONMENT_SCOPES],
|
|
2157
2369
|
sync = "all",
|
|
@@ -2164,7 +2376,9 @@ function finalizeTreeseedConfig({
|
|
|
2164
2376
|
const summary = {
|
|
2165
2377
|
scopes,
|
|
2166
2378
|
synced: {},
|
|
2167
|
-
|
|
2379
|
+
reconciled: [],
|
|
2380
|
+
deployed: [],
|
|
2381
|
+
resourceInventoryByScope: {},
|
|
2168
2382
|
connectionChecks: [],
|
|
2169
2383
|
validationByScope: {},
|
|
2170
2384
|
readinessByScope: {}
|
|
@@ -2175,9 +2389,24 @@ function finalizeTreeseedConfig({
|
|
|
2175
2389
|
}
|
|
2176
2390
|
};
|
|
2177
2391
|
progress(`Validating configuration for ${scopes.join(", ")}...`);
|
|
2392
|
+
const scopeSeedValues = Object.fromEntries(
|
|
2393
|
+
scopes.map((scope) => [scope, collectTreeseedConfigSeedValues(tenantRoot, scope, env)])
|
|
2394
|
+
);
|
|
2178
2395
|
for (const scope of scopes) {
|
|
2396
|
+
const seedValues = scopeSeedValues[scope];
|
|
2397
|
+
const suggestedValues = getTreeseedEnvironmentSuggestedValues({
|
|
2398
|
+
scope,
|
|
2399
|
+
purpose: "config",
|
|
2400
|
+
deployConfig: registry.context.deployConfig,
|
|
2401
|
+
tenantConfig: registry.context.tenantConfig,
|
|
2402
|
+
plugins: registry.context.plugins,
|
|
2403
|
+
values: seedValues
|
|
2404
|
+
});
|
|
2179
2405
|
const validation = validateTreeseedEnvironmentValues({
|
|
2180
|
-
values:
|
|
2406
|
+
values: {
|
|
2407
|
+
...suggestedValues,
|
|
2408
|
+
...seedValues
|
|
2409
|
+
},
|
|
2181
2410
|
scope,
|
|
2182
2411
|
purpose: "config",
|
|
2183
2412
|
deployConfig: registry.context.deployConfig,
|
|
@@ -2187,21 +2416,37 @@ function finalizeTreeseedConfig({
|
|
|
2187
2416
|
summary.validationByScope[scope] = validation;
|
|
2188
2417
|
if (checkConnections) {
|
|
2189
2418
|
progress(`Checking provider connectivity for ${scope}...`);
|
|
2190
|
-
summary.connectionChecks.push(checkTreeseedProviderConnections({ tenantRoot, scope, env }));
|
|
2419
|
+
summary.connectionChecks.push(await checkTreeseedProviderConnections({ tenantRoot, scope, env: seedValues }));
|
|
2191
2420
|
}
|
|
2192
2421
|
}
|
|
2193
2422
|
for (const scope of scopes) {
|
|
2194
|
-
|
|
2423
|
+
if (scope !== "local") {
|
|
2424
|
+
const target = createPersistentDeployTarget(scope);
|
|
2425
|
+
const deployState = loadDeployState(tenantRoot, registry.context.deployConfig, { target });
|
|
2426
|
+
const inventory = buildProvisioningSummary(registry.context.deployConfig, deployState, target);
|
|
2427
|
+
const railwayWorkspace = resolveRailwayWorkspace(scopeSeedValues[scope]);
|
|
2428
|
+
summary.resourceInventoryByScope[scope] = inventory;
|
|
2429
|
+
progress(
|
|
2430
|
+
`Resolved ${scope} resources: deployment=${inventory.identity?.deploymentKey}, pages=${inventory.resources?.pagesProject}, web-domain=${inventory.resources?.webDomain ?? "(none)"}, api-domain=${inventory.resources?.apiDomain ?? "(none)"}, r2=${inventory.resources?.contentBucket}, queue=${inventory.resources?.queue}, d1=${inventory.resources?.database}, railway=${inventory.resources?.railwayProject}, workspace=${railwayWorkspace}.`
|
|
2431
|
+
);
|
|
2432
|
+
}
|
|
2433
|
+
summary.readinessByScope[scope] = await summarizePersistentReadiness(
|
|
2195
2434
|
tenantRoot,
|
|
2196
2435
|
scope,
|
|
2197
2436
|
summary.validationByScope[scope],
|
|
2198
|
-
summary.connectionChecks.find((report) => report.scope === scope)?.checks ?? []
|
|
2437
|
+
summary.connectionChecks.find((report) => report.scope === scope)?.checks ?? [],
|
|
2438
|
+
scopeSeedValues[scope],
|
|
2439
|
+
{ includeReconcileStatus: !initializePersistent }
|
|
2199
2440
|
);
|
|
2200
2441
|
}
|
|
2201
2442
|
const invalidScopes = scopes.filter((scope) => summary.validationByScope[scope]?.ok !== true);
|
|
2202
2443
|
if (invalidScopes.length > 0) {
|
|
2203
2444
|
throw new Error(formatTreeseedConfigValidationFailure(summary.validationByScope, scopes));
|
|
2204
2445
|
}
|
|
2446
|
+
const failingConnectionReports = summary.connectionChecks.filter((report) => report.ok !== true);
|
|
2447
|
+
if (failingConnectionReports.length > 0) {
|
|
2448
|
+
throw new Error(formatTreeseedProviderConnectionFailures(failingConnectionReports));
|
|
2449
|
+
}
|
|
2205
2450
|
progress("Syncing managed service settings from treeseed.site.yaml...");
|
|
2206
2451
|
syncManagedServiceSettingsFromDeployConfig(tenantRoot);
|
|
2207
2452
|
if (initializePersistent) {
|
|
@@ -2209,34 +2454,110 @@ function finalizeTreeseedConfig({
|
|
|
2209
2454
|
if (scope === "local") {
|
|
2210
2455
|
continue;
|
|
2211
2456
|
}
|
|
2212
|
-
progress(`
|
|
2457
|
+
progress(`Deriving desired units for ${scope}...`);
|
|
2458
|
+
const initialized = await reconcileTreeseedTarget({
|
|
2459
|
+
tenantRoot,
|
|
2460
|
+
target: createPersistentDeployTarget(scope),
|
|
2461
|
+
env: scopeSeedValues[scope],
|
|
2462
|
+
write: progress
|
|
2463
|
+
});
|
|
2464
|
+
summary.reconciled.push({
|
|
2465
|
+
scope,
|
|
2466
|
+
target: scope,
|
|
2467
|
+
units: initialized.units.length,
|
|
2468
|
+
actions: initialized.results.map((result) => ({
|
|
2469
|
+
unitId: result.unit.unitId,
|
|
2470
|
+
unitType: result.unit.unitType,
|
|
2471
|
+
provider: result.unit.provider,
|
|
2472
|
+
action: result.action,
|
|
2473
|
+
verified: result.verification?.verified === true,
|
|
2474
|
+
missing: result.verification?.missing ?? [],
|
|
2475
|
+
drifted: result.verification?.drifted ?? []
|
|
2476
|
+
}))
|
|
2477
|
+
});
|
|
2478
|
+
if (scope === "staging") {
|
|
2479
|
+
progress(`Ensuring ${STAGING_BRANCH} exists on origin from ${PRODUCTION_BRANCH}...`);
|
|
2480
|
+
const repository = maybeResolveGitHubRepositorySlug(tenantRoot);
|
|
2481
|
+
if (!repository) {
|
|
2482
|
+
throw new Error("Unable to determine the GitHub repository from the origin remote for staging branch bootstrap.");
|
|
2483
|
+
}
|
|
2484
|
+
const branchBootstrap = await ensureGitHubBranchFromBase(repository, STAGING_BRANCH, {
|
|
2485
|
+
baseBranch: PRODUCTION_BRANCH,
|
|
2486
|
+
client: createGitHubApiClient({
|
|
2487
|
+
env: scopeSeedValues[scope]
|
|
2488
|
+
})
|
|
2489
|
+
});
|
|
2490
|
+
summary.deployed.push({
|
|
2491
|
+
scope,
|
|
2492
|
+
branchBootstrap,
|
|
2493
|
+
result: {}
|
|
2494
|
+
});
|
|
2495
|
+
}
|
|
2496
|
+
progress(`Deploying ${scope}...`);
|
|
2213
2497
|
applyTreeseedEnvironmentToProcess({ tenantRoot, scope, override: true });
|
|
2214
|
-
|
|
2215
|
-
|
|
2498
|
+
process.env.TREESEED_RAILWAY_WORKSPACE = process.env.TREESEED_RAILWAY_WORKSPACE || scopeSeedValues[scope].TREESEED_RAILWAY_WORKSPACE || resolveRailwayWorkspace(scopeSeedValues[scope]);
|
|
2499
|
+
const { deployProjectPlatform } = await import("./project-platform.js");
|
|
2500
|
+
const deployResult = await deployProjectPlatform({
|
|
2501
|
+
tenantRoot,
|
|
2216
2502
|
scope,
|
|
2217
|
-
|
|
2218
|
-
target: initialized.summary.target
|
|
2503
|
+
skipProvision: true
|
|
2219
2504
|
});
|
|
2505
|
+
const deployEntry = summary.deployed.find((entry) => entry.scope === scope);
|
|
2506
|
+
if (deployEntry) {
|
|
2507
|
+
deployEntry.result = deployResult;
|
|
2508
|
+
} else {
|
|
2509
|
+
summary.deployed.push({
|
|
2510
|
+
scope,
|
|
2511
|
+
branchBootstrap: null,
|
|
2512
|
+
result: deployResult
|
|
2513
|
+
});
|
|
2514
|
+
}
|
|
2515
|
+
progress(`Re-verifying ${scope} after deployment...`);
|
|
2516
|
+
const finalized = await reconcileTreeseedTarget({
|
|
2517
|
+
tenantRoot,
|
|
2518
|
+
target: createPersistentDeployTarget(scope),
|
|
2519
|
+
env: scopeSeedValues[scope],
|
|
2520
|
+
write: progress
|
|
2521
|
+
});
|
|
2522
|
+
const index = summary.reconciled.findIndex((entry) => entry.scope === scope);
|
|
2523
|
+
const nextSummary = {
|
|
2524
|
+
scope,
|
|
2525
|
+
target: scope,
|
|
2526
|
+
units: finalized.units.length,
|
|
2527
|
+
actions: finalized.results.map((result) => ({
|
|
2528
|
+
unitId: result.unit.unitId,
|
|
2529
|
+
unitType: result.unit.unitType,
|
|
2530
|
+
provider: result.unit.provider,
|
|
2531
|
+
action: result.action,
|
|
2532
|
+
verified: result.verification?.verified === true,
|
|
2533
|
+
missing: result.verification?.missing ?? [],
|
|
2534
|
+
drifted: result.verification?.drifted ?? []
|
|
2535
|
+
}))
|
|
2536
|
+
};
|
|
2537
|
+
if (index >= 0) {
|
|
2538
|
+
summary.reconciled[index] = nextSummary;
|
|
2539
|
+
} else {
|
|
2540
|
+
summary.reconciled.push(nextSummary);
|
|
2541
|
+
}
|
|
2220
2542
|
}
|
|
2221
2543
|
}
|
|
2222
2544
|
if (sync === "github" || sync === "all") {
|
|
2223
2545
|
progress(`Syncing GitHub environment for ${scopes.at(-1) ?? "prod"}...`);
|
|
2224
|
-
summary.synced.github = syncTreeseedGitHubEnvironment({ tenantRoot, scope: scopes.at(-1) ?? "prod" });
|
|
2225
|
-
}
|
|
2226
|
-
if (sync === "cloudflare" || sync === "all") {
|
|
2227
|
-
progress(`Syncing Cloudflare environment for ${scopes.at(-1) ?? "prod"}...`);
|
|
2228
|
-
summary.synced.cloudflare = syncTreeseedCloudflareEnvironment({ tenantRoot, scope: scopes.at(-1) ?? "prod" });
|
|
2229
|
-
}
|
|
2230
|
-
if (sync === "railway" || sync === "all") {
|
|
2231
|
-
progress(`Syncing Railway environment for ${scopes.at(-1) ?? "prod"}...`);
|
|
2232
|
-
summary.synced.railway = syncTreeseedRailwayEnvironment({ tenantRoot, scope: scopes.at(-1) ?? "prod" });
|
|
2546
|
+
summary.synced.github = await syncTreeseedGitHubEnvironment({ tenantRoot, scope: scopes.at(-1) ?? "prod" });
|
|
2233
2547
|
}
|
|
2234
2548
|
for (const scope of scopes) {
|
|
2235
|
-
summary.
|
|
2549
|
+
const reconciled = summary.reconciled.find((entry) => entry.scope === scope) ?? null;
|
|
2550
|
+
summary.readinessByScope[scope] = initializePersistent && reconciled ? summarizeReconciledPersistentReadiness(
|
|
2551
|
+
scope,
|
|
2552
|
+
summary.validationByScope[scope],
|
|
2553
|
+
summary.connectionChecks.find((report) => report.scope === scope)?.checks ?? [],
|
|
2554
|
+
reconciled
|
|
2555
|
+
) : await summarizePersistentReadiness(
|
|
2236
2556
|
tenantRoot,
|
|
2237
2557
|
scope,
|
|
2238
2558
|
summary.validationByScope[scope],
|
|
2239
|
-
summary.connectionChecks.find((report) => report.scope === scope)?.checks ?? []
|
|
2559
|
+
summary.connectionChecks.find((report) => report.scope === scope)?.checks ?? [],
|
|
2560
|
+
env
|
|
2240
2561
|
);
|
|
2241
2562
|
}
|
|
2242
2563
|
return summary;
|
|
@@ -2294,6 +2615,8 @@ export {
|
|
|
2294
2615
|
getTreeseedRemoteAuthPaths,
|
|
2295
2616
|
initializeTreeseedPersistentEnvironment,
|
|
2296
2617
|
inspectTreeseedKeyAgentStatus,
|
|
2618
|
+
inspectTreeseedKeyAgentTransportDiagnostic,
|
|
2619
|
+
inspectTreeseedPassphraseEnvDiagnostic,
|
|
2297
2620
|
listDeprecatedTreeseedLocalEnvFiles,
|
|
2298
2621
|
listRelevantTreeseedConfigEntries,
|
|
2299
2622
|
loadTreeseedMachineConfig,
|