@treeseed/sdk 0.10.28 → 0.11.0

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 (148) hide show
  1. package/README.md +207 -6
  2. package/dist/capacity-provider.d.ts +3 -1
  3. package/dist/capacity-provider.js +25 -5
  4. package/dist/control-plane.d.ts +1 -0
  5. package/dist/control-plane.js +38 -13
  6. package/dist/db/market-schema.d.ts +8860 -6172
  7. package/dist/db/market-schema.js +108 -0
  8. package/dist/db/node-sqlite.js +7 -2
  9. package/dist/hosting/apps.d.ts +12 -0
  10. package/dist/hosting/apps.js +107 -0
  11. package/dist/hosting/builtins.d.ts +25 -0
  12. package/dist/hosting/builtins.js +791 -0
  13. package/dist/hosting/contracts.d.ts +207 -0
  14. package/dist/hosting/contracts.js +0 -0
  15. package/dist/hosting/graph.d.ts +192 -0
  16. package/dist/hosting/graph.js +1106 -0
  17. package/dist/hosting/index.d.ts +4 -0
  18. package/dist/hosting/index.js +4 -0
  19. package/dist/index.d.ts +10 -3
  20. package/dist/index.js +63 -6
  21. package/dist/managed-dependencies.js +1 -2
  22. package/dist/market-client.d.ts +63 -3
  23. package/dist/market-client.js +83 -11
  24. package/dist/operations/services/bootstrap-runner.d.ts +3 -1
  25. package/dist/operations/services/bootstrap-runner.js +22 -2
  26. package/dist/operations/services/config-runtime.d.ts +10 -5
  27. package/dist/operations/services/config-runtime.js +209 -66
  28. package/dist/operations/services/deploy.d.ts +70 -7
  29. package/dist/operations/services/deploy.js +579 -64
  30. package/dist/operations/services/deployment-readiness.d.ts +30 -0
  31. package/dist/operations/services/deployment-readiness.js +175 -0
  32. package/dist/operations/services/git-workflow.d.ts +2 -1
  33. package/dist/operations/services/git-workflow.js +9 -3
  34. package/dist/operations/services/github-actions-verification.d.ts +1 -0
  35. package/dist/operations/services/github-actions-verification.js +1 -0
  36. package/dist/operations/services/github-api.js +1 -1
  37. package/dist/operations/services/github-automation.d.ts +1 -1
  38. package/dist/operations/services/github-automation.js +4 -3
  39. package/dist/operations/services/github-credentials.d.ts +13 -0
  40. package/dist/operations/services/github-credentials.js +58 -0
  41. package/dist/operations/services/hosted-service-checks.d.ts +63 -0
  42. package/dist/operations/services/hosted-service-checks.js +327 -0
  43. package/dist/operations/services/hub-provider-launch.js +3 -3
  44. package/dist/operations/services/live-hosted-service-checks.d.ts +25 -0
  45. package/dist/operations/services/live-hosted-service-checks.js +350 -0
  46. package/dist/operations/services/managed-host-security.js +1 -1
  47. package/dist/operations/services/operations-runner-smoke.d.ts +30 -0
  48. package/dist/operations/services/operations-runner-smoke.js +180 -0
  49. package/dist/operations/services/package-adapters.d.ts +95 -0
  50. package/dist/operations/services/package-adapters.js +288 -0
  51. package/dist/operations/services/package-reference-policy.d.ts +1 -0
  52. package/dist/operations/services/package-reference-policy.js +15 -2
  53. package/dist/operations/services/project-platform.d.ts +80 -22
  54. package/dist/operations/services/project-platform.js +49 -8
  55. package/dist/operations/services/project-web-monitor.js +26 -4
  56. package/dist/operations/services/railway-api.d.ts +88 -5
  57. package/dist/operations/services/railway-api.js +626 -35
  58. package/dist/operations/services/railway-deploy.d.ts +46 -40
  59. package/dist/operations/services/railway-deploy.js +261 -293
  60. package/dist/operations/services/release-candidate.d.ts +19 -0
  61. package/dist/operations/services/release-candidate.js +375 -38
  62. package/dist/operations/services/repository-save-orchestrator.d.ts +3 -1
  63. package/dist/operations/services/repository-save-orchestrator.js +279 -66
  64. package/dist/operations/services/runtime-tools.d.ts +1 -0
  65. package/dist/operations/services/runtime-tools.js +10 -9
  66. package/dist/operations/services/verification-cache.d.ts +25 -0
  67. package/dist/operations/services/verification-cache.js +71 -0
  68. package/dist/operations/services/workspace-dependency-mode.js +9 -1
  69. package/dist/operations/services/workspace-save.js +1 -1
  70. package/dist/operations/services/workspace-tools.js +2 -1
  71. package/dist/platform/contracts.d.ts +32 -1
  72. package/dist/platform/deploy-config.js +73 -8
  73. package/dist/platform/env.yaml +163 -35
  74. package/dist/platform/environment.d.ts +1 -0
  75. package/dist/platform/environment.js +74 -5
  76. package/dist/platform/plugin.d.ts +9 -0
  77. package/dist/platform-operation-store.js +2 -2
  78. package/dist/platform-operations.js +1 -1
  79. package/dist/reconcile/bootstrap-systems.js +2 -2
  80. package/dist/reconcile/builtin-adapters.js +372 -189
  81. package/dist/reconcile/contracts.d.ts +9 -5
  82. package/dist/reconcile/desired-state.d.ts +1 -0
  83. package/dist/reconcile/desired-state.js +5 -5
  84. package/dist/reconcile/engine.d.ts +5 -2
  85. package/dist/reconcile/engine.js +53 -32
  86. package/dist/reconcile/index.d.ts +2 -0
  87. package/dist/reconcile/index.js +2 -0
  88. package/dist/reconcile/live-acceptance.d.ts +79 -0
  89. package/dist/reconcile/live-acceptance.js +1615 -0
  90. package/dist/reconcile/platform.d.ts +104 -0
  91. package/dist/reconcile/platform.js +100 -0
  92. package/dist/reconcile/state.js +4 -4
  93. package/dist/reconcile/units.js +2 -2
  94. package/dist/scripts/deployment-readiness.js +20 -0
  95. package/dist/scripts/generate-treedx-openapi-types.js +186 -0
  96. package/dist/scripts/operations-runner-smoke.js +16 -0
  97. package/dist/scripts/release-verify.js +4 -1
  98. package/dist/scripts/tenant-workflow-action.js +10 -1
  99. package/dist/sdk-types.d.ts +169 -4
  100. package/dist/sdk-types.js +20 -2
  101. package/dist/sdk.d.ts +35 -24
  102. package/dist/sdk.js +186 -17
  103. package/dist/template-launch-requirements.js +9 -0
  104. package/dist/treedx/adapters.d.ts +6 -0
  105. package/dist/treedx/adapters.js +36 -0
  106. package/dist/treedx/client.d.ts +222 -0
  107. package/dist/treedx/client.js +871 -0
  108. package/dist/treedx/errors.d.ts +13 -0
  109. package/dist/treedx/errors.js +17 -0
  110. package/dist/treedx/federated-client.d.ts +27 -0
  111. package/dist/treedx/federated-client.js +158 -0
  112. package/dist/treedx/generated/openapi-types.d.ts +3558 -0
  113. package/dist/treedx/generated/openapi-types.js +0 -0
  114. package/dist/treedx/graph-adapter.d.ts +33 -0
  115. package/dist/treedx/graph-adapter.js +156 -0
  116. package/dist/treedx/index.d.ts +14 -0
  117. package/dist/treedx/index.js +48 -0
  118. package/dist/treedx/market-integration.d.ts +27 -0
  119. package/dist/treedx/market-integration.js +131 -0
  120. package/dist/treedx/ports.d.ts +166 -0
  121. package/dist/treedx/ports.js +231 -0
  122. package/dist/treedx/query-adapter.d.ts +19 -0
  123. package/dist/treedx/query-adapter.js +62 -0
  124. package/dist/treedx/registry-client.d.ts +11 -0
  125. package/dist/treedx/registry-client.js +19 -0
  126. package/dist/treedx/repository-adapter.d.ts +45 -0
  127. package/dist/treedx/repository-adapter.js +308 -0
  128. package/dist/treedx/sdk-integration.d.ts +27 -0
  129. package/dist/treedx/sdk-integration.js +63 -0
  130. package/dist/treedx/types.d.ts +1084 -0
  131. package/dist/treedx/types.js +8 -0
  132. package/dist/treedx/workspace-adapter.d.ts +27 -0
  133. package/dist/treedx/workspace-adapter.js +65 -0
  134. package/dist/treedx-backends.d.ts +218 -0
  135. package/dist/treedx-backends.js +632 -0
  136. package/dist/treedx-client.d.ts +86 -0
  137. package/dist/treedx-client.js +175 -0
  138. package/dist/treeseed/template-catalog/catalog.fixture.json +23 -23
  139. package/dist/workflow/operations.d.ts +119 -13
  140. package/dist/workflow/operations.js +309 -53
  141. package/dist/workflow-state.d.ts +13 -0
  142. package/dist/workflow-state.js +43 -26
  143. package/dist/workflow-support.d.ts +11 -3
  144. package/dist/workflow-support.js +67 -3
  145. package/dist/workflow.d.ts +5 -0
  146. package/drizzle/market/0004_treedx_market_integration.sql +99 -0
  147. package/package.json +34 -3
  148. package/templates/github/deploy-web.workflow.yml +39 -6
