@treeseed/sdk 0.6.1 → 0.6.3
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/operations/services/bootstrap-runner.d.ts +33 -0
- package/dist/operations/services/bootstrap-runner.js +136 -0
- package/dist/operations/services/config-runtime.d.ts +27 -8
- package/dist/operations/services/config-runtime.js +303 -127
- package/dist/operations/services/github-api.d.ts +34 -0
- package/dist/operations/services/github-api.js +187 -4
- package/dist/operations/services/github-automation.d.ts +30 -0
- package/dist/operations/services/github-automation.js +107 -1
- package/dist/operations/services/project-platform.d.ts +38 -2
- package/dist/operations/services/project-platform.js +319 -15
- package/dist/operations/services/railway-deploy.d.ts +6 -2
- package/dist/operations/services/railway-deploy.js +26 -18
- package/dist/operations/services/runtime-tools.d.ts +0 -2
- package/dist/operations/services/runtime-tools.js +0 -2
- package/dist/platform/env.yaml +71 -96
- package/dist/platform/environment.d.ts +4 -0
- package/dist/platform/environment.js +54 -0
- package/dist/reconcile/bootstrap-systems.d.ts +32 -0
- package/dist/reconcile/bootstrap-systems.js +175 -0
- package/dist/reconcile/builtin-adapters.js +1 -9
- package/dist/reconcile/desired-state.js +16 -14
- package/dist/reconcile/engine.d.ts +9 -4
- package/dist/reconcile/engine.js +57 -14
- package/dist/reconcile/index.d.ts +1 -0
- package/dist/reconcile/index.js +1 -0
- package/dist/scripts/config-treeseed.js +30 -0
- package/dist/scripts/package-tools.js +0 -2
- package/dist/scripts/tenant-deploy.js +16 -36
- package/dist/scripts/test-cloudflare-local.js +0 -2
- package/dist/workflow/operations.js +23 -4
- package/dist/workflow.d.ts +5 -0
- package/package.json +1 -1
- package/templates/github/deploy.workflow.yml +15 -15
|
@@ -6,6 +6,7 @@ import { spawn, spawnSync } from "node:child_process";
|
|
|
6
6
|
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
7
7
|
import {
|
|
8
8
|
getTreeseedEnvironmentSuggestedValues,
|
|
9
|
+
isTreeseedEnvironmentEntryRelevant,
|
|
9
10
|
isTreeseedEnvironmentEntryRequired,
|
|
10
11
|
resolveTreeseedEnvironmentRegistry,
|
|
11
12
|
TREESEED_ENVIRONMENT_SCOPES,
|
|
@@ -19,8 +20,15 @@ import {
|
|
|
19
20
|
loadDeployState,
|
|
20
21
|
syncCloudflareSecrets
|
|
21
22
|
} from "./deploy.js";
|
|
22
|
-
import {
|
|
23
|
-
|
|
23
|
+
import {
|
|
24
|
+
collectTreeseedReconcileStatus,
|
|
25
|
+
reconcileTreeseedTarget,
|
|
26
|
+
resolveTreeseedBootstrapSelection
|
|
27
|
+
} from "../../reconcile/index.js";
|
|
28
|
+
import {
|
|
29
|
+
ensureGitHubBootstrapRepository,
|
|
30
|
+
maybeResolveGitHubRepositorySlug
|
|
31
|
+
} from "./github-automation.js";
|
|
24
32
|
import {
|
|
25
33
|
buildRailwayCommandEnv
|
|
26
34
|
} from "./railway-deploy.js";
|
|
@@ -30,12 +38,12 @@ import {
|
|
|
30
38
|
} from "./railway-api.js";
|
|
31
39
|
import {
|
|
32
40
|
createGitHubApiClient,
|
|
41
|
+
ensureGitHubActionsEnvironment,
|
|
33
42
|
ensureGitHubBranchFromBase,
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
upsertGitHubRepositoryVariableWithGhCli
|
|
43
|
+
listGitHubEnvironmentSecretNames,
|
|
44
|
+
listGitHubEnvironmentVariableNames,
|
|
45
|
+
upsertGitHubEnvironmentSecret,
|
|
46
|
+
upsertGitHubEnvironmentVariable
|
|
39
47
|
} from "./github-api.js";
|
|
40
48
|
import { loadCliDeployConfig, packageScriptPath, resolveWranglerBin, withProcessCwd } from "./runtime-tools.js";
|
|
41
49
|
import { PRODUCTION_BRANCH, STAGING_BRANCH } from "./git-workflow.js";
|
|
@@ -53,9 +61,6 @@ import {
|
|
|
53
61
|
} from "./key-agent.js";
|
|
54
62
|
import { TREESEED_MACHINE_KEY_PASSPHRASE_ENV as TREESEED_MACHINE_KEY_PASSPHRASE_ENV2, TreeseedKeyAgentError as TreeseedKeyAgentError2 } from "./key-agent.js";
|
|
55
63
|
const MACHINE_CONFIG_RELATIVE_PATH = ".treeseed/config/machine.yaml";
|
|
56
|
-
const LEGACY_ENVIRONMENT_ALIASES = {
|
|
57
|
-
RAILWAY_API_KEY: "RAILWAY_API_TOKEN"
|
|
58
|
-
};
|
|
59
64
|
const MACHINE_KEY_HOME_RELATIVE_PATH = ".treeseed/config/machine.key";
|
|
60
65
|
const LEGACY_MACHINE_KEY_RELATIVE_PATH = ".treeseed/config/machine.key";
|
|
61
66
|
const REMOTE_AUTH_RELATIVE_PATH = ".treeseed/config/remote-auth.json";
|
|
@@ -72,8 +77,8 @@ const DEPRECATED_LOCAL_ENV_FILES = [".env.local", ".dev.vars"];
|
|
|
72
77
|
const warnedDeprecatedLocalEnvRoots = /* @__PURE__ */ new Set();
|
|
73
78
|
const inlineTreeseedSecretSessions = /* @__PURE__ */ new Map();
|
|
74
79
|
const railwayConnectionCheckCache = /* @__PURE__ */ new Map();
|
|
75
|
-
function filterEnvironmentValuesByRegistry(values, registry) {
|
|
76
|
-
const registeredKeys = new Set(registry.entries.map((entry) => entry.id));
|
|
80
|
+
function filterEnvironmentValuesByRegistry(values, registry, scope, purpose = "config") {
|
|
81
|
+
const registeredKeys = new Set(registry.entries.filter((entry) => isTreeseedEnvironmentEntryRelevant(entry, registry.context, scope, purpose)).map((entry) => entry.id));
|
|
77
82
|
return Object.fromEntries(
|
|
78
83
|
Object.entries(values).filter(([key]) => registeredKeys.has(key))
|
|
79
84
|
);
|
|
@@ -1399,21 +1404,15 @@ function collectTreeseedConfigSeedValues(tenantRoot, scope, env = process.env) {
|
|
|
1399
1404
|
throw error;
|
|
1400
1405
|
}
|
|
1401
1406
|
}
|
|
1402
|
-
const normalizedEnv = { ...env };
|
|
1403
|
-
for (const [legacyKey, canonicalKey] of Object.entries(LEGACY_ENVIRONMENT_ALIASES)) {
|
|
1404
|
-
if ((!normalizedEnv[canonicalKey] || String(normalizedEnv[canonicalKey]).length === 0) && normalizedEnv[legacyKey]) {
|
|
1405
|
-
normalizedEnv[canonicalKey] = normalizedEnv[legacyKey];
|
|
1406
|
-
}
|
|
1407
|
-
}
|
|
1408
1407
|
return filterEnvironmentValuesByRegistry({
|
|
1409
1408
|
...machineValues,
|
|
1410
|
-
...Object.fromEntries(Object.entries(
|
|
1411
|
-
}, registry);
|
|
1409
|
+
...Object.fromEntries(Object.entries(env).map(([key, value]) => [key, value ?? void 0]))
|
|
1410
|
+
}, registry, scope);
|
|
1412
1411
|
}
|
|
1413
1412
|
function collectTreeseedConfigSeedValueSources(tenantRoot, scope, env = process.env) {
|
|
1414
1413
|
warnDeprecatedTreeseedLocalEnvFiles(tenantRoot);
|
|
1415
1414
|
const registry = collectTreeseedEnvironmentContext(tenantRoot);
|
|
1416
|
-
const registeredKeys = new Set(registry.entries.map((entry) => entry.id));
|
|
1415
|
+
const registeredKeys = new Set(registry.entries.filter((entry) => isTreeseedEnvironmentEntryRelevant(entry, registry.context, scope, "config")).map((entry) => entry.id));
|
|
1417
1416
|
const values = {};
|
|
1418
1417
|
const sources = {};
|
|
1419
1418
|
const merge = (source, entries) => {
|
|
@@ -1856,8 +1855,31 @@ function formatTreeseedProviderConnectionFailures(reports) {
|
|
|
1856
1855
|
function writeProviderConnectionReport(write, report) {
|
|
1857
1856
|
write(formatTreeseedProviderConnectionReport(report));
|
|
1858
1857
|
}
|
|
1859
|
-
async function
|
|
1860
|
-
const
|
|
1858
|
+
async function runBounded(items, limit, worker) {
|
|
1859
|
+
const concurrency = Math.max(1, Math.min(limit, items.length || 1));
|
|
1860
|
+
let nextIndex = 0;
|
|
1861
|
+
const workers = Array.from({ length: concurrency }, async () => {
|
|
1862
|
+
for (; ; ) {
|
|
1863
|
+
const index = nextIndex;
|
|
1864
|
+
nextIndex += 1;
|
|
1865
|
+
if (index >= items.length) {
|
|
1866
|
+
return;
|
|
1867
|
+
}
|
|
1868
|
+
await worker(items[index], index);
|
|
1869
|
+
}
|
|
1870
|
+
});
|
|
1871
|
+
await Promise.all(workers);
|
|
1872
|
+
}
|
|
1873
|
+
async function syncTreeseedGitHubEnvironment({
|
|
1874
|
+
tenantRoot,
|
|
1875
|
+
scope = "prod",
|
|
1876
|
+
dryRun = false,
|
|
1877
|
+
repository: repositoryInput,
|
|
1878
|
+
execution = "parallel",
|
|
1879
|
+
concurrency = 4,
|
|
1880
|
+
onProgress
|
|
1881
|
+
}) {
|
|
1882
|
+
const repository = repositoryInput ?? maybeResolveGitHubRepositorySlug(tenantRoot);
|
|
1861
1883
|
if (!repository) {
|
|
1862
1884
|
throw new Error("Unable to determine the GitHub repository from the origin remote.");
|
|
1863
1885
|
}
|
|
@@ -1870,41 +1892,63 @@ async function syncTreeseedGitHubEnvironment({ tenantRoot, scope = "prod", dryRu
|
|
|
1870
1892
|
GITHUB_TOKEN: ghToken
|
|
1871
1893
|
} : {};
|
|
1872
1894
|
const githubClient = createGitHubApiClient({ env: ghEnv });
|
|
1873
|
-
const
|
|
1874
|
-
const
|
|
1895
|
+
const environment = scope === "prod" ? "production" : scope;
|
|
1896
|
+
const deploymentBranch = scope === "prod" ? PRODUCTION_BRANCH : scope === "staging" ? STAGING_BRANCH : null;
|
|
1897
|
+
const progress = (message, stream = "stdout") => onProgress?.(message, stream);
|
|
1898
|
+
if (!dryRun) {
|
|
1899
|
+
progress(`[${scope}][github][environment] Ensuring GitHub environment ${environment} exists...`);
|
|
1900
|
+
await ensureGitHubActionsEnvironment(repository, environment, {
|
|
1901
|
+
client: githubClient,
|
|
1902
|
+
branchName: deploymentBranch
|
|
1903
|
+
});
|
|
1904
|
+
}
|
|
1905
|
+
progress(`[${scope}][github][sync] Loading existing GitHub secrets and variables...`);
|
|
1906
|
+
const [secretNames, variableNames] = dryRun ? [/* @__PURE__ */ new Set(), /* @__PURE__ */ new Set()] : await Promise.all([
|
|
1907
|
+
listGitHubEnvironmentSecretNames(repository, environment, { client: githubClient }),
|
|
1908
|
+
listGitHubEnvironmentVariableNames(repository, environment, { client: githubClient })
|
|
1909
|
+
]);
|
|
1875
1910
|
const synced = {
|
|
1876
1911
|
secrets: [],
|
|
1877
1912
|
variables: []
|
|
1878
1913
|
};
|
|
1914
|
+
const items = [];
|
|
1879
1915
|
for (const entry of relevant) {
|
|
1880
1916
|
const value = values[entry.id];
|
|
1881
1917
|
if (!value) {
|
|
1882
1918
|
continue;
|
|
1883
1919
|
}
|
|
1884
|
-
if (entry.
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
}
|
|
1888
|
-
synced.secrets.push({ name: entry.id, existed: secretNames.has(entry.id) });
|
|
1920
|
+
if (entry.sensitivity === "secret") {
|
|
1921
|
+
items.push({ kind: "secret", name: entry.id, value, existed: secretNames.has(entry.id) });
|
|
1922
|
+
} else {
|
|
1923
|
+
items.push({ kind: "variable", name: entry.id, value, existed: variableNames.has(entry.id) });
|
|
1889
1924
|
}
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
}
|
|
1925
|
+
}
|
|
1926
|
+
let completed = 0;
|
|
1927
|
+
const total = items.length;
|
|
1928
|
+
progress(`[${scope}][github][sync] Syncing GitHub environment ${environment}: 0/${total} items...`);
|
|
1929
|
+
const limit = execution === "sequential" ? 1 : concurrency;
|
|
1930
|
+
await runBounded(items, limit, async (item) => {
|
|
1931
|
+
if (!dryRun) {
|
|
1932
|
+
if (item.kind === "secret") {
|
|
1933
|
+
await upsertGitHubEnvironmentSecret(repository, environment, item.name, item.value, { client: githubClient });
|
|
1934
|
+
} else {
|
|
1935
|
+
await upsertGitHubEnvironmentVariable(repository, environment, item.name, item.value, { client: githubClient });
|
|
1901
1936
|
}
|
|
1902
|
-
synced.variables.push({ name: entry.id, existed: variableNames.has(entry.id) });
|
|
1903
1937
|
}
|
|
1904
|
-
|
|
1938
|
+
completed += 1;
|
|
1939
|
+
const action = item.existed ? "updated" : "created";
|
|
1940
|
+
progress(`[${scope}][github][${item.kind}] ${action} ${item.name} (${completed}/${total})`);
|
|
1941
|
+
if (item.kind === "secret") {
|
|
1942
|
+
synced.secrets.push({ name: item.name, existed: item.existed });
|
|
1943
|
+
} else {
|
|
1944
|
+
synced.variables.push({ name: item.name, existed: item.existed });
|
|
1945
|
+
}
|
|
1946
|
+
});
|
|
1947
|
+
progress(`[${scope}][github][sync] Complete: ${synced.secrets.length} secrets, ${synced.variables.length} variables, ${total} total.`);
|
|
1905
1948
|
return {
|
|
1906
1949
|
repository,
|
|
1907
1950
|
scope,
|
|
1951
|
+
environment,
|
|
1908
1952
|
...synced
|
|
1909
1953
|
};
|
|
1910
1954
|
}
|
|
@@ -1990,7 +2034,7 @@ async function initializeTreeseedPersistentEnvironment({ tenantRoot, scope = "pr
|
|
|
1990
2034
|
secrets: summary.results.filter((result) => result.unit.provider === "cloudflare").flatMap((result) => Object.keys(result.resourceLocators ?? {}))
|
|
1991
2035
|
};
|
|
1992
2036
|
}
|
|
1993
|
-
async function summarizePersistentReadiness(tenantRoot, scope, validation, connectionChecks, env = process.env, { includeReconcileStatus = true } = {}) {
|
|
2037
|
+
async function summarizePersistentReadiness(tenantRoot, scope, validation, connectionChecks, env = process.env, { includeReconcileStatus = true, systems } = {}) {
|
|
1994
2038
|
const validationProblems = [...validation.missing, ...validation.invalid];
|
|
1995
2039
|
const validationBlockers = validationProblems.map((problem) => problem.message);
|
|
1996
2040
|
const connectionReady = connectionChecks.every((check) => check.ready || check.skipped);
|
|
@@ -2051,7 +2095,8 @@ async function summarizePersistentReadiness(tenantRoot, scope, validation, conne
|
|
|
2051
2095
|
const reconcile = await collectTreeseedReconcileStatus({
|
|
2052
2096
|
tenantRoot,
|
|
2053
2097
|
target: createPersistentDeployTarget(scope),
|
|
2054
|
-
env
|
|
2098
|
+
env,
|
|
2099
|
+
systems
|
|
2055
2100
|
});
|
|
2056
2101
|
const provisioned = reconcile.ready;
|
|
2057
2102
|
const deployable = configured && provisioned && connectionReady;
|
|
@@ -2165,21 +2210,31 @@ function createConfigReadiness(values, validation) {
|
|
|
2165
2210
|
...(validation?.invalid ?? []).map((problem) => problem.id)
|
|
2166
2211
|
]);
|
|
2167
2212
|
const validConfigValue = (key) => hasConfigValue(values, key) && !invalidIds.has(key);
|
|
2168
|
-
const
|
|
2169
|
-
const railwayReady = validConfigValue("RAILWAY_API_TOKEN");
|
|
2170
|
-
const localDevelopmentIssues = [
|
|
2213
|
+
const configProblems = [
|
|
2171
2214
|
...validation?.missing ?? [],
|
|
2172
2215
|
...validation?.invalid ?? []
|
|
2216
|
+
];
|
|
2217
|
+
const providerIssues = (provider) => configProblems.filter((problem) => {
|
|
2218
|
+
if (provider === "github") {
|
|
2219
|
+
return problem.id === "GH_TOKEN" || problem.id === "GITHUB_TOKEN" || problem.entry.group === "github";
|
|
2220
|
+
}
|
|
2221
|
+
if (provider === "cloudflare") {
|
|
2222
|
+
return problem.id.startsWith("CLOUDFLARE_") || problem.id.includes("TURNSTILE") || problem.entry.group === "cloudflare";
|
|
2223
|
+
}
|
|
2224
|
+
return problem.id.startsWith("RAILWAY_") || problem.entry.group === "railway";
|
|
2225
|
+
});
|
|
2226
|
+
const localDevelopmentIssues = [
|
|
2227
|
+
...configProblems
|
|
2173
2228
|
].filter((problem) => problem.entry.group === "local-development");
|
|
2174
2229
|
return {
|
|
2175
2230
|
github: {
|
|
2176
2231
|
configured: validConfigValue("GH_TOKEN")
|
|
2177
2232
|
},
|
|
2178
2233
|
cloudflare: {
|
|
2179
|
-
configured:
|
|
2234
|
+
configured: providerIssues("cloudflare").length === 0
|
|
2180
2235
|
},
|
|
2181
2236
|
railway: {
|
|
2182
|
-
configured:
|
|
2237
|
+
configured: providerIssues("railway").length === 0
|
|
2183
2238
|
},
|
|
2184
2239
|
localDevelopment: {
|
|
2185
2240
|
configured: localDevelopmentIssues.length === 0
|
|
@@ -2193,7 +2248,7 @@ function configGroupRank(group) {
|
|
|
2193
2248
|
}
|
|
2194
2249
|
function listRelevantTreeseedConfigEntries(registry, scope) {
|
|
2195
2250
|
return registry.entries.filter(
|
|
2196
|
-
(entry) => entry.scopes.includes(scope) && (!entry.isRelevant || entry.isRelevant(registry.context, scope, "config") || Boolean(entry.onboardingFeature))
|
|
2251
|
+
(entry) => entry.visibility !== "system" && entry.scopes.includes(scope) && (!entry.isRelevant || entry.isRelevant(registry.context, scope, "config") || Boolean(entry.onboardingFeature))
|
|
2197
2252
|
).sort((left, right) => {
|
|
2198
2253
|
const leftRequired = isTreeseedEnvironmentEntryRequired(left, registry.context, scope, "config");
|
|
2199
2254
|
const rightRequired = isTreeseedEnvironmentEntryRequired(right, registry.context, scope, "config");
|
|
@@ -2363,6 +2418,37 @@ function applyTreeseedConfigValues({
|
|
|
2363
2418
|
sharedStorageMigrations
|
|
2364
2419
|
};
|
|
2365
2420
|
}
|
|
2421
|
+
function configProblemBootstrapSystems(problem) {
|
|
2422
|
+
switch (problem?.id) {
|
|
2423
|
+
case "GH_TOKEN":
|
|
2424
|
+
case "GITHUB_TOKEN":
|
|
2425
|
+
return ["github"];
|
|
2426
|
+
case "CLOUDFLARE_API_TOKEN":
|
|
2427
|
+
case "CLOUDFLARE_ACCOUNT_ID":
|
|
2428
|
+
case "CLOUDFLARE_ZONE_ID":
|
|
2429
|
+
return ["data", "web"];
|
|
2430
|
+
case "RAILWAY_API_TOKEN":
|
|
2431
|
+
case "TREESEED_RAILWAY_WORKSPACE":
|
|
2432
|
+
return ["api", "agents"];
|
|
2433
|
+
default:
|
|
2434
|
+
return null;
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
function filterValidationForBootstrapSystems(validation, runnableSystems) {
|
|
2438
|
+
const runnable = new Set(runnableSystems);
|
|
2439
|
+
const keepProblem = (problem) => {
|
|
2440
|
+
const systems = configProblemBootstrapSystems(problem);
|
|
2441
|
+
return !systems || systems.some((system) => runnable.has(system));
|
|
2442
|
+
};
|
|
2443
|
+
const missing = validation.missing.filter(keepProblem);
|
|
2444
|
+
const invalid = validation.invalid.filter(keepProblem);
|
|
2445
|
+
return {
|
|
2446
|
+
...validation,
|
|
2447
|
+
ok: missing.length === 0 && invalid.length === 0,
|
|
2448
|
+
missing,
|
|
2449
|
+
invalid
|
|
2450
|
+
};
|
|
2451
|
+
}
|
|
2366
2452
|
async function finalizeTreeseedConfig({
|
|
2367
2453
|
tenantRoot,
|
|
2368
2454
|
scopes = [...TREESEED_ENVIRONMENT_SCOPES],
|
|
@@ -2370,6 +2456,9 @@ async function finalizeTreeseedConfig({
|
|
|
2370
2456
|
env = process.env,
|
|
2371
2457
|
checkConnections = true,
|
|
2372
2458
|
initializePersistent = true,
|
|
2459
|
+
systems,
|
|
2460
|
+
skipUnavailable,
|
|
2461
|
+
bootstrapExecution = "parallel",
|
|
2373
2462
|
onProgress
|
|
2374
2463
|
}) {
|
|
2375
2464
|
const registry = collectTreeseedEnvironmentContext(tenantRoot);
|
|
@@ -2381,17 +2470,39 @@ async function finalizeTreeseedConfig({
|
|
|
2381
2470
|
resourceInventoryByScope: {},
|
|
2382
2471
|
connectionChecks: [],
|
|
2383
2472
|
validationByScope: {},
|
|
2384
|
-
|
|
2473
|
+
bootstrapSystemsByScope: {},
|
|
2474
|
+
githubRepository: null,
|
|
2475
|
+
readinessByScope: {},
|
|
2476
|
+
bootstrapExecution
|
|
2385
2477
|
};
|
|
2386
|
-
const progress = (message) => {
|
|
2478
|
+
const progress = (message, stream = "stdout") => {
|
|
2387
2479
|
if (typeof onProgress === "function") {
|
|
2388
|
-
onProgress(message);
|
|
2480
|
+
onProgress(message, stream);
|
|
2389
2481
|
}
|
|
2390
2482
|
};
|
|
2391
2483
|
progress(`Validating configuration for ${scopes.join(", ")}...`);
|
|
2392
2484
|
const scopeSeedValues = Object.fromEntries(
|
|
2393
2485
|
scopes.map((scope) => [scope, collectTreeseedConfigSeedValues(tenantRoot, scope, env)])
|
|
2394
2486
|
);
|
|
2487
|
+
for (const scope of scopes) {
|
|
2488
|
+
const selection = resolveTreeseedBootstrapSelection({
|
|
2489
|
+
deployConfig: registry.context.deployConfig,
|
|
2490
|
+
env: scopeSeedValues[scope],
|
|
2491
|
+
systems: scope === "local" ? ["github"] : systems,
|
|
2492
|
+
skipUnavailable: scope === "local" ? true : skipUnavailable
|
|
2493
|
+
});
|
|
2494
|
+
summary.bootstrapSystemsByScope[scope] = selection;
|
|
2495
|
+
const strictUnavailable = selection.unavailable.filter(
|
|
2496
|
+
(status) => !selection.skipped.some((skipped) => skipped.system === status.system && skipped.reason === status.reason)
|
|
2497
|
+
);
|
|
2498
|
+
if (initializePersistent && strictUnavailable.length > 0) {
|
|
2499
|
+
throw new Error(`Treeseed bootstrap cannot run the selected systems for ${scope}:
|
|
2500
|
+
- ${strictUnavailable.map((status) => `${status.system}: ${status.reason}`).join("\n- ")}`);
|
|
2501
|
+
}
|
|
2502
|
+
for (const skipped of selection.skipped) {
|
|
2503
|
+
progress(`[${scope}][${skipped.system}][skip] ${skipped.reason}`);
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2395
2506
|
for (const scope of scopes) {
|
|
2396
2507
|
const seedValues = scopeSeedValues[scope];
|
|
2397
2508
|
const suggestedValues = getTreeseedEnvironmentSuggestedValues({
|
|
@@ -2413,7 +2524,7 @@ async function finalizeTreeseedConfig({
|
|
|
2413
2524
|
tenantConfig: registry.context.tenantConfig,
|
|
2414
2525
|
plugins: registry.context.plugins
|
|
2415
2526
|
});
|
|
2416
|
-
summary.validationByScope[scope] = validation;
|
|
2527
|
+
summary.validationByScope[scope] = initializePersistent ? filterValidationForBootstrapSystems(validation, summary.bootstrapSystemsByScope[scope].runnable) : validation;
|
|
2417
2528
|
if (checkConnections) {
|
|
2418
2529
|
progress(`Checking provider connectivity for ${scope}...`);
|
|
2419
2530
|
summary.connectionChecks.push(await checkTreeseedProviderConnections({ tenantRoot, scope, env: seedValues }));
|
|
@@ -2436,7 +2547,10 @@ async function finalizeTreeseedConfig({
|
|
|
2436
2547
|
summary.validationByScope[scope],
|
|
2437
2548
|
summary.connectionChecks.find((report) => report.scope === scope)?.checks ?? [],
|
|
2438
2549
|
scopeSeedValues[scope],
|
|
2439
|
-
{
|
|
2550
|
+
{
|
|
2551
|
+
includeReconcileStatus: initializePersistent && summary.bootstrapSystemsByScope[scope].runnable.some((system) => system !== "github"),
|
|
2552
|
+
systems: summary.bootstrapSystemsByScope[scope].runnable.filter((system) => system !== "github")
|
|
2553
|
+
}
|
|
2440
2554
|
);
|
|
2441
2555
|
}
|
|
2442
2556
|
const invalidScopes = scopes.filter((scope) => summary.validationByScope[scope]?.ok !== true);
|
|
@@ -2449,39 +2563,66 @@ async function finalizeTreeseedConfig({
|
|
|
2449
2563
|
}
|
|
2450
2564
|
progress("Syncing managed service settings from treeseed.site.yaml...");
|
|
2451
2565
|
syncManagedServiceSettingsFromDeployConfig(tenantRoot);
|
|
2566
|
+
const githubSelected = scopes.some((scope) => scope !== "local" && summary.bootstrapSystemsByScope[scope].runnable.includes("github"));
|
|
2567
|
+
let githubRepository = maybeResolveGitHubRepositorySlug(tenantRoot);
|
|
2568
|
+
if (githubSelected && initializePersistent) {
|
|
2569
|
+
const localSeedValues = collectTreeseedConfigSeedValues(tenantRoot, "local", env);
|
|
2570
|
+
const localSuggestedValues = getTreeseedEnvironmentSuggestedValues({
|
|
2571
|
+
scope: "local",
|
|
2572
|
+
purpose: "config",
|
|
2573
|
+
deployConfig: registry.context.deployConfig,
|
|
2574
|
+
tenantConfig: registry.context.tenantConfig,
|
|
2575
|
+
plugins: registry.context.plugins,
|
|
2576
|
+
values: localSeedValues
|
|
2577
|
+
});
|
|
2578
|
+
const repositoryBootstrap = await ensureGitHubBootstrapRepository(tenantRoot, {
|
|
2579
|
+
values: {
|
|
2580
|
+
...localSuggestedValues,
|
|
2581
|
+
...localSeedValues
|
|
2582
|
+
},
|
|
2583
|
+
defaultName: registry.context.deployConfig.slug,
|
|
2584
|
+
onProgress: (line) => progress(line)
|
|
2585
|
+
});
|
|
2586
|
+
summary.githubRepository = repositoryBootstrap;
|
|
2587
|
+
githubRepository = repositoryBootstrap.repository;
|
|
2588
|
+
}
|
|
2452
2589
|
if (initializePersistent) {
|
|
2453
2590
|
for (const scope of scopes) {
|
|
2454
2591
|
if (scope === "local") {
|
|
2455
2592
|
continue;
|
|
2456
2593
|
}
|
|
2457
|
-
|
|
2458
|
-
const
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2594
|
+
const selection = summary.bootstrapSystemsByScope[scope];
|
|
2595
|
+
const reconcileSystems = selection.runnable.filter((system) => system !== "github");
|
|
2596
|
+
if (reconcileSystems.length > 0) {
|
|
2597
|
+
progress(`[${scope}][bootstrap][plan] Deriving desired units for ${reconcileSystems.join(", ")}...`);
|
|
2598
|
+
const initialized = await reconcileTreeseedTarget({
|
|
2599
|
+
tenantRoot,
|
|
2600
|
+
target: createPersistentDeployTarget(scope),
|
|
2601
|
+
env: scopeSeedValues[scope],
|
|
2602
|
+
systems: reconcileSystems,
|
|
2603
|
+
write: (line) => progress(`[${scope}][reconcile] ${line}`)
|
|
2604
|
+
});
|
|
2605
|
+
summary.reconciled.push({
|
|
2606
|
+
scope,
|
|
2607
|
+
target: scope,
|
|
2608
|
+
units: initialized.units.length,
|
|
2609
|
+
actions: initialized.results.map((result) => ({
|
|
2610
|
+
unitId: result.unit.unitId,
|
|
2611
|
+
unitType: result.unit.unitType,
|
|
2612
|
+
provider: result.unit.provider,
|
|
2613
|
+
action: result.action,
|
|
2614
|
+
verified: result.verification?.verified === true,
|
|
2615
|
+
missing: result.verification?.missing ?? [],
|
|
2616
|
+
drifted: result.verification?.drifted ?? []
|
|
2617
|
+
}))
|
|
2618
|
+
});
|
|
2619
|
+
}
|
|
2620
|
+
if (scope === "staging" && selection.runnable.includes("github")) {
|
|
2621
|
+
progress(`[${scope}][github][branch] Ensuring ${STAGING_BRANCH} exists on origin from ${PRODUCTION_BRANCH}...`);
|
|
2622
|
+
if (!githubRepository) {
|
|
2482
2623
|
throw new Error("Unable to determine the GitHub repository from the origin remote for staging branch bootstrap.");
|
|
2483
2624
|
}
|
|
2484
|
-
const branchBootstrap = await ensureGitHubBranchFromBase(
|
|
2625
|
+
const branchBootstrap = await ensureGitHubBranchFromBase(githubRepository, STAGING_BRANCH, {
|
|
2485
2626
|
baseBranch: PRODUCTION_BRANCH,
|
|
2486
2627
|
client: createGitHubApiClient({
|
|
2487
2628
|
env: scopeSeedValues[scope]
|
|
@@ -2493,57 +2634,88 @@ async function finalizeTreeseedConfig({
|
|
|
2493
2634
|
result: {}
|
|
2494
2635
|
});
|
|
2495
2636
|
}
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
});
|
|
2505
|
-
const deployEntry = summary.deployed.find((entry) => entry.scope === scope);
|
|
2506
|
-
if (deployEntry) {
|
|
2507
|
-
deployEntry.result = deployResult;
|
|
2508
|
-
} else {
|
|
2509
|
-
summary.deployed.push({
|
|
2637
|
+
const deploySystems = selection.runnable.filter((system) => system === "data" || system === "web" || system === "api" || system === "agents");
|
|
2638
|
+
if (deploySystems.length > 0) {
|
|
2639
|
+
progress(`[${scope}][bootstrap][deploy] Deploying ${deploySystems.join(", ")}...`);
|
|
2640
|
+
applyTreeseedEnvironmentToProcess({ tenantRoot, scope, override: true });
|
|
2641
|
+
process.env.TREESEED_RAILWAY_WORKSPACE = process.env.TREESEED_RAILWAY_WORKSPACE || scopeSeedValues[scope].TREESEED_RAILWAY_WORKSPACE || resolveRailwayWorkspace(scopeSeedValues[scope]);
|
|
2642
|
+
const { deployProjectPlatform } = await import("./project-platform.js");
|
|
2643
|
+
const deployResult = await deployProjectPlatform({
|
|
2644
|
+
tenantRoot,
|
|
2510
2645
|
scope,
|
|
2511
|
-
|
|
2512
|
-
|
|
2646
|
+
skipProvision: true,
|
|
2647
|
+
bootstrapSystems: deploySystems,
|
|
2648
|
+
bootstrapExecution,
|
|
2649
|
+
env: scopeSeedValues[scope],
|
|
2650
|
+
write: (line, stream) => progress(line, stream)
|
|
2513
2651
|
});
|
|
2652
|
+
const deployEntry = summary.deployed.find((entry) => entry.scope === scope);
|
|
2653
|
+
if (deployEntry) {
|
|
2654
|
+
deployEntry.result = deployResult;
|
|
2655
|
+
} else {
|
|
2656
|
+
summary.deployed.push({
|
|
2657
|
+
scope,
|
|
2658
|
+
branchBootstrap: null,
|
|
2659
|
+
result: deployResult
|
|
2660
|
+
});
|
|
2661
|
+
}
|
|
2662
|
+
progress(`[${scope}][bootstrap][verify] Re-verifying after deployment...`);
|
|
2663
|
+
const finalized = await reconcileTreeseedTarget({
|
|
2664
|
+
tenantRoot,
|
|
2665
|
+
target: createPersistentDeployTarget(scope),
|
|
2666
|
+
env: scopeSeedValues[scope],
|
|
2667
|
+
systems: reconcileSystems,
|
|
2668
|
+
write: (line) => progress(`[${scope}][verify] ${line}`)
|
|
2669
|
+
});
|
|
2670
|
+
const index = summary.reconciled.findIndex((entry) => entry.scope === scope);
|
|
2671
|
+
const nextSummary = {
|
|
2672
|
+
scope,
|
|
2673
|
+
target: scope,
|
|
2674
|
+
units: finalized.units.length,
|
|
2675
|
+
actions: finalized.results.map((result) => ({
|
|
2676
|
+
unitId: result.unit.unitId,
|
|
2677
|
+
unitType: result.unit.unitType,
|
|
2678
|
+
provider: result.unit.provider,
|
|
2679
|
+
action: result.action,
|
|
2680
|
+
verified: result.verification?.verified === true,
|
|
2681
|
+
missing: result.verification?.missing ?? [],
|
|
2682
|
+
drifted: result.verification?.drifted ?? []
|
|
2683
|
+
}))
|
|
2684
|
+
};
|
|
2685
|
+
if (index >= 0) {
|
|
2686
|
+
summary.reconciled[index] = nextSummary;
|
|
2687
|
+
} else {
|
|
2688
|
+
summary.reconciled.push(nextSummary);
|
|
2689
|
+
}
|
|
2514
2690
|
}
|
|
2515
|
-
|
|
2516
|
-
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
if (sync === "github" || sync === "all") {
|
|
2694
|
+
const githubScopes = scopes.filter((scope) => scope !== "local" && summary.bootstrapSystemsByScope[scope].runnable.includes("github"));
|
|
2695
|
+
const syncScope = async (scope) => {
|
|
2696
|
+
progress(`[${scope}][github][sync] Syncing GitHub environment...`);
|
|
2697
|
+
return await syncTreeseedGitHubEnvironment({
|
|
2517
2698
|
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
2699
|
scope,
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
drifted: result.verification?.drifted ?? []
|
|
2535
|
-
}))
|
|
2536
|
-
};
|
|
2537
|
-
if (index >= 0) {
|
|
2538
|
-
summary.reconciled[index] = nextSummary;
|
|
2539
|
-
} else {
|
|
2540
|
-
summary.reconciled.push(nextSummary);
|
|
2700
|
+
repository: githubRepository,
|
|
2701
|
+
execution: bootstrapExecution,
|
|
2702
|
+
onProgress: progress
|
|
2703
|
+
});
|
|
2704
|
+
};
|
|
2705
|
+
const githubResults = [];
|
|
2706
|
+
if (bootstrapExecution === "sequential") {
|
|
2707
|
+
for (const scope of githubScopes) {
|
|
2708
|
+
githubResults.push(await syncScope(scope));
|
|
2541
2709
|
}
|
|
2710
|
+
} else {
|
|
2711
|
+
githubResults.push(...await Promise.all(githubScopes.map((scope) => syncScope(scope))));
|
|
2542
2712
|
}
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2713
|
+
summary.synced.github = {
|
|
2714
|
+
scopes: githubResults,
|
|
2715
|
+
repository: githubResults[0]?.repository ?? githubRepository ?? maybeResolveGitHubRepositorySlug(tenantRoot),
|
|
2716
|
+
secrets: githubResults.flatMap((entry) => entry.secrets),
|
|
2717
|
+
variables: githubResults.flatMap((entry) => entry.variables)
|
|
2718
|
+
};
|
|
2547
2719
|
}
|
|
2548
2720
|
for (const scope of scopes) {
|
|
2549
2721
|
const reconciled = summary.reconciled.find((entry) => entry.scope === scope) ?? null;
|
|
@@ -2557,7 +2729,11 @@ async function finalizeTreeseedConfig({
|
|
|
2557
2729
|
scope,
|
|
2558
2730
|
summary.validationByScope[scope],
|
|
2559
2731
|
summary.connectionChecks.find((report) => report.scope === scope)?.checks ?? [],
|
|
2560
|
-
|
|
2732
|
+
scopeSeedValues[scope],
|
|
2733
|
+
{
|
|
2734
|
+
includeReconcileStatus: initializePersistent && summary.bootstrapSystemsByScope[scope].runnable.some((system) => system !== "github"),
|
|
2735
|
+
systems: summary.bootstrapSystemsByScope[scope].runnable.filter((system) => system !== "github")
|
|
2736
|
+
}
|
|
2561
2737
|
);
|
|
2562
2738
|
}
|
|
2563
2739
|
return summary;
|
|
@@ -63,18 +63,52 @@ export declare function listGitHubRepositoryVariableNames(repository: string | {
|
|
|
63
63
|
}, { client }?: {
|
|
64
64
|
client?: GitHubApiClient;
|
|
65
65
|
}): Promise<Set<string>>;
|
|
66
|
+
export declare function ensureGitHubActionsEnvironment(repository: string | {
|
|
67
|
+
owner: string;
|
|
68
|
+
name: string;
|
|
69
|
+
}, environmentName: string, { client, branchName, }?: {
|
|
70
|
+
client?: GitHubApiClient;
|
|
71
|
+
branchName?: string | null;
|
|
72
|
+
}): Promise<{
|
|
73
|
+
repository: string;
|
|
74
|
+
environment: string;
|
|
75
|
+
}>;
|
|
76
|
+
export declare function listGitHubEnvironmentSecretNames(repository: string | {
|
|
77
|
+
owner: string;
|
|
78
|
+
name: string;
|
|
79
|
+
}, environmentName: string, { client }?: {
|
|
80
|
+
client?: GitHubApiClient;
|
|
81
|
+
}): Promise<Set<string>>;
|
|
82
|
+
export declare function listGitHubEnvironmentVariableNames(repository: string | {
|
|
83
|
+
owner: string;
|
|
84
|
+
name: string;
|
|
85
|
+
}, environmentName: string, { client }?: {
|
|
86
|
+
client?: GitHubApiClient;
|
|
87
|
+
}): Promise<Set<string>>;
|
|
66
88
|
export declare function upsertGitHubRepositorySecret(repository: string | {
|
|
67
89
|
owner: string;
|
|
68
90
|
name: string;
|
|
69
91
|
}, name: string, value: string, { client }?: {
|
|
70
92
|
client?: GitHubApiClient;
|
|
71
93
|
}): Promise<void>;
|
|
94
|
+
export declare function upsertGitHubEnvironmentSecret(repository: string | {
|
|
95
|
+
owner: string;
|
|
96
|
+
name: string;
|
|
97
|
+
}, environmentName: string, name: string, value: string, { client }?: {
|
|
98
|
+
client?: GitHubApiClient;
|
|
99
|
+
}): Promise<void>;
|
|
72
100
|
export declare function upsertGitHubRepositoryVariable(repository: string | {
|
|
73
101
|
owner: string;
|
|
74
102
|
name: string;
|
|
75
103
|
}, name: string, value: string, { client }?: {
|
|
76
104
|
client?: GitHubApiClient;
|
|
77
105
|
}): Promise<void>;
|
|
106
|
+
export declare function upsertGitHubEnvironmentVariable(repository: string | {
|
|
107
|
+
owner: string;
|
|
108
|
+
name: string;
|
|
109
|
+
}, environmentName: string, name: string, value: string, { client }?: {
|
|
110
|
+
client?: GitHubApiClient;
|
|
111
|
+
}): Promise<void>;
|
|
78
112
|
export declare function upsertGitHubRepositoryVariableWithGhCli(repository: string | {
|
|
79
113
|
owner: string;
|
|
80
114
|
name: string;
|