@treeseed/sdk 0.10.6 → 0.10.7

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 (41) hide show
  1. package/dist/index.d.ts +2 -0
  2. package/dist/index.js +58 -0
  3. package/dist/operations/repository-operations.d.ts +129 -0
  4. package/dist/operations/repository-operations.js +634 -0
  5. package/dist/operations/services/config-runtime.d.ts +7 -6
  6. package/dist/operations/services/config-runtime.js +45 -25
  7. package/dist/operations/services/deploy.d.ts +42 -0
  8. package/dist/operations/services/deploy.js +1 -1
  9. package/dist/operations/services/project-platform.d.ts +41 -1
  10. package/dist/operations/services/project-platform.js +13 -0
  11. package/dist/operations/services/railway-api.d.ts +35 -1
  12. package/dist/operations/services/railway-api.js +240 -35
  13. package/dist/operations/services/railway-deploy.d.ts +16 -234
  14. package/dist/operations/services/railway-deploy.js +177 -62
  15. package/dist/operations/services/release-candidate.js +1 -2
  16. package/dist/operations/services/runtime-tools.d.ts +14 -0
  17. package/dist/operations/services/runtime-tools.js +15 -1
  18. package/dist/operations/services/workspace-save.d.ts +24 -0
  19. package/dist/operations/services/workspace-save.js +143 -3
  20. package/dist/operations/services/workspace-tools.js +1 -1
  21. package/dist/platform/env.yaml +163 -2
  22. package/dist/platform/environment.d.ts +1 -0
  23. package/dist/platform/environment.js +9 -0
  24. package/dist/platform-operation-store.d.ts +90 -0
  25. package/dist/platform-operation-store.js +505 -0
  26. package/dist/platform-operations.d.ts +265 -0
  27. package/dist/platform-operations.js +421 -0
  28. package/dist/reconcile/bootstrap-systems.js +3 -3
  29. package/dist/reconcile/builtin-adapters.js +225 -29
  30. package/dist/reconcile/contracts.d.ts +1 -1
  31. package/dist/reconcile/desired-state.d.ts +14 -0
  32. package/dist/reconcile/desired-state.js +4 -0
  33. package/dist/reconcile/engine.d.ts +28 -0
  34. package/dist/reconcile/state.js +3 -0
  35. package/dist/reconcile/units.js +2 -0
  36. package/dist/workflow/operations.d.ts +13 -5
  37. package/dist/workflow/operations.js +69 -12
  38. package/dist/workflow-state.d.ts +2 -0
  39. package/dist/workflow-state.js +7 -2
  40. package/dist/workflow.d.ts +2 -0
  41. package/package.json +15 -2
@@ -175,9 +175,23 @@ export declare function loadCliDeployConfig(tenantRoot: any): {
175
175
  projectName: string | undefined;
176
176
  serviceId: string | undefined;
177
177
  serviceName: string | undefined;
178
+ resourceType: string | undefined;
179
+ environmentVariable: string | undefined;
180
+ serviceTargets: any;
178
181
  rootDir: string | undefined;
179
182
  buildCommand: string | undefined;
180
183
  startCommand: string | undefined;
184
+ healthcheckPath: string | undefined;
185
+ healthcheckTimeoutSeconds: number | undefined;
186
+ healthcheckIntervalSeconds: number | undefined;
187
+ restartPolicy: string | undefined;
188
+ runtimeMode: string | undefined;
189
+ volumeMountPath: string | undefined;
190
+ runnerPool: {
191
+ bootstrapCount: number | undefined;
192
+ maxRunners: number | undefined;
193
+ volumeMountPath: string | undefined;
194
+ } | undefined;
181
195
  schedule: any;
182
196
  };
183
197
  environments: {
@@ -53,7 +53,7 @@ const TREESEED_DEFAULT_PROVIDER_SELECTIONS = {
53
53
  },
54
54
  site: "default"
55
55
  };
56
- const TRESEED_MANAGED_SERVICE_KEYS = ["api", "workdayManager", "workerRunner"];
56
+ const TRESEED_MANAGED_SERVICE_KEYS = ["marketDatabase", "api", "marketOperationsRunner"];
57
57
  const TRESEED_WORKSPACE_PACKAGE_DIRS = ["sdk", "core", "cli"];
58
58
  const CLOUDFLARE_ACCOUNT_ID_PLACEHOLDER = "replace-with-cloudflare-account-id";
