@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
@@ -27,11 +27,23 @@ function parseGitHubRepositorySlug(value) {
27
27
  name: rest.join("/")
28
28
  };
29
29
  }
30
- function createGitHubSignal(timeoutMs = DEFAULT_GITHUB_API_TIMEOUT_MS) {
30
+ function createGitHubRequestSignal(timeoutMs = DEFAULT_GITHUB_API_TIMEOUT_MS, upstreamSignal) {
31
31
  if (typeof AbortSignal !== "undefined" && typeof AbortSignal.timeout === "function") {
32
- return AbortSignal.timeout(timeoutMs);
32
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
33
+ if (upstreamSignal) {
34
+ const abortSignalAny = AbortSignal.any;
35
+ return typeof abortSignalAny === "function" ? abortSignalAny([upstreamSignal, timeoutSignal]) : timeoutSignal;
36
+ }
37
+ return timeoutSignal;
33
38
  }
34
- return void 0;
39
+ return upstreamSignal ?? void 0;
40
+ }
41
+ function createGitHubTimeoutFetch(timeoutMs = DEFAULT_GITHUB_API_TIMEOUT_MS) {
42
+ const baseFetch = globalThis.fetch.bind(globalThis);
43
+ return ((input, init) => {
44
+ const signal = createGitHubRequestSignal(timeoutMs, init?.signal ?? null);
45
+ return baseFetch(input, signal ? { ...init, signal } : init);
46
+ });
35
47
  }
36
48
  function normalizeGitHubApiError(error, context) {
37
49
  if (error && typeof error === "object") {
@@ -100,7 +112,7 @@ function createGitHubApiClient({
100
112
  return new Octokit({
101
113
  auth: token,
102
114
  request: {
103
- signal: createGitHubSignal(timeoutMs)
115
+ fetch: createGitHubTimeoutFetch(timeoutMs)
104
116
  }
105
117
  });
106
118
  }
@@ -208,6 +220,50 @@ async function listGitHubRepositoryVariableNames(repository, { client = createGi
208
220
  throw normalizeGitHubApiError(error, `Unable to list GitHub variables for ${owner}/${name}`);
209
221
  }
210
222
  }
223
+ async function ensureGitHubActionsEnvironment(repository, environmentName, { client = createGitHubApiClient() } = {}) {
224
+ const { owner, name: repo } = typeof repository === "string" ? parseGitHubRepositorySlug(repository) : repository;
225
+ try {
226
+ await withGitHubApiRetries(() => client.request("PUT /repos/{owner}/{repo}/environments/{environment_name}", {
227
+ owner,
228
+ repo,
229
+ environment_name: environmentName
230
+ }));
231
+ return { repository: `${owner}/${repo}`, environment: environmentName };
232
+ } catch (error) {
233
+ throw normalizeGitHubApiError(error, `Unable to ensure GitHub environment ${environmentName} for ${owner}/${repo}`);
234
+ }
235
+ }
236
+ async function paginateGitHubEnvironmentNames(client, route, params) {
237
+ const paginate = client.paginate;
238
+ return await paginateNames(() => paginate(route, {
239
+ ...params,
240
+ per_page: 100
241
+ }));
242
+ }
243
+ async function listGitHubEnvironmentSecretNames(repository, environmentName, { client = createGitHubApiClient() } = {}) {
244
+ const { owner, name } = typeof repository === "string" ? parseGitHubRepositorySlug(repository) : repository;
245
+ try {
246
+ return await paginateGitHubEnvironmentNames(
247
+ client,
248
+ "GET /repos/{owner}/{repo}/environments/{environment_name}/secrets",
249
+ { owner, repo: name, environment_name: environmentName }
250
+ );
251
+ } catch (error) {
252
+ throw normalizeGitHubApiError(error, `Unable to list GitHub environment secrets for ${owner}/${name}:${environmentName}`);
253
+ }
254
+ }
255
+ async function listGitHubEnvironmentVariableNames(repository, environmentName, { client = createGitHubApiClient() } = {}) {
256
+ const { owner, name } = typeof repository === "string" ? parseGitHubRepositorySlug(repository) : repository;
257
+ try {
258
+ return await paginateGitHubEnvironmentNames(
259
+ client,
260
+ "GET /repos/{owner}/{repo}/environments/{environment_name}/variables",
261
+ { owner, repo: name, environment_name: environmentName }
262
+ );
263
+ } catch (error) {
264
+ throw normalizeGitHubApiError(error, `Unable to list GitHub environment variables for ${owner}/${name}:${environmentName}`);
265
+ }
266
+ }
211
267
  async function encryptGitHubSecret(secret, key) {
212
268
  await sodium.ready;
213
269
  const messageBytes = Buffer.from(secret);
@@ -233,6 +289,27 @@ async function upsertGitHubRepositorySecret(repository, name, value, { client =
233
289
  throw normalizeGitHubApiError(error, `Unable to upsert GitHub secret ${name} for ${owner}/${repo}`);
234
290
  }
235
291
  }
292
+ async function upsertGitHubEnvironmentSecret(repository, environmentName, name, value, { client = createGitHubApiClient() } = {}) {
293
+ const { owner, name: repo } = typeof repository === "string" ? parseGitHubRepositorySlug(repository) : repository;
294
+ try {
295
+ const key = await client.request("GET /repos/{owner}/{repo}/environments/{environment_name}/secrets/public-key", {
296
+ owner,
297
+ repo,
298
+ environment_name: environmentName
299
+ });
300
+ const encryptedValue = await encryptGitHubSecret(value, String(key.data.key ?? ""));
301
+ await withGitHubApiRetries(() => client.request("PUT /repos/{owner}/{repo}/environments/{environment_name}/secrets/{secret_name}", {
302
+ owner,
303
+ repo,
304
+ environment_name: environmentName,
305
+ secret_name: name,
306
+ encrypted_value: encryptedValue,
307
+ key_id: String(key.data.key_id ?? "")
308
+ }));
309
+ } catch (error) {
310
+ throw normalizeGitHubApiError(error, `Unable to upsert GitHub environment secret ${name} for ${owner}/${repo}:${environmentName}`);
311
+ }
312
+ }
236
313
  async function upsertGitHubRepositoryVariable(repository, name, value, { client = createGitHubApiClient() } = {}) {
237
314
  const { owner, name: repo } = typeof repository === "string" ? parseGitHubRepositorySlug(repository) : repository;
238
315
  try {
@@ -273,6 +350,38 @@ async function upsertGitHubRepositoryVariable(repository, name, value, { client
273
350
  throw normalizeGitHubApiError(error, `Unable to upsert GitHub variable ${name} for ${owner}/${repo}`);
274
351
  }
275
352
  }
353
+ async function upsertGitHubEnvironmentVariable(repository, environmentName, name, value, { client = createGitHubApiClient() } = {}) {
354
+ const { owner, name: repo } = typeof repository === "string" ? parseGitHubRepositorySlug(repository) : repository;
355
+ try {
356
+ await withGitHubApiRetries(async () => {
357
+ try {
358
+ await client.request("POST /repos/{owner}/{repo}/environments/{environment_name}/variables", {
359
+ owner,
360
+ repo,
361
+ environment_name: environmentName,
362
+ name,
363
+ value
364
+ });
365
+ return;
366
+ } catch (error) {
367
+ const status = typeof error?.status === "number" ? Number(error.status) : null;
368
+ const message = error instanceof Error ? error.message : String(error ?? "");
369
+ if (status !== 409 && status !== 422 && !/already exists/iu.test(message)) {
370
+ throw error;
371
+ }
372
+ }
373
+ await client.request("PATCH /repos/{owner}/{repo}/environments/{environment_name}/variables/{name}", {
374
+ owner,
375
+ repo,
376
+ environment_name: environmentName,
377
+ name,
378
+ value
379
+ });
380
+ });
381
+ } catch (error) {
382
+ throw normalizeGitHubApiError(error, `Unable to upsert GitHub environment variable ${name} for ${owner}/${repo}:${environmentName}`);
383
+ }
384
+ }
276
385
  function upsertGitHubRepositoryVariableWithGhCli(repository, name, value, {
277
386
  env = process.env
278
387
  } = {}) {
@@ -440,14 +549,19 @@ async function ensureGitHubBranchFromBase(repository, branch, {
440
549
  }
441
550
  export {
442
551
  createGitHubApiClient,
552
+ ensureGitHubActionsEnvironment,
443
553
  ensureGitHubBranchFromBase,
444
554
  ensureGitHubRepository,
445
555
  getGitHubRepository,
556
+ listGitHubEnvironmentSecretNames,
557
+ listGitHubEnvironmentVariableNames,
446
558
  listGitHubRepositorySecretNames,
447
559
  listGitHubRepositoryVariableNames,
448
560
  maybeGetGitHubRepository,
449
561
  parseGitHubRepositorySlug,
450
562
  resolveGitHubApiToken,
563
+ upsertGitHubEnvironmentSecret,
564
+ upsertGitHubEnvironmentVariable,
451
565
  upsertGitHubRepositorySecret,
452
566
  upsertGitHubRepositoryVariable,
453
567
  upsertGitHubRepositoryVariableWithGhCli,
@@ -16,11 +16,41 @@ export interface GitHubProvisionedRepository {
16
16
  visibility: 'private' | 'public' | 'internal';
17
17
  defaultBranch: string;
18
18
  }
19
+ export interface TreeseedGitHubRepositoryTarget {
20
+ owner: string;
21
+ name: string;
22
+ visibility: 'private' | 'public' | 'internal';
23
+ source: 'config' | 'origin' | 'default';
24
+ }
19
25
  export declare function getGitHubAutomationMode(): "stub" | "real";
20
26
  export declare function parseGitHubRepositoryFromRemote(remoteUrl: any): string | null;
21
27
  export declare function resolveGitHubRepositorySlug(tenantRoot: any): string;
22
28
  export declare function maybeResolveGitHubRepositorySlug(tenantRoot: any): string | null;
23
29
  export declare function resolveDefaultGitHubOwner(): string;
30
+ export declare function resolveGitHubRepositoryTarget(tenantRoot: string, { values, defaultName, }?: {
31
+ values?: Record<string, string | undefined>;
32
+ defaultName?: string;
33
+ }): TreeseedGitHubRepositoryTarget;
34
+ export declare function ensureGitHubBootstrapRepository(tenantRoot: string, { values, defaultName, onProgress, }?: {
35
+ values?: Record<string, string | undefined>;
36
+ defaultName?: string;
37
+ onProgress?: (message: string) => void;
38
+ }): Promise<{
39
+ repository: string;
40
+ target: TreeseedGitHubRepositoryTarget;
41
+ created: boolean;
42
+ remote: {
43
+ changed: boolean;
44
+ previous: null;
45
+ next: string;
46
+ } | {
47
+ changed: boolean;
48
+ previous: string;
49
+ next: string;
50
+ };
51
+ pushed: boolean;
52
+ mode: string;
53
+ }>;
24
54
  export declare function createGitHubRepository(input: any): Promise<import("./github-api.ts").GitHubRepositorySummary | {
25
55
  visibility: any;
26
56
  defaultBranch: string;
@@ -6,6 +6,8 @@ import { packageRoot, loadCliDeployConfig } from "./runtime-tools.js";
6
6
  import {
7
7
  createGitHubApiClient,
8
8
  ensureGitHubRepository,
9
+ maybeGetGitHubRepository,
10
+ parseGitHubRepositorySlug,
9
11
  listGitHubRepositorySecretNames,
10
12
  listGitHubRepositoryVariableNames,
11
13
  upsertGitHubRepositorySecret,
@@ -97,7 +99,7 @@ function maybeResolveGitHubRepositorySlug(tenantRoot) {
97
99
  }
98
100
  }
99
101
  function resolveDefaultGitHubOwner() {
100
- const explicit = envOrNull("TREESEED_KNOWLEDGE_COOP_GITHUB_OWNER");
102
+ const explicit = envOrNull("TREESEED_GITHUB_OWNER");
101
103
  if (explicit) {
102
104
  return explicit;
103
105
  }
@@ -110,6 +112,108 @@ function resolveDefaultGitHubOwner() {
110
112
  }
111
113
  return "treeseed-ai";
112
114
  }
115
+ function normalizeGitHubVisibility(value) {
116
+ const normalized = String(value ?? "").trim().toLowerCase();
117
+ return normalized === "public" || normalized === "internal" || normalized === "private" ? normalized : "private";
118
+ }
119
+ function configuredValue(values, key) {
120
+ const value = values?.[key] ?? process.env[key];
121
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : "";
122
+ }
123
+ function resolveGitHubRepositoryTarget(tenantRoot, {
124
+ values = {},
125
+ defaultName
126
+ } = {}) {
127
+ const origin = maybeResolveGitHubRepositorySlug(tenantRoot);
128
+ const parsedOrigin = origin ? parseGitHubRepositorySlug(origin) : null;
129
+ const owner = configuredValue(values, "TREESEED_GITHUB_OWNER") || parsedOrigin?.owner || "";
130
+ const name = configuredValue(values, "TREESEED_GITHUB_REPOSITORY_NAME") || parsedOrigin?.name || defaultName || "project";
131
+ if (!owner) {
132
+ throw new Error("Configure TREESEED_GITHUB_OWNER before GitHub repository bootstrap.");
133
+ }
134
+ return {
135
+ owner: slugifySegment(owner, "owner"),
136
+ name: slugifySegment(name, "project"),
137
+ visibility: normalizeGitHubVisibility(configuredValue(values, "TREESEED_GITHUB_REPOSITORY_VISIBILITY")),
138
+ source: configuredValue(values, "TREESEED_GITHUB_OWNER") || configuredValue(values, "TREESEED_GITHUB_REPOSITORY_NAME") ? "config" : parsedOrigin ? "origin" : "default"
139
+ };
140
+ }
141
+ function ensureGitRepositoryInitialized(cwd, defaultBranch) {
142
+ const insideWorkTree = runGit(["rev-parse", "--is-inside-work-tree"], { cwd, allowFailure: true }).stdout?.trim() === "true";
143
+ if (!insideWorkTree) {
144
+ runGit(["init", "-b", defaultBranch], { cwd });
145
+ }
146
+ ensureGitIdentity(cwd);
147
+ }
148
+ function ensureOriginRemote(cwd, repository, remoteName = "origin") {
149
+ const currentRemote = runGit(["remote", "get-url", remoteName], { cwd, allowFailure: true }).stdout?.trim() ?? "";
150
+ if (!currentRemote) {
151
+ runGit(["remote", "add", remoteName, repository.sshUrl], { cwd });
152
+ return { changed: true, previous: null, next: repository.sshUrl };
153
+ }
154
+ if (currentRemote !== repository.sshUrl && currentRemote !== repository.httpsUrl) {
155
+ runGit(["remote", "set-url", remoteName, repository.sshUrl], { cwd });
156
+ return { changed: true, previous: currentRemote, next: repository.sshUrl };
157
+ }
158
+ return { changed: false, previous: currentRemote, next: currentRemote };
159
+ }
160
+ function pushAllGitHubRefs(cwd, remoteName = "origin") {
161
+ runGit(["push", "-u", remoteName, "--all"], { cwd, capture: false });
162
+ runGit(["push", remoteName, "--tags"], { cwd, capture: false });
163
+ }
164
+ async function ensureGitHubBootstrapRepository(tenantRoot, {
165
+ values = {},
166
+ defaultName,
167
+ onProgress
168
+ } = {}) {
169
+ const target = resolveGitHubRepositoryTarget(tenantRoot, { values, defaultName });
170
+ const remotes = resolveGitHubRemoteUrls(target.owner, target.name);
171
+ const slug = remotes.slug;
172
+ onProgress?.(`[local][github][repo] Preparing ${slug} from ${target.source}...`);
173
+ if (isGitHubAutomationStubbed()) {
174
+ onProgress?.(`[local][github][repo] Stubbed GitHub automation; repository ${slug} not changed.`);
175
+ return {
176
+ repository: slug,
177
+ target,
178
+ created: false,
179
+ remote: { changed: false, previous: null, next: remotes.sshUrl },
180
+ pushed: false,
181
+ mode: "stub"
182
+ };
183
+ }
184
+ const client = createGitHubApiClient({
185
+ env: {
186
+ GH_TOKEN: configuredValue(values, "GH_TOKEN") || configuredValue(values, "GITHUB_TOKEN"),
187
+ GITHUB_TOKEN: configuredValue(values, "GH_TOKEN") || configuredValue(values, "GITHUB_TOKEN")
188
+ }
189
+ });
190
+ const existing = await maybeGetGitHubRepository({ owner: target.owner, name: target.name }, { client });
191
+ const repository = existing ?? await ensureGitHubRepository({
192
+ owner: target.owner,
193
+ name: target.name,
194
+ visibility: target.visibility
195
+ }, { client });
196
+ const created = !existing;
197
+ onProgress?.(`[local][github][repo] ${created ? "Created" : "Verified"} ${repository.slug}.`);
198
+ ensureGitRepositoryInitialized(tenantRoot, repository.defaultBranch || "main");
199
+ const remote = ensureOriginRemote(tenantRoot, repository);
200
+ if (remote.changed) {
201
+ onProgress?.(`[local][github][repo] Updated origin remote to ${repository.slug}.`);
202
+ }
203
+ if (created) {
204
+ onProgress?.(`[local][github][repo] Pushing all local branches and tags to ${repository.slug}...`);
205
+ pushAllGitHubRefs(tenantRoot);
206
+ onProgress?.(`[local][github][repo] Pushed all local branches and tags to ${repository.slug}.`);
207
+ }
208
+ return {
209
+ repository: repository.slug,
210
+ target,
211
+ created,
212
+ remote,
213
+ pushed: created,
214
+ mode: "real"
215
+ };
216
+ }
113
217
  async function createGitHubRepository(input) {
114
218
  const visibility = input.visibility ?? "private";
115
219
  const remotes = resolveGitHubRemoteUrls(input.owner, input.name);
@@ -422,6 +526,7 @@ async function waitForGitHubWorkflowCompletion(tenantRoot, {
422
526
  export {
423
527
  createGitHubRepository,
424
528
  ensureDeployWorkflow,
529
+ ensureGitHubBootstrapRepository,
425
530
  ensureGitHubDeployAutomation,
426
531
  ensureGitHubEnvironment,
427
532
  ensureGitHubSecrets,
@@ -440,6 +545,7 @@ export {
440
545
  requiredGitHubSecrets,
441
546
  resolveDefaultGitHubOwner,
442
547
  resolveGitHubRepositorySlug,
548
+ resolveGitHubRepositoryTarget,
443
549
  resolveGitRepositoryRoot,
444
550
  waitForGitHubWorkflowCompletion
445
551
  };
@@ -1,4 +1,6 @@
1
1
  import { type ControlPlaneReporter } from '../../control-plane.ts';
2
+ import type { TreeseedRunnableBootstrapSystem } from '../../reconcile/index.ts';
3
+ import { type TreeseedBootstrapExecution, type TreeseedBootstrapWriter } from './bootstrap-runner.ts';
2
4
  export type ProjectPlatformScope = 'local' | 'staging' | 'prod';
3
5
  export type ProjectPlatformAction = 'provision' | 'deploy_code' | 'publish_content' | 'monitor';
4
6
  export interface ProjectPlatformActionOptions {
@@ -9,9 +11,43 @@ export interface ProjectPlatformActionOptions {
9
11
  dryRun?: boolean;
10
12
  reporter?: ControlPlaneReporter;
11
13
  skipProvision?: boolean;
14
+ bootstrapSystems?: TreeseedRunnableBootstrapSystem[];
15
+ bootstrapExecution?: TreeseedBootstrapExecution;
16
+ write?: TreeseedBootstrapWriter;
17
+ env?: NodeJS.ProcessEnv | Record<string, string | undefined>;
12
18
  }
13
19
  export declare function inferEnvironmentFromBranch(tenantRoot: string): "staging" | "prod";
14
20
  export declare function resolveScope(environment: string | null): ProjectPlatformScope;
21
+ export type TenantCloudflareDeployContext = {
22
+ tenantRoot: string;
23
+ scope: ProjectPlatformScope;
24
+ target: any;
25
+ dryRun?: boolean;
26
+ wranglerPath: string;
27
+ databaseName: string;
28
+ pagesProjectName: string | null;
29
+ pagesBranchName: string | null;
30
+ env: NodeJS.ProcessEnv | Record<string, string | undefined>;
31
+ write?: TreeseedBootstrapWriter;
32
+ };
33
+ export declare function prepareTenantCloudflareDeploy({ tenantRoot, scope, target: explicitTarget, dryRun, write, env, }: {
34
+ tenantRoot: string;
35
+ scope: ProjectPlatformScope;
36
+ target?: any;
37
+ dryRun?: boolean;
38
+ write?: TreeseedBootstrapWriter;
39
+ env?: NodeJS.ProcessEnv | Record<string, string | undefined>;
40
+ }): TenantCloudflareDeployContext;
41
+ export declare function runTenantDataMigration(context: TenantCloudflareDeployContext): Promise<{
42
+ databaseName: string;
43
+ dryRun: boolean;
44
+ }>;
45
+ export declare function runTenantWebBuild(context: Pick<TenantCloudflareDeployContext, 'tenantRoot' | 'scope' | 'dryRun' | 'env' | 'write'>): Promise<{
46
+ dryRun: boolean;
47
+ }>;
48
+ export declare function runTenantWebPublish(context: TenantCloudflareDeployContext): Promise<{
49
+ dryRun: boolean;
50
+ }>;
15
51
  export declare function provisionProjectPlatform(options: ProjectPlatformActionOptions): Promise<{
16
52
  ok: boolean;
17
53
  scope: ProjectPlatformScope;
@@ -209,7 +245,7 @@ export declare function deployProjectPlatform(options: ProjectPlatformActionOpti
209
245
  healthcheckTimeoutSeconds: number | null;
210
246
  runtimeMode: string | null;
211
247
  } | null;
212
- })[];
248
+ } | undefined)[];
213
249
  }>;
214
250
  export declare function publishProjectContent(options: ProjectPlatformActionOptions): Promise<{
215
251
  ok: boolean;
@@ -698,5 +734,5 @@ export declare function runProjectPlatformAction(action: ProjectPlatformAction,
698
734
  healthcheckTimeoutSeconds: number | null;
699
735
  runtimeMode: string | null;
700
736
  } | null;
701
- })[];
737
+ } | undefined)[];
702
738
  }>;