@treeseed/sdk 0.12.5 → 0.12.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.
@@ -221,6 +221,7 @@ function beamPackageAdapter(root, dir) {
221
221
  const versionSourceRel = typeof manifest?.versionSource === "string" && manifest.versionSource.trim() ? manifest.versionSource.trim() : existsSync(resolve(dir, "apps/api/mix.exs")) ? "apps/api/mix.exs" : "mix.exs";
222
222
  const versionSource = resolve(dir, versionSourceRel);
223
223
  const image = typeof manifest?.image === "string" && manifest.image.trim() ? manifest.image.trim() : id === "treedx" ? "treeseed/treedx" : null;
224
+ const dockerArtifacts = manifestDockerArtifacts(manifest?.artifacts);
224
225
  const repository = stringValue(manifest?.repository) ?? (id === "treedx" ? "treeseed-ai/treedx" : null);
225
226
  const hostedVerifyWorkflow = stringValue(manifest?.hostedVerifyWorkflow) ?? stringValue(stringRecord(manifest?.releaseGate).workflow) ?? (existsSync(resolve(dir, ".github/workflows/release-gate.yml")) ? "release-gate.yml" : null);
226
227
  const projectArchitecture = normalizeTreeseedPackageProjectArchitecture(manifest?.projectArchitecture, id);
@@ -246,7 +247,16 @@ function beamPackageAdapter(root, dir) {
246
247
  local: commandFromScript(dir, local, "local"),
247
248
  release: commandFromScript(dir, releaseGate, "release")
248
249
  },
249
- artifacts: image ? [{
250
+ artifacts: dockerArtifacts.length > 0 ? dockerArtifacts.map((artifact) => ({
251
+ provider: "docker",
252
+ name: artifact.name,
253
+ tags: version ? [version, shaTag] : [shaTag],
254
+ dockerfile: artifact.dockerfile ?? "Dockerfile",
255
+ context: artifact.context ?? ".",
256
+ target: artifact.target,
257
+ role: artifact.role ?? id,
258
+ architectures: artifact.architectures.length > 0 ? artifact.architectures : stringArray(stringRecord(manifest?.dockerImages).architectures)
259
+ })) : image ? [{
250
260
  provider: "docker",
251
261
  name: image,
252
262
  tags: version ? [version, shaTag] : [shaTag],
@@ -256,7 +266,7 @@ function beamPackageAdapter(root, dir) {
256
266
  role: id,
257
267
  architectures: stringArray(stringRecord(manifest?.dockerImages).architectures)
258
268
  }] : [],
259
- releaseChecks: image ? [{ kind: "docker-manifest", name: "Docker image manifest", detail: `${image}:${version ?? "<version>"}` }] : [],
269
+ releaseChecks: dockerArtifacts.length > 0 ? dockerArtifacts.map((artifact) => ({ kind: "docker-manifest", name: `${artifact.name} Docker image manifest`, detail: `${artifact.name}:${version ?? "<version>"}` })) : image ? [{ kind: "docker-manifest", name: "Docker image manifest", detail: `${image}:${version ?? "<version>"}` }] : [],
260
270
  metadata: {
261
271
  hasCargo: existsSync(resolve(dir, "Cargo.toml")),
262
272
  hasDockerfile: existsSync(resolve(dir, "Dockerfile")),
@@ -16,7 +16,7 @@ function internalDependencyFields(packageJson) {
16
16
  return INTERNAL_DEPENDENCY_FIELDS.filter((field) => packageJson[field] && typeof packageJson[field] === "object" && !Array.isArray(packageJson[field]));
17
17
  }
18
18
  function isPrereleaseVersion(version) {
19
- return /^\d+\.\d+\.\d+-[0-9A-Za-z.-]+$/u.test(String(version).trim());
19
+ return /^[~^]?\d+\.\d+\.\d+-[0-9A-Za-z.-]+$/u.test(String(version).trim());
20
20
  }
21
21
  function isStableVersion(version) {
22
22
  return /^\d+\.\d+\.\d+$/u.test(String(version).trim());
@@ -136,13 +136,22 @@ function updateInternalDependencySpecs(packageJson, references) {
136
136
  return changed;
137
137
  }
138
138
  function installableInternalDependencyVersions(root = workspaceRoot(), versions) {
139
- const publishTargets = /* @__PURE__ */ new Map();
139
+ const installablePackages = /* @__PURE__ */ new Set();
140
140
  for (const adapter of discoverTreeseedPackageAdapters(root)) {
141
- const publishTarget = adapter.publishTarget ?? "npm";
142
- publishTargets.set(adapter.id, publishTarget);
143
- publishTargets.set(adapter.name, publishTarget);
141
+ if (adapter.publishTarget === "npm" || publicNpmPackageManifest(adapter.dir)) {
142
+ installablePackages.add(adapter.id);
143
+ installablePackages.add(adapter.name);
144
+ }
144
145
  }
145
- return new Map([...versions.entries()].filter(([packageName]) => (publishTargets.get(packageName) ?? "npm") === "npm"));
146
+ return new Map([...versions.entries()].filter(([packageName]) => installablePackages.has(packageName)));
147
+ }
148
+ function publicNpmPackageManifest(packageRoot) {
149
+ const packageJsonPath = resolve(packageRoot, "package.json");
150
+ if (!existsSync(packageJsonPath)) return false;
151
+ const packageJson = readJson(packageJsonPath);
152
+ if (packageJson.private === true) return false;
153
+ const publishConfig = packageJson.publishConfig;
154
+ return Boolean(publishConfig && typeof publishConfig === "object" && publishConfig.access === "public");
146
155
  }
147
156
  function rewriteInternalDependenciesToStableVersions(root = workspaceRoot(), versions) {
148
157
  const rewrites = [];
@@ -83,6 +83,27 @@ function packageUnitFromAdapter(adapter) {
83
83
  githubEnvironments: Array.isArray(adapter.metadata.githubEnvironments) ? adapter.metadata.githubEnvironments : []
84
84
  };
85
85
  }
86
+ const PRODUCTION_ONLY_GITHUB_SECRET_NAMES = /* @__PURE__ */ new Set([
87
+ "NPM_TOKEN",
88
+ "PYPI_API_TOKEN",
89
+ "CARGO_REGISTRY_TOKEN",
90
+ "HEX_API_KEY"
91
+ ]);
92
+ function packageRequiredSecretsForGitHubEnvironment(adapter, environmentName) {
93
+ const requiredSecrets = Array.isArray(adapter.metadata.requiredSecrets) ? adapter.metadata.requiredSecrets : [];
94
+ const isProduction = environmentName === "production";
95
+ return requiredSecrets.filter((secretName) => {
96
+ if (typeof secretName !== "string" || !secretName.trim()) return false;
97
+ return isProduction || !PRODUCTION_ONLY_GITHUB_SECRET_NAMES.has(secretName);
98
+ });
99
+ }
100
+ function packageRequiredVariablesForGitHubEnvironment(adapter, _environmentName) {
101
+ return (Array.isArray(adapter.metadata.requiredVariables) ? adapter.metadata.requiredVariables : []).filter((variableName) => typeof variableName === "string" && Boolean(variableName.trim()));
102
+ }
103
+ function packageUnitRequiredSecretsForGitHubEnvironment(pkg, environmentName) {
104
+ const isProduction = environmentName === "production";
105
+ return pkg.requiredSecrets.filter((secretName) => isProduction || !PRODUCTION_ONLY_GITHUB_SECRET_NAMES.has(secretName));
106
+ }
86
107
  function templateReleaseTag(manifest) {
87
108
  return manifest.version ? `${manifest.release.tagPrefix}${manifest.id}/v${manifest.version}` : null;
88
109
  }
@@ -299,8 +320,7 @@ function packageResources(adapter, environment) {
299
320
  },
300
321
  source: { type: "package-adapter", id: packageId }
301
322
  });
302
- for (const secretName of Array.isArray(adapter.metadata.requiredSecrets) ? adapter.metadata.requiredSecrets : []) {
303
- if (typeof secretName !== "string" || !secretName.trim()) continue;
323
+ for (const secretName of packageRequiredSecretsForGitHubEnvironment(adapter, environmentName)) {
304
324
  resources.push({
305
325
  id: `github-secret-binding:${packageId}:${environmentName}:${secretName}`,
306
326
  kind: "github-secret-binding",
@@ -321,8 +341,7 @@ function packageResources(adapter, environment) {
321
341
  source: { type: "package-adapter", id: packageId }
322
342
  });
323
343
  }
324
- for (const variableName of Array.isArray(adapter.metadata.requiredVariables) ? adapter.metadata.requiredVariables : []) {
325
- if (typeof variableName !== "string" || !variableName.trim()) continue;
344
+ for (const variableName of packageRequiredVariablesForGitHubEnvironment(adapter, environmentName)) {
326
345
  resources.push({
327
346
  id: `github-variable-binding:${packageId}:${environmentName}:${variableName}`,
328
347
  kind: "github-variable-binding",
@@ -731,7 +750,7 @@ function releaseGateResources(packages, templates, environment) {
731
750
  };
732
751
  const publishGateKind = pkg.releaseCapability === "npm" ? "release-gate:npm-publish" : pkg.releaseCapability === "image" ? "release-gate:image-publish" : null;
733
752
  const imageCredentialDependencies = publishGateKind === "release-gate:image-publish" && pkg.githubEnvironments.includes(hostedEnvironment) ? [
734
- ...pkg.requiredSecrets.map((secretName) => `github-secret-binding:${pkg.id}:${hostedEnvironment}:${secretName}`),
753
+ ...packageUnitRequiredSecretsForGitHubEnvironment(pkg, hostedEnvironment).map((secretName) => `github-secret-binding:${pkg.id}:${hostedEnvironment}:${secretName}`),
735
754
  ...pkg.requiredVariables.map((variableName) => `github-variable-binding:${pkg.id}:${hostedEnvironment}:${variableName}`)
736
755
  ] : [];
737
756
  const publishDependencies = publishGateKind === "release-gate:image-publish" ? [verifyGate.id, ...imageCredentialDependencies] : [verifyGate.id];
@@ -12,7 +12,7 @@ import type { TreeseedCloseInput, TreeseedCiInput, TreeseedConfigInput, Treeseed
12
12
  type WorkflowWrite = NonNullable<TreeseedWorkflowContext['write']>;
13
13
  type WorkflowStatePayload = ReturnType<typeof resolveTreeseedWorkflowState>;
14
14
  type ReleaseCandidateMode = TreeseedReleaseCandidateMode;
15
- export type TreeseedWorkflowErrorCode = 'validation_failed' | 'merge_conflict' | 'missing_runtime_auth' | 'deployment_timeout' | 'confirmation_required' | 'unsupported_transport' | 'unsupported_state' | 'workflow_locked' | 'resume_unavailable' | 'workflow_contract_missing' | 'github_workflow_failed' | 'github_auth_unavailable';
15
+ export type TreeseedWorkflowErrorCode = 'validation_failed' | 'merge_conflict' | 'missing_runtime_auth' | 'deployment_timeout' | 'confirmation_required' | 'unsupported_transport' | 'unsupported_state' | 'workflow_locked' | 'resume_unavailable' | 'workflow_contract_missing' | 'github_workflow_failed' | 'github_auth_unavailable' | 'hosted_reconcile_failed' | 'hosted_live_verification_failed';
16
16
  export declare class TreeseedWorkflowError extends Error {
17
17
  code: TreeseedWorkflowErrorCode;
18
18
  operation: TreeseedWorkflowOperationId;
@@ -89,6 +89,16 @@ export declare function workflowReleaseCandidate(helpers: WorkflowOperationHelpe
89
89
  finalState?: WorkflowStatePayload;
90
90
  timing?: TreeseedWorkflowTiming;
91
91
  }>>;
92
+ type PublishedArtifactCheck = {
93
+ id: string;
94
+ kind: 'npm' | 'docker' | 'pypi' | 'crates' | 'hex';
95
+ name: string;
96
+ version: string;
97
+ url: string;
98
+ ok: boolean;
99
+ status?: number | null;
100
+ message?: string;
101
+ };
92
102
  export declare function reconcileTreeseedBranchPreview(input: {
93
103
  root: string;
94
104
  branch: string;
@@ -1001,29 +1011,20 @@ export declare function workflowRelease(helpers: WorkflowOperationHelpers, input
1001
1011
  version: string | null;
1002
1012
  result: {
1003
1013
  status: string;
1004
- adapter: string;
1005
- command: string;
1006
- } | {
1007
- status: string;
1008
- reason: string;
1009
- adapter: string;
1010
- command?: undefined;
1011
- } | {
1012
- status: string;
1013
- reason: null;
1014
- adapter: string;
1015
- command?: undefined;
1014
+ reason?: undefined;
1016
1015
  } | {
1017
1016
  status: string;
1018
1017
  reason: string;
1019
1018
  };
1020
1019
  }[];
1021
- rootInstall: {
1020
+ rootPackageLock: {
1022
1021
  status: string;
1023
1022
  reason: string;
1023
+ path?: undefined;
1024
1024
  } | {
1025
1025
  status: string;
1026
- reason: null;
1026
+ path: string;
1027
+ reason?: undefined;
1027
1028
  };
1028
1029
  workspaceUnlink: import("../operations/services/workspace-dependency-mode.js").WorkspaceDependencyModeReport;
1029
1030
  };
@@ -1044,6 +1045,13 @@ export declare function workflowRelease(helpers: WorkflowOperationHelpers, input
1044
1045
  changelogUpdated: boolean;
1045
1046
  entry: string;
1046
1047
  };
1048
+ rootInstall: {
1049
+ status: string;
1050
+ reason: string;
1051
+ } | {
1052
+ status: string;
1053
+ reason: null;
1054
+ };
1047
1055
  commit: {
1048
1056
  committed: boolean;
1049
1057
  commitSha: string;
@@ -1077,6 +1085,62 @@ export declare function workflowRelease(helpers: WorkflowOperationHelpers, input
1077
1085
  timeoutSeconds: number | null;
1078
1086
  cached: boolean;
1079
1087
  }[];
1088
+ publishedArtifacts: {
1089
+ checks: PublishedArtifactCheck[];
1090
+ };
1091
+ productionHosting: {
1092
+ status: "skipped";
1093
+ reason: string;
1094
+ environment: "staging" | "prod";
1095
+ selectedApps: string[];
1096
+ selectedResources: {
1097
+ id: string;
1098
+ host: string;
1099
+ serviceType: string;
1100
+ placement: import("../hosting/contracts.js").TreeseedServicePlacement;
1101
+ serviceName: string | null;
1102
+ }[];
1103
+ reconcile?: undefined;
1104
+ postApplyStatus?: undefined;
1105
+ liveVerification?: undefined;
1106
+ } | {
1107
+ status: "reconciled";
1108
+ environment: "staging" | "prod";
1109
+ selectedApps: string[];
1110
+ selectedResources: {
1111
+ id: string;
1112
+ host: string;
1113
+ serviceType: string;
1114
+ placement: import("../hosting/contracts.js").TreeseedServicePlacement;
1115
+ serviceName: string | null;
1116
+ }[];
1117
+ reconcile: {
1118
+ target: TreeseedReconcileTarget;
1119
+ units: import("../reconcile/contracts.js").TreeseedDesiredUnit[];
1120
+ plans: import("../reconcile/contracts.js").TreeseedReconcilePlan[];
1121
+ results: TreeseedReconcileResult[];
1122
+ state: import("../reconcile/contracts.js").TreeseedReconcileStateRecord;
1123
+ timings: import("../timing.js").TreeseedTimingEntry[];
1124
+ };
1125
+ postApplyStatus: {
1126
+ target: TreeseedReconcileTarget;
1127
+ ready: boolean;
1128
+ blockers: string[];
1129
+ warnings: string[];
1130
+ units: {
1131
+ unitId: string;
1132
+ unitType: import("../reconcile/contracts.js").TreeseedReconcileUnitType;
1133
+ provider: string;
1134
+ status: import("../reconcile/contracts.js").TreeseedReconcileStatusKind;
1135
+ exists: boolean;
1136
+ locators: Record<string, string | null>;
1137
+ warnings: string[];
1138
+ verification: import("../reconcile/contracts.js").TreeseedUnitVerificationResult | null;
1139
+ }[];
1140
+ };
1141
+ liveVerification: import("../workflow-support.js").TreeseedLiveHostedServiceCheckReport;
1142
+ reason?: undefined;
1143
+ };
1080
1144
  backMerge: {
1081
1145
  packages: {
1082
1146
  status: string;
@@ -114,8 +114,7 @@ import {
114
114
  import { discoverTreeseedPackageAdapters } from "../operations/services/package-adapters.js";
115
115
  import {
116
116
  collectInternalDevReferenceIssues,
117
- installableInternalDependencyVersions,
118
- normalizeGitRemoteForManifest
117
+ rewriteProjectInternalDependenciesToStableVersions
119
118
  } from "../operations/services/package-reference-policy.js";
120
119
  import {
121
120
  ensureLocalWorkspaceLinks,
@@ -525,7 +524,7 @@ function selectorFromWorkflowHostingGraph(graph) {
525
524
  }))]
526
525
  };
527
526
  }
528
- async function reconcileSaveHostedEnvironment(root, environment, helpers, workflowRunId) {
527
+ async function reconcileSaveHostedEnvironment(root, environment, helpers, workflowRunId, operation = "save") {
529
528
  const graph = compileTreeseedHostingGraph({ tenantRoot: root, environment });
530
529
  const selector = selectorFromWorkflowHostingGraph(graph);
531
530
  if (process.env.TREESEED_WORKFLOW_HOSTED_RECONCILE_MODE === "skip") {
@@ -548,24 +547,26 @@ async function reconcileSaveHostedEnvironment(root, environment, helpers, workfl
548
547
  ...helpers.context.env,
549
548
  ...collectTreeseedConfigSeedValues(root, environment, helpers.context.env)
550
549
  };
551
- helpers.write(`[save][workflow] Reconciling ${environment} hosted deployments for ${graph.units.length} selected resources.`);
550
+ const reconcileSession = /* @__PURE__ */ new Map([["workflowRunId", workflowRunId]]);
551
+ helpers.write(`[${operation}][workflow] Reconciling ${environment} hosted deployments for ${graph.units.length} selected resources.`);
552
552
  const reconcile = await reconcileTreeseedTarget({
553
553
  tenantRoot: root,
554
554
  target,
555
555
  env,
556
556
  selector,
557
557
  dryRun: false,
558
- write: (line) => helpers.write(`[save][reconcile] ${line}`, "stderr"),
559
- session: /* @__PURE__ */ new Map([["workflowRunId", workflowRunId]])
558
+ write: (line) => helpers.write(`[${operation}][reconcile] ${line}`, "stderr"),
559
+ session: reconcileSession
560
560
  });
561
561
  const status = await collectTreeseedReconcileStatus({
562
562
  tenantRoot: root,
563
563
  target,
564
564
  env,
565
- selector
565
+ selector,
566
+ session: reconcileSession
566
567
  });
567
568
  if (!status.ready) {
568
- workflowError("save", "hosted_reconcile_failed", `Hosted reconciliation for ${environment} did not verify:
569
+ workflowError(operation, "hosted_reconcile_failed", `Hosted reconciliation for ${environment} did not verify:
569
570
  ${status.blockers.join("\n")}`, {
570
571
  details: { environment, selector, status, reconcile }
571
572
  });
@@ -583,7 +584,7 @@ ${status.blockers.join("\n")}`, {
583
584
  ...live.liveObservation.issues
584
585
  ];
585
586
  if (liveFailures.length > 0) {
586
- workflowError("save", "hosted_live_verification_failed", `Hosted live verification for ${environment} failed:
587
+ workflowError(operation, "hosted_live_verification_failed", `Hosted live verification for ${environment} failed:
587
588
  ${liveFailures.join("\n")}`, {
588
589
  details: { environment, selector, live, reconcile }
589
590
  });
@@ -972,9 +973,31 @@ function writeJsonFile(path, value) {
972
973
  writeFileSync(path, `${JSON.stringify(value, null, 2)}
973
974
  `, "utf8");
974
975
  }
976
+ function updatePackageLockRootVersion(root, version) {
977
+ const packageLockPath = resolve(root, "package-lock.json");
978
+ if (!existsSync(packageLockPath)) return { status: "skipped", reason: "no package-lock.json" };
979
+ const packageLock = JSON.parse(readFileSync(packageLockPath, "utf8"));
980
+ let changed = false;
981
+ if (packageLock.version !== version) {
982
+ packageLock.version = version;
983
+ changed = true;
984
+ }
985
+ const packages = packageLock.packages;
986
+ if (packages && typeof packages === "object" && !Array.isArray(packages)) {
987
+ const rootPackage = packages[""];
988
+ if (rootPackage && typeof rootPackage === "object" && !Array.isArray(rootPackage)) {
989
+ if (rootPackage.version !== version) {
990
+ rootPackage.version = version;
991
+ changed = true;
992
+ }
993
+ }
994
+ }
995
+ if (changed) {
996
+ writeJsonFile(packageLockPath, packageLock);
997
+ }
998
+ return { status: changed ? "updated" : "unchanged", path: "package-lock.json" };
999
+ }
975
1000
  function applyStableWorkspaceVersionChanges(root, versions) {
976
- const dependencyVersions = installableInternalDependencyVersions(root, versions);
977
- const stableGitReferences = stablePackageGitReferences(root, dependencyVersions);
978
1001
  for (const target of [{ name: "@treeseed/market", dir: root }, ...workspacePackages(root).map((pkg) => ({ name: pkg.name, dir: pkg.dir }))]) {
979
1002
  const packageJsonPath = resolve(target.dir, "package.json");
980
1003
  if (!existsSync(packageJsonPath)) continue;
@@ -988,9 +1011,9 @@ function applyStableWorkspaceVersionChanges(root, versions) {
988
1011
  for (const field of ["dependencies", "optionalDependencies", "peerDependencies", "devDependencies"]) {
989
1012
  const values = packageJson[field];
990
1013
  if (!values || typeof values !== "object" || Array.isArray(values)) continue;
991
- for (const [dependencyName, version] of dependencyVersions.entries()) {
1014
+ for (const [dependencyName, version] of versions.entries()) {
992
1015
  if (!(dependencyName in values)) continue;
993
- const dependencySpec = stableGitReferences.get(dependencyName) ?? version;
1016
+ const dependencySpec = version;
994
1017
  if (String(values[dependencyName]) === dependencySpec) continue;
995
1018
  values[dependencyName] = dependencySpec;
996
1019
  changed = true;
@@ -1000,20 +1023,7 @@ function applyStableWorkspaceVersionChanges(root, versions) {
1000
1023
  writeJsonFile(packageJsonPath, packageJson);
1001
1024
  }
1002
1025
  }
1003
- }
1004
- function stablePackageGitReferences(root, versions) {
1005
- return new Map(workspacePackages(root).map((pkg) => {
1006
- const version = versions.get(pkg.name);
1007
- if (!version) return null;
1008
- let remote = null;
1009
- try {
1010
- remote = originRemoteUrl(pkg.dir);
1011
- } catch {
1012
- remote = null;
1013
- }
1014
- const manifestRemote = normalizeGitRemoteForManifest(remote ?? "", "preserve-origin");
1015
- return manifestRemote ? [pkg.name, `${manifestRemote}#${version}`] : null;
1016
- }).filter((entry) => Boolean(entry)));
1026
+ rewriteProjectInternalDependenciesToStableVersions(root, versions);
1017
1027
  }
1018
1028
  function gitObjectCommit(repoDir, ref) {
1019
1029
  try {
@@ -1855,12 +1865,7 @@ function failWorkflowRun(root, runId, error, recovery) {
1855
1865
  releaseWorkflowLock(root, runId);
1856
1866
  }
1857
1867
  function validatePackageReleaseWorkflows(root, packageNames) {
1858
- const adapterById = new Map(discoverTreeseedPackageAdapters(root).map((adapter) => [adapter.id, adapter]));
1859
- const missing = checkedOutWorkspacePackageRepos(root).filter((pkg) => packageNames.includes(pkg.name)).filter((pkg) => {
1860
- const adapter = adapterById.get(pkg.name);
1861
- const workflow = adapter?.kind === "beam-elixir-rust" && typeof adapter.metadata.hostedVerifyWorkflow === "string" ? String(adapter.metadata.hostedVerifyWorkflow) : ".github/workflows/publish.yml";
1862
- return !existsSync(resolve(pkg.dir, workflow));
1863
- }).map((pkg) => pkg.name);
1868
+ const missing = checkedOutWorkspacePackageRepos(root).filter((pkg) => packageNames.includes(pkg.name)).filter((pkg) => !existsSync(resolve(pkg.dir, ".github/workflows/publish.yml"))).map((pkg) => pkg.name);
1864
1869
  if (missing.length > 0) {
1865
1870
  workflowError("release", "workflow_contract_missing", `Treeseed release requires .github/workflows/publish.yml in: ${missing.join(", ")}.`, {
1866
1871
  details: {
@@ -1870,9 +1875,9 @@ function validatePackageReleaseWorkflows(root, packageNames) {
1870
1875
  }
1871
1876
  }
1872
1877
  function releaseWorkflowForPackage(root, packageName) {
1873
- const adapter = discoverTreeseedPackageAdapters(root).find((entry) => entry.id === packageName || entry.name === packageName);
1874
- const workflow = adapter?.kind === "beam-elixir-rust" && typeof adapter.metadata.hostedVerifyWorkflow === "string" ? String(adapter.metadata.hostedVerifyWorkflow) : ".github/workflows/publish.yml";
1875
- return workflow.split("/").at(-1) || "publish.yml";
1878
+ void root;
1879
+ void packageName;
1880
+ return "publish.yml";
1876
1881
  }
1877
1882
  function prepareAdapterReleaseMetadata(root, pkg, version) {
1878
1883
  const adapter = discoverTreeseedPackageAdapters(root).find((entry) => entry.id === pkg.name || entry.name === pkg.name);
@@ -1970,8 +1975,10 @@ function assertNoInternalDevReferencesForRepo(root, repoDir, packageNames) {
1970
1975
  ${rendered}`);
1971
1976
  }
1972
1977
  function backMergeProductionIntoStaging(repoDir, repoName, message) {
1973
- syncBranchWithOrigin(repoDir, PRODUCTION_BRANCH);
1974
1978
  syncBranchWithOrigin(repoDir, STAGING_BRANCH);
1979
+ if (!remoteBranchExists(repoDir, PRODUCTION_BRANCH)) {
1980
+ throw new Error(`Remote branch "origin/${PRODUCTION_BRANCH}" does not exist.`);
1981
+ }
1975
1982
  checkoutBranch(repoDir, STAGING_BRANCH);
1976
1983
  try {
1977
1984
  runGit(["merge-base", "--is-ancestor", `origin/${PRODUCTION_BRANCH}`, "HEAD"], { cwd: repoDir, capture: true });
@@ -2435,6 +2442,180 @@ ${workflow.url}` : "";
2435
2442
  }
2436
2443
  });
2437
2444
  }
2445
+ function npmRegistryPackageUrl(packageName) {
2446
+ return `https://registry.npmjs.org/${packageName.replace("/", "%2f")}`;
2447
+ }
2448
+ async function fetchJsonForArtifact(url) {
2449
+ const controller = new AbortController();
2450
+ const timeout = setTimeout(() => controller.abort(), 2e4);
2451
+ try {
2452
+ const response = await fetch(url, {
2453
+ headers: { accept: "application/json" },
2454
+ signal: controller.signal
2455
+ });
2456
+ let json = null;
2457
+ try {
2458
+ json = await response.json();
2459
+ } catch {
2460
+ json = null;
2461
+ }
2462
+ return { ok: response.ok, status: response.status, json };
2463
+ } finally {
2464
+ clearTimeout(timeout);
2465
+ }
2466
+ }
2467
+ function hasObjectKey(value, key) {
2468
+ return Boolean(value && typeof value === "object" && Object.prototype.hasOwnProperty.call(value, key));
2469
+ }
2470
+ async function verifyNpmArtifact(packageName, version) {
2471
+ const url = npmRegistryPackageUrl(packageName);
2472
+ try {
2473
+ const response = await fetchJsonForArtifact(url);
2474
+ const versions = stringRecord(response.json)?.versions;
2475
+ const ok = response.ok && hasObjectKey(versions, version);
2476
+ return {
2477
+ id: `npm:${packageName}:${version}`,
2478
+ kind: "npm",
2479
+ name: packageName,
2480
+ version,
2481
+ url,
2482
+ ok,
2483
+ status: response.status,
2484
+ ...ok ? {} : { message: `${packageName}@${version} was not found in npm registry metadata.` }
2485
+ };
2486
+ } catch (error) {
2487
+ return {
2488
+ id: `npm:${packageName}:${version}`,
2489
+ kind: "npm",
2490
+ name: packageName,
2491
+ version,
2492
+ url,
2493
+ ok: false,
2494
+ status: null,
2495
+ message: error instanceof Error ? error.message : String(error)
2496
+ };
2497
+ }
2498
+ }
2499
+ async function verifyDockerHubArtifact(image, version) {
2500
+ const [namespace, repository] = image.split("/");
2501
+ const url = `https://hub.docker.com/v2/repositories/${namespace}/${repository}/tags/${version}`;
2502
+ try {
2503
+ const response = await fetchJsonForArtifact(url);
2504
+ const images = Array.isArray(stringRecord(response.json)?.images) ? stringRecord(response.json)?.images : [];
2505
+ const architectures = new Set(images.map((entry) => stringRecord(entry)).map((entry) => typeof entry?.architecture === "string" ? entry.architecture : null).filter((entry) => Boolean(entry)));
2506
+ const ok = response.ok && architectures.has("amd64") && architectures.has("arm64");
2507
+ return {
2508
+ id: `docker:${image}:${version}`,
2509
+ kind: "docker",
2510
+ name: image,
2511
+ version,
2512
+ url,
2513
+ ok,
2514
+ status: response.status,
2515
+ ...ok ? {} : { message: `${image}:${version} was not found on Docker Hub with amd64 and arm64 images.` }
2516
+ };
2517
+ } catch (error) {
2518
+ return {
2519
+ id: `docker:${image}:${version}`,
2520
+ kind: "docker",
2521
+ name: image,
2522
+ version,
2523
+ url,
2524
+ ok: false,
2525
+ status: null,
2526
+ message: error instanceof Error ? error.message : String(error)
2527
+ };
2528
+ }
2529
+ }
2530
+ async function verifySimpleRegistryArtifact(input) {
2531
+ try {
2532
+ const response = await fetchJsonForArtifact(input.url);
2533
+ return {
2534
+ id: `${input.kind}:${input.name}:${input.version}`,
2535
+ kind: input.kind,
2536
+ name: input.name,
2537
+ version: input.version,
2538
+ url: input.url,
2539
+ ok: response.ok,
2540
+ status: response.status,
2541
+ ...response.ok ? {} : { message: `${input.name} ${input.version} was not found in ${input.kind}.` }
2542
+ };
2543
+ } catch (error) {
2544
+ return {
2545
+ id: `${input.kind}:${input.name}:${input.version}`,
2546
+ kind: input.kind,
2547
+ name: input.name,
2548
+ version: input.version,
2549
+ url: input.url,
2550
+ ok: false,
2551
+ status: null,
2552
+ message: error instanceof Error ? error.message : String(error)
2553
+ };
2554
+ }
2555
+ }
2556
+ async function collectPublishedReleaseArtifactChecks(selectedVersions) {
2557
+ const checks = [];
2558
+ const npmPackages = ["@treeseed/sdk", "@treeseed/ui", "@treeseed/core", "@treeseed/admin", "@treeseed/cli", "@treeseed/agent"];
2559
+ for (const packageName of npmPackages) {
2560
+ const version = selectedVersions.get(packageName);
2561
+ if (version) checks.push(await verifyNpmArtifact(packageName, version));
2562
+ }
2563
+ const agentVersion = selectedVersions.get("@treeseed/agent");
2564
+ if (agentVersion) {
2565
+ for (const image of ["treeseed/agent-manager", "treeseed/agent-runner"]) {
2566
+ checks.push(await verifyDockerHubArtifact(image, agentVersion));
2567
+ }
2568
+ }
2569
+ const apiVersion = selectedVersions.get("@treeseed/api");
2570
+ if (apiVersion) {
2571
+ for (const image of ["treeseed/api", "treeseed/op-runner"]) {
2572
+ checks.push(await verifyDockerHubArtifact(image, apiVersion));
2573
+ }
2574
+ }
2575
+ const treedxVersion = selectedVersions.get("treedx") ?? selectedVersions.get("@treeseed/treedx");
2576
+ if (treedxVersion) {
2577
+ checks.push(await verifyNpmArtifact("@treeseed/treedx", treedxVersion));
2578
+ checks.push(await verifySimpleRegistryArtifact({
2579
+ kind: "pypi",
2580
+ name: "treedx",
2581
+ version: treedxVersion,
2582
+ url: `https://pypi.org/pypi/treedx/${treedxVersion}/json`
2583
+ }));
2584
+ checks.push(await verifySimpleRegistryArtifact({
2585
+ kind: "crates",
2586
+ name: "treedx",
2587
+ version: treedxVersion,
2588
+ url: `https://crates.io/api/v1/crates/treedx/${treedxVersion}`
2589
+ }));
2590
+ checks.push(await verifySimpleRegistryArtifact({
2591
+ kind: "hex",
2592
+ name: "treedx",
2593
+ version: treedxVersion,
2594
+ url: `https://hex.pm/api/packages/treedx/releases/${treedxVersion}`
2595
+ }));
2596
+ for (const image of ["treeseed/treedx", "treeseed/treedx-profiler"]) {
2597
+ checks.push(await verifyDockerHubArtifact(image, treedxVersion));
2598
+ }
2599
+ }
2600
+ return checks;
2601
+ }
2602
+ async function verifyPublishedReleaseArtifacts(selectedVersions) {
2603
+ let checks = await collectPublishedReleaseArtifactChecks(selectedVersions);
2604
+ const deadline = Date.now() + 5 * 60 * 1e3;
2605
+ while (checks.some((check) => !check.ok) && Date.now() < deadline) {
2606
+ await sleep(15e3);
2607
+ checks = await collectPublishedReleaseArtifactChecks(selectedVersions);
2608
+ }
2609
+ const failures = checks.filter((check) => !check.ok);
2610
+ if (failures.length > 0) {
2611
+ const rendered = failures.map((check) => `${check.id}: ${check.message ?? `registry returned ${check.status ?? "unknown"}`} (${check.url})`).join("\n");
2612
+ workflowError("release", "validation_failed", `Published release artifact verification failed.
2613
+ ${rendered}`, {
2614
+ details: { checks }
2615
+ });
2616
+ }
2617
+ return { checks };
2618
+ }
2438
2619
  function assertSessionBranchSafety(operation, session, {
2439
2620
  requireCleanPackages = false,
2440
2621
  requireCurrentBranch = false,
@@ -5128,6 +5309,8 @@ ${blockers.join("\n")}`, {
5128
5309
  }),
5129
5310
  { id: "release-root", description: `Release market ${plannedRelease.rootVersion}`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
5130
5311
  { id: "publish-wait", description: "Wait for production release workflows", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
5312
+ { id: "verify-published-artifacts", description: "Verify immutable registry artifacts exist after publish workflows", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
5313
+ { id: "production-hosting", description: "Reconcile and live-verify production hosted resources after publish", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
5131
5314
  { id: "release-back-merge", description: "Back-merge production release history into staging", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
5132
5315
  { id: "workspace-link", description: "Restore local workspace links after release", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true }
5133
5316
  ],
@@ -5167,10 +5350,10 @@ ${blockers.join("\n")}`, {
5167
5350
  const adapterMetadata = checkedOutWorkspacePackageRepos(root).filter((pkg) => selectedPackageSet.has(pkg.name)).map((pkg) => ({
5168
5351
  name: pkg.name,
5169
5352
  version: selectedVersions.get(pkg.name) ?? null,
5170
- result: selectedVersions.has(pkg.name) ? prepareAdapterReleaseMetadata(root, pkg, selectedVersions.get(pkg.name)) : { status: "skipped", reason: "no planned version" }
5353
+ result: selectedVersions.has(pkg.name) ? { status: "pending-package-release-step" } : { status: "skipped", reason: "no planned version" }
5171
5354
  }));
5172
- const rootInstall = runReleaseNpmInstall(root, { workspaceRoot: root });
5173
- const remainingDevReferences = collectInternalDevReferenceIssues(root, selectedPackageSet).filter((issue) => issue.reason !== "git-release-ref" && issue.reason !== "lockfile-git-release-ref");
5355
+ const rootPackageLock = updatePackageLockRootVersion(root, plannedRelease.rootVersion);
5356
+ const remainingDevReferences = collectInternalDevReferenceIssues(root, selectedPackageSet).filter((issue) => !issue.reason.startsWith("lockfile-"));
5174
5357
  if (remainingDevReferences.length > 0) {
5175
5358
  const rendered = remainingDevReferences.map((issue) => `${issue.repoName}: ${issue.filePath} ${issue.dependencyName ?? ""} ${issue.reason} ${issue.spec}`).join("\n");
5176
5359
  throw new Error(`Stable release metadata still contains development references.
@@ -5179,7 +5362,7 @@ ${rendered}`);
5179
5362
  return {
5180
5363
  versions: Object.fromEntries(allVersions.entries()),
5181
5364
  adapterMetadata,
5182
- rootInstall,
5365
+ rootPackageLock,
5183
5366
  workspaceUnlink
5184
5367
  };
5185
5368
  });
@@ -5187,7 +5370,8 @@ ${rendered}`);
5187
5370
  for (const pkg of checkedOutWorkspacePackageRepos(root).filter((entry) => selectedPackageSet.has(entry.name))) {
5188
5371
  const version = selectedVersions.get(pkg.name);
5189
5372
  if (!version) continue;
5190
- const packageRelease = await executeJournalStep(root, workflowRun.runId, `release-${pkg.name}`, () => {
5373
+ const packageRelease = await executeJournalStep(root, workflowRun.runId, `release-${pkg.name}`, async () => {
5374
+ const metadata = prepareAdapterReleaseMetadata(root, pkg, version);
5191
5375
  const changelog = updateReleaseChangelog(pkg.dir, {
5192
5376
  version,
5193
5377
  sourceRef: `origin/${PRODUCTION_BRANCH}`,
@@ -5204,19 +5388,36 @@ ${rendered}`);
5204
5388
  pushBranch(pkg.dir, STAGING_BRANCH);
5205
5389
  const promotion = promoteCommitToProductionBranch(pkg.dir, commit.commitSha);
5206
5390
  const tag = ensureReleaseTag(pkg.dir, version, commit.commitSha, `release: ${pkg.name} ${version}`);
5391
+ const publishGate = {
5392
+ name: pkg.name,
5393
+ repoPath: pkg.dir,
5394
+ workflow: releaseWorkflowForPackage(root, pkg.name),
5395
+ branch: version,
5396
+ headSha: commit.commitSha
5397
+ };
5398
+ const publishWait2 = await waitForWorkflowGates("release", [publishGate], ciMode, {
5399
+ root,
5400
+ runId: workflowRun.runId,
5401
+ onProgress: (line, stream) => helpers.write(line, stream)
5402
+ });
5403
+ const publishedArtifacts2 = await verifyPublishedReleaseArtifacts(/* @__PURE__ */ new Map([[pkg.name, version]]));
5207
5404
  return {
5208
5405
  name: pkg.name,
5209
5406
  path: relative(root, pkg.dir),
5210
5407
  version,
5211
5408
  changelog,
5409
+ metadata,
5212
5410
  commit,
5213
5411
  promotion,
5214
- tag
5412
+ tag,
5413
+ publishWait: publishWait2,
5414
+ publishedArtifacts: publishedArtifacts2
5215
5415
  };
5216
5416
  });
5217
5417
  packageReleases.push(packageRelease);
5218
5418
  }
5219
5419
  const rootRelease = await executeJournalStep(root, workflowRun.runId, "release-root", () => {
5420
+ const rootInstall = runReleaseNpmInstall(root, { workspaceRoot: root });
5220
5421
  const changelog = updateReleaseChangelog(repoRoot(root), {
5221
5422
  version: plannedRelease.rootVersion,
5222
5423
  sourceRef: `origin/${PRODUCTION_BRANCH}`,
@@ -5240,6 +5441,7 @@ ${rendered}`);
5240
5441
  version: plannedRelease.rootVersion,
5241
5442
  releaseTag: plannedRelease.releaseTag,
5242
5443
  changelog,
5444
+ rootInstall,
5243
5445
  commit,
5244
5446
  promotion,
5245
5447
  tag
@@ -5257,20 +5459,15 @@ ${rendered}`);
5257
5459
  environment: "prod",
5258
5460
  action_kind: "deploy_web"
5259
5461
  }
5260
- }),
5261
- ...packageReleases.map((entry) => ({
5262
- name: String(entry.name),
5263
- repoPath: resolve(root, String(entry.path)),
5264
- workflow: releaseWorkflowForPackage(root, String(entry.name)),
5265
- branch: String(entry.version),
5266
- headSha: String(entry.commit.commitSha ?? "")
5267
- }))
5462
+ })
5268
5463
  ].filter((gate) => gate.headSha);
5269
5464
  const publishWait = await executeJournalStep(root, workflowRun.runId, "publish-wait", () => waitForWorkflowGates("release", publishGates, ciMode, {
5270
5465
  root,
5271
5466
  runId: workflowRun.runId,
5272
5467
  onProgress: (line, stream) => helpers.write(line, stream)
5273
5468
  }).then((workflowGates) => ({ workflowGates })));
5469
+ const publishedArtifacts = await executeJournalStep(root, workflowRun.runId, "verify-published-artifacts", () => verifyPublishedReleaseArtifacts(selectedVersions));
5470
+ const productionHosting = await executeJournalStep(root, workflowRun.runId, "production-hosting", () => reconcileSaveHostedEnvironment(root, "prod", helpers, workflowRun.runId, "release"));
5274
5471
  const backMerge = await executeJournalStep(root, workflowRun.runId, "release-back-merge", () => {
5275
5472
  const packageBackMerges = checkedOutWorkspacePackageRepos(root).filter((pkg) => selectedPackageSet.has(pkg.name)).map((pkg) => backMergeProductionIntoStaging(pkg.dir, pkg.name, releaseAdminMessage({
5276
5473
  subject: `release: back-merge ${PRODUCTION_BRANCH} into ${STAGING_BRANCH}`,
@@ -5296,6 +5493,8 @@ ${rendered}`);
5296
5493
  packageReleases,
5297
5494
  rootRelease,
5298
5495
  publishWait: publishWait.workflowGates,
5496
+ publishedArtifacts,
5497
+ productionHosting,
5299
5498
  backMerge,
5300
5499
  workspaceLinks,
5301
5500
  releasedCommit: String(rootRelease.commit.commitSha ?? ""),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@treeseed/sdk",
3
- "version": "0.12.5",
3
+ "version": "0.12.7",
4
4
  "description": "Shared Treeseed SDK for content-backed and D1-backed object models.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {