@treeseed/sdk 0.12.56 → 0.12.59
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/package-reference-policy.d.ts +2 -2
- package/dist/operations/services/package-reference-policy.js +6 -5
- package/dist/operations/services/repository-save-orchestrator.js +11 -8
- package/dist/workflow/operations.d.ts +4 -0
- package/dist/workflow/operations.js +54 -10
- package/package.json +1 -1
|
@@ -40,11 +40,11 @@ export declare function rewriteInternalDependenciesToStableVersions(root: any, v
|
|
|
40
40
|
repoName: string;
|
|
41
41
|
packageJsonPath: string;
|
|
42
42
|
})[];
|
|
43
|
-
export declare function rewriteProjectInternalDependenciesToStableVersions(root: any, versions: Map<string, string>): (RewrittenDevReference & {
|
|
43
|
+
export declare function rewriteProjectInternalDependenciesToStableVersions(root: any, versions: Map<string, string>, targetPackageNames?: ReadonlySet<string>): (RewrittenDevReference & {
|
|
44
44
|
repoName: string;
|
|
45
45
|
packageJsonPath: string;
|
|
46
46
|
})[];
|
|
47
|
-
export declare function collectInternalDevReferenceIssues(root?: any, packageNames?: Set<any>): {
|
|
47
|
+
export declare function collectInternalDevReferenceIssues(root?: any, packageNames?: Set<any>, targetPackageNames?: ReadonlySet<string>): {
|
|
48
48
|
repoName: string;
|
|
49
49
|
filePath: string;
|
|
50
50
|
field?: string;
|
|
@@ -182,12 +182,12 @@ function rewriteInternalDependenciesToStableVersions(root = workspaceRoot(), ver
|
|
|
182
182
|
}
|
|
183
183
|
return rewrites;
|
|
184
184
|
}
|
|
185
|
-
function rewriteProjectInternalDependenciesToStableVersions(root = workspaceRoot(), versions) {
|
|
185
|
+
function rewriteProjectInternalDependenciesToStableVersions(root = workspaceRoot(), versions, targetPackageNames) {
|
|
186
186
|
const rewrites = [];
|
|
187
187
|
const installableVersions = installableInternalDependencyVersions(root, versions);
|
|
188
188
|
const targets = [
|
|
189
189
|
{ name: "@treeseed/market", dir: root },
|
|
190
|
-
...workspacePackages(root).map((pkg) => ({ name: pkg.name, dir: pkg.dir }))
|
|
190
|
+
...workspacePackages(root).filter((pkg) => !targetPackageNames || targetPackageNames.has(pkg.name)).map((pkg) => ({ name: pkg.name, dir: pkg.dir }))
|
|
191
191
|
];
|
|
192
192
|
for (const target of targets) {
|
|
193
193
|
const packageJsonPath = resolve(target.dir, "package.json");
|
|
@@ -216,11 +216,12 @@ function rewriteProjectInternalDependenciesToStableVersions(root = workspaceRoot
|
|
|
216
216
|
}
|
|
217
217
|
return rewrites;
|
|
218
218
|
}
|
|
219
|
-
function collectInternalDevReferenceIssues(root = workspaceRoot(), packageNames = new Set(workspacePackages(root).map((pkg) => pkg.name))) {
|
|
219
|
+
function collectInternalDevReferenceIssues(root = workspaceRoot(), packageNames = new Set(workspacePackages(root).map((pkg) => pkg.name)), targetPackageNames) {
|
|
220
220
|
const issues = [];
|
|
221
|
+
const workspaceTargets = workspacePackages(root).filter((pkg) => !targetPackageNames || targetPackageNames.has(pkg.name)).map((pkg) => ({ name: pkg.name, dir: pkg.dir }));
|
|
221
222
|
const manifestRoots = [
|
|
222
223
|
{ name: "@treeseed/market", dir: root },
|
|
223
|
-
...
|
|
224
|
+
...workspaceTargets
|
|
224
225
|
];
|
|
225
226
|
for (const pkg of manifestRoots) {
|
|
226
227
|
const packageJsonPath = resolve(pkg.dir, "package.json");
|
|
@@ -239,7 +240,7 @@ function collectInternalDevReferenceIssues(root = workspaceRoot(), packageNames
|
|
|
239
240
|
}
|
|
240
241
|
}
|
|
241
242
|
}
|
|
242
|
-
const lockRoots = [{ name: "@treeseed/market", dir: root }, ...
|
|
243
|
+
const lockRoots = [{ name: "@treeseed/market", dir: root }, ...workspaceTargets];
|
|
243
244
|
for (const lockRoot of lockRoots) {
|
|
244
245
|
for (const lockName of ["package-lock.json", "npm-shrinkwrap.json"]) {
|
|
245
246
|
const lockPath = resolve(lockRoot.dir, lockName);
|
|
@@ -944,9 +944,10 @@ async function runGitDependencySmoke(node, options, reference) {
|
|
|
944
944
|
void reference;
|
|
945
945
|
}
|
|
946
946
|
async function runNpmInstallWithRetry(node, options, gitDependencyRefreshSpecs = []) {
|
|
947
|
-
if (shouldSkipNetworkInstall()) {
|
|
948
|
-
|
|
949
|
-
|
|
947
|
+
if (shouldSkipNetworkInstall() || options.deferPushUntilVerified === true) {
|
|
948
|
+
const reason = options.deferPushUntilVerified === true ? "atomic-save" : "disabled";
|
|
949
|
+
emitProgress(options, node, "install", `Skipped npm install because ${reason === "atomic-save" ? "atomic save validates local lock metadata before publishing commits" : "network install mode is disabled"}.`);
|
|
950
|
+
return { status: "skipped", attempts: 0, reason };
|
|
950
951
|
}
|
|
951
952
|
let lastError = null;
|
|
952
953
|
const packageJson = node.packageJson ?? (existsSync(resolve(node.path, "package.json")) ? readJson(resolve(node.path, "package.json")) : null);
|
|
@@ -1030,9 +1031,10 @@ async function validateRepositoryLockfile(node, options) {
|
|
|
1030
1031
|
}
|
|
1031
1032
|
const { command, args } = lockfileValidationCommand(node, options);
|
|
1032
1033
|
const commandText = `${command} ${args.join(" ")}`;
|
|
1033
|
-
if (shouldSkipNetworkInstall()) {
|
|
1034
|
-
|
|
1035
|
-
|
|
1034
|
+
if (shouldSkipNetworkInstall() || options.deferPushUntilVerified === true) {
|
|
1035
|
+
const reason = options.deferPushUntilVerified === true ? "atomic-save" : "disabled";
|
|
1036
|
+
emitProgress(options, node, "lockfile", `Validated lockfile structure without ${commandText} because ${reason === "atomic-save" ? "upstream commits are intentionally unpublished" : "network install mode is disabled"}.`);
|
|
1037
|
+
return { status: "passed", command: "treeseed structural lockfile validation", issues: [], error: null };
|
|
1036
1038
|
}
|
|
1037
1039
|
try {
|
|
1038
1040
|
runCapturedCommand(node, options, "lockfile", command, args, { timeoutMs: lockfileValidationTimeoutMs(node, options), emitOutputOnSuccess: false });
|
|
@@ -1773,8 +1775,9 @@ async function saveOneRepository(node, options, state) {
|
|
|
1773
1775
|
return value && typeof value === "object" && !Array.isArray(value) ? Object.keys(value) : [];
|
|
1774
1776
|
}));
|
|
1775
1777
|
const gitDependencyRefreshReferences = [...state.finalizedReferences.values()].filter((reference) => reference.mode === "dev-git-commit" && directDependencyNames.has(reference.packageName));
|
|
1778
|
+
const deferredGitDependencyValidation = options.deferPushUntilVerified === true && gitDependencyRefreshReferences.length > 0;
|
|
1776
1779
|
const lockfileGitDependenciesSynced = syncDirectGitDependencyLockfileEntries(node, options, gitDependencyRefreshReferences);
|
|
1777
|
-
if (!isRootWorkspaceRepository(node, options) && (lockfileGitDependenciesSynced || gitDependencyRefreshReferences.length > 0 && !existsSync(resolve(node.path, "package-lock.json")))) {
|
|
1780
|
+
if (!isRootWorkspaceRepository(node, options) && (lockfileGitDependenciesSynced || gitDependencyRefreshReferences.length > 0 && !existsSync(resolve(node.path, "package-lock.json"))) && !deferredGitDependencyValidation) {
|
|
1778
1781
|
validateStandaloneGitDependencyLockfile(node, options);
|
|
1779
1782
|
}
|
|
1780
1783
|
const gitDependencyRefreshSpecs = lockfileGitDependenciesSynced ? [] : gitDependencyRefreshReferences.map((reference) => `${reference.packageName}@${reference.installSpec ?? reference.spec}`);
|
|
@@ -1809,7 +1812,7 @@ async function saveOneRepository(node, options, state) {
|
|
|
1809
1812
|
} else if (node.kind === "project" && (dependencyChanged || node.path === options.root && submodulesChanged) && hasNpmLockfile(node.path)) {
|
|
1810
1813
|
report.install = await runNpmInstallWithRetry(node, options, gitDependencyRefreshSpecs);
|
|
1811
1814
|
}
|
|
1812
|
-
if (!isRootWorkspaceRepository(node, options) && hasNpmLockfile(node.path) && (packageNeedsVersion || dependencyChanged)) {
|
|
1815
|
+
if (!isRootWorkspaceRepository(node, options) && hasNpmLockfile(node.path) && (packageNeedsVersion || dependencyChanged) && !deferredGitDependencyValidation) {
|
|
1813
1816
|
validateStandaloneGitDependencyLockfile(node, options);
|
|
1814
1817
|
}
|
|
1815
1818
|
if (hasNpmLockfile(node.path) && (node.kind === "project" || packageNeedsVersion || dependencyChanged || submodulesChanged)) {
|
|
@@ -1144,6 +1144,10 @@ export declare function workflowRelease(helpers: WorkflowOperationHelpers, input
|
|
|
1144
1144
|
cached: boolean;
|
|
1145
1145
|
}[];
|
|
1146
1146
|
};
|
|
1147
|
+
productionImageRefs: {
|
|
1148
|
+
persisted: Record<string, string>;
|
|
1149
|
+
readiness: import("../workflow-support.js").TreeseedDeploymentReadinessReport;
|
|
1150
|
+
};
|
|
1147
1151
|
backMerge: {
|
|
1148
1152
|
packages: {
|
|
1149
1153
|
status: string;
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
applyTreeseedSafeRepairs,
|
|
18
18
|
assertTreeseedCommandEnvironment,
|
|
19
19
|
checkTreeseedProviderConnections,
|
|
20
|
+
collectTreeseedEnvironmentContext,
|
|
20
21
|
collectTreeseedConfigContext,
|
|
21
22
|
collectTreeseedConfigSeedValues,
|
|
22
23
|
collectTreeseedPrintEnvReport,
|
|
@@ -33,6 +34,7 @@ import {
|
|
|
33
34
|
resolveTreeseedRemoteSession,
|
|
34
35
|
rotateTreeseedMachineKey,
|
|
35
36
|
setTreeseedRemoteSession,
|
|
37
|
+
setTreeseedMachineEnvironmentValue,
|
|
36
38
|
writeTreeseedMachineConfig
|
|
37
39
|
} from "../operations/services/config-runtime.js";
|
|
38
40
|
import { formatTreeseedDependencyFailureDetails, installTreeseedDependencies } from "../managed-dependencies.js";
|
|
@@ -746,6 +748,20 @@ function productionReleaseImageRefVersions(root, selectedVersions) {
|
|
|
746
748
|
}
|
|
747
749
|
return versions;
|
|
748
750
|
}
|
|
751
|
+
function persistProductionReleaseImageRefs(root, releaseImageRefs) {
|
|
752
|
+
const registry = collectTreeseedEnvironmentContext(root);
|
|
753
|
+
const entries = new Map(registry.entries.map((entry) => [entry.id, entry]));
|
|
754
|
+
const persisted = {};
|
|
755
|
+
for (const [id, value] of Object.entries(releaseImageRefs)) {
|
|
756
|
+
const entry = entries.get(id) ?? { id, storage: "scoped", sensitivity: "plain" };
|
|
757
|
+
if (entry.sensitivity === "secret") {
|
|
758
|
+
workflowError("release", "validation_failed", `Production release image ref ${id} must be a non-secret environment value.`);
|
|
759
|
+
}
|
|
760
|
+
setTreeseedMachineEnvironmentValue(root, "prod", entry, value);
|
|
761
|
+
persisted[id] = value;
|
|
762
|
+
}
|
|
763
|
+
return persisted;
|
|
764
|
+
}
|
|
749
765
|
function stableVersionLine(version) {
|
|
750
766
|
const match = version.match(/^(\d+\.\d+)\.\d+(?:-[0-9A-Za-z.-]+)?$/u);
|
|
751
767
|
return match?.[1] ?? null;
|
|
@@ -1194,8 +1210,12 @@ function updatePackageLockRootVersion(root, version) {
|
|
|
1194
1210
|
}
|
|
1195
1211
|
return { status: changed ? "updated" : "unchanged", path: "package-lock.json" };
|
|
1196
1212
|
}
|
|
1197
|
-
function applyStableWorkspaceVersionChanges(root, versions) {
|
|
1198
|
-
|
|
1213
|
+
function applyStableWorkspaceVersionChanges(root, versions, targetPackageNames) {
|
|
1214
|
+
const targets = [
|
|
1215
|
+
{ name: "@treeseed/market", dir: root },
|
|
1216
|
+
...workspacePackages(root).filter((pkg) => targetPackageNames.has(pkg.name)).map((pkg) => ({ name: pkg.name, dir: pkg.dir }))
|
|
1217
|
+
];
|
|
1218
|
+
for (const target of targets) {
|
|
1199
1219
|
const packageJsonPath = resolve(target.dir, "package.json");
|
|
1200
1220
|
if (!existsSync(packageJsonPath)) continue;
|
|
1201
1221
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
@@ -1220,7 +1240,7 @@ function applyStableWorkspaceVersionChanges(root, versions) {
|
|
|
1220
1240
|
writeJsonFile(packageJsonPath, packageJson);
|
|
1221
1241
|
}
|
|
1222
1242
|
}
|
|
1223
|
-
rewriteProjectInternalDependenciesToStableVersions(root, versions);
|
|
1243
|
+
rewriteProjectInternalDependenciesToStableVersions(root, versions, targetPackageNames);
|
|
1224
1244
|
}
|
|
1225
1245
|
function gitObjectCommit(repoDir, ref) {
|
|
1226
1246
|
try {
|
|
@@ -2221,7 +2241,7 @@ function runReleaseNpmInstall(repoDir, options = {}) {
|
|
|
2221
2241
|
if (shouldSkipReleaseInstall()) {
|
|
2222
2242
|
return { status: "skipped", reason: "disabled" };
|
|
2223
2243
|
}
|
|
2224
|
-
const args = repoDir === options.workspaceRoot ? ["install", "--package-lock-only", "--ignore-scripts", "--no-audit", "--no-fund"] : ["install", "--package-lock-only", "--ignore-scripts", "--workspaces=false", "--no-audit", "--no-fund"];
|
|
2244
|
+
const args = repoDir === options.workspaceRoot ? ["install", "--package-lock-only", "--ignore-scripts", "--workspaces=false", "--no-audit", "--no-fund"] : ["install", "--package-lock-only", "--ignore-scripts", "--workspaces=false", "--no-audit", "--no-fund"];
|
|
2225
2245
|
const spawnCommand = npmCommandForWorkflowSpawn(args);
|
|
2226
2246
|
let lastDetail = "";
|
|
2227
2247
|
for (let attempt = 1; attempt <= 10; attempt += 1) {
|
|
@@ -4718,7 +4738,7 @@ async function workflowSave(helpers, input) {
|
|
|
4718
4738
|
verifyMode: normalizeSaveVerifyMode(effectiveInput.verify === false ? "skip" : effectiveInput.verifyMode),
|
|
4719
4739
|
commitMessageMode: effectiveInput.commitMessageMode ?? "auto",
|
|
4720
4740
|
workflowRunId: workflowRun.runId,
|
|
4721
|
-
deferPushUntilVerified:
|
|
4741
|
+
deferPushUntilVerified: true,
|
|
4722
4742
|
onProgress: (line, stream) => helpers.write(line, stream),
|
|
4723
4743
|
onWaveSaved: branch === STAGING_BRANCH && shouldUseHostedSaveCi(effectiveInput, branch, saveLane) ? async ({ nodes, reports, rootRepo: waveRootRepo }) => {
|
|
4724
4744
|
const nonRootReportsForWave = reports.filter((repo, index) => nodes[index]?.id !== ".");
|
|
@@ -5691,10 +5711,12 @@ ${currentBlockers.map((entry) => `- ${entry}`).join("\n")}`, {
|
|
|
5691
5711
|
{ root, runId: workflowRun.runId, onProgress: (line, stream) => helpers.write(line, stream) }
|
|
5692
5712
|
)) : (skipJournalStep(root, workflowRun.runId, "hosted-ci", { skippedReason: "ci off" }), { status: "skipped", reason: "ci off" });
|
|
5693
5713
|
const workspaceLinks = await executeJournalStep(root, workflowRun.runId, "workspace-link-restore", () => ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"));
|
|
5694
|
-
|
|
5695
|
-
|
|
5714
|
+
if (!managedWorkflowWorktreeMetadata(root)) {
|
|
5715
|
+
for (const repo of checkedOutStagePromotionRepos(root)) {
|
|
5716
|
+
syncBranchWithOrigin(repo.dir, STAGING_BRANCH);
|
|
5717
|
+
}
|
|
5718
|
+
syncBranchWithOrigin(repoRoot(root), STAGING_BRANCH);
|
|
5696
5719
|
}
|
|
5697
|
-
syncBranchWithOrigin(repoRoot(root), STAGING_BRANCH);
|
|
5698
5720
|
const cleanup = cleanupMode === "success" ? await executeJournalStep(root, workflowRun.runId, "cleanup-source", () => cleanupStageSourceBranches(root, featureBranch, typedManifest)) : (skipJournalStep(root, workflowRun.runId, "cleanup-source", { skippedReason: "manual cleanup selected" }), { status: "skipped", reason: "manual cleanup selected" });
|
|
5699
5721
|
const payload = {
|
|
5700
5722
|
...basePayload,
|
|
@@ -5996,6 +6018,7 @@ ${blockers.join("\n")}`, {
|
|
|
5996
6018
|
},
|
|
5997
6019
|
{ id: "verify-published-artifacts", description: "Verify immutable registry artifacts exist after publish workflows", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
|
|
5998
6020
|
{ id: "production-package-deploy-workflows", description: "Wait for production package deploy workflows before live verification", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
|
|
6021
|
+
{ id: "persist-production-image-refs", description: "Persist released production image refs and verify deployment readiness", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
|
|
5999
6022
|
{ id: "release-root", description: `Release market ${plannedRelease.rootVersion}`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
|
|
6000
6023
|
{ id: "publish-wait", description: "Wait for production release workflows", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
|
|
6001
6024
|
{ id: "release-back-merge", description: "Back-merge production release history into staging", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
|
|
@@ -6038,14 +6061,14 @@ ${blockers.join("\n")}`, {
|
|
|
6038
6061
|
});
|
|
6039
6062
|
const workspaceUnlink = await executeJournalStep(root, workflowRun.runId, "workspace-unlink", () => unlinkWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"));
|
|
6040
6063
|
const releaseMetadata = await executeJournalStep(root, workflowRun.runId, "prepare-release-metadata", () => {
|
|
6041
|
-
applyStableWorkspaceVersionChanges(root, allVersions);
|
|
6064
|
+
applyStableWorkspaceVersionChanges(root, allVersions, selectedPackageSet);
|
|
6042
6065
|
const adapterMetadata = checkedOutWorkspacePackageRepos(root).filter((pkg) => selectedPackageSet.has(pkg.name)).map((pkg) => ({
|
|
6043
6066
|
name: pkg.name,
|
|
6044
6067
|
version: selectedVersions.get(pkg.name) ?? null,
|
|
6045
6068
|
result: selectedVersions.has(pkg.name) ? { status: "pending-package-release-step" } : { status: "skipped", reason: "no planned version" }
|
|
6046
6069
|
}));
|
|
6047
6070
|
const rootPackageLock = updatePackageLockRootVersion(root, plannedRelease.rootVersion);
|
|
6048
|
-
const remainingDevReferences = collectInternalDevReferenceIssues(root, selectedPackageSet).filter((issue) => !issue.reason.startsWith("lockfile-"));
|
|
6071
|
+
const remainingDevReferences = collectInternalDevReferenceIssues(root, selectedPackageSet, selectedPackageSet).filter((issue) => !issue.reason.startsWith("lockfile-"));
|
|
6049
6072
|
if (remainingDevReferences.length > 0) {
|
|
6050
6073
|
const rendered = remainingDevReferences.map((issue) => `${issue.repoName}: ${issue.filePath} ${issue.dependencyName ?? ""} ${issue.reason} ${issue.spec}`).join("\n");
|
|
6051
6074
|
throw new Error(`Stable release metadata still contains development references.
|
|
@@ -6128,6 +6151,26 @@ ${rendered}`);
|
|
|
6128
6151
|
onProgress: (line, stream) => helpers.write(line, stream)
|
|
6129
6152
|
}).then((workflowGates) => ({ workflowGates }));
|
|
6130
6153
|
});
|
|
6154
|
+
const productionImageRefs = await executeJournalStep(root, workflowRun.runId, "persist-production-image-refs", async () => {
|
|
6155
|
+
const persisted = persistProductionReleaseImageRefs(root, releaseImageRefs);
|
|
6156
|
+
if (helpers.context.env) {
|
|
6157
|
+
Object.assign(helpers.context.env, persisted);
|
|
6158
|
+
Object.assign(process.env, persisted);
|
|
6159
|
+
}
|
|
6160
|
+
const readiness = await withContextEnv(persisted, () => collectTreeseedDeploymentReadiness({
|
|
6161
|
+
tenantRoot: root,
|
|
6162
|
+
environment: "prod",
|
|
6163
|
+
appId: singleSelectedWorkflowAppId(plannedRelease.applicationSelection)
|
|
6164
|
+
}));
|
|
6165
|
+
const failures = readiness.checks.filter((check) => check.status === "failed").map((check) => `${check.id}: ${check.message}${check.remediation ? ` Remediation: ${check.remediation}` : ""}`);
|
|
6166
|
+
if (failures.length > 0) {
|
|
6167
|
+
workflowError("release", "hosted_live_verification_failed", `Production readiness failed after persisting released image refs:
|
|
6168
|
+
${failures.join("\n")}`, {
|
|
6169
|
+
details: { releaseImageRefs: persisted, readiness }
|
|
6170
|
+
});
|
|
6171
|
+
}
|
|
6172
|
+
return { persisted, readiness };
|
|
6173
|
+
});
|
|
6131
6174
|
const rootRelease = await executeJournalStep(root, workflowRun.runId, "release-root", () => {
|
|
6132
6175
|
const rootInstall = runReleaseNpmInstall(root, { workspaceRoot: root });
|
|
6133
6176
|
const changelog = updateReleaseChangelog(repoRoot(root), {
|
|
@@ -6201,6 +6244,7 @@ ${rendered}`);
|
|
|
6201
6244
|
publishWait: publishWait.workflowGates,
|
|
6202
6245
|
publishedArtifacts,
|
|
6203
6246
|
productionPackageDeployWorkflows,
|
|
6247
|
+
productionImageRefs,
|
|
6204
6248
|
backMerge,
|
|
6205
6249
|
workspaceLinks,
|
|
6206
6250
|
releasedCommit: String(rootRelease.commit.commitSha ?? ""),
|