@treeseed/sdk 0.12.45 → 0.12.47

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.
@@ -440,7 +440,12 @@ function checkoutTaskBranchFromStaging(cwd, branchName, { createIfMissing = true
440
440
  function checkoutNewTaskBranchWithChanges(cwd, branchName, { pushIfCreated = false } = {}) {
441
441
  const repoDir = repoRoot(cwd);
442
442
  if (currentBranch(repoDir) !== STAGING_BRANCH) {
443
- throw new Error(`Dirty change adoption requires ${repoDir} to be on ${STAGING_BRANCH}.`);
443
+ const stagingHead = remoteBranchExists(repoDir, STAGING_BRANCH) ? remoteHeadCommit(repoDir, STAGING_BRANCH) : null;
444
+ const canNormalizeStagingCheckout = gitStatusPorcelain(repoDir).length === 0 && stagingHead !== null && headCommit(repoDir) === stagingHead;
445
+ if (!canNormalizeStagingCheckout) {
446
+ throw new Error(`Dirty change adoption requires ${repoDir} to be on ${STAGING_BRANCH}.`);
447
+ }
448
+ syncBranchWithOrigin(repoDir, STAGING_BRANCH);
444
449
  }
445
450
  if (branchExists(repoDir, branchName) || remoteBranchExists(repoDir, branchName)) {
446
451
  throw new Error(`Dirty change adoption requires a new branch; ${branchName} already exists.`);
@@ -19,7 +19,7 @@ import {
19
19
  } from "./hosted-service-checks.js";
20
20
  const DEFAULT_RETRY_ATTEMPTS = 3;
21
21
  const DEFAULT_RETRY_INTERVAL_MS = 1500;
22
- const DEFAULT_RAILWAY_DEPLOYMENT_SETTLE_ATTEMPTS = 60;
22
+ const DEFAULT_RAILWAY_DEPLOYMENT_SETTLE_ATTEMPTS = 120;
23
23
  const DEFAULT_RAILWAY_DEPLOYMENT_SETTLE_INTERVAL_MS = 5e3;
24
24
  function sleep(ms) {
25
25
  return new Promise((resolve2) => setTimeout(resolve2, ms));
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
1
+ import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { basename, resolve, relative } from "node:path";
3
3
  import { spawn, spawnSync } from "node:child_process";
4
4
  import { tmpdir } from "node:os";
@@ -731,18 +731,28 @@ function syncDirectGitDependencyLockfileEntries(node, options, references) {
731
731
  let changed = false;
732
732
  for (const reference of references) {
733
733
  const manifestSpec = reference.manifestSpec ?? reference.spec;
734
- for (const field of dependencyFields(rootPackage)) {
735
- const values = rootPackage[field];
736
- if (!values || typeof values !== "object" || Array.isArray(values)) continue;
737
- const dependencies = values;
738
- if (reference.packageName in dependencies && dependencies[reference.packageName] !== manifestSpec) {
739
- dependencies[reference.packageName] = manifestSpec;
740
- changed = true;
734
+ const visitDependencyMaps = (value) => {
735
+ if (!value || typeof value !== "object") return;
736
+ if (Array.isArray(value)) {
737
+ for (const item of value) visitDependencyMaps(item);
738
+ return;
741
739
  }
742
- }
743
- const entryKey = `node_modules/${reference.packageName}`;
744
- const entry = packageEntries[entryKey];
745
- if (entry) {
740
+ const record = value;
741
+ for (const field of ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"]) {
742
+ const dependencies = record[field];
743
+ if (!dependencies || typeof dependencies !== "object" || Array.isArray(dependencies)) continue;
744
+ const dependencyMap = dependencies;
745
+ const current = dependencyMap[reference.packageName];
746
+ if (typeof current === "string" && current !== manifestSpec) {
747
+ dependencyMap[reference.packageName] = manifestSpec;
748
+ changed = true;
749
+ }
750
+ }
751
+ for (const nested of Object.values(record)) visitDependencyMaps(nested);
752
+ };
753
+ visitDependencyMaps(lockfile);
754
+ for (const [entryKey, entry] of Object.entries(packageEntries)) {
755
+ if (entryKey !== `node_modules/${reference.packageName}` && !entryKey.endsWith(`/node_modules/${reference.packageName}`)) continue;
746
756
  const nextResolved = normalizeGitRemoteForDependency(reference.remoteUrl ?? "", "ssh");
747
757
  const resolved = nextResolved ? `${nextResolved}#${manifestSpec.slice(manifestSpec.lastIndexOf("#") + 1)}` : manifestSpec;
748
758
  if (entry.resolved !== resolved) {
@@ -764,6 +774,51 @@ function syncDirectGitDependencyLockfileEntries(node, options, references) {
764
774
  emitProgress(options, node, "lockfile", "Synchronized direct internal Git dependency lockfile entries without npm git preparation.");
765
775
  return true;
766
776
  }
777
+ function validateStandaloneGitDependencyLockfile(node, options) {
778
+ const lockfilePath = resolve(node.path, "package-lock.json");
779
+ const lockfileExists = existsSync(lockfilePath);
780
+ const validateArgs = [
781
+ "ci",
782
+ "--package-lock-only",
783
+ "--ignore-scripts",
784
+ "--workspaces=false",
785
+ "--no-audit",
786
+ "--no-fund"
787
+ ];
788
+ try {
789
+ if (!lockfileExists) throw new Error("standalone lockfile missing");
790
+ runCapturedCommand(node, options, "lockfile", "npm", validateArgs, { timeoutMs: 5 * 6e4 });
791
+ } catch (validationError) {
792
+ const previousLockfile = lockfileExists ? readFileSync(lockfilePath, "utf8") : null;
793
+ const isolatedRoot = mkdtempSync(resolve(tmpdir(), "treeseed-lockfile-"));
794
+ try {
795
+ copyFileSync(resolve(node.path, "package.json"), resolve(isolatedRoot, "package.json"));
796
+ runCapturedCommand(node, options, "lockfile", "npm", [
797
+ "install",
798
+ "--package-lock-only",
799
+ "--ignore-scripts",
800
+ "--workspaces=false",
801
+ "--no-audit",
802
+ "--no-fund"
803
+ ], {
804
+ cwd: isolatedRoot,
805
+ timeoutMs: 15 * 6e4
806
+ });
807
+ runCapturedCommand(node, options, "lockfile", "npm", validateArgs, {
808
+ cwd: isolatedRoot,
809
+ timeoutMs: 5 * 6e4
810
+ });
811
+ copyFileSync(resolve(isolatedRoot, "package-lock.json"), lockfilePath);
812
+ } catch (regenerationError) {
813
+ if (previousLockfile !== null) writeFileSync(lockfilePath, previousLockfile, "utf8");
814
+ throw regenerationError instanceof Error ? regenerationError : validationError;
815
+ } finally {
816
+ rmSync(isolatedRoot, { recursive: true, force: true });
817
+ }
818
+ }
819
+ emitProgress(options, node, "lockfile", "Validated the standalone lockfile for exact internal Git refs.");
820
+ return true;
821
+ }
767
822
  function planPackageVersion(node, options) {
768
823
  if (!node.packageJson || !node.packageJsonPath) return null;
769
824
  const current = String(node.packageJson.version ?? "0.0.0");
@@ -827,7 +882,7 @@ function syncRootWorkspaceLockfileMetadata(node, options) {
827
882
  const mergedDeps = { ...currentDeps };
828
883
  let fieldChanged = false;
829
884
  for (const [dependencyName, dependencySpec] of Object.entries(nextValue)) {
830
- if (mergedDeps[dependencyName] != null) continue;
885
+ if (mergedDeps[dependencyName] === dependencySpec) continue;
831
886
  mergedDeps[dependencyName] = dependencySpec;
832
887
  fieldChanged = true;
833
888
  }
@@ -1711,8 +1766,15 @@ async function saveOneRepository(node, options, state) {
1711
1766
  ensureWritableRemote(node, options);
1712
1767
  const dependencyUpdates = isRootWorkspaceRepository(node, options) ? [] : updateDependencyReferences(node, state.finalizedReferences);
1713
1768
  const dependencyChanged = dependencyUpdates.length > 0;
1714
- const gitDependencyRefreshReferences = dependencyUpdates.map((update) => state.finalizedReferences.get(update.packageName)).filter((reference) => Boolean(reference) && reference.mode === "dev-git-commit");
1769
+ const directDependencyNames = new Set(dependencyFields(node.packageJson ?? {}).flatMap((field) => {
1770
+ const value = node.packageJson?.[field];
1771
+ return value && typeof value === "object" && !Array.isArray(value) ? Object.keys(value) : [];
1772
+ }));
1773
+ const gitDependencyRefreshReferences = [...state.finalizedReferences.values()].filter((reference) => reference.mode === "dev-git-commit" && directDependencyNames.has(reference.packageName));
1715
1774
  const lockfileGitDependenciesSynced = syncDirectGitDependencyLockfileEntries(node, options, gitDependencyRefreshReferences);
1775
+ if (!isRootWorkspaceRepository(node, options) && (lockfileGitDependenciesSynced || gitDependencyRefreshReferences.length > 0 && !existsSync(resolve(node.path, "package-lock.json")))) {
1776
+ validateStandaloneGitDependencyLockfile(node, options);
1777
+ }
1716
1778
  const gitDependencyRefreshSpecs = lockfileGitDependenciesSynced ? [] : gitDependencyRefreshReferences.map((reference) => `${reference.packageName}@${reference.installSpec ?? reference.spec}`);
1717
1779
  const submodulePointers = collectSubmodulePointerChanges(node, state.finalizedCommits);
1718
1780
  const submodulesChanged = submodulePointers.length > 0;
@@ -274,6 +274,10 @@ function removeLinkCandidate(link, managedLinks) {
274
274
  unlinkSync(link.linkPath);
275
275
  return true;
276
276
  }
277
+ if (managed) {
278
+ rmSync(link.linkPath, { recursive: true, force: true });
279
+ return true;
280
+ }
277
281
  if (isInstalledTreeseedPackage(link.linkPath, link.packageName)) {
278
282
  rmSync(link.linkPath, { recursive: true, force: true });
279
283
  return true;
@@ -62,6 +62,7 @@ function environmentFromTarget(target) {
62
62
  return "staging";
63
63
  }
64
64
  function packageReleaseCapability(adapter) {
65
+ if (!adapter.capabilities.publish) return adapter.releaseChecks.length > 0 ? "deploy-only" : "none";
65
66
  if (adapter.artifacts.some((artifact) => artifact.provider === "docker")) return "image";
66
67
  if (adapter.artifacts.some((artifact) => artifact.provider === "npm")) return "npm";
67
68
  if (adapter.releaseChecks.length > 0) return "deploy-only";
@@ -8,20 +8,17 @@ export declare function runReleaseVerifyCommand(input: {
8
8
  ok: boolean;
9
9
  skipped: boolean;
10
10
  reason: string;
11
- status?: undefined;
12
- signal?: undefined;
13
- command?: undefined;
14
- stdout?: undefined;
15
- stderr?: undefined;
11
+ dependencies?: undefined;
16
12
  } | {
17
13
  ok: boolean;
18
- status: number | null;
19
- signal: NodeJS.Signals | null;
20
- command: import("../../index.js").TreeseedPackageCommand;
21
- stdout: string;
22
- stderr: string;
23
- skipped?: undefined;
24
- reason?: undefined;
14
+ skipped: boolean;
15
+ reason: string;
16
+ dependencies: {
17
+ status: "staging-proof-reused";
18
+ candidatePath: string;
19
+ packageCommit: string;
20
+ rootCommit: string;
21
+ };
25
22
  }>;
26
23
  export declare function runTemplateReleaseVerifyCommand(input: {
27
24
  tenantRoot: string;
@@ -1,9 +1,28 @@
1
- import { spawn, spawnSync } from "node:child_process";
2
- import { mkdirSync, writeFileSync } from "node:fs";
1
+ import { spawnSync } from "node:child_process";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { dirname, resolve } from "node:path";
4
4
  import { findTreeseedPackageAdapter } from "../../operations/services/package-adapters.js";
5
5
  import { checkedOutTemplateRepositories } from "../../operations/services/managed-repositories.js";
6
6
  import { runTreeseedGitText } from "../../operations/services/git-runner.js";
7
+ function requireMatchingStageCandidate(tenantRoot, packageId, packageDir) {
8
+ const candidatePath = resolve(tenantRoot, ".treeseed/workflow/stage-candidates/latest.json");
9
+ if (!existsSync(candidatePath)) {
10
+ throw new Error(`Release verification requires a successful staged candidate; ${candidatePath} is missing.`);
11
+ }
12
+ const candidate = JSON.parse(readFileSync(candidatePath, "utf8"));
13
+ const packageProof = candidate.packages?.find((entry) => entry.name === packageId);
14
+ const packageHead = runTreeseedGitText(["rev-parse", "HEAD"], { cwd: packageDir, mode: "read" }).trim();
15
+ const rootHead = runTreeseedGitText(["rev-parse", "HEAD"], { cwd: tenantRoot, mode: "read" }).trim();
16
+ if (candidate.targetBranch !== "staging" || candidate.root?.verified !== true || candidate.root.commit !== rootHead || packageProof?.verified !== true || packageProof.commit !== packageHead) {
17
+ throw new Error(`Release verification requires ${packageId} and the Market root to match the latest verified staging candidate.`);
18
+ }
19
+ return {
20
+ status: "staging-proof-reused",
21
+ candidatePath,
22
+ packageCommit: packageHead,
23
+ rootCommit: rootHead
24
+ };
25
+ }
7
26
  async function runReleaseVerifyCommand(input) {
8
27
  const adapter = findTreeseedPackageAdapter(input.tenantRoot, input.packageId);
9
28
  if (!adapter) {
@@ -17,44 +36,13 @@ async function runReleaseVerifyCommand(input) {
17
36
  reason: `${input.packageId} has no release verify command.`
18
37
  };
19
38
  }
20
- const renderedCommand = [command.command, ...command.args].join(" ");
21
- input.onProgress?.(`Running ${input.packageId} release verification: ${renderedCommand}`);
22
- const started = Date.now();
23
- let stdout = "";
24
- let stderr = "";
25
- const result = await new Promise((resolve2, reject) => {
26
- const child = spawn(command.command, command.args, {
27
- cwd: command.cwd,
28
- env: { ...process.env, ...input.env ?? {} },
29
- stdio: ["ignore", "pipe", "pipe"]
30
- });
31
- const heartbeat = setInterval(() => {
32
- input.onProgress?.(`Still running ${input.packageId} release verification after ${Math.round((Date.now() - started) / 1e3)}s.`);
33
- }, 3e4);
34
- child.stdout?.on("data", (chunk) => {
35
- stdout += String(chunk);
36
- });
37
- child.stderr?.on("data", (chunk) => {
38
- stderr += String(chunk);
39
- });
40
- child.on("error", (error) => {
41
- clearInterval(heartbeat);
42
- reject(error);
43
- });
44
- child.on("close", (status, signal) => {
45
- clearInterval(heartbeat);
46
- resolve2({ status, signal });
47
- });
48
- });
49
- const elapsedSeconds = Math.round((Date.now() - started) / 1e3);
50
- input.onProgress?.(`${input.packageId} release verification ${result.status === 0 ? "passed" : "failed"} in ${elapsedSeconds}s.`);
39
+ const stagingProof = requireMatchingStageCandidate(input.tenantRoot, input.packageId, adapter.dir);
40
+ input.onProgress?.(`Reusing exact-SHA staging verification for ${input.packageId} at ${stagingProof.packageCommit.slice(0, 12)}.`);
51
41
  return {
52
- ok: (result.status ?? 1) === 0,
53
- status: result.status,
54
- signal: result.signal,
55
- command,
56
- stdout,
57
- stderr
42
+ ok: true,
43
+ skipped: true,
44
+ reason: `${input.packageId} matches the latest verified staging candidate; tag workflows perform independent release verification before publication or deployment.`,
45
+ dependencies: stagingProof
58
46
  };
59
47
  }
60
48
  function runTemplateCommand(command, cwd, env) {
@@ -1222,6 +1222,14 @@ function remoteTagCommit(repoDir, tagName) {
1222
1222
  const direct = output.split("\n").find((line) => line.endsWith(`refs/tags/${tagName}`));
1223
1223
  return (peeled ?? direct)?.split(/\s+/u)[0] ?? null;
1224
1224
  }
1225
+ function releaseTagExists(repoDir, tagName) {
1226
+ if (gitObjectCommit(repoDir, tagName)) return true;
1227
+ try {
1228
+ return remoteTagCommit(repoDir, tagName) !== null;
1229
+ } catch {
1230
+ return false;
1231
+ }
1232
+ }
1225
1233
  function ensureReleaseTag(repoDir, tagName, commitSha, message) {
1226
1234
  const localCommit = gitObjectCommit(repoDir, tagName);
1227
1235
  if (localCommit && localCommit !== commitSha) {
@@ -2125,8 +2133,12 @@ function productionPackageDeployGates(root, versions) {
2125
2133
  function prepareAdapterReleaseMetadata(root, pkg, version) {
2126
2134
  const adapter = discoverTreeseedPackageAdapters(root).find((entry) => entry.id === pkg.name || entry.name === pkg.name);
2127
2135
  if (adapter?.kind === "beam-elixir-rust" && existsSync(resolve(pkg.dir, "scripts", "bump-release-version.ts"))) {
2128
- run("tsx", ["scripts/bump-release-version.ts", version], { cwd: pkg.dir });
2129
- return { status: "updated", adapter: adapter.id, command: "tsx scripts/bump-release-version.ts" };
2136
+ const tsx = resolve(root, "node_modules/.bin/tsx");
2137
+ if (!existsSync(tsx)) {
2138
+ throw new Error(`TreeSeed release requires the workspace tsx executable at ${tsx}. Run trsd install and restore workspace dependencies before retrying.`);
2139
+ }
2140
+ run(tsx, ["scripts/bump-release-version.ts", version], { cwd: pkg.dir });
2141
+ return { status: "updated", adapter: adapter.id, command: `${tsx} scripts/bump-release-version.ts` };
2130
2142
  }
2131
2143
  if (existsSync(resolve(pkg.dir, "package.json"))) {
2132
2144
  return {
@@ -2629,6 +2641,14 @@ function buildReleasePlanSnapshot(input) {
2629
2641
  versionPlan.versions.set(adapter.id, incrementVersion(adapter.version, input.level));
2630
2642
  }
2631
2643
  }
2644
+ for (const adapter of discoverTreeseedPackageAdapters(input.root)) {
2645
+ let version = versionPlan.versions.get(adapter.id);
2646
+ if (!version) continue;
2647
+ while (releaseTagExists(adapter.dir, version)) {
2648
+ version = incrementVersion(version, input.level);
2649
+ }
2650
+ versionPlan.versions.set(adapter.id, version);
2651
+ }
2632
2652
  const plannedSelected = orderReleasePackageNames([...versionPlan.selected].filter((name) => versionPlan.versions.has(name)));
2633
2653
  const plannedChanged = input.repairVersionLine === true ? plannedSelected : Array.from(new Set(publishablePackageSelection.changed.filter((name) => plannedSelected.includes(name))));
2634
2654
  const plannedDependents = plannedSelected.filter((name) => !plannedChanged.includes(name));
@@ -4633,7 +4653,7 @@ async function workflowSave(helpers, input) {
4633
4653
  verifyMode: normalizeSaveVerifyMode(effectiveInput.verify === false ? "skip" : effectiveInput.verifyMode),
4634
4654
  commitMessageMode: effectiveInput.commitMessageMode ?? "auto",
4635
4655
  workflowRunId: workflowRun.runId,
4636
- deferPushUntilVerified: true,
4656
+ deferPushUntilVerified: false,
4637
4657
  onProgress: (line, stream) => helpers.write(line, stream),
4638
4658
  onWaveSaved: branch === STAGING_BRANCH && shouldUseHostedSaveCi(effectiveInput, branch, saveLane) ? async ({ nodes, reports, rootRepo: waveRootRepo }) => {
4639
4659
  const nonRootReportsForWave = reports.filter((repo, index) => nodes[index]?.id !== ".");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@treeseed/sdk",
3
- "version": "0.12.45",
3
+ "version": "0.12.47",
4
4
  "description": "Shared Treeseed SDK for content-backed and D1-backed object models.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {