@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.
- package/dist/operations/services/git-workflow.js +6 -1
- package/dist/operations/services/live-hosted-service-checks.js +1 -1
- package/dist/operations/services/repository-save-orchestrator.js +76 -14
- package/dist/operations/services/workspace-dependency-mode.js +4 -0
- package/dist/platform/desired-state.js +1 -0
- package/dist/reconcile/providers/release-private.d.ts +9 -12
- package/dist/reconcile/providers/release-private.js +27 -39
- package/dist/workflow/operations.js +23 -3
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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 =
|
|
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
|
-
|
|
735
|
-
|
|
736
|
-
if (
|
|
737
|
-
|
|
738
|
-
|
|
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
|
-
|
|
744
|
-
|
|
745
|
-
|
|
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]
|
|
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
|
|
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
|
-
|
|
12
|
-
signal?: undefined;
|
|
13
|
-
command?: undefined;
|
|
14
|
-
stdout?: undefined;
|
|
15
|
-
stderr?: undefined;
|
|
11
|
+
dependencies?: undefined;
|
|
16
12
|
} | {
|
|
17
13
|
ok: boolean;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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 {
|
|
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
|
|
21
|
-
input.onProgress?.(`
|
|
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:
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
2129
|
-
|
|
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:
|
|
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 !== ".");
|