@treeseed/sdk 0.12.60 → 0.12.62
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/guarantees/index.js +59 -56
- package/dist/hosting/contracts.d.ts +0 -19
- package/dist/hosting/graph.d.ts +1 -86
- package/dist/hosting/graph.js +96 -245
- package/dist/local-dev/managed-dev.js +30 -8
- package/dist/managed-dependencies.d.ts +3 -0
- package/dist/managed-dependencies.js +294 -20
- package/dist/operations/services/deploy.js +5 -5
- package/dist/operations/services/deployment-readiness.js +5 -4
- package/dist/operations/services/git-runner.d.ts +2 -0
- package/dist/operations/services/git-runner.js +23 -2
- package/dist/operations/services/hosted-service-checks.js +28 -0
- package/dist/operations/services/live-hosted-service-checks.js +51 -15
- package/dist/operations/services/local-cleanup.d.ts +1 -0
- package/dist/operations/services/local-cleanup.js +28 -9
- package/dist/operations/services/package-adapters.js +3 -3
- package/dist/operations/services/railway-api.d.ts +72 -28
- package/dist/operations/services/railway-api.js +321 -876
- package/dist/operations/services/railway-cli.d.ts +47 -0
- package/dist/operations/services/railway-cli.js +142 -0
- package/dist/operations/services/railway-deploy.d.ts +2 -2
- package/dist/operations/services/railway-deploy.js +36 -91
- package/dist/operations/services/railway-source-policy.d.ts +6 -0
- package/dist/operations/services/railway-source-policy.js +52 -9
- package/dist/operations/services/repository-save-orchestrator.d.ts +2 -0
- package/dist/operations/services/repository-save-orchestrator.js +45 -14
- package/dist/operations-types.d.ts +3 -1
- package/dist/platform/contracts.d.ts +1 -0
- package/dist/platform/deploy-config.js +2 -1
- package/dist/reconcile/builtin-adapters.js +519 -684
- package/dist/reconcile/desired-state.js +5 -3
- package/dist/reconcile/engine.js +34 -28
- package/dist/reconcile/live-acceptance.js +2 -11
- package/dist/reconcile/providers/railway-iac.d.ts +147 -0
- package/dist/reconcile/providers/railway-iac.js +289 -16
- package/dist/scenes/runner.js +11 -11
- package/dist/scripts/build-dist.js +22 -0
- package/dist/workflow/operations.d.ts +12 -0
- package/dist/workflow/operations.js +441 -105
- package/dist/workflow/runs.d.ts +5 -0
- package/dist/workflow/runs.js +9 -3
- package/dist/workflow-support.d.ts +1 -1
- package/dist/workflow-support.js +3 -1
- package/package.json +1 -2
|
@@ -153,7 +153,6 @@ import {
|
|
|
153
153
|
archiveWorkflowRun,
|
|
154
154
|
cacheWorkflowGateResult,
|
|
155
155
|
classifyWorkflowRunJournal,
|
|
156
|
-
classifyWorkflowRunJournals,
|
|
157
156
|
createWorkflowRunJournal,
|
|
158
157
|
generateWorkflowRunId,
|
|
159
158
|
getCachedSuccessfulWorkflowGate,
|
|
@@ -479,6 +478,14 @@ async function waitForWorkflowGates(operation, gates, ciMode, options = {}) {
|
|
|
479
478
|
env: gateEnv,
|
|
480
479
|
onProgress: options.onProgress
|
|
481
480
|
});
|
|
481
|
+
} else if (result.status === "completed" && result.conclusion !== "success" && options.retryFailedOnce) {
|
|
482
|
+
const retry = await rerunGitHubActionsFailedJobs(result, gateEnv);
|
|
483
|
+
options.onProgress?.(`[${operation}][gate][${gateWithTimeout.name}] Retrying failed jobs once for adopted immutable release run ${retry.runId}.`);
|
|
484
|
+
result = await waitForGitHubActionsGate(gateWithTimeout, {
|
|
485
|
+
operation,
|
|
486
|
+
env: gateEnv,
|
|
487
|
+
onProgress: options.onProgress
|
|
488
|
+
});
|
|
482
489
|
}
|
|
483
490
|
const normalized = {
|
|
484
491
|
name: gateWithTimeout.name,
|
|
@@ -551,11 +558,12 @@ function selectorFromWorkflowHostingGraph(graph) {
|
|
|
551
558
|
].filter((hostId) => hostId !== "smtp" && hostId !== "local-process" && hostId !== "local-docker"))],
|
|
552
559
|
serviceId: [...new Set(graph.units.flatMap((unit) => [
|
|
553
560
|
unit.id,
|
|
561
|
+
typeof unit.config.poolKey === "string" ? unit.config.poolKey : null,
|
|
554
562
|
typeof unit.config.serviceName === "string" ? unit.config.serviceName : null
|
|
555
563
|
]).concat(domainServiceIds).filter((value) => Boolean(value)))],
|
|
556
564
|
serviceType: [...new Set(graph.units.flatMap((unit) => {
|
|
557
565
|
if (unit.id === "api") return ["api-runtime", "railway-service:api", "custom-domain:api", "dns-record"];
|
|
558
|
-
if (unit.id === "operationsRunner") return ["operations-runner-runtime", "railway-service:operations-runner"];
|
|
566
|
+
if (unit.id === "operationsRunner" || unit.config.poolKey === "operationsRunner") return ["operations-runner-runtime", "railway-service:operations-runner"];
|
|
559
567
|
if (unit.placement === "runner-capacity") return ["api-runtime", "operations-runner-runtime", "railway-service:api", "railway-service:operations-runner"];
|
|
560
568
|
if (unit.host.id === "cloudflare") return ["web-ui", "edge-worker", "content-store", "database", "kv-form-guard", "turnstile-widget", "pages-project", "custom-domain:web", "dns-record"];
|
|
561
569
|
return [];
|
|
@@ -707,6 +715,40 @@ ${liveFailures.join("\n")}`, {
|
|
|
707
715
|
}
|
|
708
716
|
return live;
|
|
709
717
|
}
|
|
718
|
+
async function verifyReleaseApiEnvironmentIsolation(root, helpers, releaseImageRefs) {
|
|
719
|
+
const reports = {};
|
|
720
|
+
for (const environment of ["prod", "staging"]) {
|
|
721
|
+
const env = {
|
|
722
|
+
...helpers.context.env,
|
|
723
|
+
...collectTreeseedConfigSeedValues(root, environment, helpers.context.env),
|
|
724
|
+
...environment === "prod" ? releaseImageRefs : {}
|
|
725
|
+
};
|
|
726
|
+
helpers.write(`[release][railway] read-only ${environment} source-invariance verification started.`, "stderr");
|
|
727
|
+
const report = await collectTreeseedLiveHostedServiceChecks({
|
|
728
|
+
tenantRoot: root,
|
|
729
|
+
target: environment,
|
|
730
|
+
appId: "api",
|
|
731
|
+
serviceKeys: ["api", "operationsRunner", "public-treedx-node-01"],
|
|
732
|
+
strict: true,
|
|
733
|
+
requireLiveRailway: true,
|
|
734
|
+
requireLiveHttp: true,
|
|
735
|
+
env
|
|
736
|
+
});
|
|
737
|
+
const failures = [
|
|
738
|
+
...report.checks.filter((check) => check.status === "failed").map((check) => `${check.id}: ${check.issues.join("; ") || "failed"}`),
|
|
739
|
+
...report.liveObservation.issues
|
|
740
|
+
];
|
|
741
|
+
if (failures.length > 0) {
|
|
742
|
+
workflowError("release", "hosted_live_verification_failed", `${environment} API source-invariance verification failed after production deployment:
|
|
743
|
+
${failures.join("\n")}`, {
|
|
744
|
+
details: { environment, report }
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
reports[environment] = report;
|
|
748
|
+
helpers.write(`[release][railway] read-only ${environment} source-invariance verification passed.`, "stderr");
|
|
749
|
+
}
|
|
750
|
+
return { status: "verified", order: ["prod", "staging"], reports };
|
|
751
|
+
}
|
|
710
752
|
function productionReleaseImageRefEnv(selectedVersions) {
|
|
711
753
|
const refs = {};
|
|
712
754
|
const apiVersion = selectedVersions.get("@treeseed/api");
|
|
@@ -1285,6 +1327,62 @@ function ensureReleaseTag(repoDir, tagName, commitSha, message) {
|
|
|
1285
1327
|
remote: remoteCommit ? "existing" : "pushed"
|
|
1286
1328
|
};
|
|
1287
1329
|
}
|
|
1330
|
+
async function adoptPublishedPackageRelease(pkg, version) {
|
|
1331
|
+
const commitSha = tagCommitSha(pkg.dir, version);
|
|
1332
|
+
if (!commitSha) return null;
|
|
1333
|
+
const remoteCommit = remoteTagCommit(pkg.dir, version);
|
|
1334
|
+
if (remoteCommit !== commitSha) {
|
|
1335
|
+
throw new Error(`Release tag ${version} for ${pkg.name} is inconsistent: local=${commitSha}, origin=${remoteCommit ?? "(missing)"}.`);
|
|
1336
|
+
}
|
|
1337
|
+
let taggedManifest;
|
|
1338
|
+
try {
|
|
1339
|
+
taggedManifest = JSON.parse(runGit(["show", `${commitSha}:package.json`], { cwd: pkg.dir, capture: true }));
|
|
1340
|
+
} catch (error) {
|
|
1341
|
+
throw new Error(`Release tag ${version} for ${pkg.name} does not contain a valid package.json: ${error instanceof Error ? error.message : String(error)}`);
|
|
1342
|
+
}
|
|
1343
|
+
if (taggedManifest.name !== pkg.name || taggedManifest.version !== version) {
|
|
1344
|
+
throw new Error(`Release tag ${version} does not identify ${pkg.name}@${version}.`);
|
|
1345
|
+
}
|
|
1346
|
+
if (hasMeaningfulChanges(pkg.dir)) {
|
|
1347
|
+
throw new Error(`Cannot adopt published ${pkg.name}@${version} while ${pkg.dir} has uncommitted changes.`);
|
|
1348
|
+
}
|
|
1349
|
+
const publishedArtifacts = await verifyPublishedReleaseArtifacts(/* @__PURE__ */ new Map([[pkg.name, version]]));
|
|
1350
|
+
const previousStagingHead = remoteHeadCommit(pkg.dir, STAGING_BRANCH);
|
|
1351
|
+
if (previousStagingHead !== commitSha) {
|
|
1352
|
+
runGit([
|
|
1353
|
+
"push",
|
|
1354
|
+
`--force-with-lease=refs/heads/${STAGING_BRANCH}:${previousStagingHead}`,
|
|
1355
|
+
"origin",
|
|
1356
|
+
`${commitSha}:refs/heads/${STAGING_BRANCH}`
|
|
1357
|
+
], { cwd: pkg.dir });
|
|
1358
|
+
}
|
|
1359
|
+
const previousProductionHead = remoteHeadCommit(pkg.dir, PRODUCTION_BRANCH);
|
|
1360
|
+
if (previousProductionHead !== commitSha) {
|
|
1361
|
+
promoteCommitToProductionBranch(pkg.dir, commitSha);
|
|
1362
|
+
}
|
|
1363
|
+
if (currentBranch(pkg.dir) !== STAGING_BRANCH) {
|
|
1364
|
+
checkoutBranch(pkg.dir, STAGING_BRANCH);
|
|
1365
|
+
}
|
|
1366
|
+
if (headCommit(pkg.dir) !== commitSha) {
|
|
1367
|
+
runGit(["reset", "--hard", commitSha], { cwd: pkg.dir });
|
|
1368
|
+
}
|
|
1369
|
+
const observedStagingHead = remoteHeadCommit(pkg.dir, STAGING_BRANCH);
|
|
1370
|
+
const observedProductionHead = remoteHeadCommit(pkg.dir, PRODUCTION_BRANCH);
|
|
1371
|
+
if (headCommit(pkg.dir) !== commitSha || observedStagingHead !== commitSha || observedProductionHead !== commitSha) {
|
|
1372
|
+
throw new Error(`Published release adoption failed for ${pkg.name}@${version}; local=${headCommit(pkg.dir)}, staging=${observedStagingHead}, main=${observedProductionHead}, expected=${commitSha}.`);
|
|
1373
|
+
}
|
|
1374
|
+
return {
|
|
1375
|
+
name: pkg.name,
|
|
1376
|
+
version,
|
|
1377
|
+
commit: { commitSha, status: "adopted-published-tag" },
|
|
1378
|
+
tag: { tagName: version, local: "existing", remote: "existing" },
|
|
1379
|
+
branches: {
|
|
1380
|
+
staging: previousStagingHead === commitSha ? "existing" : "restored",
|
|
1381
|
+
production: previousProductionHead === commitSha ? "existing" : "restored"
|
|
1382
|
+
},
|
|
1383
|
+
publishedArtifacts
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1288
1386
|
function promoteCommitToProductionBranch(repoDir, commitSha) {
|
|
1289
1387
|
const expectedBefore = remoteBranchExists(repoDir, PRODUCTION_BRANCH) ? remoteHeadCommit(repoDir, PRODUCTION_BRANCH) : null;
|
|
1290
1388
|
const lease = expectedBefore ? `--force-with-lease=refs/heads/${PRODUCTION_BRANCH}:${expectedBefore}` : "--force-with-lease";
|
|
@@ -1797,7 +1895,32 @@ function gateForSavedRootReport(report, branch, scope) {
|
|
|
1797
1895
|
}
|
|
1798
1896
|
function findAutoResumableTaskRun(root, command, branch) {
|
|
1799
1897
|
if (!branch) return null;
|
|
1800
|
-
|
|
1898
|
+
const currentHeads = Object.fromEntries([
|
|
1899
|
+
["@treeseed/market", runGit(["rev-parse", "HEAD"], { cwd: repoRoot(root), capture: true }).trim()],
|
|
1900
|
+
...checkedOutWorkspacePackageRepos(root).map((repo) => [
|
|
1901
|
+
repo.name,
|
|
1902
|
+
runGit(["rev-parse", "HEAD"], { cwd: repo.dir, capture: true }).trim()
|
|
1903
|
+
])
|
|
1904
|
+
]);
|
|
1905
|
+
return listInterruptedWorkflowRuns(root).find((journal) => {
|
|
1906
|
+
if (journal.command !== command || !journal.resumable || journal.session.branchName !== branch) {
|
|
1907
|
+
return false;
|
|
1908
|
+
}
|
|
1909
|
+
const classification = classifyWorkflowRunJournal(journal, {
|
|
1910
|
+
currentBranch: branch,
|
|
1911
|
+
currentHeads
|
|
1912
|
+
});
|
|
1913
|
+
if (classification.state === "resumable") {
|
|
1914
|
+
return true;
|
|
1915
|
+
}
|
|
1916
|
+
if (classification.state === "stale") {
|
|
1917
|
+
archiveWorkflowRun(root, journal.runId, {
|
|
1918
|
+
...classification,
|
|
1919
|
+
reasons: [`${command} implicit resume skipped stale failed run`, ...classification.reasons]
|
|
1920
|
+
});
|
|
1921
|
+
}
|
|
1922
|
+
return false;
|
|
1923
|
+
}) ?? null;
|
|
1801
1924
|
}
|
|
1802
1925
|
function rejectImplicitWorkflowResume(operation, journal) {
|
|
1803
1926
|
if (!journal) return;
|
|
@@ -1980,7 +2103,8 @@ function findAutoResumableReleaseRun(root, branch, rootRepo, packageReports, opt
|
|
|
1980
2103
|
}
|
|
1981
2104
|
const classification = classifyWorkflowRunJournal(journal, {
|
|
1982
2105
|
currentBranch: branch,
|
|
1983
|
-
currentHeads
|
|
2106
|
+
currentHeads,
|
|
2107
|
+
acceptedReleaseHeads: acceptedPublishedReleaseHeads(root, journal, currentHeads)
|
|
1984
2108
|
});
|
|
1985
2109
|
if (classification.state !== "resumable") {
|
|
1986
2110
|
if (options.archiveStale && classification.state === "stale") {
|
|
@@ -2002,6 +2126,45 @@ function findAutoResumableReleaseRun(root, branch, rootRepo, packageReports, opt
|
|
|
2002
2126
|
return releasePlan ? releasePlanMatchesCurrentHeads(releasePlan, rootRepo, packageReports) : true;
|
|
2003
2127
|
}) ?? null;
|
|
2004
2128
|
}
|
|
2129
|
+
function acceptedPublishedReleaseHeads(root, journal, currentHeads) {
|
|
2130
|
+
if (journal.command !== "release") return {};
|
|
2131
|
+
const plan = stringRecord(journal.steps.find((step) => step.id === "release-plan")?.data);
|
|
2132
|
+
const plannedVersions = stringRecord(plan?.plannedVersions);
|
|
2133
|
+
if (!plannedVersions) return {};
|
|
2134
|
+
const repos = new Map(checkedOutWorkspacePackageRepos(root).map((repo) => [repo.name, repo.dir]));
|
|
2135
|
+
const accepted = {};
|
|
2136
|
+
for (const [name, versionValue] of Object.entries(plannedVersions)) {
|
|
2137
|
+
if (name === "@treeseed/market" || typeof versionValue !== "string") continue;
|
|
2138
|
+
const repoDir = repos.get(name);
|
|
2139
|
+
const currentHead = currentHeads[name];
|
|
2140
|
+
if (!repoDir || !currentHead) continue;
|
|
2141
|
+
let manifestVersion = null;
|
|
2142
|
+
try {
|
|
2143
|
+
manifestVersion = JSON.parse(readFileSync(resolve(repoDir, "package.json"), "utf8")).version ?? null;
|
|
2144
|
+
} catch {
|
|
2145
|
+
continue;
|
|
2146
|
+
}
|
|
2147
|
+
const tagHead = tagCommitSha(repoDir, versionValue);
|
|
2148
|
+
const remoteTagHead = remoteTagCommit(repoDir, versionValue);
|
|
2149
|
+
const productionHead = remoteHeadCommit(repoDir, PRODUCTION_BRANCH);
|
|
2150
|
+
const stagingHead = remoteHeadCommit(repoDir, STAGING_BRANCH);
|
|
2151
|
+
const exactPublishedHead = tagHead === currentHead && productionHead === currentHead && stagingHead === currentHead;
|
|
2152
|
+
const boundedInterruptedRetry = tagHead.length > 0 && remoteTagHead === tagHead && !hasMeaningfulChanges(repoDir) && [productionHead, stagingHead].every((head) => head === currentHead || head === tagHead);
|
|
2153
|
+
if (manifestVersion !== versionValue || !exactPublishedHead && !boundedInterruptedRetry) {
|
|
2154
|
+
if (process.env.TREESEED_RECONCILE_TRACE === "1") {
|
|
2155
|
+
process.stderr.write(`[release][resume-adoption] package=${name} accepted=false manifest=${manifestVersion === versionValue} tag=${tagHead === currentHead} remoteTag=${remoteTagHead === tagHead} main=${productionHead === currentHead} staging=${stagingHead === currentHead} bounded=${boundedInterruptedRetry}
|
|
2156
|
+
`);
|
|
2157
|
+
}
|
|
2158
|
+
continue;
|
|
2159
|
+
}
|
|
2160
|
+
accepted[name] = currentHead;
|
|
2161
|
+
if (process.env.TREESEED_RECONCILE_TRACE === "1") {
|
|
2162
|
+
process.stderr.write(`[release][resume-adoption] package=${name} accepted=true
|
|
2163
|
+
`);
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
return accepted;
|
|
2167
|
+
}
|
|
2005
2168
|
async function executeJournalStep(root, runId, stepId, action, options = {}) {
|
|
2006
2169
|
const current = readWorkflowRunJournal(root, runId);
|
|
2007
2170
|
const step = current?.steps.find((entry) => entry.id === stepId) ?? null;
|
|
@@ -2011,17 +2174,48 @@ async function executeJournalStep(root, runId, stepId, action, options = {}) {
|
|
|
2011
2174
|
if (step.status === "completed" && !options.rerunCompleted) {
|
|
2012
2175
|
return step.data ?? null;
|
|
2013
2176
|
}
|
|
2014
|
-
const
|
|
2177
|
+
const startedAt = /* @__PURE__ */ new Date();
|
|
2178
|
+
const retryCount = Number(step.retryCount ?? 0) + (step.startedAt ? 1 : 0);
|
|
2179
|
+
updateWorkflowRunJournal(root, runId, (journal) => ({
|
|
2180
|
+
...journal,
|
|
2181
|
+
steps: journal.steps.map((entry) => entry.id === stepId ? { ...entry, startedAt: startedAt.toISOString(), retryCount, lastFailure: null } : entry)
|
|
2182
|
+
}));
|
|
2183
|
+
refreshWorkflowLock(root, runId);
|
|
2184
|
+
const lockHeartbeat = setInterval(() => refreshWorkflowLock(root, runId), 15e3);
|
|
2185
|
+
lockHeartbeat.unref();
|
|
2186
|
+
process.stderr.write(`[workflow][step] start ${stepId} attempt=${retryCount + 1}
|
|
2187
|
+
`);
|
|
2188
|
+
let data;
|
|
2189
|
+
try {
|
|
2190
|
+
data = await Promise.resolve(action());
|
|
2191
|
+
} catch (error) {
|
|
2192
|
+
clearInterval(lockHeartbeat);
|
|
2193
|
+
const elapsedMs2 = Date.now() - startedAt.getTime();
|
|
2194
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2195
|
+
updateWorkflowRunJournal(root, runId, (journal) => ({
|
|
2196
|
+
...journal,
|
|
2197
|
+
steps: journal.steps.map((entry) => entry.id === stepId ? { ...entry, elapsedMs: elapsedMs2, lastFailure: message } : entry)
|
|
2198
|
+
}));
|
|
2199
|
+
process.stderr.write(`[workflow][step] fail ${stepId} elapsed=${Math.ceil(elapsedMs2 / 1e3)}s retries=${retryCount}
|
|
2200
|
+
`);
|
|
2201
|
+
throw error;
|
|
2202
|
+
}
|
|
2203
|
+
clearInterval(lockHeartbeat);
|
|
2204
|
+
const elapsedMs = Date.now() - startedAt.getTime();
|
|
2015
2205
|
updateWorkflowRunJournal(root, runId, (journal) => ({
|
|
2016
2206
|
...journal,
|
|
2017
2207
|
steps: journal.steps.map((entry) => entry.id === stepId ? {
|
|
2018
2208
|
...entry,
|
|
2019
2209
|
status: "completed",
|
|
2020
2210
|
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2211
|
+
elapsedMs,
|
|
2212
|
+
lastFailure: null,
|
|
2021
2213
|
data: data ?? null
|
|
2022
2214
|
} : entry)
|
|
2023
2215
|
}));
|
|
2024
2216
|
refreshWorkflowLock(root, runId);
|
|
2217
|
+
process.stderr.write(`[workflow][step] complete ${stepId} elapsed=${Math.ceil(elapsedMs / 1e3)}s retries=${retryCount}
|
|
2218
|
+
`);
|
|
2025
2219
|
return data;
|
|
2026
2220
|
}
|
|
2027
2221
|
function skipJournalStep(root, runId, stepId, data = null) {
|
|
@@ -2185,7 +2379,7 @@ function productionPackageDeployGates(root, versions) {
|
|
|
2185
2379
|
})];
|
|
2186
2380
|
});
|
|
2187
2381
|
}
|
|
2188
|
-
function prepareAdapterReleaseMetadata(root, pkg, version) {
|
|
2382
|
+
async function prepareAdapterReleaseMetadata(root, pkg, version) {
|
|
2189
2383
|
const adapter = discoverTreeseedPackageAdapters(root).find((entry) => entry.id === pkg.name || entry.name === pkg.name);
|
|
2190
2384
|
if (adapter?.kind === "beam-elixir-rust" && existsSync(resolve(pkg.dir, "scripts", "bump-release-version.ts"))) {
|
|
2191
2385
|
const tsx = resolve(root, "node_modules/.bin/tsx");
|
|
@@ -2199,7 +2393,7 @@ function prepareAdapterReleaseMetadata(root, pkg, version) {
|
|
|
2199
2393
|
return {
|
|
2200
2394
|
status: "npm-install",
|
|
2201
2395
|
adapter: adapter?.id ?? pkg.name,
|
|
2202
|
-
...runReleaseNpmInstall(pkg.dir, { workspaceRoot: root })
|
|
2396
|
+
...await runReleaseNpmInstall(pkg.dir, { workspaceRoot: root })
|
|
2203
2397
|
};
|
|
2204
2398
|
}
|
|
2205
2399
|
return { status: "skipped", adapter: adapter?.id ?? pkg.name, reason: "no package metadata updater" };
|
|
@@ -2237,42 +2431,96 @@ function npmCommandForWorkflowSpawn(args) {
|
|
|
2237
2431
|
]
|
|
2238
2432
|
};
|
|
2239
2433
|
}
|
|
2240
|
-
function
|
|
2241
|
-
|
|
2242
|
-
|
|
2434
|
+
function lockfileRootMatchesManifest(repoDir) {
|
|
2435
|
+
try {
|
|
2436
|
+
const manifest = JSON.parse(readFileSync(resolve(repoDir, "package.json"), "utf8"));
|
|
2437
|
+
const lockfile = JSON.parse(readFileSync(resolve(repoDir, "package-lock.json"), "utf8"));
|
|
2438
|
+
const root = lockfile.packages?.[""];
|
|
2439
|
+
if (!root || root.version !== manifest.version) return false;
|
|
2440
|
+
for (const field of ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"]) {
|
|
2441
|
+
const expected = manifest[field] ?? {};
|
|
2442
|
+
const observed = root[field] ?? {};
|
|
2443
|
+
if (JSON.stringify(expected) !== JSON.stringify(observed)) return false;
|
|
2444
|
+
}
|
|
2445
|
+
return true;
|
|
2446
|
+
} catch {
|
|
2447
|
+
return false;
|
|
2243
2448
|
}
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2449
|
+
}
|
|
2450
|
+
function waitForReleaseInstall(command, args, repoDir, attempt) {
|
|
2451
|
+
return new Promise((resolvePromise) => {
|
|
2452
|
+
const startedAt = Date.now();
|
|
2453
|
+
let settled = false;
|
|
2454
|
+
const child = spawn(command, args, {
|
|
2249
2455
|
cwd: repoDir,
|
|
2250
2456
|
env: {
|
|
2251
2457
|
...process.env,
|
|
2252
2458
|
npm_config_audit: "false",
|
|
2253
|
-
npm_config_fetch_retries: "
|
|
2459
|
+
npm_config_fetch_retries: "2",
|
|
2254
2460
|
npm_config_fund: "false",
|
|
2255
|
-
npm_config_foreground_scripts: "
|
|
2461
|
+
npm_config_foreground_scripts: "false",
|
|
2256
2462
|
npm_config_loglevel: "warn",
|
|
2257
2463
|
npm_config_maxsockets: "4",
|
|
2258
|
-
|
|
2464
|
+
npm_config_prefer_offline: "true",
|
|
2259
2465
|
npm_config_progress: "false"
|
|
2260
2466
|
},
|
|
2261
|
-
stdio: "pipe",
|
|
2262
|
-
encoding: "utf8"
|
|
2467
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2263
2468
|
});
|
|
2469
|
+
const output = [];
|
|
2470
|
+
child.stdout?.on("data", (chunk) => output.push(Buffer.from(chunk)));
|
|
2471
|
+
child.stderr?.on("data", (chunk) => output.push(Buffer.from(chunk)));
|
|
2472
|
+
const heartbeat = setInterval(() => {
|
|
2473
|
+
process.stderr.write(`[release][restore] repository=${repoDir} phase=package-lock attempt=${attempt} elapsed=${Math.ceil((Date.now() - startedAt) / 1e3)}s
|
|
2474
|
+
`);
|
|
2475
|
+
}, 15e3);
|
|
2476
|
+
child.once("close", (status) => {
|
|
2477
|
+
if (settled) return;
|
|
2478
|
+
settled = true;
|
|
2479
|
+
clearInterval(heartbeat);
|
|
2480
|
+
resolvePromise({ status, detail: Buffer.concat(output).toString("utf8").trim() });
|
|
2481
|
+
});
|
|
2482
|
+
child.once("error", (error) => {
|
|
2483
|
+
if (settled) return;
|
|
2484
|
+
settled = true;
|
|
2485
|
+
clearInterval(heartbeat);
|
|
2486
|
+
output.push(Buffer.from(error.message));
|
|
2487
|
+
resolvePromise({ status: null, detail: Buffer.concat(output).toString("utf8").trim() });
|
|
2488
|
+
});
|
|
2489
|
+
});
|
|
2490
|
+
}
|
|
2491
|
+
async function runReleaseNpmInstall(repoDir, options = {}) {
|
|
2492
|
+
if (shouldSkipReleaseInstall()) {
|
|
2493
|
+
return { status: "skipped", reason: "disabled" };
|
|
2494
|
+
}
|
|
2495
|
+
if (repoDir === options.workspaceRoot && lockfileRootMatchesManifest(repoDir)) {
|
|
2496
|
+
return { status: "skipped", reason: "root-lockfile-already-matches", attempts: 0 };
|
|
2497
|
+
}
|
|
2498
|
+
const baseArgs = ["install", "--package-lock-only", "--ignore-scripts", "--workspaces=false", "--no-audit", "--no-fund"];
|
|
2499
|
+
const propagationDelaysMs = [15e3, 3e4, 6e4, 12e4, 18e4];
|
|
2500
|
+
const startedAt = Date.now();
|
|
2501
|
+
let lastDetail = "";
|
|
2502
|
+
for (let attempt = 1; attempt <= propagationDelaysMs.length + 1; attempt += 1) {
|
|
2503
|
+
const args = [...baseArgs, attempt === 1 ? "--prefer-offline" : "--prefer-online"];
|
|
2504
|
+
const spawnCommand = npmCommandForWorkflowSpawn(args);
|
|
2505
|
+
process.stderr.write(`[release][restore] repository=${repoDir} phase=package-lock attempt=${attempt} elapsed=${Math.ceil((Date.now() - startedAt) / 1e3)}s
|
|
2506
|
+
`);
|
|
2507
|
+
const result = await waitForReleaseInstall(spawnCommand.command, spawnCommand.args, repoDir, attempt);
|
|
2264
2508
|
if (result.status === 0) {
|
|
2265
2509
|
return { status: "completed", reason: null, attempts: attempt };
|
|
2266
2510
|
}
|
|
2267
|
-
lastDetail =
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2511
|
+
lastDetail = result.detail;
|
|
2512
|
+
const propagationDelayMs = propagationDelaysMs[attempt - 1];
|
|
2513
|
+
if (!/No matching version found|notarget|ETARGET|E404/u.test(lastDetail) || propagationDelayMs == null) break;
|
|
2514
|
+
let remainingMs = propagationDelayMs;
|
|
2515
|
+
while (remainingMs > 0) {
|
|
2516
|
+
process.stderr.write(`[release][restore] repository=${repoDir} phase=registry-propagation nextAttempt=${attempt + 1} remaining=${Math.ceil(remainingMs / 1e3)}s elapsed=${Math.ceil((Date.now() - startedAt) / 1e3)}s
|
|
2517
|
+
`);
|
|
2518
|
+
const sliceMs = Math.min(15e3, remainingMs);
|
|
2519
|
+
await new Promise((resolvePromise) => setTimeout(resolvePromise, sliceMs));
|
|
2520
|
+
remainingMs -= sliceMs;
|
|
2521
|
+
}
|
|
2274
2522
|
}
|
|
2275
|
-
throw new Error(lastDetail || `npm ${
|
|
2523
|
+
throw new Error(lastDetail || `npm ${baseArgs.join(" ")} failed`);
|
|
2276
2524
|
}
|
|
2277
2525
|
function pathIsWithin(parent, candidate) {
|
|
2278
2526
|
const path = relative(parent, candidate);
|
|
@@ -4543,7 +4791,7 @@ async function workflowSave(helpers, input) {
|
|
|
4543
4791
|
const recursiveWorkspace = session.mode === "recursive-workspace";
|
|
4544
4792
|
const mode = session.mode;
|
|
4545
4793
|
const executionMode = normalizeExecutionMode(input);
|
|
4546
|
-
const explicitResumeRunId = helpers.context.workflow?.resumeRunId ?? null;
|
|
4794
|
+
const explicitResumeRunId = helpers.context.workflow?.resumeRunId ?? input.resumeRunId ?? null;
|
|
4547
4795
|
const autoResumeRun = executionMode === "execute" && !explicitResumeRunId ? findAutoResumableSaveRun(root, branch) : null;
|
|
4548
4796
|
rejectImplicitWorkflowResume("save", autoResumeRun);
|
|
4549
4797
|
const planAutoResumeRun = null;
|
|
@@ -4708,7 +4956,10 @@ async function workflowSave(helpers, input) {
|
|
|
4708
4956
|
resumable: true
|
|
4709
4957
|
}] : []
|
|
4710
4958
|
],
|
|
4711
|
-
|
|
4959
|
+
explicitResumeRunId ? {
|
|
4960
|
+
...helpers.context,
|
|
4961
|
+
workflow: { ...helpers.context.workflow ?? {}, resumeRunId: explicitResumeRunId }
|
|
4962
|
+
} : autoResumeRun ? {
|
|
4712
4963
|
...helpers.context,
|
|
4713
4964
|
workflow: {
|
|
4714
4965
|
...helpers.context.workflow ?? {},
|
|
@@ -4994,7 +5245,7 @@ async function workflowClose(helpers, input) {
|
|
|
4994
5245
|
const root = workspaceRoot(tenantRoot);
|
|
4995
5246
|
const executionMode = normalizeExecutionMode(input);
|
|
4996
5247
|
const session = resolveTreeseedWorkflowSession(root);
|
|
4997
|
-
const explicitResumeRunId = helpers.context.workflow?.resumeRunId ?? null;
|
|
5248
|
+
const explicitResumeRunId = helpers.context.workflow?.resumeRunId ?? input.resumeRunId ?? null;
|
|
4998
5249
|
const autoResumeRun = executionMode === "execute" && !explicitResumeRunId ? findAutoResumableTaskRun(root, "close", session.branchName) : null;
|
|
4999
5250
|
rejectImplicitWorkflowResume("close", autoResumeRun);
|
|
5000
5251
|
const planAutoResumeRun = null;
|
|
@@ -5081,7 +5332,13 @@ async function workflowClose(helpers, input) {
|
|
|
5081
5332
|
{ id: "workspace-link", description: "Restore local workspace links", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
|
|
5082
5333
|
...isManagedWorkflowWorktree(root) ? [{ id: "worktree-cleanup", description: "Remove managed workflow worktree", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: false }] : []
|
|
5083
5334
|
],
|
|
5084
|
-
|
|
5335
|
+
explicitResumeRunId ? {
|
|
5336
|
+
...helpers.context,
|
|
5337
|
+
workflow: {
|
|
5338
|
+
...helpers.context.workflow ?? {},
|
|
5339
|
+
resumeRunId: explicitResumeRunId
|
|
5340
|
+
}
|
|
5341
|
+
} : autoResumeRun ? {
|
|
5085
5342
|
...helpers.context,
|
|
5086
5343
|
workflow: {
|
|
5087
5344
|
...helpers.context.workflow ?? {},
|
|
@@ -5192,6 +5449,7 @@ async function workflowClose(helpers, input) {
|
|
|
5192
5449
|
}
|
|
5193
5450
|
function stagingCandidateWorkflowGates(root, manifest) {
|
|
5194
5451
|
const gates = [];
|
|
5452
|
+
const adapters = discoverTreeseedPackageAdapters(root);
|
|
5195
5453
|
const add = (name, repoPath, headSha, workflow, deploy = false) => {
|
|
5196
5454
|
if (!workflowFileExists(repoPath, workflow)) return;
|
|
5197
5455
|
const gate = { name, repoPath, workflow, branch: STAGING_BRANCH, headSha };
|
|
@@ -5199,10 +5457,11 @@ function stagingCandidateWorkflowGates(root, manifest) {
|
|
|
5199
5457
|
};
|
|
5200
5458
|
for (const pkg of manifest.packages) {
|
|
5201
5459
|
const repoPath = resolve(root, pkg.path);
|
|
5460
|
+
const adapter = adapters.find((candidate) => candidate.id === pkg.name || candidate.name === pkg.name);
|
|
5202
5461
|
if (manifest.stagingHeadsBefore[pkg.name] !== pkg.commit) {
|
|
5203
5462
|
add(pkg.name, repoPath, pkg.commit, "verify.yml");
|
|
5204
5463
|
}
|
|
5205
|
-
if (manifest.stagingHeadsBefore[pkg.name] !== pkg.commit && existsSync(resolve(repoPath, "treeseed.site.yaml"))) {
|
|
5464
|
+
if (manifest.stagingHeadsBefore[pkg.name] !== pkg.commit && adapter?.capabilities.deploy === true && existsSync(resolve(repoPath, "treeseed.site.yaml"))) {
|
|
5206
5465
|
add(pkg.name, repoPath, pkg.commit, "deploy.yml", true);
|
|
5207
5466
|
}
|
|
5208
5467
|
}
|
|
@@ -5476,7 +5735,7 @@ async function workflowStage(helpers, input) {
|
|
|
5476
5735
|
const root = workspaceRoot(tenantRoot);
|
|
5477
5736
|
const executionMode = normalizeExecutionMode(input);
|
|
5478
5737
|
const session = resolveTreeseedWorkflowSession(root);
|
|
5479
|
-
const explicitResumeRunId = helpers.context.workflow?.resumeRunId ?? null;
|
|
5738
|
+
const explicitResumeRunId = helpers.context.workflow?.resumeRunId ?? input.resumeRunId ?? null;
|
|
5480
5739
|
const rawAutoResumeRun = executionMode === "execute" && !explicitResumeRunId ? findAutoResumableTaskRun(root, "stage", session.branchName) : null;
|
|
5481
5740
|
rejectImplicitWorkflowResume("stage", rawAutoResumeRun);
|
|
5482
5741
|
const autoResumeRun = rawAutoResumeRun?.steps.some((step) => step.id === "preflight") ? rawAutoResumeRun : null;
|
|
@@ -5891,7 +6150,9 @@ async function workflowRelease(helpers, input) {
|
|
|
5891
6150
|
const rootRepo = createWorkspaceRootRepoReport(root);
|
|
5892
6151
|
const packageReports = createWorkspacePackageReports(root);
|
|
5893
6152
|
const releaseHelperRepos = checkedOutReleaseHelperRepos(root);
|
|
5894
|
-
const explicitResumeRunId = helpers.context.workflow?.resumeRunId ?? null;
|
|
6153
|
+
const explicitResumeRunId = helpers.context.workflow?.resumeRunId ?? input.resumeRunId ?? null;
|
|
6154
|
+
const explicitResumeJournal = explicitResumeRunId ? readWorkflowRunJournal(root, explicitResumeRunId) : null;
|
|
6155
|
+
const recordedReleasePlan = explicitResumeJournal?.command === "release" ? stringRecord(explicitResumeJournal.steps.find((step) => step.id === "release-plan")?.data) : null;
|
|
5895
6156
|
const autoResumeRun = executionMode === "execute" && !explicitResumeRunId && input.fresh !== true ? findAutoResumableReleaseRun(root, session.branchName, rootRepo, packageReports, { archiveStale: false }) : null;
|
|
5896
6157
|
const planAutoResumeRun = executionMode === "plan" && input.fresh !== true ? findAutoResumableReleaseRun(root, session.branchName, rootRepo, packageReports) : null;
|
|
5897
6158
|
const effectiveInput = autoResumeRun ? {
|
|
@@ -5902,7 +6163,7 @@ async function workflowRelease(helpers, input) {
|
|
|
5902
6163
|
const level = effectiveInput.bump ?? "patch";
|
|
5903
6164
|
const ciMode = normalizeCiMode(effectiveInput.ciMode, "release");
|
|
5904
6165
|
const packageSelection = session.packageSelection;
|
|
5905
|
-
const plannedRelease = buildReleasePlanSnapshot({
|
|
6166
|
+
const plannedRelease = recordedReleasePlan ?? buildReleasePlanSnapshot({
|
|
5906
6167
|
root,
|
|
5907
6168
|
mode: session.mode,
|
|
5908
6169
|
level,
|
|
@@ -5914,12 +6175,14 @@ async function workflowRelease(helpers, input) {
|
|
|
5914
6175
|
blockers: []
|
|
5915
6176
|
});
|
|
5916
6177
|
const selectedPackageNames = releasePlanPackageSelection(plannedRelease.packageSelection).selected;
|
|
5917
|
-
const blockers = collectReleasePlanBlockers(session, session.mode, selectedPackageNames, {
|
|
6178
|
+
const blockers = explicitResumeJournal ? [] : collectReleasePlanBlockers(session, session.mode, selectedPackageNames, {
|
|
5918
6179
|
level,
|
|
5919
6180
|
repairVersionLine: effectiveInput.repairVersionLine === true
|
|
5920
6181
|
});
|
|
5921
|
-
|
|
5922
|
-
|
|
6182
|
+
if (!explicitResumeJournal) {
|
|
6183
|
+
blockers.push(...collectReleaseHelperRepoBlockers(root));
|
|
6184
|
+
blockers.push(...stageCandidateAttestationBlockers(root));
|
|
6185
|
+
}
|
|
5923
6186
|
const selectedVersions = releasePlanVersionMap(plannedRelease.plannedVersions);
|
|
5924
6187
|
const releaseImageVersions = productionReleaseImageRefVersions(root, selectedVersions);
|
|
5925
6188
|
const releaseImageRefs = productionReleaseImageRefEnv(releaseImageVersions);
|
|
@@ -6018,13 +6281,20 @@ ${blockers.join("\n")}`, {
|
|
|
6018
6281
|
},
|
|
6019
6282
|
{ id: "verify-published-artifacts", description: "Verify immutable registry artifacts exist after publish workflows", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
|
|
6020
6283
|
{ 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 },
|
|
6284
|
+
{ id: "verify-api-environment-isolation", description: "Verify production images and staging Git sources remained isolated", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
|
|
6021
6285
|
{ 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 },
|
|
6022
6286
|
{ id: "release-root", description: `Release market ${plannedRelease.rootVersion}`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
|
|
6023
6287
|
{ id: "publish-wait", description: "Wait for production release workflows", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
|
|
6024
6288
|
{ id: "release-back-merge", description: "Back-merge production release history into staging", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
|
|
6025
6289
|
{ id: "workspace-link", description: "Restore local workspace links after release", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true }
|
|
6026
6290
|
],
|
|
6027
|
-
|
|
6291
|
+
explicitResumeRunId ? {
|
|
6292
|
+
...helpers.context,
|
|
6293
|
+
workflow: {
|
|
6294
|
+
...helpers.context.workflow ?? {},
|
|
6295
|
+
resumeRunId: explicitResumeRunId
|
|
6296
|
+
}
|
|
6297
|
+
} : autoResumeRun ? {
|
|
6028
6298
|
...helpers.context,
|
|
6029
6299
|
workflow: {
|
|
6030
6300
|
...helpers.context.workflow ?? {},
|
|
@@ -6083,57 +6353,113 @@ ${rendered}`);
|
|
|
6083
6353
|
});
|
|
6084
6354
|
const packageReleases = [];
|
|
6085
6355
|
const packageRepoByName = new Map(checkedOutWorkspacePackageRepos(root).map((entry) => [entry.name, entry]));
|
|
6086
|
-
|
|
6356
|
+
const pendingPackageReleases = new Set(selectedPackageNames.filter((name) => selectedVersions.has(name) && packageRepoByName.has(name)));
|
|
6357
|
+
const completedPackageReleases = /* @__PURE__ */ new Set();
|
|
6358
|
+
const packageReleaseResults = /* @__PURE__ */ new Map();
|
|
6359
|
+
const selectedPackageDependencies = new Map([...pendingPackageReleases].map((packageName) => {
|
|
6087
6360
|
const pkg = packageRepoByName.get(packageName);
|
|
6088
|
-
if (!pkg)
|
|
6089
|
-
const
|
|
6090
|
-
if (!
|
|
6091
|
-
|
|
6092
|
-
|
|
6093
|
-
|
|
6094
|
-
|
|
6095
|
-
|
|
6096
|
-
|
|
6097
|
-
|
|
6098
|
-
|
|
6099
|
-
|
|
6100
|
-
|
|
6101
|
-
|
|
6102
|
-
|
|
6103
|
-
|
|
6104
|
-
|
|
6105
|
-
|
|
6106
|
-
|
|
6107
|
-
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
|
|
6113
|
-
|
|
6114
|
-
|
|
6115
|
-
|
|
6116
|
-
const
|
|
6117
|
-
|
|
6118
|
-
|
|
6119
|
-
|
|
6361
|
+
if (!pkg) return [packageName, []];
|
|
6362
|
+
const manifest = readJsonFile(resolve(pkg.dir, "package.json"));
|
|
6363
|
+
if (!manifest) {
|
|
6364
|
+
throw new Error(`Release package manifest is missing or invalid for ${packageName}.`);
|
|
6365
|
+
}
|
|
6366
|
+
const dependencyNames = /* @__PURE__ */ new Set([
|
|
6367
|
+
...Object.keys(manifest.dependencies ?? {}),
|
|
6368
|
+
...Object.keys(manifest.optionalDependencies ?? {}),
|
|
6369
|
+
...Object.keys(manifest.peerDependencies ?? {})
|
|
6370
|
+
]);
|
|
6371
|
+
const dependencies = [...dependencyNames].filter((name) => pendingPackageReleases.has(name));
|
|
6372
|
+
if (packageName !== "@treeseed/sdk" && pendingPackageReleases.has("@treeseed/sdk") && !dependencies.includes("@treeseed/sdk")) {
|
|
6373
|
+
dependencies.push("@treeseed/sdk");
|
|
6374
|
+
}
|
|
6375
|
+
if (packageName === "@treeseed/api" && pendingPackageReleases.has("@treeseed/cli") && !dependencies.includes("@treeseed/cli")) {
|
|
6376
|
+
dependencies.push("@treeseed/cli");
|
|
6377
|
+
}
|
|
6378
|
+
return [packageName, dependencies];
|
|
6379
|
+
}));
|
|
6380
|
+
while (pendingPackageReleases.size > 0) {
|
|
6381
|
+
const eligible = selectedPackageNames.filter((packageName) => pendingPackageReleases.has(packageName) && (selectedPackageDependencies.get(packageName) ?? []).every((dependency) => completedPackageReleases.has(dependency)));
|
|
6382
|
+
if (eligible.length === 0) {
|
|
6383
|
+
throw new Error(`Release package dependency graph is cyclic or unresolved: ${[...pendingPackageReleases].join(", ")}.`);
|
|
6384
|
+
}
|
|
6385
|
+
const batch = eligible.slice(0, 2);
|
|
6386
|
+
helpers.write(`[release][packages] starting batch ${batch.join(", ")} (concurrency=${batch.length}/2).`, "stderr");
|
|
6387
|
+
const results = await Promise.all(batch.map(async (packageName) => {
|
|
6388
|
+
const pkg = packageRepoByName.get(packageName);
|
|
6389
|
+
const version = selectedVersions.get(pkg.name);
|
|
6390
|
+
const packageRelease = await executeJournalStep(root, workflowRun.runId, `release-${pkg.name}`, async () => {
|
|
6391
|
+
const adopted = await adoptPublishedPackageRelease(pkg, version);
|
|
6392
|
+
if (adopted) {
|
|
6393
|
+
helpers.write(`[release][packages] adopted verified published release ${pkg.name}@${version}.`, "stderr");
|
|
6394
|
+
if (pkg.name === "@treeseed/api") {
|
|
6395
|
+
const publishWait3 = await waitForWorkflowGates("release", [{
|
|
6396
|
+
name: pkg.name,
|
|
6397
|
+
repoPath: pkg.dir,
|
|
6398
|
+
workflow: releaseWorkflowForPackage(root, pkg.name),
|
|
6399
|
+
branch: version,
|
|
6400
|
+
headSha: String(stringRecord(adopted.commit)?.commitSha ?? tagCommitSha(pkg.dir, version))
|
|
6401
|
+
}], ciMode, {
|
|
6402
|
+
root,
|
|
6403
|
+
runId: workflowRun.runId,
|
|
6404
|
+
onProgress: (line, stream) => helpers.write(line, stream),
|
|
6405
|
+
retryFailedOnce: true
|
|
6406
|
+
});
|
|
6407
|
+
return { ...adopted, publishWait: publishWait3 };
|
|
6408
|
+
}
|
|
6409
|
+
return adopted;
|
|
6410
|
+
}
|
|
6411
|
+
const metadata = await prepareAdapterReleaseMetadata(root, pkg, version);
|
|
6412
|
+
const changelog = updateReleaseChangelog(pkg.dir, {
|
|
6413
|
+
version,
|
|
6414
|
+
sourceRef: `origin/${PRODUCTION_BRANCH}`,
|
|
6415
|
+
targetRef: "HEAD"
|
|
6416
|
+
});
|
|
6417
|
+
const commit = commitAllIfChanged(pkg.dir, releaseAdminMessage({
|
|
6418
|
+
subject: `release: ${pkg.name} ${version}`,
|
|
6419
|
+
version,
|
|
6420
|
+
tagName: version,
|
|
6421
|
+
sourceRef: STAGING_BRANCH,
|
|
6422
|
+
targetRef: PRODUCTION_BRANCH,
|
|
6423
|
+
changelog
|
|
6424
|
+
}));
|
|
6425
|
+
pushBranch(pkg.dir, STAGING_BRANCH);
|
|
6426
|
+
const promotion = promoteCommitToProductionBranch(pkg.dir, commit.commitSha);
|
|
6427
|
+
const tag = ensureReleaseTag(pkg.dir, version, commit.commitSha, `release: ${pkg.name} ${version}`);
|
|
6428
|
+
const publishGate = {
|
|
6429
|
+
name: pkg.name,
|
|
6430
|
+
repoPath: pkg.dir,
|
|
6431
|
+
workflow: releaseWorkflowForPackage(root, pkg.name),
|
|
6432
|
+
branch: version,
|
|
6433
|
+
headSha: commit.commitSha
|
|
6434
|
+
};
|
|
6435
|
+
const publishWait2 = await waitForWorkflowGates("release", [publishGate], ciMode, {
|
|
6436
|
+
root,
|
|
6437
|
+
runId: workflowRun.runId,
|
|
6438
|
+
onProgress: (line, stream) => helpers.write(line, stream)
|
|
6439
|
+
});
|
|
6440
|
+
const publishedArtifacts2 = await verifyPublishedReleaseArtifacts(/* @__PURE__ */ new Map([[pkg.name, version]]));
|
|
6441
|
+
return {
|
|
6442
|
+
name: pkg.name,
|
|
6443
|
+
path: relative(root, pkg.dir),
|
|
6444
|
+
version,
|
|
6445
|
+
changelog,
|
|
6446
|
+
metadata,
|
|
6447
|
+
commit,
|
|
6448
|
+
promotion,
|
|
6449
|
+
tag,
|
|
6450
|
+
publishWait: publishWait2,
|
|
6451
|
+
publishedArtifacts: publishedArtifacts2
|
|
6452
|
+
};
|
|
6120
6453
|
});
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
|
|
6127
|
-
|
|
6128
|
-
commit,
|
|
6129
|
-
promotion,
|
|
6130
|
-
tag,
|
|
6131
|
-
publishWait: publishWait2,
|
|
6132
|
-
publishedArtifacts: publishedArtifacts2
|
|
6133
|
-
};
|
|
6134
|
-
});
|
|
6135
|
-
packageReleases.push(packageRelease);
|
|
6454
|
+
return [packageName, packageRelease];
|
|
6455
|
+
}));
|
|
6456
|
+
for (const [packageName, packageRelease] of results) {
|
|
6457
|
+
pendingPackageReleases.delete(packageName);
|
|
6458
|
+
completedPackageReleases.add(packageName);
|
|
6459
|
+
packageReleaseResults.set(packageName, packageRelease);
|
|
6460
|
+
}
|
|
6136
6461
|
}
|
|
6462
|
+
packageReleases.push(...selectedPackageNames.map((name) => packageReleaseResults.get(name)).filter((entry) => Boolean(entry)));
|
|
6137
6463
|
const managedHelperReleases = await executeJournalStep(root, workflowRun.runId, "release-helper-repos", () => {
|
|
6138
6464
|
const releases = releaseHelperRepos.map((repo) => releaseHelperRepoToProduction(repo));
|
|
6139
6465
|
syncAllCheckedOutReleaseHelperRepos(root, STAGING_BRANCH);
|
|
@@ -6151,6 +6477,10 @@ ${rendered}`);
|
|
|
6151
6477
|
onProgress: (line, stream) => helpers.write(line, stream)
|
|
6152
6478
|
}).then((workflowGates) => ({ workflowGates }));
|
|
6153
6479
|
});
|
|
6480
|
+
const apiEnvironmentIsolation = effectiveInput.verifyDeployedResources === true ? await executeJournalStep(root, workflowRun.runId, "verify-api-environment-isolation", () => verifyReleaseApiEnvironmentIsolation(root, helpers, releaseImageRefs)) : (skipJournalStep(root, workflowRun.runId, "verify-api-environment-isolation", {
|
|
6481
|
+
status: "skipped",
|
|
6482
|
+
reason: "--verify-deployed-resources was not requested"
|
|
6483
|
+
}), { status: "skipped", reason: "--verify-deployed-resources was not requested" });
|
|
6154
6484
|
const productionImageRefs = await executeJournalStep(root, workflowRun.runId, "persist-production-image-refs", async () => {
|
|
6155
6485
|
const persisted = persistProductionReleaseImageRefs(root, releaseImageRefs);
|
|
6156
6486
|
if (helpers.context.env) {
|
|
@@ -6171,8 +6501,8 @@ ${failures.join("\n")}`, {
|
|
|
6171
6501
|
}
|
|
6172
6502
|
return { persisted, readiness };
|
|
6173
6503
|
});
|
|
6174
|
-
const rootRelease = await executeJournalStep(root, workflowRun.runId, "release-root", () => {
|
|
6175
|
-
const rootInstall = runReleaseNpmInstall(root, { workspaceRoot: root });
|
|
6504
|
+
const rootRelease = await executeJournalStep(root, workflowRun.runId, "release-root", async () => {
|
|
6505
|
+
const rootInstall = await runReleaseNpmInstall(root, { workspaceRoot: root });
|
|
6176
6506
|
const changelog = updateReleaseChangelog(repoRoot(root), {
|
|
6177
6507
|
version: plannedRelease.rootVersion,
|
|
6178
6508
|
sourceRef: `origin/${PRODUCTION_BRANCH}`,
|
|
@@ -6244,6 +6574,7 @@ ${failures.join("\n")}`, {
|
|
|
6244
6574
|
publishWait: publishWait.workflowGates,
|
|
6245
6575
|
publishedArtifacts,
|
|
6246
6576
|
productionPackageDeployWorkflows,
|
|
6577
|
+
apiEnvironmentIsolation,
|
|
6247
6578
|
productionImageRefs,
|
|
6248
6579
|
backMerge,
|
|
6249
6580
|
workspaceLinks,
|
|
@@ -6309,7 +6640,8 @@ async function workflowResume(helpers, input) {
|
|
|
6309
6640
|
);
|
|
6310
6641
|
const classification = classifyWorkflowRunJournal(journal, {
|
|
6311
6642
|
currentBranch: session.branchName,
|
|
6312
|
-
currentHeads
|
|
6643
|
+
currentHeads,
|
|
6644
|
+
acceptedReleaseHeads: acceptedPublishedReleaseHeads(root, journal, currentHeads)
|
|
6313
6645
|
});
|
|
6314
6646
|
if (classification.state !== "resumable") {
|
|
6315
6647
|
workflowError("resume", "resume_unavailable", `Run ${runId} is ${classification.state} and is not safe to resume.`, {
|
|
@@ -6339,7 +6671,10 @@ async function workflowResume(helpers, input) {
|
|
|
6339
6671
|
case "stage":
|
|
6340
6672
|
return workflowStage(resumedHelpers, journal.input);
|
|
6341
6673
|
case "release":
|
|
6342
|
-
return workflowRelease(resumedHelpers,
|
|
6674
|
+
return workflowRelease(resumedHelpers, {
|
|
6675
|
+
...journal.input,
|
|
6676
|
+
resumeRunId: runId
|
|
6677
|
+
});
|
|
6343
6678
|
case "destroy":
|
|
6344
6679
|
return workflowDestroy(resumedHelpers, journal.input);
|
|
6345
6680
|
default:
|
|
@@ -6397,17 +6732,21 @@ async function workflowRecover(helpers, input = {}) {
|
|
|
6397
6732
|
return null;
|
|
6398
6733
|
}).filter((entry) => entry !== null);
|
|
6399
6734
|
const journals = listWorkflowRunJournals(root);
|
|
6735
|
+
const actionableJournals = journals.filter((journal) => journal.status !== "completed" && !journal.classification?.archivedAt);
|
|
6400
6736
|
const session = resolveTreeseedWorkflowSession(root);
|
|
6401
6737
|
const currentHeads = Object.fromEntries(
|
|
6402
6738
|
[createWorkspaceRootRepoReport(root), ...createWorkspacePackageReports(root)].map((report) => [report.name, report.commitSha ?? null])
|
|
6403
6739
|
);
|
|
6404
|
-
const classifiedRuns =
|
|
6405
|
-
|
|
6406
|
-
|
|
6407
|
-
|
|
6740
|
+
const classifiedRuns = actionableJournals.map((journal) => ({
|
|
6741
|
+
journal,
|
|
6742
|
+
classification: classifyWorkflowRunJournal(journal, {
|
|
6743
|
+
currentBranch: session.branchName,
|
|
6744
|
+
currentHeads
|
|
6745
|
+
})
|
|
6746
|
+
}));
|
|
6408
6747
|
const markedObsoleteRun = input.obsoleteRunId ? (() => {
|
|
6409
|
-
const
|
|
6410
|
-
if (!
|
|
6748
|
+
const journal = journals.find((candidate) => candidate.runId === input.obsoleteRunId);
|
|
6749
|
+
if (!journal) {
|
|
6411
6750
|
workflowError("recover", "validation_failed", `Treeseed recover could not find workflow run ${input.obsoleteRunId}.`);
|
|
6412
6751
|
}
|
|
6413
6752
|
const reason = input.obsoleteReason?.trim() || "marked obsolete by operator";
|
|
@@ -6416,17 +6755,14 @@ async function workflowRecover(helpers, input = {}) {
|
|
|
6416
6755
|
reasons: [reason],
|
|
6417
6756
|
classifiedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6418
6757
|
};
|
|
6419
|
-
archiveWorkflowRun(root,
|
|
6758
|
+
archiveWorkflowRun(root, journal.runId, classification);
|
|
6420
6759
|
return {
|
|
6421
|
-
runId:
|
|
6422
|
-
command:
|
|
6760
|
+
runId: journal.runId,
|
|
6761
|
+
command: journal.command,
|
|
6423
6762
|
reason
|
|
6424
6763
|
};
|
|
6425
6764
|
})() : null;
|
|
6426
|
-
const effectiveClassifiedRuns = markedObsoleteRun ?
|
|
6427
|
-
currentBranch: session.branchName,
|
|
6428
|
-
currentHeads
|
|
6429
|
-
}) : classifiedRuns;
|
|
6765
|
+
const effectiveClassifiedRuns = markedObsoleteRun ? classifiedRuns.filter((entry) => entry.journal.runId !== markedObsoleteRun.runId) : classifiedRuns;
|
|
6430
6766
|
const interruptedRuns = effectiveClassifiedRuns.filter((entry) => entry.classification.state === "resumable").map(({ journal }) => ({
|
|
6431
6767
|
runId: journal.runId,
|
|
6432
6768
|
command: journal.command,
|