@treeseed/sdk 0.6.0 → 0.6.2
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 +297 -124
- package/dist/operations/services/github-api.d.ts +33 -0
- package/dist/operations/services/github-api.js +118 -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 +281 -9
- 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 +68 -96
- package/dist/platform/environment.js +51 -0
- package/dist/reconcile/bootstrap-systems.d.ts +32 -0
- package/dist/reconcile/bootstrap-systems.js +175 -0
- package/dist/reconcile/builtin-adapters.js +24 -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,60 @@ 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 progress = (message, stream = "stdout") => onProgress?.(message, stream);
|
|
1897
|
+
if (!dryRun) {
|
|
1898
|
+
progress(`[${scope}][github][environment] Ensuring GitHub environment ${environment} exists...`);
|
|
1899
|
+
await ensureGitHubActionsEnvironment(repository, environment, { client: githubClient });
|
|
1900
|
+
}
|
|
1901
|
+
progress(`[${scope}][github][sync] Loading existing GitHub secrets and variables...`);
|
|
1902
|
+
const [secretNames, variableNames] = dryRun ? [/* @__PURE__ */ new Set(), /* @__PURE__ */ new Set()] : await Promise.all([
|
|
1903
|
+
listGitHubEnvironmentSecretNames(repository, environment, { client: githubClient }),
|
|
1904
|
+
listGitHubEnvironmentVariableNames(repository, environment, { client: githubClient })
|
|
1905
|
+
]);
|
|
1875
1906
|
const synced = {
|
|
1876
1907
|
secrets: [],
|
|
1877
1908
|
variables: []
|
|
1878
1909
|
};
|
|
1910
|
+
const items = [];
|
|
1879
1911
|
for (const entry of relevant) {
|
|
1880
1912
|
const value = values[entry.id];
|
|
1881
1913
|
if (!value) {
|
|
1882
1914
|
continue;
|
|
1883
1915
|
}
|
|
1884
1916
|
if (entry.targets.includes("github-secret")) {
|
|
1885
|
-
|
|
1886
|
-
await upsertGitHubRepositorySecret(repository, entry.id, value, { client: githubClient });
|
|
1887
|
-
}
|
|
1888
|
-
synced.secrets.push({ name: entry.id, existed: secretNames.has(entry.id) });
|
|
1917
|
+
items.push({ kind: "secret", name: entry.id, value, existed: secretNames.has(entry.id) });
|
|
1889
1918
|
}
|
|
1890
1919
|
if (entry.targets.includes("github-variable")) {
|
|
1891
|
-
|
|
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
|
-
}
|
|
1902
|
-
synced.variables.push({ name: entry.id, existed: variableNames.has(entry.id) });
|
|
1920
|
+
items.push({ kind: "variable", name: entry.id, value, existed: variableNames.has(entry.id) });
|
|
1903
1921
|
}
|
|
1904
1922
|
}
|
|
1923
|
+
let completed = 0;
|
|
1924
|
+
const total = items.length;
|
|
1925
|
+
progress(`[${scope}][github][sync] Syncing GitHub environment ${environment}: 0/${total} items...`);
|
|
1926
|
+
const limit = execution === "sequential" ? 1 : concurrency;
|
|
1927
|
+
await runBounded(items, limit, async (item) => {
|
|
1928
|
+
if (!dryRun) {
|
|
1929
|
+
if (item.kind === "secret") {
|
|
1930
|
+
await upsertGitHubEnvironmentSecret(repository, environment, item.name, item.value, { client: githubClient });
|
|
1931
|
+
} else {
|
|
1932
|
+
await upsertGitHubEnvironmentVariable(repository, environment, item.name, item.value, { client: githubClient });
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
completed += 1;
|
|
1936
|
+
const action = item.existed ? "updated" : "created";
|
|
1937
|
+
progress(`[${scope}][github][${item.kind}] ${action} ${item.name} (${completed}/${total})`);
|
|
1938
|
+
if (item.kind === "secret") {
|
|
1939
|
+
synced.secrets.push({ name: item.name, existed: item.existed });
|
|
1940
|
+
} else {
|
|
1941
|
+
synced.variables.push({ name: item.name, existed: item.existed });
|
|
1942
|
+
}
|
|
1943
|
+
});
|
|
1944
|
+
progress(`[${scope}][github][sync] Complete: ${synced.secrets.length} secrets, ${synced.variables.length} variables, ${total} total.`);
|
|
1905
1945
|
return {
|
|
1906
1946
|
repository,
|
|
1907
1947
|
scope,
|
|
1948
|
+
environment,
|
|
1908
1949
|
...synced
|
|
1909
1950
|
};
|
|
1910
1951
|
}
|
|
@@ -1990,7 +2031,7 @@ async function initializeTreeseedPersistentEnvironment({ tenantRoot, scope = "pr
|
|
|
1990
2031
|
secrets: summary.results.filter((result) => result.unit.provider === "cloudflare").flatMap((result) => Object.keys(result.resourceLocators ?? {}))
|
|
1991
2032
|
};
|
|
1992
2033
|
}
|
|
1993
|
-
async function summarizePersistentReadiness(tenantRoot, scope, validation, connectionChecks, env = process.env, { includeReconcileStatus = true } = {}) {
|
|
2034
|
+
async function summarizePersistentReadiness(tenantRoot, scope, validation, connectionChecks, env = process.env, { includeReconcileStatus = true, systems } = {}) {
|
|
1994
2035
|
const validationProblems = [...validation.missing, ...validation.invalid];
|
|
1995
2036
|
const validationBlockers = validationProblems.map((problem) => problem.message);
|
|
1996
2037
|
const connectionReady = connectionChecks.every((check) => check.ready || check.skipped);
|
|
@@ -2051,7 +2092,8 @@ async function summarizePersistentReadiness(tenantRoot, scope, validation, conne
|
|
|
2051
2092
|
const reconcile = await collectTreeseedReconcileStatus({
|
|
2052
2093
|
tenantRoot,
|
|
2053
2094
|
target: createPersistentDeployTarget(scope),
|
|
2054
|
-
env
|
|
2095
|
+
env,
|
|
2096
|
+
systems
|
|
2055
2097
|
});
|
|
2056
2098
|
const provisioned = reconcile.ready;
|
|
2057
2099
|
const deployable = configured && provisioned && connectionReady;
|
|
@@ -2165,21 +2207,31 @@ function createConfigReadiness(values, validation) {
|
|
|
2165
2207
|
...(validation?.invalid ?? []).map((problem) => problem.id)
|
|
2166
2208
|
]);
|
|
2167
2209
|
const validConfigValue = (key) => hasConfigValue(values, key) && !invalidIds.has(key);
|
|
2168
|
-
const
|
|
2169
|
-
const railwayReady = validConfigValue("RAILWAY_API_TOKEN");
|
|
2170
|
-
const localDevelopmentIssues = [
|
|
2210
|
+
const configProblems = [
|
|
2171
2211
|
...validation?.missing ?? [],
|
|
2172
2212
|
...validation?.invalid ?? []
|
|
2213
|
+
];
|
|
2214
|
+
const providerIssues = (provider) => configProblems.filter((problem) => {
|
|
2215
|
+
if (provider === "github") {
|
|
2216
|
+
return problem.id === "GH_TOKEN" || problem.id === "GITHUB_TOKEN" || problem.entry.group === "github";
|
|
2217
|
+
}
|
|
2218
|
+
if (provider === "cloudflare") {
|
|
2219
|
+
return problem.id.startsWith("CLOUDFLARE_") || problem.id.includes("TURNSTILE") || problem.entry.group === "cloudflare";
|
|
2220
|
+
}
|
|
2221
|
+
return problem.id.startsWith("RAILWAY_") || problem.entry.group === "railway";
|
|
2222
|
+
});
|
|
2223
|
+
const localDevelopmentIssues = [
|
|
2224
|
+
...configProblems
|
|
2173
2225
|
].filter((problem) => problem.entry.group === "local-development");
|
|
2174
2226
|
return {
|
|
2175
2227
|
github: {
|
|
2176
2228
|
configured: validConfigValue("GH_TOKEN")
|
|
2177
2229
|
},
|
|
2178
2230
|
cloudflare: {
|
|
2179
|
-
configured:
|
|
2231
|
+
configured: providerIssues("cloudflare").length === 0
|
|
2180
2232
|
},
|
|
2181
2233
|
railway: {
|
|
2182
|
-
configured:
|
|
2234
|
+
configured: providerIssues("railway").length === 0
|
|
2183
2235
|
},
|
|
2184
2236
|
localDevelopment: {
|
|
2185
2237
|
configured: localDevelopmentIssues.length === 0
|
|
@@ -2363,6 +2415,37 @@ function applyTreeseedConfigValues({
|
|
|
2363
2415
|
sharedStorageMigrations
|
|
2364
2416
|
};
|
|
2365
2417
|
}
|
|
2418
|
+
function configProblemBootstrapSystems(problem) {
|
|
2419
|
+
switch (problem?.id) {
|
|
2420
|
+
case "GH_TOKEN":
|
|
2421
|
+
case "GITHUB_TOKEN":
|
|
2422
|
+
return ["github"];
|
|
2423
|
+
case "CLOUDFLARE_API_TOKEN":
|
|
2424
|
+
case "CLOUDFLARE_ACCOUNT_ID":
|
|
2425
|
+
case "CLOUDFLARE_ZONE_ID":
|
|
2426
|
+
return ["data", "web"];
|
|
2427
|
+
case "RAILWAY_API_TOKEN":
|
|
2428
|
+
case "TREESEED_RAILWAY_WORKSPACE":
|
|
2429
|
+
return ["api", "agents"];
|
|
2430
|
+
default:
|
|
2431
|
+
return null;
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
function filterValidationForBootstrapSystems(validation, runnableSystems) {
|
|
2435
|
+
const runnable = new Set(runnableSystems);
|
|
2436
|
+
const keepProblem = (problem) => {
|
|
2437
|
+
const systems = configProblemBootstrapSystems(problem);
|
|
2438
|
+
return !systems || systems.some((system) => runnable.has(system));
|
|
2439
|
+
};
|
|
2440
|
+
const missing = validation.missing.filter(keepProblem);
|
|
2441
|
+
const invalid = validation.invalid.filter(keepProblem);
|
|
2442
|
+
return {
|
|
2443
|
+
...validation,
|
|
2444
|
+
ok: missing.length === 0 && invalid.length === 0,
|
|
2445
|
+
missing,
|
|
2446
|
+
invalid
|
|
2447
|
+
};
|
|
2448
|
+
}
|
|
2366
2449
|
async function finalizeTreeseedConfig({
|
|
2367
2450
|
tenantRoot,
|
|
2368
2451
|
scopes = [...TREESEED_ENVIRONMENT_SCOPES],
|
|
@@ -2370,6 +2453,9 @@ async function finalizeTreeseedConfig({
|
|
|
2370
2453
|
env = process.env,
|
|
2371
2454
|
checkConnections = true,
|
|
2372
2455
|
initializePersistent = true,
|
|
2456
|
+
systems,
|
|
2457
|
+
skipUnavailable,
|
|
2458
|
+
bootstrapExecution = "parallel",
|
|
2373
2459
|
onProgress
|
|
2374
2460
|
}) {
|
|
2375
2461
|
const registry = collectTreeseedEnvironmentContext(tenantRoot);
|
|
@@ -2381,17 +2467,39 @@ async function finalizeTreeseedConfig({
|
|
|
2381
2467
|
resourceInventoryByScope: {},
|
|
2382
2468
|
connectionChecks: [],
|
|
2383
2469
|
validationByScope: {},
|
|
2384
|
-
|
|
2470
|
+
bootstrapSystemsByScope: {},
|
|
2471
|
+
githubRepository: null,
|
|
2472
|
+
readinessByScope: {},
|
|
2473
|
+
bootstrapExecution
|
|
2385
2474
|
};
|
|
2386
|
-
const progress = (message) => {
|
|
2475
|
+
const progress = (message, stream = "stdout") => {
|
|
2387
2476
|
if (typeof onProgress === "function") {
|
|
2388
|
-
onProgress(message);
|
|
2477
|
+
onProgress(message, stream);
|
|
2389
2478
|
}
|
|
2390
2479
|
};
|
|
2391
2480
|
progress(`Validating configuration for ${scopes.join(", ")}...`);
|
|
2392
2481
|
const scopeSeedValues = Object.fromEntries(
|
|
2393
2482
|
scopes.map((scope) => [scope, collectTreeseedConfigSeedValues(tenantRoot, scope, env)])
|
|
2394
2483
|
);
|
|
2484
|
+
for (const scope of scopes) {
|
|
2485
|
+
const selection = resolveTreeseedBootstrapSelection({
|
|
2486
|
+
deployConfig: registry.context.deployConfig,
|
|
2487
|
+
env: scopeSeedValues[scope],
|
|
2488
|
+
systems: scope === "local" ? ["github"] : systems,
|
|
2489
|
+
skipUnavailable: scope === "local" ? true : skipUnavailable
|
|
2490
|
+
});
|
|
2491
|
+
summary.bootstrapSystemsByScope[scope] = selection;
|
|
2492
|
+
const strictUnavailable = selection.unavailable.filter(
|
|
2493
|
+
(status) => !selection.skipped.some((skipped) => skipped.system === status.system && skipped.reason === status.reason)
|
|
2494
|
+
);
|
|
2495
|
+
if (initializePersistent && strictUnavailable.length > 0) {
|
|
2496
|
+
throw new Error(`Treeseed bootstrap cannot run the selected systems for ${scope}:
|
|
2497
|
+
- ${strictUnavailable.map((status) => `${status.system}: ${status.reason}`).join("\n- ")}`);
|
|
2498
|
+
}
|
|
2499
|
+
for (const skipped of selection.skipped) {
|
|
2500
|
+
progress(`[${scope}][${skipped.system}][skip] ${skipped.reason}`);
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2395
2503
|
for (const scope of scopes) {
|
|
2396
2504
|
const seedValues = scopeSeedValues[scope];
|
|
2397
2505
|
const suggestedValues = getTreeseedEnvironmentSuggestedValues({
|
|
@@ -2413,7 +2521,7 @@ async function finalizeTreeseedConfig({
|
|
|
2413
2521
|
tenantConfig: registry.context.tenantConfig,
|
|
2414
2522
|
plugins: registry.context.plugins
|
|
2415
2523
|
});
|
|
2416
|
-
summary.validationByScope[scope] = validation;
|
|
2524
|
+
summary.validationByScope[scope] = initializePersistent ? filterValidationForBootstrapSystems(validation, summary.bootstrapSystemsByScope[scope].runnable) : validation;
|
|
2417
2525
|
if (checkConnections) {
|
|
2418
2526
|
progress(`Checking provider connectivity for ${scope}...`);
|
|
2419
2527
|
summary.connectionChecks.push(await checkTreeseedProviderConnections({ tenantRoot, scope, env: seedValues }));
|
|
@@ -2436,7 +2544,10 @@ async function finalizeTreeseedConfig({
|
|
|
2436
2544
|
summary.validationByScope[scope],
|
|
2437
2545
|
summary.connectionChecks.find((report) => report.scope === scope)?.checks ?? [],
|
|
2438
2546
|
scopeSeedValues[scope],
|
|
2439
|
-
{
|
|
2547
|
+
{
|
|
2548
|
+
includeReconcileStatus: initializePersistent && summary.bootstrapSystemsByScope[scope].runnable.some((system) => system !== "github"),
|
|
2549
|
+
systems: summary.bootstrapSystemsByScope[scope].runnable.filter((system) => system !== "github")
|
|
2550
|
+
}
|
|
2440
2551
|
);
|
|
2441
2552
|
}
|
|
2442
2553
|
const invalidScopes = scopes.filter((scope) => summary.validationByScope[scope]?.ok !== true);
|
|
@@ -2449,39 +2560,66 @@ async function finalizeTreeseedConfig({
|
|
|
2449
2560
|
}
|
|
2450
2561
|
progress("Syncing managed service settings from treeseed.site.yaml...");
|
|
2451
2562
|
syncManagedServiceSettingsFromDeployConfig(tenantRoot);
|
|
2563
|
+
const githubSelected = scopes.some((scope) => scope !== "local" && summary.bootstrapSystemsByScope[scope].runnable.includes("github"));
|
|
2564
|
+
let githubRepository = maybeResolveGitHubRepositorySlug(tenantRoot);
|
|
2565
|
+
if (githubSelected && initializePersistent) {
|
|
2566
|
+
const localSeedValues = collectTreeseedConfigSeedValues(tenantRoot, "local", env);
|
|
2567
|
+
const localSuggestedValues = getTreeseedEnvironmentSuggestedValues({
|
|
2568
|
+
scope: "local",
|
|
2569
|
+
purpose: "config",
|
|
2570
|
+
deployConfig: registry.context.deployConfig,
|
|
2571
|
+
tenantConfig: registry.context.tenantConfig,
|
|
2572
|
+
plugins: registry.context.plugins,
|
|
2573
|
+
values: localSeedValues
|
|
2574
|
+
});
|
|
2575
|
+
const repositoryBootstrap = await ensureGitHubBootstrapRepository(tenantRoot, {
|
|
2576
|
+
values: {
|
|
2577
|
+
...localSuggestedValues,
|
|
2578
|
+
...localSeedValues
|
|
2579
|
+
},
|
|
2580
|
+
defaultName: registry.context.deployConfig.slug,
|
|
2581
|
+
onProgress: (line) => progress(line)
|
|
2582
|
+
});
|
|
2583
|
+
summary.githubRepository = repositoryBootstrap;
|
|
2584
|
+
githubRepository = repositoryBootstrap.repository;
|
|
2585
|
+
}
|
|
2452
2586
|
if (initializePersistent) {
|
|
2453
2587
|
for (const scope of scopes) {
|
|
2454
2588
|
if (scope === "local") {
|
|
2455
2589
|
continue;
|
|
2456
2590
|
}
|
|
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
|
-
|
|
2591
|
+
const selection = summary.bootstrapSystemsByScope[scope];
|
|
2592
|
+
const reconcileSystems = selection.runnable.filter((system) => system !== "github");
|
|
2593
|
+
if (reconcileSystems.length > 0) {
|
|
2594
|
+
progress(`[${scope}][bootstrap][plan] Deriving desired units for ${reconcileSystems.join(", ")}...`);
|
|
2595
|
+
const initialized = await reconcileTreeseedTarget({
|
|
2596
|
+
tenantRoot,
|
|
2597
|
+
target: createPersistentDeployTarget(scope),
|
|
2598
|
+
env: scopeSeedValues[scope],
|
|
2599
|
+
systems: reconcileSystems,
|
|
2600
|
+
write: (line) => progress(`[${scope}][reconcile] ${line}`)
|
|
2601
|
+
});
|
|
2602
|
+
summary.reconciled.push({
|
|
2603
|
+
scope,
|
|
2604
|
+
target: scope,
|
|
2605
|
+
units: initialized.units.length,
|
|
2606
|
+
actions: initialized.results.map((result) => ({
|
|
2607
|
+
unitId: result.unit.unitId,
|
|
2608
|
+
unitType: result.unit.unitType,
|
|
2609
|
+
provider: result.unit.provider,
|
|
2610
|
+
action: result.action,
|
|
2611
|
+
verified: result.verification?.verified === true,
|
|
2612
|
+
missing: result.verification?.missing ?? [],
|
|
2613
|
+
drifted: result.verification?.drifted ?? []
|
|
2614
|
+
}))
|
|
2615
|
+
});
|
|
2616
|
+
}
|
|
2617
|
+
if (scope === "staging" && selection.runnable.includes("github")) {
|
|
2618
|
+
progress(`[${scope}][github][branch] Ensuring ${STAGING_BRANCH} exists on origin from ${PRODUCTION_BRANCH}...`);
|
|
2619
|
+
if (!githubRepository) {
|
|
2482
2620
|
throw new Error("Unable to determine the GitHub repository from the origin remote for staging branch bootstrap.");
|
|
2483
2621
|
}
|
|
2484
|
-
const branchBootstrap = await ensureGitHubBranchFromBase(
|
|
2622
|
+
const branchBootstrap = await ensureGitHubBranchFromBase(githubRepository, STAGING_BRANCH, {
|
|
2485
2623
|
baseBranch: PRODUCTION_BRANCH,
|
|
2486
2624
|
client: createGitHubApiClient({
|
|
2487
2625
|
env: scopeSeedValues[scope]
|
|
@@ -2493,57 +2631,88 @@ async function finalizeTreeseedConfig({
|
|
|
2493
2631
|
result: {}
|
|
2494
2632
|
});
|
|
2495
2633
|
}
|
|
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({
|
|
2634
|
+
const deploySystems = selection.runnable.filter((system) => system === "data" || system === "web" || system === "api" || system === "agents");
|
|
2635
|
+
if (deploySystems.length > 0) {
|
|
2636
|
+
progress(`[${scope}][bootstrap][deploy] Deploying ${deploySystems.join(", ")}...`);
|
|
2637
|
+
applyTreeseedEnvironmentToProcess({ tenantRoot, scope, override: true });
|
|
2638
|
+
process.env.TREESEED_RAILWAY_WORKSPACE = process.env.TREESEED_RAILWAY_WORKSPACE || scopeSeedValues[scope].TREESEED_RAILWAY_WORKSPACE || resolveRailwayWorkspace(scopeSeedValues[scope]);
|
|
2639
|
+
const { deployProjectPlatform } = await import("./project-platform.js");
|
|
2640
|
+
const deployResult = await deployProjectPlatform({
|
|
2641
|
+
tenantRoot,
|
|
2510
2642
|
scope,
|
|
2511
|
-
|
|
2512
|
-
|
|
2643
|
+
skipProvision: true,
|
|
2644
|
+
bootstrapSystems: deploySystems,
|
|
2645
|
+
bootstrapExecution,
|
|
2646
|
+
env: scopeSeedValues[scope],
|
|
2647
|
+
write: (line, stream) => progress(line, stream)
|
|
2513
2648
|
});
|
|
2649
|
+
const deployEntry = summary.deployed.find((entry) => entry.scope === scope);
|
|
2650
|
+
if (deployEntry) {
|
|
2651
|
+
deployEntry.result = deployResult;
|
|
2652
|
+
} else {
|
|
2653
|
+
summary.deployed.push({
|
|
2654
|
+
scope,
|
|
2655
|
+
branchBootstrap: null,
|
|
2656
|
+
result: deployResult
|
|
2657
|
+
});
|
|
2658
|
+
}
|
|
2659
|
+
progress(`[${scope}][bootstrap][verify] Re-verifying after deployment...`);
|
|
2660
|
+
const finalized = await reconcileTreeseedTarget({
|
|
2661
|
+
tenantRoot,
|
|
2662
|
+
target: createPersistentDeployTarget(scope),
|
|
2663
|
+
env: scopeSeedValues[scope],
|
|
2664
|
+
systems: reconcileSystems,
|
|
2665
|
+
write: (line) => progress(`[${scope}][verify] ${line}`)
|
|
2666
|
+
});
|
|
2667
|
+
const index = summary.reconciled.findIndex((entry) => entry.scope === scope);
|
|
2668
|
+
const nextSummary = {
|
|
2669
|
+
scope,
|
|
2670
|
+
target: scope,
|
|
2671
|
+
units: finalized.units.length,
|
|
2672
|
+
actions: finalized.results.map((result) => ({
|
|
2673
|
+
unitId: result.unit.unitId,
|
|
2674
|
+
unitType: result.unit.unitType,
|
|
2675
|
+
provider: result.unit.provider,
|
|
2676
|
+
action: result.action,
|
|
2677
|
+
verified: result.verification?.verified === true,
|
|
2678
|
+
missing: result.verification?.missing ?? [],
|
|
2679
|
+
drifted: result.verification?.drifted ?? []
|
|
2680
|
+
}))
|
|
2681
|
+
};
|
|
2682
|
+
if (index >= 0) {
|
|
2683
|
+
summary.reconciled[index] = nextSummary;
|
|
2684
|
+
} else {
|
|
2685
|
+
summary.reconciled.push(nextSummary);
|
|
2686
|
+
}
|
|
2514
2687
|
}
|
|
2515
|
-
|
|
2516
|
-
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
if (sync === "github" || sync === "all") {
|
|
2691
|
+
const githubScopes = scopes.filter((scope) => scope !== "local" && summary.bootstrapSystemsByScope[scope].runnable.includes("github"));
|
|
2692
|
+
const syncScope = async (scope) => {
|
|
2693
|
+
progress(`[${scope}][github][sync] Syncing GitHub environment...`);
|
|
2694
|
+
return await syncTreeseedGitHubEnvironment({
|
|
2517
2695
|
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
2696
|
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);
|
|
2697
|
+
repository: githubRepository,
|
|
2698
|
+
execution: bootstrapExecution,
|
|
2699
|
+
onProgress: progress
|
|
2700
|
+
});
|
|
2701
|
+
};
|
|
2702
|
+
const githubResults = [];
|
|
2703
|
+
if (bootstrapExecution === "sequential") {
|
|
2704
|
+
for (const scope of githubScopes) {
|
|
2705
|
+
githubResults.push(await syncScope(scope));
|
|
2541
2706
|
}
|
|
2707
|
+
} else {
|
|
2708
|
+
githubResults.push(...await Promise.all(githubScopes.map((scope) => syncScope(scope))));
|
|
2542
2709
|
}
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2710
|
+
summary.synced.github = {
|
|
2711
|
+
scopes: githubResults,
|
|
2712
|
+
repository: githubResults[0]?.repository ?? githubRepository ?? maybeResolveGitHubRepositorySlug(tenantRoot),
|
|
2713
|
+
secrets: githubResults.flatMap((entry) => entry.secrets),
|
|
2714
|
+
variables: githubResults.flatMap((entry) => entry.variables)
|
|
2715
|
+
};
|
|
2547
2716
|
}
|
|
2548
2717
|
for (const scope of scopes) {
|
|
2549
2718
|
const reconciled = summary.reconciled.find((entry) => entry.scope === scope) ?? null;
|
|
@@ -2557,7 +2726,11 @@ async function finalizeTreeseedConfig({
|
|
|
2557
2726
|
scope,
|
|
2558
2727
|
summary.validationByScope[scope],
|
|
2559
2728
|
summary.connectionChecks.find((report) => report.scope === scope)?.checks ?? [],
|
|
2560
|
-
|
|
2729
|
+
scopeSeedValues[scope],
|
|
2730
|
+
{
|
|
2731
|
+
includeReconcileStatus: initializePersistent && summary.bootstrapSystemsByScope[scope].runnable.some((system) => system !== "github"),
|
|
2732
|
+
systems: summary.bootstrapSystemsByScope[scope].runnable.filter((system) => system !== "github")
|
|
2733
|
+
}
|
|
2561
2734
|
);
|
|
2562
2735
|
}
|
|
2563
2736
|
return summary;
|
|
@@ -63,18 +63,51 @@ 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 }?: {
|
|
70
|
+
client?: GitHubApiClient;
|
|
71
|
+
}): Promise<{
|
|
72
|
+
repository: string;
|
|
73
|
+
environment: string;
|
|
74
|
+
}>;
|
|
75
|
+
export declare function listGitHubEnvironmentSecretNames(repository: string | {
|
|
76
|
+
owner: string;
|
|
77
|
+
name: string;
|
|
78
|
+
}, environmentName: string, { client }?: {
|
|
79
|
+
client?: GitHubApiClient;
|
|
80
|
+
}): Promise<Set<string>>;
|
|
81
|
+
export declare function listGitHubEnvironmentVariableNames(repository: string | {
|
|
82
|
+
owner: string;
|
|
83
|
+
name: string;
|
|
84
|
+
}, environmentName: string, { client }?: {
|
|
85
|
+
client?: GitHubApiClient;
|
|
86
|
+
}): Promise<Set<string>>;
|
|
66
87
|
export declare function upsertGitHubRepositorySecret(repository: string | {
|
|
67
88
|
owner: string;
|
|
68
89
|
name: string;
|
|
69
90
|
}, name: string, value: string, { client }?: {
|
|
70
91
|
client?: GitHubApiClient;
|
|
71
92
|
}): Promise<void>;
|
|
93
|
+
export declare function upsertGitHubEnvironmentSecret(repository: string | {
|
|
94
|
+
owner: string;
|
|
95
|
+
name: string;
|
|
96
|
+
}, environmentName: string, name: string, value: string, { client }?: {
|
|
97
|
+
client?: GitHubApiClient;
|
|
98
|
+
}): Promise<void>;
|
|
72
99
|
export declare function upsertGitHubRepositoryVariable(repository: string | {
|
|
73
100
|
owner: string;
|
|
74
101
|
name: string;
|
|
75
102
|
}, name: string, value: string, { client }?: {
|
|
76
103
|
client?: GitHubApiClient;
|
|
77
104
|
}): Promise<void>;
|
|
105
|
+
export declare function upsertGitHubEnvironmentVariable(repository: string | {
|
|
106
|
+
owner: string;
|
|
107
|
+
name: string;
|
|
108
|
+
}, environmentName: string, name: string, value: string, { client }?: {
|
|
109
|
+
client?: GitHubApiClient;
|
|
110
|
+
}): Promise<void>;
|
|
78
111
|
export declare function upsertGitHubRepositoryVariableWithGhCli(repository: string | {
|
|
79
112
|
owner: string;
|
|
80
113
|
name: string;
|