@treeseed/sdk 0.6.1 → 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.
Files changed (32) hide show
  1. package/dist/operations/services/bootstrap-runner.d.ts +33 -0
  2. package/dist/operations/services/bootstrap-runner.js +136 -0
  3. package/dist/operations/services/config-runtime.d.ts +27 -8
  4. package/dist/operations/services/config-runtime.js +297 -124
  5. package/dist/operations/services/github-api.d.ts +33 -0
  6. package/dist/operations/services/github-api.js +118 -4
  7. package/dist/operations/services/github-automation.d.ts +30 -0
  8. package/dist/operations/services/github-automation.js +107 -1
  9. package/dist/operations/services/project-platform.d.ts +38 -2
  10. package/dist/operations/services/project-platform.js +281 -9
  11. package/dist/operations/services/railway-deploy.d.ts +6 -2
  12. package/dist/operations/services/railway-deploy.js +26 -18
  13. package/dist/operations/services/runtime-tools.d.ts +0 -2
  14. package/dist/operations/services/runtime-tools.js +0 -2
  15. package/dist/platform/env.yaml +68 -96
  16. package/dist/platform/environment.js +51 -0
  17. package/dist/reconcile/bootstrap-systems.d.ts +32 -0
  18. package/dist/reconcile/bootstrap-systems.js +175 -0
  19. package/dist/reconcile/builtin-adapters.js +1 -9
  20. package/dist/reconcile/desired-state.js +16 -14
  21. package/dist/reconcile/engine.d.ts +9 -4
  22. package/dist/reconcile/engine.js +57 -14
  23. package/dist/reconcile/index.d.ts +1 -0
  24. package/dist/reconcile/index.js +1 -0
  25. package/dist/scripts/config-treeseed.js +30 -0
  26. package/dist/scripts/package-tools.js +0 -2
  27. package/dist/scripts/tenant-deploy.js +16 -36
  28. package/dist/scripts/test-cloudflare-local.js +0 -2
  29. package/dist/workflow/operations.js +23 -4
  30. package/dist/workflow.d.ts +5 -0
  31. package/package.json +1 -1
  32. 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 { collectTreeseedReconcileStatus, reconcileTreeseedTarget } from "../../reconcile/index.js";
23
- import { maybeResolveGitHubRepositorySlug } from "./github-automation.js";
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
- listGitHubRepositorySecretNames,
35
- listGitHubRepositoryVariableNames,
36
- upsertGitHubRepositorySecret,
37
- upsertGitHubRepositoryVariable,
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(normalizedEnv).map(([key, value]) => [key, value ?? void 0]))
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 syncTreeseedGitHubEnvironment({ tenantRoot, scope = "prod", dryRun = false } = {}) {
1860
- const repository = maybeResolveGitHubRepositorySlug(tenantRoot);
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 secretNames = await listGitHubRepositorySecretNames(repository, { client: githubClient });
1874
- const variableNames = await listGitHubRepositoryVariableNames(repository, { client: githubClient });
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
- if (!dryRun) {
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
- 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
- }
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 cloudflareReady = validConfigValue("CLOUDFLARE_API_TOKEN");
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: cloudflareReady
2231
+ configured: providerIssues("cloudflare").length === 0
2180
2232
  },
2181
2233
  railway: {
2182
- configured: railwayReady
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
- readinessByScope: {}
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
- { includeReconcileStatus: !initializePersistent }
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
- 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) {
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(repository, STAGING_BRANCH, {
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
- progress(`Deploying ${scope}...`);
2497
- applyTreeseedEnvironmentToProcess({ tenantRoot, scope, override: true });
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,
2502
- scope,
2503
- skipProvision: true
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
- branchBootstrap: null,
2512
- result: deployResult
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
- progress(`Re-verifying ${scope} after deployment...`);
2516
- const finalized = await reconcileTreeseedTarget({
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
- 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);
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
- if (sync === "github" || sync === "all") {
2545
- progress(`Syncing GitHub environment for ${scopes.at(-1) ?? "prod"}...`);
2546
- summary.synced.github = await syncTreeseedGitHubEnvironment({ tenantRoot, scope: scopes.at(-1) ?? "prod" });
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
- env
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;