@@ -1,4 +1,5 @@
1
1
  export type ReleaseCandidateStatus = 'passed' | 'failed';
2
+ export type ReleaseCandidateMode = 'hybrid' | 'strict' | 'skip';
2
3
  export type ReleaseCandidateFailure = {
3
4
  code: string;
4
5
  scope: string;
@@ -15,6 +16,14 @@ export type ReleaseCandidateFingerprint = {
15
16
  lockfiles: Record<string, string | null>;
16
17
  selectedPackages: string[];
17
18
  };
19
+ export type ReleaseCandidateTopologyFingerprint = {
20
+ key: string;
21
+ policyVersion: string;
22
+ packageManifests: Record<string, string | null>;
23
+ lockfiles: Record<string, string | null>;
24
+ treeseedManifests: Record<string, string | null>;
25
+ selectedPackages: string[];
26
+ };
18
27
  export type ReleaseCandidateCheck = {
19
28
  name: string;
20
29
  status: 'passed' | 'skipped' | 'failed';
@@ -23,6 +32,9 @@ export type ReleaseCandidateCheck = {
23
32
  export type ReleaseCandidateReport = {
24
33
  status: ReleaseCandidateStatus;
25
34
  fingerprint: ReleaseCandidateFingerprint;
35
+ mode: ReleaseCandidateMode;
36
+ reason: string;
37
+ topology: ReleaseCandidateTopologyFingerprint;
26
38
  reused: boolean;
27
39
  checkedAt: string;
28
40
  failures: ReleaseCandidateFailure[];
@@ -33,9 +45,16 @@ export type ReleaseCandidateInput = {
33
45
  plannedVersions: Record<string, unknown>;
34
46
  selectedPackageNames?: string[];
35
47
  allowReuse?: boolean;
48
+ mode?: ReleaseCandidateMode;
36
49
  };
37
50
  export declare function buildReleaseCandidateFingerprint(input: ReleaseCandidateInput): ReleaseCandidateFingerprint;
51
+ export declare function buildReleaseCandidateTopologyFingerprint(input: ReleaseCandidateInput): ReleaseCandidateTopologyFingerprint;
38
52
  export declare function readCachedReleaseCandidateReport(root: string, key: string): ReleaseCandidateReport | null;
39
53
  export declare function writeReleaseCandidateReport(root: string, report: ReleaseCandidateReport): ReleaseCandidateReport;
40
54
  export declare function collectReleaseCandidateOutputFailures(line: string): string[];
55
+ export declare function isRootWebReleaseCandidateEntry(entry: {
56
+ id: string;
57
+ group?: string | null;
58
+ serviceTargets?: unknown;
59
+ }): boolean;
41
60
  export declare function runReleaseCandidateGate(input: ReleaseCandidateInput): Promise<ReleaseCandidateReport>;
@@ -6,15 +6,20 @@ import { dirname, join, relative, resolve } from "node:path";
6
6
  import { isTreeseedEnvironmentEntryRelevant, isTreeseedEnvironmentEntryRequired } from "../../platform/environment.js";
7
7
  import { maybeResolveGitHubRepositorySlug } from "./github-automation.js";
8
8
  import { createGitHubApiClient, listGitHubEnvironmentSecretNames, listGitHubEnvironmentVariableNames } from "./github-api.js";
9
- import { collectInternalDevReferenceIssues, normalizeGitRemoteForManifest } from "./package-reference-policy.js";
9
+ import { resolveGitHubCredentialForRepository } from "./github-credentials.js";
10
+ import { collectInternalDevReferenceIssues, installableInternalDependencyVersions, normalizeGitRemoteForManifest } from "./package-reference-policy.js";
10
11
  import { collectTreeseedEnvironmentContext, resolveTreeseedMachineEnvironmentValues, validateTreeseedCommandEnvironment } from "./config-runtime.js";
11
12
  import { loadDeployState } from "./deploy.js";
12
13
  import { loadCliDeployConfig } from "./runtime-tools.js";
13
14
  import { packagesWithScript, run, workspacePackages } from "./workspace-tools.js";
14
15
  import { createBuildWarningSummary, formatAllowedBuildWarnings } from "./build-warning-policy.js";
16
+ import { discoverTreeseedPackageAdapters } from "./package-adapters.js";
15
17
  const RELEASE_CANDIDATE_CACHE_DIR = ".treeseed/workflow/release-candidates";
16
- const RELEASE_CANDIDATE_POLICY_VERSION = "strict-output-v1";
18
+ const RELEASE_CANDIDATE_POLICY_VERSION = "package-adapters-v2-hybrid";
19
+ const RELEASE_CANDIDATE_TOPOLOGY_POLICY_VERSION = "topology-v1";
17
20
  const STABLE_SEMVER = /^\d+\.\d+\.\d+$/u;
21
+ const INTERNAL_DEPENDENCY_FIELDS = ["dependencies", "optionalDependencies", "peerDependencies", "devDependencies"];
22
+ const TOPOLOGY_SCRIPT_PREFIXES = ["build", "check", "prepare", "prepack", "postpack", "release:", "verify", "sandbox:"];
18
23
  const REHEARSAL_IGNORED_SEGMENTS = /* @__PURE__ */ new Set([
19
24
  ".git",
20
25
  ".treeseed",
@@ -24,6 +29,22 @@ const REHEARSAL_IGNORED_SEGMENTS = /* @__PURE__ */ new Set([
24
29
  "dist",
25
30
  "node_modules"
26
31
  ]);
32
+ const ROOT_WEB_EXCLUDED_DEPLOY_CONFIG_IDS = /* @__PURE__ */ new Set([
33
+ "DOCKERHUB_TOKEN",
34
+ "SECRET_KEY_BASE",
35
+ "TREEDX_JWT_HS256_SECRET",
36
+ "TREESEED_CREDENTIAL_SESSION_SECRET",
37
+ "TREESEED_PLATFORM_RUNNER_SECRET"
38
+ ]);
39
+ const API_APP_SERVICE_TARGETS = /* @__PURE__ */ new Set([
40
+ "api",
41
+ "operationsRunner",
42
+ "marketOperationsRunner",
43
+ "publicTreeDxFederation",
44
+ "publicTreeDxNode",
45
+ "treedx",
46
+ "treeDx"
47
+ ]);
27
48
  function nowIso() {
28
49
  return (/* @__PURE__ */ new Date()).toISOString();
29
50
  }
@@ -51,6 +72,9 @@ function safePackageJson(filePath) {
51
72
  return null;
52
73
  }
53
74
  }
75
+ function dockerManifestCheckMode() {
76
+ return process.env.TREESEED_RELEASE_CANDIDATE_DOCKER_MANIFEST_MODE === "check" ? "check" : "skip";
77
+ }
54
78
  function writeJsonFile(filePath, value) {
55
79
  writeFileSync(filePath, `${JSON.stringify(value, null, 2)}
56
80
  `, "utf8");
@@ -62,6 +86,9 @@ function packageScripts(filePath) {
62
86
  function releaseCandidateCachePath(root, key) {
63
87
  return resolve(root, RELEASE_CANDIDATE_CACHE_DIR, `${key}.json`);
64
88
  }
89
+ function releaseCandidateTopologyCachePath(root, key) {
90
+ return resolve(root, RELEASE_CANDIDATE_CACHE_DIR, `topology-${key}.json`);
91
+ }
65
92
  function ensureReleaseCandidateCacheDir(root) {
66
93
  const dir = resolve(root, RELEASE_CANDIDATE_CACHE_DIR);
67
94
  mkdirSync(dir, { recursive: true });
@@ -75,16 +102,19 @@ function ensureReleaseCandidateCacheDir(root) {
75
102
  function buildReleaseCandidateFingerprint(input) {
76
103
  const selectedPackages = [...new Set((input.selectedPackageNames ?? []).map(String))].sort();
77
104
  const selectedPackageSet = new Set(selectedPackages);
78
- const packages = workspacePackages(input.root).filter((pkg) => typeof pkg.name === "string" && pkg.name.startsWith("@treeseed/"));
105
+ const packages = discoverTreeseedPackageAdapters(input.root).filter((pkg) => selectedPackageSet.size === 0 || selectedPackageSet.has(pkg.id) || selectedPackageSet.has(pkg.name));
79
106
  const packageShas = sortedRecord(Object.fromEntries(
80
- packages.map((pkg) => [pkg.name, safeGitHead(pkg.dir)])
107
+ packages.map((pkg) => [pkg.id, safeGitHead(pkg.dir)])
81
108
  ));
82
109
  const plannedVersions = sortedRecord(Object.fromEntries(
83
110
  Object.entries(input.plannedVersions).filter(([name]) => name === "@treeseed/market" || selectedPackageSet.has(name)).map(([name, version]) => [name, String(version)])
84
111
  ));
85
112
  const lockfiles = sortedRecord({
86
113
  "@treeseed/market": fileSha256(resolve(input.root, "package-lock.json")),
87
- ...Object.fromEntries(packages.map((pkg) => [pkg.name, fileSha256(resolve(pkg.dir, "package-lock.json"))]))
114
+ ...Object.fromEntries(packages.map((pkg) => [
115
+ pkg.id,
116
+ pkg.kind === "node-typescript" ? fileSha256(resolve(pkg.dir, "package-lock.json")) : fileSha256(resolve(pkg.dir, "Cargo.lock")) ?? fileSha256(resolve(pkg.dir, "mix.lock"))
117
+ ]))
88
118
  });
89
119
  const base = {
90
120
  policyVersion: RELEASE_CANDIDATE_POLICY_VERSION,
@@ -99,6 +129,123 @@ function buildReleaseCandidateFingerprint(input) {
99
129
  key: sha256(JSON.stringify(base))
100
130
  };
101
131
  }
132
+ function isInternalTreeseedPackageName(name, internalPackageNames) {
133
+ return internalPackageNames.has(name) || name.startsWith("@treeseed/");
134
+ }
135
+ function normalizeDependencySpecForTopology(name, spec, internalPackageNames) {
136
+ if (!isInternalTreeseedPackageName(name, internalPackageNames)) return spec;
137
+ const value = String(spec ?? "").trim();
138
+ if (!value) return value;
139
+ if (/^(?:git\+|github:|gitlab:|bitbucket:|ssh:\/\/|https:\/\/|file:)/u.test(value) || /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u.test(value) || /^workspace:/u.test(value)) {
140
+ return "<internal-treeseed-reference>";
141
+ }
142
+ return value;
143
+ }
144
+ function normalizePackageJsonForTopology(packageJson, internalPackageNames) {
145
+ const normalized = {};
146
+ for (const [key, value] of Object.entries(packageJson)) {
147
+ if (key === "version") continue;
148
+ if (INTERNAL_DEPENDENCY_FIELDS.includes(key) && value && typeof value === "object" && !Array.isArray(value)) {
149
+ normalized[key] = sortedRecord(Object.fromEntries(Object.entries(value).map(([dependencyName, spec]) => [dependencyName, normalizeDependencySpecForTopology(dependencyName, spec, internalPackageNames)])));
150
+ continue;
151
+ }
152
+ if (key === "scripts" && value && typeof value === "object" && !Array.isArray(value)) {
153
+ normalized[key] = sortedRecord(Object.fromEntries(Object.entries(value).filter(([scriptName]) => TOPOLOGY_SCRIPT_PREFIXES.some((prefix) => scriptName === prefix || scriptName.startsWith(prefix))).map(([scriptName, command]) => [scriptName, command])));
154
+ continue;
155
+ }
156
+ if (["name", "type", "private", "workspaces", "main", "module", "exports", "files", "bin", "publishConfig", "repository", "engines", "packageManager"].includes(key)) {
157
+ normalized[key] = value;
158
+ }
159
+ }
160
+ return sortedRecord(normalized);
161
+ }
162
+ function normalizePackageLockForTopology(lockfile, internalPackageNames) {
163
+ const packages = lockfile.packages && typeof lockfile.packages === "object" && !Array.isArray(lockfile.packages) ? lockfile.packages : {};
164
+ const normalizedPackages = {};
165
+ for (const [path, entry] of Object.entries(packages)) {
166
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
167
+ const record = entry;
168
+ const packageName = typeof record.name === "string" ? record.name : path.startsWith("node_modules/") ? path.replace(/^node_modules\//u, "") : null;
169
+ const normalized = {};
170
+ for (const key of ["name", "link", "dev", "optional", "peer"]) {
171
+ if (key in record) normalized[key] = record[key];
172
+ }
173
+ for (const field of INTERNAL_DEPENDENCY_FIELDS) {
174
+ const values = record[field];
175
+ if (values && typeof values === "object" && !Array.isArray(values)) {
176
+ normalized[field] = sortedRecord(Object.fromEntries(Object.entries(values).map(([dependencyName, spec]) => [dependencyName, normalizeDependencySpecForTopology(dependencyName, spec, internalPackageNames)])));
177
+ }
178
+ }
179
+ if (packageName && isInternalTreeseedPackageName(packageName, internalPackageNames)) {
180
+ normalized.version = "<internal-treeseed-reference>";
181
+ normalized.resolved = "<internal-treeseed-reference>";
182
+ normalized.integrity = "<internal-treeseed-reference>";
183
+ } else {
184
+ for (const key of ["version", "resolved", "integrity", "license"]) {
185
+ if (key in record) normalized[key] = record[key];
186
+ }
187
+ }
188
+ normalizedPackages[path] = sortedRecord(normalized);
189
+ }
190
+ return sortedRecord({
191
+ name: lockfile.name,
192
+ lockfileVersion: lockfile.lockfileVersion,
193
+ requires: lockfile.requires,
194
+ packages: sortedRecord(normalizedPackages)
195
+ });
196
+ }
197
+ function topologyJsonHash(value) {
198
+ return sha256(JSON.stringify(value));
199
+ }
200
+ function topologyPackageHash(packageJsonPath, internalPackageNames) {
201
+ const packageJson = safePackageJson(packageJsonPath);
202
+ if (!packageJson) return null;
203
+ return topologyJsonHash(normalizePackageJsonForTopology(packageJson, internalPackageNames));
204
+ }
205
+ function topologyLockfileHash(lockfilePath, internalPackageNames) {
206
+ const lockfile = safePackageJson(lockfilePath);
207
+ if (!lockfile) return null;
208
+ return topologyJsonHash(normalizePackageLockForTopology(lockfile, internalPackageNames));
209
+ }
210
+ function buildReleaseCandidateTopologyFingerprint(input) {
211
+ const selectedPackages = [...new Set((input.selectedPackageNames ?? []).map(String))].sort();
212
+ const selectedPackageSet = new Set(selectedPackages);
213
+ const packages = discoverTreeseedPackageAdapters(input.root).filter((pkg) => selectedPackageSet.size === 0 || selectedPackageSet.has(pkg.id) || selectedPackageSet.has(pkg.name));
214
+ const internalPackageNames = /* @__PURE__ */ new Set([
215
+ "@treeseed/market",
216
+ ...discoverTreeseedPackageAdapters(input.root).map((pkg) => pkg.name)
217
+ ]);
218
+ const packageManifests = sortedRecord({
219
+ "@treeseed/market": topologyPackageHash(resolve(input.root, "package.json"), internalPackageNames),
220
+ ...Object.fromEntries(packages.map((pkg) => [pkg.id, topologyPackageHash(resolve(pkg.dir, "package.json"), internalPackageNames)]))
221
+ });
222
+ const lockfiles = sortedRecord({
223
+ "@treeseed/market": topologyLockfileHash(resolve(input.root, "package-lock.json"), internalPackageNames),
224
+ ...Object.fromEntries(packages.map((pkg) => [
225
+ pkg.id,
226
+ pkg.kind === "node-typescript" ? topologyLockfileHash(resolve(pkg.dir, "package-lock.json"), internalPackageNames) : fileSha256(resolve(pkg.dir, "Cargo.lock")) ?? fileSha256(resolve(pkg.dir, "mix.lock"))
227
+ ]))
228
+ });
229
+ const manifestEntries = {
230
+ "treeseed.site.yaml": fileSha256(resolve(input.root, "treeseed.site.yaml")),
231
+ "treeseed.package.yaml": fileSha256(resolve(input.root, "treeseed.package.yaml"))
232
+ };
233
+ for (const pkg of packages) {
234
+ manifestEntries[`${pkg.id}:treeseed.package.yaml`] = fileSha256(resolve(pkg.dir, "treeseed.package.yaml"));
235
+ manifestEntries[`${pkg.id}:treeseed.site.yaml`] = fileSha256(resolve(pkg.dir, "treeseed.site.yaml"));
236
+ }
237
+ const base = {
238
+ policyVersion: RELEASE_CANDIDATE_TOPOLOGY_POLICY_VERSION,
239
+ packageManifests,
240
+ lockfiles,
241
+ treeseedManifests: sortedRecord(manifestEntries),
242
+ selectedPackages
243
+ };
244
+ return {
245
+ ...base,
246
+ key: sha256(JSON.stringify(base))
247
+ };
248
+ }
102
249
  function readCachedReleaseCandidateReport(root, key) {
103
250
  const cachePath = releaseCandidateCachePath(root, key);
104
251
  if (!existsSync(cachePath)) return null;
@@ -112,8 +259,27 @@ function writeReleaseCandidateReport(root, report) {
112
259
  ensureReleaseCandidateCacheDir(root);
113
260
  writeFileSync(releaseCandidateCachePath(root, report.fingerprint.key), `${JSON.stringify(report, null, 2)}
114
261
  `, "utf8");
262
+ if (report.status === "passed" && report.mode === "strict") {
263
+ writeFileSync(releaseCandidateTopologyCachePath(root, report.topology.key), `${JSON.stringify({
264
+ key: report.topology.key,
265
+ checkedAt: report.checkedAt,
266
+ fingerprintKey: report.fingerprint.key,
267
+ mode: report.mode,
268
+ reason: report.reason
269
+ }, null, 2)}
270
+ `, "utf8");
271
+ }
115
272
  return report;
116
273
  }
274
+ function readStrictTopologyProof(root, key) {
275
+ const cachePath = releaseCandidateTopologyCachePath(root, key);
276
+ if (!existsSync(cachePath)) return null;
277
+ try {
278
+ return JSON.parse(readFileSync(cachePath, "utf8"));
279
+ } catch {
280
+ return null;
281
+ }
282
+ }
117
283
  function addFailure(failures, failure) {
118
284
  failures.push({
119
285
  ...failure,
@@ -121,53 +287,108 @@ function addFailure(failures, failure) {
121
287
  provider: failure.provider ?? null
122
288
  });
123
289
  }
124
- function packageReadinessChecks(root, selectedPackageNames, failures) {
290
+ function packageReadinessChecks(root, selectedPackageNames, failures, options = {}) {
125
291
  if (selectedPackageNames.length === 0) {
126
292
  return { name: "package-release-readiness", status: "skipped", detail: "No packages are selected for this release." };
127
293
  }
128
294
  const selected = new Set(selectedPackageNames);
129
- const packages = workspacePackages(root).filter((pkg) => selected.has(pkg.name));
295
+ const packages = discoverTreeseedPackageAdapters(root).filter((pkg) => selected.has(pkg.id) || selected.has(pkg.name));
130
296
  for (const pkg of packages) {
297
+ checkPackageAdapterReadiness(pkg, failures, options);
298
+ }
299
+ return {
300
+ name: "package-release-readiness",
301
+ status: failures.some((failure) => failure.code.startsWith("missing_") || failure.code === "npm_pack_dry_run_failed" || failure.code === "docker_manifest_check_failed") ? "failed" : "passed",
302
+ detail: `Checked ${packages.length} selected package adapter${packages.length === 1 ? "" : "s"}${options.skipNpmPack ? " without npm pack rehearsal" : ""}: ${packages.map((pkg) => `${pkg.id} (${pkg.kind})`).join(", ") || "none"}.`
303
+ };
304
+ }
305
+ function checkPackageAdapterReadiness(pkg, failures, options = {}) {
306
+ if (pkg.kind === "node-typescript") {
131
307
  const packageJson = safePackageJson(resolve(pkg.dir, "package.json"));
132
308
  const scripts = packageJson?.scripts && typeof packageJson.scripts === "object" && !Array.isArray(packageJson.scripts) ? packageJson.scripts : {};
133
309
  if (!existsSync(resolve(pkg.dir, ".github", "workflows", "publish.yml"))) {
134
310
  addFailure(failures, {
135
311
  code: "missing_publish_workflow",
136
- scope: pkg.name,
312
+ scope: pkg.id,
137
313
  provider: "github",
138
- message: `${pkg.name} is missing .github/workflows/publish.yml.`
314
+ message: `${pkg.id} is missing .github/workflows/publish.yml.`
139
315
  });
140
316
  }
141
317
  if (typeof scripts["release:publish"] !== "string") {
142
318
  addFailure(failures, {
143
319
  code: "missing_publish_script",
144
- scope: pkg.name,
145
- message: `${pkg.name} is missing a release:publish script.`
320
+ scope: pkg.id,
321
+ message: `${pkg.id} is missing a release:publish script.`
146
322
  });
147
323
  }
148
- if (typeof scripts["verify:local"] !== "string" && typeof scripts["verify"] !== "string" && typeof scripts["verify:action"] !== "string") {
324
+ if (typeof scripts["verify:local"] !== "string" && typeof scripts.verify !== "string" && typeof scripts["verify:action"] !== "string") {
149
325
  addFailure(failures, {
150
326
  code: "missing_verify_script",
151
- scope: pkg.name,
152
- message: `${pkg.name} is missing a release-ready verify script.`
327
+ scope: pkg.id,
328
+ message: `${pkg.id} is missing a release-ready verify script.`
153
329
  });
154
330
  }
155
- try {
156
- run("npm", ["pack", "--dry-run"], { cwd: pkg.dir, capture: true, timeoutMs: 12e4 });
157
- } catch (error) {
158
- addFailure(failures, {
159
- code: "npm_pack_dry_run_failed",
160
- scope: pkg.name,
161
- message: `${pkg.name} failed npm pack --dry-run.`,
162
- details: { error: error instanceof Error ? error.message : String(error) }
163
- });
331
+ if (!options.skipNpmPack) {
332
+ try {
333
+ run("npm", ["pack", "--dry-run"], { cwd: pkg.dir, capture: true, timeoutMs: 12e4 });
334
+ } catch (error) {
335
+ addFailure(failures, {
336
+ code: "npm_pack_dry_run_failed",
337
+ scope: pkg.id,
338
+ message: `${pkg.id} failed npm pack --dry-run.`,
339
+ details: { error: error instanceof Error ? error.message : String(error) }
340
+ });
341
+ }
342
+ }
343
+ return;
344
+ }
345
+ if (!pkg.version) {
346
+ addFailure(failures, {
347
+ code: "missing_package_version",
348
+ scope: pkg.id,
349
+ message: `${pkg.id} is missing a readable BEAM package version.`,
350
+ details: { versionSource: pkg.versionSource }
351
+ });
352
+ }
353
+ if (pkg.id === "treedx" && !existsSync(resolve(pkg.dir, ".github", "workflows", "dev-image.yml"))) {
354
+ addFailure(failures, {
355
+ code: "missing_development_image_workflow",
356
+ scope: pkg.id,
357
+ provider: "github",
358
+ message: `${pkg.id} is missing .github/workflows/dev-image.yml for staging-safe development image publication.`
359
+ });
360
+ }
361
+ if (!pkg.verifyCommands.local) {
362
+ addFailure(failures, {
363
+ code: "missing_verify_script",
364
+ scope: pkg.id,
365
+ message: `${pkg.id} is missing a BEAM package local verification command.`
366
+ });
367
+ }
368
+ if (!pkg.verifyCommands.release) {
369
+ addFailure(failures, {
370
+ code: "missing_release_gate",
371
+ scope: pkg.id,
372
+ message: `${pkg.id} is missing a BEAM package release gate command.`
373
+ });
374
+ }
375
+ if (dockerManifestCheckMode() !== "check") return;
376
+ for (const artifact of pkg.artifacts.filter((entry) => entry.provider === "docker")) {
377
+ for (const tag of artifact.tags ?? []) {
378
+ if (tag.includes("<")) continue;
379
+ try {
380
+ run("docker", ["manifest", "inspect", `${artifact.name}:${tag}`], { cwd: pkg.dir, capture: true, timeoutMs: 12e4 });
381
+ } catch (error) {
382
+ addFailure(failures, {
383
+ code: "docker_manifest_check_failed",
384
+ scope: pkg.id,
385
+ provider: "docker",
386
+ message: `${pkg.id} Docker artifact is not published: ${artifact.name}:${tag}.`,
387
+ details: { error: error instanceof Error ? error.message : String(error) }
388
+ });
389
+ }
164
390
  }
165
391
  }
166
- return {
167
- name: "package-release-readiness",
168
- status: failures.some((failure) => failure.code.startsWith("missing_") || failure.code === "npm_pack_dry_run_failed") ? "failed" : "passed",
169
- detail: `Checked ${packages.length} selected package${packages.length === 1 ? "" : "s"}.`
170
- };
171
392
  }
172
393
  function copyWorkspaceForProductionRehearsal(root) {
173
394
  const tempParent = mkdtempSync(join(tmpdir(), "treeseed-release-candidate-"));
@@ -187,7 +408,8 @@ function applyPlannedStableMetadata(root, plannedVersions) {
187
408
  const stableVersions = new Map(
188
409
  Object.entries(plannedVersions).filter(([, version]) => STABLE_SEMVER.test(version))
189
410
  );
190
- const stableGitReferences = stablePackageGitReferences(root, stableVersions);
411
+ const dependencyVersions = installableInternalDependencyVersions(root, stableVersions);
412
+ const stableGitReferences = stablePackageGitReferences(root, dependencyVersions);
191
413
  const targets = [
192
414
  { name: "@treeseed/market", dir: root },
193
415
  ...workspacePackages(root).map((pkg) => ({ name: pkg.name, dir: pkg.dir }))
@@ -205,7 +427,7 @@ function applyPlannedStableMetadata(root, plannedVersions) {
205
427
  for (const field of ["dependencies", "optionalDependencies", "peerDependencies", "devDependencies"]) {
206
428
  const values = packageJson[field];
207
429
  if (!values || typeof values !== "object" || Array.isArray(values)) continue;
208
- for (const [dependencyName, version] of stableVersions.entries()) {
430
+ for (const [dependencyName, version] of dependencyVersions.entries()) {
209
431
  if (!(dependencyName in values)) continue;
210
432
  const dependencySpec = stableGitReferences.get(dependencyName) ?? version;
211
433
  if (String(values[dependencyName]) === dependencySpec) continue;
@@ -285,6 +507,15 @@ function runNpmRehearsalCommand(args, options) {
285
507
  ].join("\n"));
286
508
  }
287
509
  }
510
+ function npmRehearsalEnv(extra = {}) {
511
+ return {
512
+ ...process.env,
513
+ npm_config_jobs: process.env.npm_config_jobs ?? "2",
514
+ npm_config_audit: process.env.npm_config_audit ?? "false",
515
+ npm_config_fund: process.env.npm_config_fund ?? "false",
516
+ ...extra
517
+ };
518
+ }
288
519
  function collectReleaseCandidateOutputFailures(line) {
289
520
  const value = String(line ?? "").trim();
290
521
  if (!value) return [];
@@ -315,17 +546,18 @@ function runProductionDependencyRehearsal(root, plannedVersions, selectedPackage
315
546
  const copied = copyWorkspaceForProductionRehearsal(root);
316
547
  tempParent = copied.tempParent;
317
548
  applyPlannedStableMetadata(copied.tempRoot, plannedVersions);
318
- runNpmRehearsalCommand(["install", "--package-lock-only", "--ignore-scripts"], { cwd: copied.tempRoot, timeoutMs: 3e5 });
319
- runNpmRehearsalCommand(["ci", "--ignore-scripts"], { cwd: copied.tempRoot, timeoutMs: 6e5 });
549
+ const npmEnv = npmRehearsalEnv();
550
+ runNpmRehearsalCommand(["install", "--package-lock-only", "--ignore-scripts", "--no-audit", "--no-fund", "--prefer-offline"], { cwd: copied.tempRoot, timeoutMs: 3e5, env: npmEnv });
551
+ runNpmRehearsalCommand(["ci", "--ignore-scripts", "--no-audit", "--no-fund", "--prefer-offline"], { cwd: copied.tempRoot, timeoutMs: 6e5, env: npmEnv });
320
552
  buildRehearsalWorkspacePackageArtifacts(copied.tempRoot);
321
553
  const scriptName = rehearsalVerifyScript(copied.tempRoot);
322
554
  if (scriptName) {
323
555
  const packageJson = safePackageJson(resolve(copied.tempRoot, "package.json"));
324
- const parallelMarketVerify = packageJson?.name === "@treeseed/market" && (scriptName === "verify:direct" || scriptName === "verify:local" || scriptName === "verify");
556
+ const parallelMarketVerify = process.env.TREESEED_RELEASE_CANDIDATE_MARKET_VERIFY_PARALLEL === "1" && packageJson?.name === "@treeseed/market" && (scriptName === "verify:direct" || scriptName === "verify:local" || scriptName === "verify");
325
557
  runNpmRehearsalCommand(["run", scriptName], {
326
558
  cwd: copied.tempRoot,
327
559
  timeoutMs: 9e5,
328
- env: parallelMarketVerify ? { ...process.env, TREESEED_VERIFY_PARALLEL: "1" } : process.env
560
+ env: parallelMarketVerify ? npmRehearsalEnv({ TREESEED_VERIFY_PARALLEL: "1" }) : npmEnv
329
561
  });
330
562
  }
331
563
  const postInstallIssues = collectInternalDevReferenceIssues(copied.tempRoot, selectedPackageSet);
@@ -400,11 +632,95 @@ function dependencyRehearsalChecks(root, plannedVersions, selectedPackageNames,
400
632
  detail: `${devReferenceIssues.length > 0 ? `Rehearsed stable replacements for ${devReferenceIssues.length} internal dev reference${devReferenceIssues.length === 1 ? "" : "s"}.` : "Checked planned stable versions and internal dependency references."} ${rehearsalDetail}`
401
633
  };
402
634
  }
635
+ function validateInternalGitReferenceTags(root, failures) {
636
+ const issues = collectInternalDevReferenceIssues(root);
637
+ let checked = 0;
638
+ const seen = /* @__PURE__ */ new Set();
639
+ for (const issue of issues) {
640
+ const spec = issue.spec;
641
+ const hashIndex = spec.lastIndexOf("#");
642
+ if (hashIndex === -1 || !/^(?:git\+|github:|gitlab:|bitbucket:|ssh:\/\/|https:\/\/|file:)/u.test(spec)) continue;
643
+ const rawRemote = spec.slice(0, hashIndex).replace(/^git\+/u, "");
644
+ const githubMatch = rawRemote.match(/^github:([^/]+\/[^/]+?)(?:\.git)?$/u);
645
+ const remote = githubMatch ? `https://github.com/${githubMatch[1]}.git` : rawRemote;
646
+ const tagName = decodeURIComponent(spec.slice(hashIndex + 1));
647
+ if (!remote || !tagName) continue;
648
+ const key = `${remote}#${tagName}`;
649
+ if (seen.has(key)) continue;
650
+ seen.add(key);
651
+ checked += 1;
652
+ try {
653
+ run("git", ["ls-remote", "--exit-code", "--tags", remote, `refs/tags/${tagName}`], { cwd: root, capture: true, timeoutMs: 12e4 });
654
+ } catch (error) {
655
+ addFailure(failures, {
656
+ code: "internal_git_tag_missing",
657
+ scope: issue.dependencyName ?? issue.repoName,
658
+ provider: "git",
659
+ message: `Internal git dependency tag is not reachable: ${issue.dependencyName ?? issue.repoName}#${tagName}.`,
660
+ details: { spec, remote, tagName, filePath: issue.filePath, error: error instanceof Error ? error.message : String(error) }
661
+ });
662
+ }
663
+ }
664
+ return checked;
665
+ }
666
+ function lightweightDependencyChecks(root, failures) {
667
+ const before = failures.length;
668
+ try {
669
+ runNpmRehearsalCommand(["install", "--package-lock-only", "--ignore-scripts", "--dry-run", "--workspaces=false", "--no-audit", "--no-fund", "--prefer-offline"], {
670
+ cwd: root,
671
+ timeoutMs: 3e5,
672
+ env: npmRehearsalEnv()
673
+ });
674
+ } catch (error) {
675
+ addFailure(failures, {
676
+ code: "lockfile_dry_run_failed",
677
+ scope: "@treeseed/market",
678
+ message: "Root lockfile dry-run validation failed.",
679
+ details: { error: error instanceof Error ? error.message : String(error) }
680
+ });
681
+ }
682
+ const checkedTags = validateInternalGitReferenceTags(root, failures);
683
+ return {
684
+ name: "hybrid-dependency-readiness",
685
+ status: failures.length > before ? "failed" : "passed",
686
+ detail: `Validated root lockfile with npm install --package-lock-only --ignore-scripts --dry-run --workspaces=false and checked ${checkedTags} internal git tag${checkedTags === 1 ? "" : "s"} without temp install rehearsal.`
687
+ };
688
+ }
689
+ function skippedReleaseCandidateReport(fingerprint, topology) {
690
+ return {
691
+ status: "passed",
692
+ fingerprint,
693
+ mode: "skip",
694
+ reason: "Release-candidate checks skipped by explicit request.",
695
+ topology,
696
+ reused: false,
697
+ checkedAt: nowIso(),
698
+ failures: [],
699
+ checks: [{
700
+ name: "release-candidate",
701
+ status: "skipped",
702
+ detail: "Skipped by --release-candidate skip or TREESEED_RELEASE_CANDIDATE_MODE=skip."
703
+ }]
704
+ };
705
+ }
706
+ function entryServiceTargets(entry) {
707
+ return Array.isArray(entry.serviceTargets) ? entry.serviceTargets.filter((target) => typeof target === "string") : [];
708
+ }
709
+ function isRootWebReleaseCandidateEntry(entry) {
710
+ if (ROOT_WEB_EXCLUDED_DEPLOY_CONFIG_IDS.has(entry.id)) return false;
711
+ const serviceTargets = entryServiceTargets(entry);
712
+ if (serviceTargets.length > 0 && serviceTargets.every((target) => API_APP_SERVICE_TARGETS.has(target))) {
713
+ return false;
714
+ }
715
+ if (entry.group === "docker") return false;
716
+ return true;
717
+ }
403
718
  function localConfigCheck(root, scope, failures) {
404
719
  try {
405
720
  const report = validateTreeseedCommandEnvironment({ tenantRoot: root, scope, purpose: "deploy" });
406
721
  const problems = [...report.validation.missing, ...report.validation.invalid];
407
722
  for (const problem of problems) {
723
+ if (!isRootWebReleaseCandidateEntry(problem.entry)) continue;
408
724
  addFailure(failures, {
409
725
  code: "missing_local_config",
410
726
  scope,
@@ -437,7 +753,11 @@ async function githubRemoteConfigCheck(root, scope, failures) {
437
753
  try {
438
754
  const environment = scope === "prod" ? "production" : scope;
439
755
  const expected = expectedGitHubDeployEnvironment(root, scope);
440
- const client = createGitHubApiClient();
756
+ const values = resolveTreeseedMachineEnvironmentValues(root, scope);
757
+ const credential = resolveGitHubCredentialForRepository(repository, { values, env: process.env });
758
+ const client = createGitHubApiClient({
759
+ env: credential.token ? { GH_TOKEN: credential.token, GITHUB_TOKEN: credential.token } : process.env
760
+ });
441
761
  const [secretNames, variableNames] = await Promise.all([
442
762
  listGitHubEnvironmentSecretNames(repository, environment, { client }),
443
763
  listGitHubEnvironmentVariableNames(repository, environment, { client })
@@ -476,6 +796,7 @@ function expectedGitHubDeployEnvironment(root, scope) {
476
796
  const registry = collectTreeseedEnvironmentContext(root);
477
797
  const values = resolveTreeseedMachineEnvironmentValues(root, scope);
478
798
  const expectedEntries = registry.entries.filter((entry) => {
799
+ if (!isRootWebReleaseCandidateEntry(entry)) return false;
479
800
  if (!isTreeseedEnvironmentEntryRelevant(entry, registry.context, scope, "deploy")) return false;
480
801
  if (isTreeseedEnvironmentEntryRequired(entry, registry.context, scope, "deploy")) return true;
481
802
  return typeof values[entry.id] === "string" && values[entry.id].trim().length > 0;
@@ -572,33 +893,47 @@ function migrationCompatibilityChecks(root, failures) {
572
893
  return {
573
894
  name: "migration-compatibility",
574
895
  status: missing.length > 0 ? "failed" : "passed",
575
- detail: "Checked required Drizzle migration artifacts for Market PostgreSQL and SDK D1."
896
+ detail: "Checked required Drizzle migration artifacts for Treeseed PostgreSQL and SDK D1."
576
897
  };
577
898
  }
578
899
  async function runReleaseCandidateGate(input) {
579
900
  const fingerprint = buildReleaseCandidateFingerprint(input);
901
+ const topology = buildReleaseCandidateTopologyFingerprint(input);
902
+ const requestedMode = input.mode ?? "strict";
580
903
  if (input.allowReuse !== false) {
581
904
  const cached = readCachedReleaseCandidateReport(input.root, fingerprint.key);
582
905
  if (cached?.status === "passed") {
583
906
  return {
584
907
  ...cached,
908
+ mode: cached.mode ?? "strict",
909
+ reason: cached.reason ?? "Reused cached release-candidate report.",
910
+ topology: cached.topology ?? topology,
585
911
  reused: true
586
912
  };
587
913
  }
588
914
  }
915
+ if (requestedMode === "skip") {
916
+ return skippedReleaseCandidateReport(fingerprint, topology);
917
+ }
589
918
  const selectedPackageNames = [...new Set((input.selectedPackageNames ?? []).map(String))].sort();
590
919
  const plannedVersions = Object.fromEntries(
591
920
  Object.entries(input.plannedVersions).map(([name, version]) => [name, String(version)])
592
921
  );
922
+ const strictTopologyProof = readStrictTopologyProof(input.root, topology.key);
923
+ const effectiveMode = requestedMode;
924
+ const reason = requestedMode === "hybrid" ? strictTopologyProof ? `Hybrid release-candidate selected; strict rehearsal skipped because topology ${topology.key.slice(0, 12)} was already proven.` : `Hybrid release-candidate selected; lightweight checks used because strict topology proof is reserved for promotion lanes.` : "Strict release-candidate selected.";
593
925
  const failures = [];
594
926
  const checks = [];
595
- checks.push(dependencyRehearsalChecks(input.root, plannedVersions, selectedPackageNames, failures));
596
- checks.push(packageReadinessChecks(input.root, selectedPackageNames, failures));
927
+ checks.push(effectiveMode === "hybrid" ? lightweightDependencyChecks(input.root, failures) : dependencyRehearsalChecks(input.root, plannedVersions, selectedPackageNames, failures));
928
+ checks.push(packageReadinessChecks(input.root, selectedPackageNames, failures, { skipNpmPack: effectiveMode === "hybrid" }));
597
929
  checks.push(await configParityChecks(input.root, failures));
598
930
  checks.push(migrationCompatibilityChecks(input.root, failures));
599
931
  const report = {
600
932
  status: failures.length === 0 ? "passed" : "failed",
601
933
  fingerprint,
934
+ mode: effectiveMode,
935
+ reason,
936
+ topology,
602
937
  reused: false,
603
938
  checkedAt: nowIso(),
604
939
  failures,
@@ -609,7 +944,9 @@ async function runReleaseCandidateGate(input) {
609
944
  }
610
945
  export {
611
946
  buildReleaseCandidateFingerprint,
947
+ buildReleaseCandidateTopologyFingerprint,
612
948
  collectReleaseCandidateOutputFailures,
949
+ isRootWebReleaseCandidateEntry,
613
950
  readCachedReleaseCandidateReport,
614
951
  runReleaseCandidateGate,
615
952
  writeReleaseCandidateReport