@treeseed/sdk 0.6.2 → 0.6.4

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.
@@ -1444,9 +1444,17 @@ function resolveTreeseedLaunchEnvironment({
1444
1444
  overrides = {}
1445
1445
  }) {
1446
1446
  warnDeprecatedTreeseedLocalEnvFiles(tenantRoot);
1447
+ let machineValues = {};
1448
+ try {
1449
+ machineValues = resolveTreeseedMachineEnvironmentValues(tenantRoot, scope);
1450
+ } catch (error) {
1451
+ if (!(error instanceof TreeseedKeyAgentError)) {
1452
+ throw error;
1453
+ }
1454
+ }
1455
+ const scopedValues = scope === "local" ? { ...baseEnv, ...machineValues } : { ...machineValues, ...baseEnv };
1447
1456
  return {
1448
- ...baseEnv,
1449
- ...resolveTreeseedMachineEnvironmentValues(tenantRoot, scope),
1457
+ ...scopedValues,
1450
1458
  ...overrides
1451
1459
  };
1452
1460
  }
@@ -1467,7 +1475,7 @@ function formatTreeseedConfigEnvironmentReport({ tenantRoot, scope, env = proces
1467
1475
  function applyTreeseedEnvironmentToProcess({ tenantRoot, scope, override = false }) {
1468
1476
  let resolvedValues = {};
1469
1477
  try {
1470
- resolvedValues = resolveTreeseedLaunchEnvironment({ tenantRoot, scope, baseEnv: {} });
1478
+ resolvedValues = resolveTreeseedLaunchEnvironment({ tenantRoot, scope });
1471
1479
  } catch (error) {
1472
1480
  if (!(error instanceof TreeseedKeyAgentError)) {
1473
1481
  throw error;
@@ -1893,10 +1901,14 @@ async function syncTreeseedGitHubEnvironment({
1893
1901
  } : {};
1894
1902
  const githubClient = createGitHubApiClient({ env: ghEnv });
1895
1903
  const environment = scope === "prod" ? "production" : scope;
1904
+ const deploymentBranch = scope === "prod" ? PRODUCTION_BRANCH : scope === "staging" ? STAGING_BRANCH : null;
1896
1905
  const progress = (message, stream = "stdout") => onProgress?.(message, stream);
1897
1906
  if (!dryRun) {
1898
1907
  progress(`[${scope}][github][environment] Ensuring GitHub environment ${environment} exists...`);
1899
- await ensureGitHubActionsEnvironment(repository, environment, { client: githubClient });
1908
+ await ensureGitHubActionsEnvironment(repository, environment, {
1909
+ client: githubClient,
1910
+ branchName: deploymentBranch
1911
+ });
1900
1912
  }
1901
1913
  progress(`[${scope}][github][sync] Loading existing GitHub secrets and variables...`);
1902
1914
  const [secretNames, variableNames] = dryRun ? [/* @__PURE__ */ new Set(), /* @__PURE__ */ new Set()] : await Promise.all([
@@ -1913,10 +1925,9 @@ async function syncTreeseedGitHubEnvironment({
1913
1925
  if (!value) {
1914
1926
  continue;
1915
1927
  }
1916
- if (entry.targets.includes("github-secret")) {
1928
+ if (entry.sensitivity === "secret") {
1917
1929
  items.push({ kind: "secret", name: entry.id, value, existed: secretNames.has(entry.id) });
1918
- }
1919
- if (entry.targets.includes("github-variable")) {
1930
+ } else {
1920
1931
  items.push({ kind: "variable", name: entry.id, value, existed: variableNames.has(entry.id) });
1921
1932
  }
1922
1933
  }
@@ -2245,7 +2256,7 @@ function configGroupRank(group) {
2245
2256
  }
2246
2257
  function listRelevantTreeseedConfigEntries(registry, scope) {
2247
2258
  return registry.entries.filter(
2248
- (entry) => entry.scopes.includes(scope) && (!entry.isRelevant || entry.isRelevant(registry.context, scope, "config") || Boolean(entry.onboardingFeature))
2259
+ (entry) => entry.visibility !== "system" && entry.scopes.includes(scope) && (!entry.isRelevant || entry.isRelevant(registry.context, scope, "config") || Boolean(entry.onboardingFeature))
2249
2260
  ).sort((left, right) => {
2250
2261
  const leftRequired = isTreeseedEnvironmentEntryRequired(left, registry.context, scope, "config");
2251
2262
  const rightRequired = isTreeseedEnvironmentEntryRequired(right, registry.context, scope, "config");
@@ -66,8 +66,9 @@ export declare function listGitHubRepositoryVariableNames(repository: string | {
66
66
  export declare function ensureGitHubActionsEnvironment(repository: string | {
67
67
  owner: string;
68
68
  name: string;
69
- }, environmentName: string, { client }?: {
69
+ }, environmentName: string, { client, branchName, }?: {
70
70
  client?: GitHubApiClient;
71
+ branchName?: string | null;
71
72
  }): Promise<{
72
73
  repository: string;
73
74
  environment: string;
@@ -220,19 +220,88 @@ async function listGitHubRepositoryVariableNames(repository, { client = createGi
220
220
  throw normalizeGitHubApiError(error, `Unable to list GitHub variables for ${owner}/${name}`);
221
221
  }
222
222
  }
223
- async function ensureGitHubActionsEnvironment(repository, environmentName, { client = createGitHubApiClient() } = {}) {
223
+ async function ensureGitHubActionsEnvironment(repository, environmentName, {
224
+ client = createGitHubApiClient(),
225
+ branchName
226
+ } = {}) {
224
227
  const { owner, name: repo } = typeof repository === "string" ? parseGitHubRepositorySlug(repository) : repository;
225
228
  try {
226
229
  await withGitHubApiRetries(() => client.request("PUT /repos/{owner}/{repo}/environments/{environment_name}", {
227
230
  owner,
228
231
  repo,
229
- environment_name: environmentName
232
+ environment_name: environmentName,
233
+ ...branchName ? {
234
+ deployment_branch_policy: {
235
+ protected_branches: false,
236
+ custom_branch_policies: true
237
+ }
238
+ } : {}
230
239
  }));
240
+ if (branchName) {
241
+ await ensureGitHubEnvironmentBranchPolicy(client, {
242
+ owner,
243
+ repo,
244
+ environmentName,
245
+ branchName
246
+ });
247
+ }
231
248
  return { repository: `${owner}/${repo}`, environment: environmentName };
232
249
  } catch (error) {
233
250
  throw normalizeGitHubApiError(error, `Unable to ensure GitHub environment ${environmentName} for ${owner}/${repo}`);
234
251
  }
235
252
  }
253
+ async function ensureGitHubEnvironmentBranchPolicy(client, {
254
+ owner,
255
+ repo,
256
+ environmentName,
257
+ branchName
258
+ }) {
259
+ const response = await withGitHubApiRetries(() => client.request(
260
+ "GET /repos/{owner}/{repo}/environments/{environment_name}/deployment-branch-policies",
261
+ {
262
+ owner,
263
+ repo,
264
+ environment_name: environmentName,
265
+ per_page: 100
266
+ }
267
+ ));
268
+ const policies = Array.isArray(response.data?.branch_policies) ? response.data.branch_policies : [];
269
+ const desired = policies.find((policy) => policy.name === branchName && (policy.type ?? "branch") === "branch");
270
+ for (const policy of policies) {
271
+ if (!policy.id || policy.name === branchName && (policy.type ?? "branch") === "branch") {
272
+ continue;
273
+ }
274
+ await withGitHubApiRetries(() => client.request(
275
+ "DELETE /repos/{owner}/{repo}/environments/{environment_name}/deployment-branch-policies/{branch_policy_id}",
276
+ {
277
+ owner,
278
+ repo,
279
+ environment_name: environmentName,
280
+ branch_policy_id: policy.id
281
+ }
282
+ ));
283
+ }
284
+ if (desired) {
285
+ return;
286
+ }
287
+ try {
288
+ await withGitHubApiRetries(() => client.request(
289
+ "POST /repos/{owner}/{repo}/environments/{environment_name}/deployment-branch-policies",
290
+ {
291
+ owner,
292
+ repo,
293
+ environment_name: environmentName,
294
+ name: branchName,
295
+ type: "branch"
296
+ }
297
+ ));
298
+ } catch (error) {
299
+ if (error && typeof error === "object" && error.status === 303) {
300
+ return;
301
+ }
302
+ throw error;
303
+ }
304
+ }
236
305
  async function paginateGitHubEnvironmentNames(client, route, params) {
237
306
  const paginate = client.paginate;
238
307
  return await paginateNames(() => paginate(route, {
@@ -12,7 +12,7 @@ import {
12
12
  signEditorialPreviewToken
13
13
  } from "../../platform/published-content.js";
14
14
  import { createPublishedContentPipeline } from "../../platform/published-content-pipeline.js";
15
- import { collectTreeseedReconcileStatus, reconcileTreeseedTarget } from "../../reconcile/index.js";
15
+ import { collectTreeseedReconcileStatus, reconcileTreeseedTarget, resolveTreeseedBootstrapSelection } from "../../reconcile/index.js";
16
16
  import { loadTreeseedManifest } from "../../platform/tenant-config.js";
17
17
  import { applyTreeseedEnvironmentToProcess } from "./config-runtime.js";
18
18
  import {
@@ -40,6 +40,7 @@ import { loadCliDeployConfig, packageScriptPath, resolveWranglerBin } from "./ru
40
40
  import { CloudflareQueuePullClient, CloudflareQueuePushClient } from "../../remote.js";
41
41
  import { runPrefixedCommand, runTreeseedBootstrapDag, sleep, writeTreeseedBootstrapLine } from "./bootstrap-runner.js";
42
42
  import { runTenantDeployPreflight } from "./save-deploy-preflight.js";
43
+ const PROJECT_PLATFORM_BOOTSTRAP_SYSTEMS = ["data", "web", "api", "agents"];
43
44
  function stableHash(value) {
44
45
  return createHash("sha256").update(value).digest("hex");
45
46
  }
@@ -90,6 +91,30 @@ function runNodeScript(tenantRoot, scriptName, scriptArgs = []) {
90
91
  throw new Error(`${scriptName} failed.`);
91
92
  }
92
93
  }
94
+ function resolveProjectPlatformBootstrapSystems(options, siteConfig = loadCliDeployConfig(options.tenantRoot)) {
95
+ if (options.bootstrapSystems && options.bootstrapSystems.length > 0) {
96
+ return [...options.bootstrapSystems];
97
+ }
98
+ const selection = resolveTreeseedBootstrapSelection({
99
+ deployConfig: siteConfig,
100
+ env: { ...process.env, ...options.env ?? {} },
101
+ systems: PROJECT_PLATFORM_BOOTSTRAP_SYSTEMS,
102
+ skipUnavailable: true
103
+ });
104
+ for (const skipped of selection.skipped) {
105
+ writeTreeseedBootstrapLine(
106
+ options.write,
107
+ {
108
+ scope: options.scope,
109
+ system: skipped.system,
110
+ task: "availability",
111
+ stage: "skip"
112
+ },
113
+ skipped.reason
114
+ );
115
+ }
116
+ return selection.runnable.filter((system) => system !== "github");
117
+ }
93
118
  function runWrangler(tenantRoot, args, extraEnv = {}, options = {}) {
94
119
  const result = spawnSync(process.execPath, [resolveWranglerBin(), ...args], {
95
120
  cwd: tenantRoot,
@@ -818,18 +843,24 @@ async function provisionProjectPlatform(options) {
818
843
  const reporter = resolveReporter(options.tenantRoot, options.reporter);
819
844
  const target = createPersistentDeployTarget(options.scope === "local" ? "staging" : options.scope);
820
845
  const siteConfig = loadCliDeployConfig(options.tenantRoot);
846
+ const bootstrapSystems = resolveProjectPlatformBootstrapSystems(options, siteConfig);
847
+ const selectedSystems = new Set(bootstrapSystems);
848
+ const env = { ...process.env, ...options.env ?? {} };
821
849
  const summary = await reconcileTreeseedTarget({
822
850
  tenantRoot: options.tenantRoot,
823
851
  target,
824
- env: process.env
852
+ env,
853
+ systems: bootstrapSystems
825
854
  });
826
855
  const verification = await collectTreeseedReconcileStatus({
827
856
  tenantRoot: options.tenantRoot,
828
857
  target,
829
- env: process.env
858
+ env,
859
+ systems: bootstrapSystems
830
860
  });
831
861
  ensureGeneratedWranglerConfig(options.tenantRoot, { target });
832
- const railwayValidation = options.scope === "local" ? validateRailwayServiceConfiguration(options.tenantRoot, options.scope) : validateRailwayDeployPrerequisites(options.tenantRoot, options.scope);
862
+ const shouldValidateRailway = selectedSystems.has("api") || selectedSystems.has("agents");
863
+ const railwayValidation = shouldValidateRailway ? options.scope === "local" ? validateRailwayServiceConfiguration(options.tenantRoot, options.scope) : validateRailwayDeployPrerequisites(options.tenantRoot, options.scope, { env }) : { services: [] };
833
864
  const railwaySchedules = [];
834
865
  const railwayScheduleVerification = {
835
866
  ok: true,
@@ -969,7 +1000,8 @@ async function deployProjectPlatform(options) {
969
1000
  const reporter = resolveReporter(options.tenantRoot, options.reporter);
970
1001
  const commitSha = currentCommit(options.tenantRoot);
971
1002
  const branchName = currentRef(options.tenantRoot);
972
- const selectedSystems = new Set(options.bootstrapSystems ?? ["data", "web", "api", "agents"]);
1003
+ const bootstrapSystems = resolveProjectPlatformBootstrapSystems(options);
1004
+ const selectedSystems = new Set(bootstrapSystems);
973
1005
  const execution = options.bootstrapExecution ?? "parallel";
974
1006
  const write = options.write;
975
1007
  const env = { ...process.env, ...options.env ?? {} };
@@ -983,7 +1015,7 @@ async function deployProjectPlatform(options) {
983
1015
  metadata: { scope: options.scope }
984
1016
  });
985
1017
  if (!options.skipProvision) {
986
- await provisionProjectPlatform({ ...options, reporter });
1018
+ await provisionProjectPlatform({ ...options, reporter, bootstrapSystems });
987
1019
  }
988
1020
  const nodes = [];
989
1021
  let cloudflareContext = null;
@@ -748,6 +748,7 @@ entries:
748
748
  TREESEED_RAILWAY_PROJECT_ID:
749
749
  label: Railway project ID
750
750
  group: hosting
751
+ visibility: system
751
752
  description: Railway project identifier used by runtime scaling and reconciliation helpers for the active environment.
752
753
  howToGet: Copy the project ID from Railway when automatic worker scaling is enabled.
753
754
  sensitivity: plain
@@ -770,6 +771,7 @@ entries:
770
771
  TREESEED_RAILWAY_ENVIRONMENT_ID:
771
772
  label: Railway environment ID
772
773
  group: hosting
774
+ visibility: system
773
775
  description: Railway environment identifier used by the runtime scaler when adjusting worker replicas.
774
776
  howToGet: Copy the environment ID from Railway for the matching staging or production environment.
775
777
  sensitivity: plain
@@ -792,6 +794,7 @@ entries:
792
794
  TREESEED_RAILWAY_WORKER_SERVICE_ID:
793
795
  label: Railway worker service ID
794
796
  group: hosting
797
+ visibility: system
795
798
  description: Railway service identifier for the scalable worker pool that the manager adjusts at runtime.
796
799
  howToGet: Copy the worker service ID from Railway after the hosted project environment is provisioned.
797
800
  sensitivity: plain
@@ -7,6 +7,7 @@ export declare const TREESEED_ENVIRONMENT_PURPOSES: readonly ["dev", "save", "de
7
7
  export declare const TREESEED_ENVIRONMENT_SENSITIVITY: readonly ["secret", "plain", "derived"];
8
8
  export declare const TREESEED_ENVIRONMENT_STORAGE: readonly ["scoped", "shared"];
9
9
  export declare const TREESEED_CONFIG_STARTUP_PROFILES: readonly ["core", "optional", "advanced"];
10
+ export declare const TREESEED_ENVIRONMENT_VISIBILITY: readonly ["user", "system"];
10
11
  export type TreeseedEnvironmentScope = (typeof TREESEED_ENVIRONMENT_SCOPES)[number];
11
12
  export type TreeseedEnvironmentRequirement = (typeof TREESEED_ENVIRONMENT_REQUIREMENTS)[number];
12
13
  export type TreeseedEnvironmentTarget = (typeof TREESEED_ENVIRONMENT_TARGETS)[number];
@@ -14,6 +15,7 @@ export type TreeseedEnvironmentPurpose = (typeof TREESEED_ENVIRONMENT_PURPOSES)[
14
15
  export type TreeseedEnvironmentSensitivity = (typeof TREESEED_ENVIRONMENT_SENSITIVITY)[number];
15
16
  export type TreeseedEnvironmentStorage = (typeof TREESEED_ENVIRONMENT_STORAGE)[number];
16
17
  export type TreeseedConfigStartupProfile = (typeof TREESEED_CONFIG_STARTUP_PROFILES)[number];
18
+ export type TreeseedEnvironmentVisibility = (typeof TREESEED_ENVIRONMENT_VISIBILITY)[number];
17
19
  export type TreeseedEnvironmentValidation = {
18
20
  kind: 'string' | 'nonempty' | 'url' | 'email';
19
21
  minLength?: number;
@@ -68,6 +70,7 @@ export type TreeseedEnvironmentEntry = {
68
70
  cluster?: string;
69
71
  onboardingFeature?: string;
70
72
  startupProfile?: TreeseedConfigStartupProfile;
73
+ visibility?: TreeseedEnvironmentVisibility;
71
74
  description: string;
72
75
  howToGet: string;
73
76
  sensitivity: TreeseedEnvironmentSensitivity;
@@ -87,6 +90,7 @@ export type TreeseedEnvironmentEntryYaml = Omit<TreeseedEnvironmentEntry, 'id' |
87
90
  cluster?: string;
88
91
  onboardingFeature?: string;
89
92
  startupProfile?: TreeseedConfigStartupProfile;
93
+ visibility?: TreeseedEnvironmentVisibility;
90
94
  defaultValueRef?: string;
91
95
  localDefaultValueRef?: string;
92
96
  relevanceRef?: string;
@@ -24,6 +24,7 @@ const TREESEED_ENVIRONMENT_PURPOSES = ["dev", "save", "deploy", "destroy", "conf
24
24
  const TREESEED_ENVIRONMENT_SENSITIVITY = ["secret", "plain", "derived"];
25
25
  const TREESEED_ENVIRONMENT_STORAGE = ["scoped", "shared"];
26
26
  const TREESEED_CONFIG_STARTUP_PROFILES = ["core", "optional", "advanced"];
27
+ const TREESEED_ENVIRONMENT_VISIBILITY = ["user", "system"];
27
28
  const moduleDir = dirname(fileURLToPath(import.meta.url));
28
29
  function resolveCoreEnvironmentPath() {
29
30
  const candidates = [
@@ -367,6 +368,7 @@ function materializeEntry(id, entry) {
367
368
  id,
368
369
  cluster: entry.cluster ?? `${entry.group}:${id}`,
369
370
  onboardingFeature: entry.onboardingFeature,
371
+ visibility: entry.visibility ?? "user",
370
372
  startupProfile: entry.startupProfile ?? (entry.onboardingFeature ? "optional" : entry.group === "auth" || entry.id === "TREESEED_FORM_TOKEN_SECRET" || entry.group === "local-development" ? "core" : "advanced"),
371
373
  storage: entry.storage ?? "scoped",
372
374
  defaultValue: resolveNamedValueResolver(entry.defaultValueRef),
@@ -578,6 +580,7 @@ export {
578
580
  TREESEED_ENVIRONMENT_SENSITIVITY,
579
581
  TREESEED_ENVIRONMENT_STORAGE,
580
582
  TREESEED_ENVIRONMENT_TARGETS,
583
+ TREESEED_ENVIRONMENT_VISIBILITY,
581
584
  getTreeseedEnvironmentSuggestedValues,
582
585
  isTreeseedEnvironmentEntryRelevant,
583
586
  isTreeseedEnvironmentEntryRequired,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@treeseed/sdk",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
4
4
  "description": "Shared Treeseed SDK for content-backed and D1-backed object models.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {