@treeseed/sdk 0.5.3 → 0.6.1
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 +449 -136
- 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 +2116 -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
|
}
|
|
@@ -1637,6 +1694,9 @@ function providerConnectionResult(provider, ready, detail, extra = {}) {
|
|
|
1637
1694
|
...extra
|
|
1638
1695
|
};
|
|
1639
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
|
+
}
|
|
1640
1700
|
function checkGitHubConnection({ tenantRoot, env }) {
|
|
1641
1701
|
if (!env.GH_TOKEN) {
|
|
1642
1702
|
return providerConnectionResult("github", false, "GH_TOKEN is not configured.", { skipped: true });
|
|
@@ -1646,76 +1706,127 @@ function checkGitHubConnection({ tenantRoot, env }) {
|
|
|
1646
1706
|
}
|
|
1647
1707
|
const repository = maybeResolveGitHubRepositorySlug(tenantRoot);
|
|
1648
1708
|
const args = repository ? ["repo", "view", repository, "--json", "nameWithOwner", "--jq", ".nameWithOwner"] : ["api", "user", "--jq", ".login"];
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
stdio: "pipe",
|
|
1652
|
-
encoding: "utf8",
|
|
1653
|
-
env: { ...process.env, ...env },
|
|
1654
|
-
timeout: CLI_CHECK_TIMEOUT_MS
|
|
1655
|
-
});
|
|
1656
|
-
if (result.status !== 0) {
|
|
1657
|
-
return providerConnectionResult("github", false, formatCheckOutput(result) || "GitHub API check failed.");
|
|
1658
|
-
}
|
|
1659
|
-
const resolved = result.stdout.trim();
|
|
1660
|
-
return providerConnectionResult(
|
|
1661
|
-
"github",
|
|
1662
|
-
true,
|
|
1663
|
-
repository ? `GitHub token can access ${resolved || repository}.` : resolved ? `Authenticated as ${resolved}.` : "GitHub API check succeeded."
|
|
1664
|
-
);
|
|
1665
|
-
}
|
|
1666
|
-
function checkCloudflareConnection({ tenantRoot, env }) {
|
|
1667
|
-
if (!env.CLOUDFLARE_API_TOKEN) {
|
|
1668
|
-
return providerConnectionResult("cloudflare", false, "CLOUDFLARE_API_TOKEN is not configured.", { skipped: true });
|
|
1669
|
-
}
|
|
1670
|
-
try {
|
|
1671
|
-
const result = spawnSync(process.execPath, [resolveWranglerBin(), "whoami"], {
|
|
1709
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
1710
|
+
const result = spawnSync("gh", args, {
|
|
1672
1711
|
cwd: tenantRoot,
|
|
1673
1712
|
stdio: "pipe",
|
|
1674
1713
|
encoding: "utf8",
|
|
1675
1714
|
env: { ...process.env, ...env },
|
|
1676
1715
|
timeout: CLI_CHECK_TIMEOUT_MS
|
|
1677
1716
|
});
|
|
1678
|
-
if (result.status
|
|
1679
|
-
|
|
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);
|
|
1680
1728
|
}
|
|
1681
|
-
return providerConnectionResult("cloudflare", true, "Wrangler authenticated with CLOUDFLARE_API_TOKEN.");
|
|
1682
|
-
} catch (error) {
|
|
1683
|
-
return providerConnectionResult("cloudflare", false, error instanceof Error ? error.message : "Cloudflare Wrangler check failed.");
|
|
1684
1729
|
}
|
|
1730
|
+
return providerConnectionResult("github", false, "GitHub API check failed.");
|
|
1685
1731
|
}
|
|
1686
|
-
function
|
|
1687
|
-
if (!env.
|
|
1688
|
-
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 });
|
|
1689
1735
|
}
|
|
1690
|
-
|
|
1691
|
-
|
|
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
|
+
}
|
|
1692
1758
|
}
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
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
|
|
1699
1775
|
});
|
|
1700
|
-
|
|
1701
|
-
|
|
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;
|
|
1702
1812
|
}
|
|
1703
|
-
return providerConnectionResult("railway", true, result.stdout.trim() || "Railway CLI check succeeded.");
|
|
1704
1813
|
}
|
|
1705
|
-
function checkTreeseedProviderConnections({ tenantRoot, scope = "prod", env = process.env } = {}) {
|
|
1814
|
+
async function checkTreeseedProviderConnections({ tenantRoot, scope = "prod", env = process.env } = {}) {
|
|
1706
1815
|
const values = collectTreeseedConfigSeedValues(tenantRoot, scope, env);
|
|
1707
|
-
const
|
|
1816
|
+
const rawCommandEnv = {
|
|
1708
1817
|
GH_TOKEN: values.GH_TOKEN,
|
|
1709
1818
|
CLOUDFLARE_API_TOKEN: values.CLOUDFLARE_API_TOKEN,
|
|
1710
1819
|
CLOUDFLARE_ACCOUNT_ID: values.CLOUDFLARE_ACCOUNT_ID,
|
|
1711
1820
|
RAILWAY_API_TOKEN: values.RAILWAY_API_TOKEN,
|
|
1712
|
-
|
|
1821
|
+
TREESEED_RAILWAY_WORKSPACE: values.TREESEED_RAILWAY_WORKSPACE || resolveRailwayWorkspace(values)
|
|
1713
1822
|
};
|
|
1823
|
+
const commandEnv = buildRailwayCommandEnv(rawCommandEnv);
|
|
1714
1824
|
const checks = [
|
|
1715
1825
|
checkGitHubConnection({ tenantRoot, env: commandEnv }),
|
|
1716
|
-
checkCloudflareConnection({ tenantRoot, env: commandEnv })
|
|
1717
|
-
checkRailwayConnection({ tenantRoot, env: commandEnv })
|
|
1826
|
+
checkCloudflareConnection({ tenantRoot, env: commandEnv })
|
|
1718
1827
|
];
|
|
1828
|
+
const railwayCheck = await checkRailwayConnection({ tenantRoot, env: commandEnv });
|
|
1829
|
+
checks.push(railwayCheck);
|
|
1719
1830
|
return {
|
|
1720
1831
|
scope,
|
|
1721
1832
|
ok: checks.every((check) => check.ready || check.skipped),
|
|
@@ -1732,14 +1843,20 @@ function formatTreeseedProviderConnectionReport(report) {
|
|
|
1732
1843
|
}
|
|
1733
1844
|
return lines.join("\n");
|
|
1734
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
|
+
}
|
|
1735
1856
|
function writeProviderConnectionReport(write, report) {
|
|
1736
1857
|
write(formatTreeseedProviderConnectionReport(report));
|
|
1737
1858
|
}
|
|
1738
|
-
function
|
|
1739
|
-
const result = runGh([command, "list", "--repo", repository, "--json", "name"], { cwd: tenantRoot });
|
|
1740
|
-
return new Set(JSON.parse(result.stdout || "[]").map((entry) => entry?.name).filter(Boolean));
|
|
1741
|
-
}
|
|
1742
|
-
function syncTreeseedGitHubEnvironment({ tenantRoot, scope = "prod", dryRun = false } = {}) {
|
|
1859
|
+
async function syncTreeseedGitHubEnvironment({ tenantRoot, scope = "prod", dryRun = false } = {}) {
|
|
1743
1860
|
const repository = maybeResolveGitHubRepositorySlug(tenantRoot);
|
|
1744
1861
|
if (!repository) {
|
|
1745
1862
|
throw new Error("Unable to determine the GitHub repository from the origin remote.");
|
|
@@ -1747,8 +1864,14 @@ function syncTreeseedGitHubEnvironment({ tenantRoot, scope = "prod", dryRun = fa
|
|
|
1747
1864
|
const registry = collectTreeseedEnvironmentContext(tenantRoot);
|
|
1748
1865
|
const values = resolveTreeseedMachineEnvironmentValues(tenantRoot, scope);
|
|
1749
1866
|
const relevant = registry.entries.filter((entry) => entry.scopes.includes(scope));
|
|
1750
|
-
const
|
|
1751
|
-
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 });
|
|
1752
1875
|
const synced = {
|
|
1753
1876
|
secrets: [],
|
|
1754
1877
|
variables: []
|
|
@@ -1759,11 +1882,23 @@ function syncTreeseedGitHubEnvironment({ tenantRoot, scope = "prod", dryRun = fa
|
|
|
1759
1882
|
continue;
|
|
1760
1883
|
}
|
|
1761
1884
|
if (entry.targets.includes("github-secret")) {
|
|
1762
|
-
|
|
1885
|
+
if (!dryRun) {
|
|
1886
|
+
await upsertGitHubRepositorySecret(repository, entry.id, value, { client: githubClient });
|
|
1887
|
+
}
|
|
1763
1888
|
synced.secrets.push({ name: entry.id, existed: secretNames.has(entry.id) });
|
|
1764
1889
|
}
|
|
1765
1890
|
if (entry.targets.includes("github-variable")) {
|
|
1766
|
-
|
|
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
|
+
}
|
|
1767
1902
|
synced.variables.push({ name: entry.id, existed: variableNames.has(entry.id) });
|
|
1768
1903
|
}
|
|
1769
1904
|
}
|
|
@@ -1815,7 +1950,7 @@ function syncTreeseedRailwayEnvironment({ tenantRoot, scope = "prod", dryRun = f
|
|
|
1815
1950
|
serviceId: service.railway?.serviceId ?? "",
|
|
1816
1951
|
rootDir: resolve(tenantRoot, service.railway?.rootDir ?? service.rootDir ?? defaultRootDir),
|
|
1817
1952
|
baseUrl: environment?.baseUrl ?? service.publicBaseUrl ?? "(unset)",
|
|
1818
|
-
environmentName: environment?.railwayEnvironment ?? scope,
|
|
1953
|
+
environmentName: normalizeRailwayEnvironmentName(environment?.railwayEnvironment ?? scope),
|
|
1819
1954
|
secrets: railwaySecretNames,
|
|
1820
1955
|
variables: railwayVariableNames,
|
|
1821
1956
|
dryRun
|
|
@@ -1840,23 +1975,22 @@ function syncTreeseedRailwayEnvironment({ tenantRoot, scope = "prod", dryRun = f
|
|
|
1840
1975
|
services
|
|
1841
1976
|
};
|
|
1842
1977
|
}
|
|
1843
|
-
function initializeTreeseedPersistentEnvironment({ tenantRoot, scope = "prod", dryRun = false } = {}) {
|
|
1978
|
+
async function initializeTreeseedPersistentEnvironment({ tenantRoot, scope = "prod", dryRun = false } = {}) {
|
|
1844
1979
|
const normalizedScope = scope === "prod" ? "prod" : scope;
|
|
1845
1980
|
const target = createPersistentDeployTarget(normalizedScope);
|
|
1846
|
-
const summary =
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
}
|
|
1981
|
+
const summary = await reconcileTreeseedTarget({
|
|
1982
|
+
tenantRoot,
|
|
1983
|
+
target,
|
|
1984
|
+
env: process.env
|
|
1985
|
+
});
|
|
1852
1986
|
return {
|
|
1853
1987
|
scope: normalizedScope,
|
|
1854
1988
|
target,
|
|
1855
1989
|
summary,
|
|
1856
|
-
secrets:
|
|
1990
|
+
secrets: summary.results.filter((result) => result.unit.provider === "cloudflare").flatMap((result) => Object.keys(result.resourceLocators ?? {}))
|
|
1857
1991
|
};
|
|
1858
1992
|
}
|
|
1859
|
-
function summarizePersistentReadiness(tenantRoot, scope, validation, connectionChecks) {
|
|
1993
|
+
async function summarizePersistentReadiness(tenantRoot, scope, validation, connectionChecks, env = process.env, { includeReconcileStatus = true } = {}) {
|
|
1860
1994
|
const validationProblems = [...validation.missing, ...validation.invalid];
|
|
1861
1995
|
const validationBlockers = validationProblems.map((problem) => problem.message);
|
|
1862
1996
|
const connectionReady = connectionChecks.every((check) => check.ready || check.skipped);
|
|
@@ -1898,37 +2032,105 @@ function summarizePersistentReadiness(tenantRoot, scope, validation, connectionC
|
|
|
1898
2032
|
}
|
|
1899
2033
|
};
|
|
1900
2034
|
}
|
|
1901
|
-
const cloudflare = verifyProvisionedCloudflareResources(tenantRoot, { scope });
|
|
1902
|
-
let railwayReady = true;
|
|
1903
|
-
let railwayIssue = null;
|
|
1904
|
-
try {
|
|
1905
|
-
validateRailwayDeployPrerequisites(tenantRoot, scope);
|
|
1906
|
-
} catch (error) {
|
|
1907
|
-
railwayReady = false;
|
|
1908
|
-
railwayIssue = error instanceof Error ? error.message : String(error);
|
|
1909
|
-
}
|
|
1910
2035
|
const configured = validation.ok;
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
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
|
+
};
|
|
1919
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];
|
|
1920
2059
|
return {
|
|
1921
2060
|
configured,
|
|
1922
2061
|
provisioned,
|
|
1923
2062
|
deployable,
|
|
1924
2063
|
phase: provisioned ? "provisioned" : "config_complete",
|
|
1925
2064
|
blockers,
|
|
1926
|
-
warnings: connectionWarnings,
|
|
2065
|
+
warnings: [...connectionWarnings, ...reconcile.warnings],
|
|
1927
2066
|
checks: {
|
|
1928
2067
|
validation: validation.ok,
|
|
1929
2068
|
connections: connectionReady,
|
|
1930
|
-
|
|
1931
|
-
|
|
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
|
|
1932
2134
|
}
|
|
1933
2135
|
};
|
|
1934
2136
|
}
|
|
@@ -2161,7 +2363,7 @@ function applyTreeseedConfigValues({
|
|
|
2161
2363
|
sharedStorageMigrations
|
|
2162
2364
|
};
|
|
2163
2365
|
}
|
|
2164
|
-
function finalizeTreeseedConfig({
|
|
2366
|
+
async function finalizeTreeseedConfig({
|
|
2165
2367
|
tenantRoot,
|
|
2166
2368
|
scopes = [...TREESEED_ENVIRONMENT_SCOPES],
|
|
2167
2369
|
sync = "all",
|
|
@@ -2174,7 +2376,9 @@ function finalizeTreeseedConfig({
|
|
|
2174
2376
|
const summary = {
|
|
2175
2377
|
scopes,
|
|
2176
2378
|
synced: {},
|
|
2177
|
-
|
|
2379
|
+
reconciled: [],
|
|
2380
|
+
deployed: [],
|
|
2381
|
+
resourceInventoryByScope: {},
|
|
2178
2382
|
connectionChecks: [],
|
|
2179
2383
|
validationByScope: {},
|
|
2180
2384
|
readinessByScope: {}
|
|
@@ -2185,9 +2389,24 @@ function finalizeTreeseedConfig({
|
|
|
2185
2389
|
}
|
|
2186
2390
|
};
|
|
2187
2391
|
progress(`Validating configuration for ${scopes.join(", ")}...`);
|
|
2392
|
+
const scopeSeedValues = Object.fromEntries(
|
|
2393
|
+
scopes.map((scope) => [scope, collectTreeseedConfigSeedValues(tenantRoot, scope, env)])
|
|
2394
|
+
);
|
|
2188
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
|
+
});
|
|
2189
2405
|
const validation = validateTreeseedEnvironmentValues({
|
|
2190
|
-
values:
|
|
2406
|
+
values: {
|
|
2407
|
+
...suggestedValues,
|
|
2408
|
+
...seedValues
|
|
2409
|
+
},
|
|
2191
2410
|
scope,
|
|
2192
2411
|
purpose: "config",
|
|
2193
2412
|
deployConfig: registry.context.deployConfig,
|
|
@@ -2197,21 +2416,37 @@ function finalizeTreeseedConfig({
|
|
|
2197
2416
|
summary.validationByScope[scope] = validation;
|
|
2198
2417
|
if (checkConnections) {
|
|
2199
2418
|
progress(`Checking provider connectivity for ${scope}...`);
|
|
2200
|
-
summary.connectionChecks.push(checkTreeseedProviderConnections({ tenantRoot, scope, env }));
|
|
2419
|
+
summary.connectionChecks.push(await checkTreeseedProviderConnections({ tenantRoot, scope, env: seedValues }));
|
|
2201
2420
|
}
|
|
2202
2421
|
}
|
|
2203
2422
|
for (const scope of scopes) {
|
|
2204
|
-
|
|
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(
|
|
2205
2434
|
tenantRoot,
|
|
2206
2435
|
scope,
|
|
2207
2436
|
summary.validationByScope[scope],
|
|
2208
|
-
summary.connectionChecks.find((report) => report.scope === scope)?.checks ?? []
|
|
2437
|
+
summary.connectionChecks.find((report) => report.scope === scope)?.checks ?? [],
|
|
2438
|
+
scopeSeedValues[scope],
|
|
2439
|
+
{ includeReconcileStatus: !initializePersistent }
|
|
2209
2440
|
);
|
|
2210
2441
|
}
|
|
2211
2442
|
const invalidScopes = scopes.filter((scope) => summary.validationByScope[scope]?.ok !== true);
|
|
2212
2443
|
if (invalidScopes.length > 0) {
|
|
2213
2444
|
throw new Error(formatTreeseedConfigValidationFailure(summary.validationByScope, scopes));
|
|
2214
2445
|
}
|
|
2446
|
+
const failingConnectionReports = summary.connectionChecks.filter((report) => report.ok !== true);
|
|
2447
|
+
if (failingConnectionReports.length > 0) {
|
|
2448
|
+
throw new Error(formatTreeseedProviderConnectionFailures(failingConnectionReports));
|
|
2449
|
+
}
|
|
2215
2450
|
progress("Syncing managed service settings from treeseed.site.yaml...");
|
|
2216
2451
|
syncManagedServiceSettingsFromDeployConfig(tenantRoot);
|
|
2217
2452
|
if (initializePersistent) {
|
|
@@ -2219,34 +2454,110 @@ function finalizeTreeseedConfig({
|
|
|
2219
2454
|
if (scope === "local") {
|
|
2220
2455
|
continue;
|
|
2221
2456
|
}
|
|
2222
|
-
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}...`);
|
|
2223
2497
|
applyTreeseedEnvironmentToProcess({ tenantRoot, scope, override: true });
|
|
2224
|
-
|
|
2225
|
-
|
|
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,
|
|
2226
2502
|
scope,
|
|
2227
|
-
|
|
2228
|
-
target: initialized.summary.target
|
|
2503
|
+
skipProvision: true
|
|
2229
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
|
+
}
|
|
2230
2542
|
}
|
|
2231
2543
|
}
|
|
2232
2544
|
if (sync === "github" || sync === "all") {
|
|
2233
2545
|
progress(`Syncing GitHub environment for ${scopes.at(-1) ?? "prod"}...`);
|
|
2234
|
-
summary.synced.github = syncTreeseedGitHubEnvironment({ tenantRoot, scope: scopes.at(-1) ?? "prod" });
|
|
2235
|
-
}
|
|
2236
|
-
if (sync === "cloudflare" || sync === "all") {
|
|
2237
|
-
progress(`Syncing Cloudflare environment for ${scopes.at(-1) ?? "prod"}...`);
|
|
2238
|
-
summary.synced.cloudflare = syncTreeseedCloudflareEnvironment({ tenantRoot, scope: scopes.at(-1) ?? "prod" });
|
|
2239
|
-
}
|
|
2240
|
-
if (sync === "railway" || sync === "all") {
|
|
2241
|
-
progress(`Syncing Railway environment for ${scopes.at(-1) ?? "prod"}...`);
|
|
2242
|
-
summary.synced.railway = syncTreeseedRailwayEnvironment({ tenantRoot, scope: scopes.at(-1) ?? "prod" });
|
|
2546
|
+
summary.synced.github = await syncTreeseedGitHubEnvironment({ tenantRoot, scope: scopes.at(-1) ?? "prod" });
|
|
2243
2547
|
}
|
|
2244
2548
|
for (const scope of scopes) {
|
|
2245
|
-
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(
|
|
2246
2556
|
tenantRoot,
|
|
2247
2557
|
scope,
|
|
2248
2558
|
summary.validationByScope[scope],
|
|
2249
|
-
summary.connectionChecks.find((report) => report.scope === scope)?.checks ?? []
|
|
2559
|
+
summary.connectionChecks.find((report) => report.scope === scope)?.checks ?? [],
|
|
2560
|
+
env
|
|
2250
2561
|
);
|
|
2251
2562
|
}
|
|
2252
2563
|
return summary;
|
|
@@ -2304,6 +2615,8 @@ export {
|
|
|
2304
2615
|
getTreeseedRemoteAuthPaths,
|
|
2305
2616
|
initializeTreeseedPersistentEnvironment,
|
|
2306
2617
|
inspectTreeseedKeyAgentStatus,
|
|
2618
|
+
inspectTreeseedKeyAgentTransportDiagnostic,
|
|
2619
|
+
inspectTreeseedPassphraseEnvDiagnostic,
|
|
2307
2620
|
listDeprecatedTreeseedLocalEnvFiles,
|
|
2308
2621
|
listRelevantTreeseedConfigEntries,
|
|
2309
2622
|
loadTreeseedMachineConfig,
|