59
59
  function normalizePlanesFromLegacyHosting(hosting) {
@@ -143,9 +143,23 @@ function parseManagedServiceConfig(value, label) {
143
143
  projectName: optionalString(railway.projectName),
144
144
  serviceId: optionalString(railway.serviceId),
145
145
  serviceName: optionalString(railway.serviceName),
146
+ resourceType: optionalString(railway.resourceType),
147
+ environmentVariable: optionalString(railway.environmentVariable),
148
+ serviceTargets: Array.isArray(railway.serviceTargets) ? railway.serviceTargets.map((entry) => optionalString(entry)).filter(Boolean) : void 0,
146
149
  rootDir: optionalString(railway.rootDir),
147
150
  buildCommand: optionalString(railway.buildCommand),
148
151
  startCommand: optionalString(railway.startCommand),
152
+ healthcheckPath: optionalString(railway.healthcheckPath),
153
+ healthcheckTimeoutSeconds: optionalNonNegativeNumber(railway.healthcheckTimeoutSeconds, `${label}.railway.healthcheckTimeoutSeconds`),
154
+ healthcheckIntervalSeconds: optionalNonNegativeNumber(railway.healthcheckIntervalSeconds, `${label}.railway.healthcheckIntervalSeconds`),
155
+ restartPolicy: optionalString(railway.restartPolicy),
156
+ runtimeMode: optionalString(railway.runtimeMode),
157
+ volumeMountPath: optionalString(railway.volumeMountPath),
158
+ runnerPool: optionalRecord(railway.runnerPool, `${label}.railway.runnerPool`) ? {
159
+ bootstrapCount: optionalNonNegativeNumber(railway.runnerPool.bootstrapCount, `${label}.railway.runnerPool.bootstrapCount`),
160
+ maxRunners: optionalNonNegativeNumber(railway.runnerPool.maxRunners, `${label}.railway.runnerPool.maxRunners`),
161
+ volumeMountPath: optionalString(railway.runnerPool.volumeMountPath)
162
+ } : void 0,
149
163
  schedule: Array.isArray(railway.schedule) ? railway.schedule.map((entry) => optionalString(entry)).filter(Boolean) : optionalString(railway.schedule)
150
164
  },
151
165
  environments: {
@@ -1,6 +1,23 @@
1
1
  export declare const MERGE_CONFLICT_EXIT_CODE = 12;
2
+ export declare const TREESEED_PUBLIC_RELEASE_PACKAGE_NAMES: string[];
3
+ export declare function highestStableGitTagOnLine(repoDir: any, lineLabel: any): string | null;
2
4
  export declare function incrementPatchVersion(version: any): string;
3
5
  export declare function incrementVersion(version: any, level?: string): string;
6
+ export declare function collectPublicPackageReleaseLineState(root?: any): {
7
+ group: string[];
8
+ packages: {
9
+ name: any;
10
+ path: any;
11
+ version: string;
12
+ line: string;
13
+ major: number;
14
+ minor: number;
15
+ }[];
16
+ lines: string[];
17
+ highestLine: string | null;
18
+ aligned: boolean;
19
+ drifted: boolean;
20
+ };
4
21
  export declare function planWorkspaceVersionChanges(root?: any): {
5
22
  packages: any[];
6
23
  publishable: Set<any>;
@@ -14,6 +31,13 @@ export declare function planWorkspaceReleaseBump(level?: string, root?: any, opt
14
31
  versions: Map<any, any>;
15
32
  level: string;
16
33
  selected: Set<any>;
34
+ releaseLine: {
35
+ group: string[];
36
+ repair: boolean;
37
+ targetLine: string;
38
+ highestCurrentLine: string;
39
+ alignedBefore: boolean;
40
+ };
17
41
  };
18
42
  export declare function collectWorkspaceVersionConsistencyIssues(root?: any): {
19
43
  packageName: any;
@@ -2,6 +2,7 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
3
  import { changedWorkspacePackages, publishableWorkspacePackages, run, sortWorkspacePackages, workspacePackages, workspaceRoot } from "./workspace-tools.js";
4
4
  const MERGE_CONFLICT_EXIT_CODE = 12;
5
+ const TREESEED_PUBLIC_RELEASE_PACKAGE_NAMES = ["@treeseed/sdk", "@treeseed/core", "@treeseed/cli", "@treeseed/agent"];
5
6
  function parseSemver(version) {
6
7
  const match = String(version).trim().match(/^(\d+)\.(\d+)\.(\d+)(?:-[0-9A-Za-z.-]+)?$/);
7
8
  if (!match) {
@@ -14,6 +15,82 @@ function parseSemver(version) {
14
15
  prerelease: String(version).includes("-")
15
16
  };
16
17
  }
18
+ function versionLine(version) {
19
+ const parsed = parseSemver(version);
20
+ return {
21
+ major: parsed.major,
22
+ minor: parsed.minor,
23
+ label: `${parsed.major}.${parsed.minor}`
24
+ };
25
+ }
26
+ function compareVersionLines(left, right) {
27
+ if (left.major !== right.major) return left.major - right.major;
28
+ return left.minor - right.minor;
29
+ }
30
+ function parseVersionLine(input) {
31
+ const match = String(input ?? "").trim().match(/^(\d+)\.(\d+)$/);
32
+ if (!match) {
33
+ throw new Error(`Unsupported release version line "${input}". Expected major.minor, for example 0.10.`);
34
+ }
35
+ return {
36
+ major: Number(match[1]),
37
+ minor: Number(match[2]),
38
+ label: `${Number(match[1])}.${Number(match[2])}`
39
+ };
40
+ }
41
+ function nextLineFor(level, highestLine) {
42
+ if (level === "major") {
43
+ return {
44
+ major: highestLine.major + 1,
45
+ minor: 0,
46
+ label: `${highestLine.major + 1}.0`
47
+ };
48
+ }
49
+ if (level === "minor") {
50
+ return {
51
+ major: highestLine.major,
52
+ minor: highestLine.minor + 1,
53
+ label: `${highestLine.major}.${highestLine.minor + 1}`
54
+ };
55
+ }
56
+ return highestLine;
57
+ }
58
+ function versionForLine(line, patch = 0) {
59
+ return `${line.major}.${line.minor}.${patch}`;
60
+ }
61
+ function localGitTagExists(repoDir, tagName) {
62
+ try {
63
+ return run("git", ["tag", "--list", tagName], { cwd: repoDir, capture: true }).trim() === tagName;
64
+ } catch {
65
+ return false;
66
+ }
67
+ }
68
+ function highestStableGitTagOnLine(repoDir, lineLabel) {
69
+ const line = parseVersionLine(lineLabel);
70
+ try {
71
+ const tags = run("git", ["tag", "--list", `${line.label}.*`], { cwd: repoDir, capture: true }).split(/\r?\n/u).map((tag) => tag.trim()).filter(Boolean).map((tag) => {
72
+ try {
73
+ const parsed = parseSemver(tag);
74
+ if (parsed.prerelease || parsed.major !== line.major || parsed.minor !== line.minor) return null;
75
+ return { tag, patch: parsed.patch };
76
+ } catch {
77
+ return null;
78
+ }
79
+ }).filter((entry) => entry != null).sort((left, right) => right.patch - left.patch);
80
+ return tags[0]?.tag ?? null;
81
+ } catch {
82
+ return null;
83
+ }
84
+ }
85
+ function firstAvailablePatchVersionOnLine(pkg, line) {
86
+ for (let patch = 0; patch < 1e3; patch += 1) {
87
+ const candidate = versionForLine(line, patch);
88
+ if (!localGitTagExists(pkg.dir, candidate)) {
89
+ return candidate;
90
+ }
91
+ }
92
+ throw new Error(`Unable to find an available ${line.label}.x version for ${pkg.name}.`);
93
+ }
17
94
  function incrementPatchVersion(version) {
18
95
  const parsed = parseSemver(version);
19
96
  return `${parsed.major}.${parsed.minor}.${parsed.patch + 1}`;
@@ -34,6 +111,32 @@ function incrementVersion(version, level = "patch") {
34
111
  }
35
112
  throw new Error(`Unsupported release bump "${level}". Expected major, minor, or patch.`);
36
113
  }
114
+ function collectPublicPackageReleaseLineState(root = workspaceRoot()) {
115
+ const publishable = new Set(publishableWorkspacePackages(root).map((pkg) => pkg.name));
116
+ const packages = sortWorkspacePackages(workspacePackages(root)).filter((pkg) => TREESEED_PUBLIC_RELEASE_PACKAGE_NAMES.includes(pkg.name)).filter((pkg) => publishable.has(pkg.name)).map((pkg) => {
117
+ const packageJson = readPackageJson(resolve(pkg.dir, "package.json"));
118
+ const line = versionLine(packageJson.version);
119
+ return {
120
+ name: pkg.name,
121
+ path: pkg.relativeDir,
122
+ version: String(packageJson.version ?? ""),
123
+ line: line.label,
124
+ major: line.major,
125
+ minor: line.minor
126
+ };
127
+ });
128
+ const lineMap = new Map(packages.map((pkg) => [pkg.line, { major: pkg.major, minor: pkg.minor, label: pkg.line }]));
129
+ const lines = [...lineMap.values()].sort(compareVersionLines);
130
+ const highestLine = lines.at(-1) ?? null;
131
+ return {
132
+ group: TREESEED_PUBLIC_RELEASE_PACKAGE_NAMES.filter((name) => packages.some((pkg) => pkg.name === name)),
133
+ packages,
134
+ lines: lines.map((line) => line.label),
135
+ highestLine: highestLine?.label ?? null,
136
+ aligned: lines.length <= 1,
137
+ drifted: lines.length > 1
138
+ };
139
+ }
37
140
  function readPackageJson(filePath) {
38
141
  return JSON.parse(readFileSync(filePath, "utf8"));
39
142
  }
@@ -124,16 +227,43 @@ function planWorkspaceReleaseBump(level = "patch", root = workspaceRoot(), optio
124
227
  packageJson: readPackageJson(resolve(pkg.dir, "package.json"))
125
228
  }));
126
229
  const publishable = new Set(publishableWorkspacePackages(root).map((pkg) => pkg.name));
127
- const selected = options.selectedPackageNames ? new Set(
230
+ const baseSelected = options.selectedPackageNames ? new Set(
128
231
  [...options.selectedPackageNames].map((name) => String(name)).filter((name) => publishable.has(name))
129
232
  ) : new Set(publishable);
233
+ const publicPackages = sortWorkspacePackages(packages).filter((pkg) => TREESEED_PUBLIC_RELEASE_PACKAGE_NAMES.includes(pkg.name)).filter((pkg) => publishable.has(pkg.name));
234
+ const publicLines = publicPackages.map((pkg) => versionLine(pkg.packageJson.version));
235
+ const highestPublicLine = publicLines.sort(compareVersionLines).at(-1) ?? { major: 0, minor: 0, label: "0.0" };
236
+ const repairVersionLine = options.repairVersionLine === true;
237
+ const explicitTargetLine = options.targetVersionLine ? parseVersionLine(options.targetVersionLine) : null;
238
+ let targetLine = explicitTargetLine ?? highestPublicLine;
239
+ if (repairVersionLine) {
240
+ if (explicitTargetLine && compareVersionLines(explicitTargetLine, highestPublicLine) !== 0) {
241
+ throw new Error(`Release line repair target must match the highest current public package line (${highestPublicLine.label}). Received ${explicitTargetLine.label}.`);
242
+ }
243
+ } else if (level === "major" || level === "minor") {
244
+ targetLine = nextLineFor(level, highestPublicLine);
245
+ }
246
+ const selected = new Set(baseSelected);
247
+ if (repairVersionLine) {
248
+ selected.clear();
249
+ for (const pkg of publicPackages) {
250
+ if (versionLine(pkg.packageJson.version).label !== targetLine.label) {
251
+ selected.add(pkg.name);
252
+ }
253
+ }
254
+ } else if (level === "major" || level === "minor") {
255
+ for (const pkg of publicPackages) {
256
+ selected.add(pkg.name);
257
+ }
258
+ }
130
259
  const touched = /* @__PURE__ */ new Set();
131
260
  const versions = /* @__PURE__ */ new Map();
132
261
  for (const pkg of packages) {
133
262
  if (!publishable.has(pkg.name) || !selected.has(pkg.name)) {
134
263
  continue;
135
264
  }
136
- const nextVersion = incrementVersion(pkg.packageJson.version, level);
265
+ const isPublicReleasePackage = TREESEED_PUBLIC_RELEASE_PACKAGE_NAMES.includes(pkg.name);
266
+ const nextVersion = repairVersionLine && isPublicReleasePackage ? firstAvailablePatchVersionOnLine(pkg, targetLine) : isPublicReleasePackage && (level === "major" || level === "minor") ? versionForLine(targetLine, 0) : incrementVersion(pkg.packageJson.version, level);
137
267
  pkg.packageJson.version = nextVersion;
138
268
  versions.set(pkg.name, nextVersion);
139
269
  touched.add(pkg.name);
@@ -157,7 +287,14 @@ function planWorkspaceReleaseBump(level = "patch", root = workspaceRoot(), optio
157
287
  touched,
158
288
  versions,
159
289
  level,
160
- selected
290
+ selected,
291
+ releaseLine: {
292
+ group: TREESEED_PUBLIC_RELEASE_PACKAGE_NAMES.filter((name) => publishable.has(name)),
293
+ repair: repairVersionLine,
294
+ targetLine: targetLine.label,
295
+ highestCurrentLine: highestPublicLine.label,
296
+ alignedBefore: new Set(publicLines.map((line) => line.label)).size <= 1
297
+ }
161
298
  };
162
299
  }
163
300
  function collectWorkspaceVersionConsistencyIssues(root = workspaceRoot()) {
@@ -272,15 +409,18 @@ function formatMergeConflictReport(report, repoDir, targetBranch = "main") {
272
409
  }
273
410
  export {
274
411
  MERGE_CONFLICT_EXIT_CODE,
412
+ TREESEED_PUBLIC_RELEASE_PACKAGE_NAMES,
275
413
  applyWorkspaceVersionChanges,
276
414
  assertWorkspaceVersionConsistency,
277
415
  collectMergeConflictReport,
416
+ collectPublicPackageReleaseLineState,
278
417
  collectWorkspaceVersionConsistencyIssues,
279
418
  countConflictMarkers,
280
419
  currentBranch,
281
420
  formatMergeConflictReport,
282
421
  gitStatusPorcelain,
283
422
  hasMeaningfulChanges,
423
+ highestStableGitTagOnLine,
284
424
  incrementPatchVersion,
285
425
  incrementVersion,
286
426
  originRemoteUrl,
@@ -1,7 +1,7 @@
1
1
  import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync } from "node:fs";
2
2
  import { spawnSync } from "node:child_process";
3
3
  import { join, relative, resolve } from "node:path";
4
- const TREESEED_WORKSPACE_PACKAGE_DIRS = ["sdk", "core", "cli"];
4
+ const TREESEED_WORKSPACE_PACKAGE_DIRS = ["sdk", "core", "cli", "agent"];
5
5
  function packageSortWeight(pkg) {
6
6
  const relativeDir = String(pkg.relativeDir ?? "");
7
7
  const dirName = relativeDir.split("/").pop() ?? "";
@@ -23,6 +23,32 @@ entries:
23
23
  sourcePriority:
24
24
  - machine-config
25
25
  - process-env
26
+ RAILWAY_API_TOKEN:
27
+ label: Railway API token
28
+ group: auth
29
+ description: Account-level Railway API token used by Treeseed to provision, reconcile, deploy, and inspect Market Railway services.
30
+ howToGet: Create a Railway account API token with access to the Treeseed Market project/workspace, then store it through Treeseed config as RAILWAY_API_TOKEN.
31
+ sensitivity: secret
32
+ targets:
33
+ - local-runtime
34
+ - github-secret
35
+ scopes:
36
+ - local
37
+ - staging
38
+ - prod
39
+ storage: shared
40
+ requirement: required
41
+ purposes:
42
+ - deploy
43
+ - config
44
+ validation:
45
+ kind: nonempty
46
+ minLength: 8
47
+ sourcePriority:
48
+ - machine-config
49
+ - process-env
50
+ relevanceRef: railwayManagedEnabled
51
+ requiredWhenRef: railwayManagedEnabled
26
52
  TREESEED_GITHUB_OWNER:
27
53
  label: GitHub repository owner
28
54
  group: github
@@ -351,7 +377,7 @@ entries:
351
377
  - staging
352
378
  - prod
353
379
  storage: shared
354
- requirement: conditional
380
+ requirement: optional
355
381
  purposes:
356
382
  - deploy
357
383
  - config
@@ -364,6 +390,142 @@ entries:
364
390
  relevanceRef: projectRegistrationEnabled
365
391
  requiredWhenRef: projectRegistrationEnabled
366
392
  defaultValueRef: marketBaseUrlDefault
393
+ TREESEED_MARKET_DATABASE_URL:
394
+ label: Market PostgreSQL database URL
395
+ group: hosting
396
+ cluster: hosting:market-control-plane
397
+ visibility: system
398
+ description: Railway PostgreSQL connection URL for Market control-plane tables. Browser/UI code must never receive this value.
399
+ howToGet: Provision the managed Market PostgreSQL resource through Treeseed/Railway reconciliation and store the generated connection URL as a service secret.
400
+ sensitivity: secret
401
+ targets:
402
+ - railway-secret
403
+ serviceTargets:
404
+ - api
405
+ - marketOperationsRunner
406
+ scopes:
407
+ - staging
408
+ - prod
409
+ storage: scoped
410
+ requirement: optional
411
+ purposes:
412
+ - deploy
413
+ - config
414
+ validation:
415
+ kind: url
416
+ sourcePriority:
417
+ - machine-config
418
+ - process-env
419
+ - treeseed.site.yaml
420
+ TREESEED_PLATFORM_RUNNER_SECRET:
421
+ label: Market operations runner secret
422
+ group: hosting
423
+ cluster: hosting:market-operations-runner
424
+ visibility: system
425
+ description: Bearer credential used only by the TreeSeed-owned Market operations runner to claim and update platform operation jobs.
426
+ howToGet: Generate a strong secret with `openssl rand -base64 48` and store it through `treeseed config` for staging and production.
427
+ sensitivity: secret
428
+ targets:
429
+ - railway-secret
430
+ serviceTargets:
431
+ - api
432
+ - marketOperationsRunner
433
+ scopes:
434
+ - staging
435
+ - prod
436
+ storage: scoped
437
+ requirement: conditional
438
+ purposes:
439
+ - deploy
440
+ - config
441
+ validation:
442
+ kind: nonempty
443
+ minLength: 24
444
+ sourcePriority:
445
+ - machine-config
446
+ - process-env
447
+ defaultValueRef: generatedSecret
448
+ TREESEED_PLATFORM_RUNNER_ID:
449
+ label: Market operations runner ID
450
+ group: hosting
451
+ cluster: hosting:market-operations-runner
452
+ visibility: system
453
+ description: Stable identity for the TreeSeed-owned Market operations runner service in each environment.
454
+ howToGet: Treeseed derives this from the environment. Override it only when replacing a runner identity deliberately.
455
+ sensitivity: plain
456
+ targets:
457
+ - railway-var
458
+ serviceTargets:
459
+ - marketOperationsRunner
460
+ scopes:
461
+ - staging
462
+ - prod
463
+ storage: scoped
464
+ requirement: conditional
465
+ purposes:
466
+ - deploy
467
+ - config
468
+ validation:
469
+ kind: nonempty
470
+ sourcePriority:
471
+ - machine-config
472
+ - process-env
473
+ - treeseed.site.yaml
474
+ defaultValueRef: platformRunnerIdDefault
475
+ TREESEED_PLATFORM_RUNNER_DATA_DIR:
476
+ label: Market operations runner data directory
477
+ group: hosting
478
+ cluster: hosting:market-operations-runner
479
+ visibility: system
480
+ description: Writable data directory mounted into the Market operations runner for repository workspaces and operation state.
481
+ howToGet: Use `/data` for Railway runner services with an attached volume.
482
+ sensitivity: plain
483
+ targets:
484
+ - railway-var
485
+ serviceTargets:
486
+ - marketOperationsRunner
487
+ scopes:
488
+ - staging
489
+ - prod
490
+ storage: scoped
491
+ requirement: conditional
492
+ purposes:
493
+ - deploy
494
+ - config
495
+ validation:
496
+ kind: nonempty
497
+ sourcePriority:
498
+ - machine-config
499
+ - process-env
500
+ - treeseed.site.yaml
501
+ defaultValueRef: platformRunnerDataDirDefault
502
+ TREESEED_PLATFORM_RUNNER_ENVIRONMENT:
503
+ label: Market operations runner environment
504
+ group: hosting
505
+ cluster: hosting:market-operations-runner
506
+ visibility: system
507
+ description: Runtime environment label reported by the Market operations runner in registration and heartbeat records.
508
+ howToGet: Treeseed derives this from the selected environment.
509
+ sensitivity: plain
510
+ targets:
511
+ - railway-var
512
+ serviceTargets:
513
+ - marketOperationsRunner
514
+ scopes:
515
+ - staging
516
+ - prod
517
+ storage: scoped
518
+ requirement: conditional
519
+ purposes:
520
+ - deploy
521
+ - config
522
+ validation:
523
+ kind: nonempty
524
+ sourcePriority:
525
+ - machine-config
526
+ - process-env
527
+ - treeseed.site.yaml
528
+ defaultValueRef: platformRunnerEnvironmentDefault
367
529
  TREESEED_CATALOG_MARKET_API_BASE_URLS:
368
530
  label: Treeseed catalog market API URLs
369
531
  group: hosting
@@ -439,4 +601,3 @@ entries:
439
601
  relevanceRef: projectRegistrationEnabled
440
602
  requiredWhenRef: projectRegistrationEnabled
441
603
  defaultValueRef: hostingProjectIdDefault
442
-
@@ -75,6 +75,7 @@ export type TreeseedEnvironmentEntry = {
75
75
  howToGet: string;
76
76
  sensitivity: TreeseedEnvironmentSensitivity;
77
77
  targets: TreeseedEnvironmentTarget[];
78
+ serviceTargets?: string[];
78
79
  scopes: TreeseedEnvironmentScope[];
79
80
  requirement: TreeseedEnvironmentRequirement;
80
81
  purposes: TreeseedEnvironmentPurpose[];
@@ -258,6 +258,12 @@ function resolveHostedProjectId(context) {
258
258
  function resolveRailwayWorkspaceDefault() {
259
259
  return "knowledge-coop";
260
260
  }
261
+ function resolvePlatformRunnerIdDefault(_context, scope) {
262
+ return scope === "prod" ? "market-ops-prod-1" : scope === "staging" ? "market-ops-staging-1" : "market-ops-local-1";
263
+ }
264
+ function resolvePlatformRunnerEnvironmentDefault(_context, scope) {
265
+ return scope === "prod" ? "production" : scope;
266
+ }
261
267
  function parseGitHubRepositorySlugFromRemote(remoteUrl) {
262
268
  const normalized = String(remoteUrl ?? "").trim();
263
269
  const sshMatch = normalized.match(/^git@github\.com:([^/]+)\/(.+?)(?:\.git)?$/u);
@@ -312,6 +318,9 @@ const VALUE_RESOLVERS = {
312
318
  hostingTeamIdDefault: (context) => resolveHostedTeamId(context),
313
319
  hostingProjectIdDefault: (context) => resolveHostedProjectId(context),
314
320
  railwayWorkspaceDefault: () => resolveRailwayWorkspaceDefault(),
321
+ platformRunnerIdDefault: (context, scope) => resolvePlatformRunnerIdDefault(context, scope),
322
+ platformRunnerEnvironmentDefault: (context, scope) => resolvePlatformRunnerEnvironmentDefault(context, scope),
323
+ platformRunnerDataDirDefault: () => "/data",
315
324
  githubOwnerDefault: (context) => resolveGitHubOwnerDefault(context),
316
325
  githubRepositoryNameDefault: (context) => resolveGitHubRepositoryNameDefault(context),
317
326
  githubRepositoryVisibilityDefault: () => "private",
@@ -0,0 +1,90 @@
1
+ import { type PlatformOperation, type PlatformOperationEvent, type PlatformRunnerClaimRequest, type PlatformRunnerHeartbeatRequest, type PlatformRunnerJobUpdateRequest, type PlatformRunnerRegistrationRequest } from './platform-operations.ts';
2
+ export type DatabaseProvider = 'd1' | 'sqlite' | 'postgres';
3
+ export interface RelationalDatabaseAdapter {
4
+ readonly provider: DatabaseProvider;
5
+ run(query: string, params?: unknown[]): Promise<void>;
6
+ first<T = Record<string, unknown>>(query: string, params?: unknown[]): Promise<T | null>;
7
+ all<T = Record<string, unknown>>(query: string, params?: unknown[]): Promise<T[]>;
8
+ exec?(query: string): Promise<void>;
9
+ transaction?<T>(callback: () => Promise<T>): Promise<T>;
10
+ close?(): Promise<void> | void;
11
+ }
12
+ export interface PlatformOperationStoreOptions {
13
+ database: RelationalDatabaseAdapter;
14
+ initializeSchema?: boolean;
15
+ now?: () => Date;
16
+ }
17
+ export interface CreatePlatformOperationStoreFromEnvOptions {
18
+ databaseUrl?: string | null;
19
+ initializeSchema?: boolean;
20
+ }
21
+ export declare const PLATFORM_OPERATION_SCHEMA_SQL = "\nCREATE TABLE IF NOT EXISTS platform_operations (\n\tid TEXT PRIMARY KEY,\n\tnamespace TEXT NOT NULL,\n\toperation TEXT NOT NULL,\n\tstatus TEXT NOT NULL,\n\ttarget TEXT NOT NULL,\n\tidempotency_key TEXT,\n\tinput_json TEXT NOT NULL DEFAULT '{}',\n\toutput_json TEXT,\n\terror_json TEXT,\n\trequested_by_type TEXT NOT NULL,\n\trequested_by_id TEXT,\n\tassigned_runner_id TEXT,\n\tlease_expires_at TEXT,\n\tcreated_at TEXT NOT NULL,\n\tupdated_at TEXT NOT NULL,\n\tstarted_at TEXT,\n\tfinished_at TEXT,\n\tcancelled_at TEXT\n);\n\nCREATE UNIQUE INDEX IF NOT EXISTS idx_platform_operations_idempotency\n\tON platform_operations(namespace, operation, idempotency_key)\n\tWHERE idempotency_key IS NOT NULL;\n\nCREATE INDEX IF NOT EXISTS idx_platform_operations_runnable\n\tON platform_operations(status, created_at ASC);\n\nCREATE TABLE IF NOT EXISTS platform_operation_events (\n\tid TEXT PRIMARY KEY,\n\toperation_id TEXT NOT NULL,\n\tseq INTEGER NOT NULL,\n\tkind TEXT NOT NULL,\n\tdata_json TEXT NOT NULL DEFAULT '{}',\n\tcreated_at TEXT NOT NULL,\n\tFOREIGN KEY (operation_id) REFERENCES platform_operations(id) ON DELETE CASCADE\n);\n\nCREATE UNIQUE INDEX IF NOT EXISTS idx_platform_operation_events_seq\n\tON platform_operation_events(operation_id, seq);\n\nCREATE TABLE IF NOT EXISTS market_operation_runners (\n\tid TEXT PRIMARY KEY,\n\trunner_key TEXT NOT NULL UNIQUE,\n\tname TEXT NOT NULL,\n\tenvironment TEXT NOT NULL,\n\tstatus TEXT NOT NULL DEFAULT 'online',\n\tversion TEXT,\n\tcapabilities_json TEXT NOT NULL DEFAULT '[]',\n\tactive_job_count INTEGER NOT NULL DEFAULT 0,\n\tmax_concurrent_jobs INTEGER NOT NULL DEFAULT 1,\n\theartbeat_at TEXT,\n\tmetadata_json TEXT NOT NULL DEFAULT '{}',\n\tcreated_at TEXT NOT NULL,\n\tupdated_at TEXT NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS platform_repository_claims (\n\tid TEXT PRIMARY KEY,\n\trepository_key TEXT NOT NULL,\n\trunner_id TEXT NOT NULL,\n\tworkspace_path TEXT NOT NULL,\n\tbranch TEXT,\n\tcommit_sha TEXT,\n\tclaim_state TEXT NOT NULL DEFAULT 'active',\n\tlease_expires_at TEXT,\n\tmetadata_json TEXT NOT NULL DEFAULT '{}',\n\tcreated_at TEXT NOT NULL,\n\tupdated_at TEXT NOT NULL\n);\n\nCREATE UNIQUE INDEX IF NOT EXISTS idx_platform_repository_claims_active\n\tON platform_repository_claims(repository_key, runner_id)\n\tWHERE claim_state = 'active';\n\nCREATE INDEX IF NOT EXISTS idx_platform_repository_claims_runner\n\tON platform_repository_claims(runner_id, claim_state);\n";
22
+ export declare function createD1RelationalAdapter(db: {
23
+ prepare(query: string): {
24
+ bind(...values: unknown[]): {
25
+ run(): Promise<unknown>;
26
+ first<T = Record<string, unknown>>(): Promise<T | null>;
27
+ all<T = Record<string, unknown>>(): Promise<{
28
+ results?: T[];
29
+ } | T[]>;
30
+ };
31
+ };
32
+ exec?(query: string): Promise<unknown>;
33
+ }): RelationalDatabaseAdapter;
34
+ export declare function createSqliteRelationalAdapter(path?: string | null): RelationalDatabaseAdapter;
35
+ export declare function createPostgresRelationalAdapter(databaseUrl: string): Promise<RelationalDatabaseAdapter>;
36
+ export declare function createRelationalAdapterFromUrl(databaseUrl: string): Promise<RelationalDatabaseAdapter>;
37
+ export declare function createPlatformOperationStoreFromEnv(options?: CreatePlatformOperationStoreFromEnvOptions): Promise<PlatformOperationStore>;
38
+ export declare class PlatformOperationStore {
39
+ private initialized;
40
+ private readonly database;
41
+ private readonly now;
42
+ private readonly initializeSchema;
43
+ constructor(options: PlatformOperationStoreOptions);
44
+ close(): Promise<void>;
45
+ ensureInitialized(): Promise<void>;
46
+ private appendPlatformOperationEvent;
47
+ register(request: PlatformRunnerRegistrationRequest): Promise<{
48
+ ok: true;
49
+ runner: Record<string, unknown> | null;
50
+ }>;
51
+ heartbeat(request: PlatformRunnerHeartbeatRequest): Promise<{
52
+ ok: true;
53
+ runner: Record<string, unknown> | null;
54
+ }>;
55
+ private upsertRunner;
56
+ getOperation(operationId: string): Promise<{
57
+ ok: true;
58
+ operation: PlatformOperation;
59
+ }>;
60
+ claimJob(input: PlatformRunnerClaimRequest): Promise<{
61
+ ok: true;
62
+ operation: PlatformOperation | null;
63
+ }>;
64
+ private assertRunnerUpdate;
65
+ appendEvent(operationId: string, request: PlatformRunnerJobUpdateRequest): Promise<{
66
+ ok: true;
67
+ event: PlatformOperationEvent;
68
+ }>;
69
+ renewLease(operationId: string, request: PlatformRunnerJobUpdateRequest & {
70
+ leaseSeconds?: number;
71
+ }): Promise<{
72
+ ok: true;
73
+ operation: PlatformOperation;
74
+ }>;
75
+ checkpoint(operationId: string, request: PlatformRunnerJobUpdateRequest): Promise<{
76
+ ok: true;
77
+ operation: PlatformOperation;
78
+ }>;
79
+ complete(operationId: string, request: PlatformRunnerJobUpdateRequest): Promise<{
80
+ ok: true;
81
+ operation: PlatformOperation;
82
+ }>;
83
+ fail(operationId: string, request: PlatformRunnerJobUpdateRequest): Promise<{
84
+ ok: true;
85
+ operation: PlatformOperation;
86
+ }>;
87
+ private upsertRepositoryClaim;
88
+ private renewRepositoryClaimsForRunner;
89
+ private releaseRepositoryClaimsForRunner;
90
+ }