@treeseed/sdk 0.12.61 → 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.
@@ -478,6 +478,14 @@ async function waitForWorkflowGates(operation, gates, ciMode, options = {}) {
478
478
  env: gateEnv,
479
479
  onProgress: options.onProgress
480
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
+ });
481
489
  }
482
490
  const normalized = {
483
491
  name: gateWithTimeout.name,
@@ -1319,6 +1327,62 @@ function ensureReleaseTag(repoDir, tagName, commitSha, message) {
1319
1327
  remote: remoteCommit ? "existing" : "pushed"
1320
1328
  };
1321
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
+ }
1322
1386
  function promoteCommitToProductionBranch(repoDir, commitSha) {
1323
1387
  const expectedBefore = remoteBranchExists(repoDir, PRODUCTION_BRANCH) ? remoteHeadCommit(repoDir, PRODUCTION_BRANCH) : null;
1324
1388
  const lease = expectedBefore ? `--force-with-lease=refs/heads/${PRODUCTION_BRANCH}:${expectedBefore}` : "--force-with-lease";
@@ -2039,7 +2103,8 @@ function findAutoResumableReleaseRun(root, branch, rootRepo, packageReports, opt
2039
2103
  }
2040
2104
  const classification = classifyWorkflowRunJournal(journal, {
2041
2105
  currentBranch: branch,
2042
- currentHeads
2106
+ currentHeads,
2107
+ acceptedReleaseHeads: acceptedPublishedReleaseHeads(root, journal, currentHeads)
2043
2108
  });
2044
2109
  if (classification.state !== "resumable") {
2045
2110
  if (options.archiveStale && classification.state === "stale") {
@@ -2061,6 +2126,45 @@ function findAutoResumableReleaseRun(root, branch, rootRepo, packageReports, opt
2061
2126
  return releasePlan ? releasePlanMatchesCurrentHeads(releasePlan, rootRepo, packageReports) : true;
2062
2127
  }) ?? null;
2063
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
+ }
2064
2168
  async function executeJournalStep(root, runId, stepId, action, options = {}) {
2065
2169
  const current = readWorkflowRunJournal(root, runId);
2066
2170
  const step = current?.steps.find((entry) => entry.id === stepId) ?? null;
@@ -2391,21 +2495,32 @@ async function runReleaseNpmInstall(repoDir, options = {}) {
2391
2495
  if (repoDir === options.workspaceRoot && lockfileRootMatchesManifest(repoDir)) {
2392
2496
  return { status: "skipped", reason: "root-lockfile-already-matches", attempts: 0 };
2393
2497
  }
2394
- const args = ["install", "--package-lock-only", "--ignore-scripts", "--prefer-offline", "--workspaces=false", "--no-audit", "--no-fund"];
2395
- const spawnCommand = npmCommandForWorkflowSpawn(args);
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();
2396
2501
  let lastDetail = "";
2397
- for (let attempt = 1; attempt <= 3; attempt += 1) {
2398
- process.stderr.write(`[release][restore] repository=${repoDir} phase=package-lock attempt=${attempt} elapsed=0s
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
2399
2506
  `);
2400
2507
  const result = await waitForReleaseInstall(spawnCommand.command, spawnCommand.args, repoDir, attempt);
2401
2508
  if (result.status === 0) {
2402
2509
  return { status: "completed", reason: null, attempts: attempt };
2403
2510
  }
2404
2511
  lastDetail = result.detail;
2405
- if (!/No matching version found|notarget|ETARGET|E404/u.test(lastDetail) || attempt === 3) break;
2406
- await new Promise((resolvePromise) => setTimeout(resolvePromise, 15e3));
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
+ }
2407
2522
  }
2408
- throw new Error(lastDetail || `npm ${args.join(" ")} failed`);
2523
+ throw new Error(lastDetail || `npm ${baseArgs.join(" ")} failed`);
2409
2524
  }
2410
2525
  function pathIsWithin(parent, candidate) {
2411
2526
  const path = relative(parent, candidate);
@@ -4676,7 +4791,7 @@ async function workflowSave(helpers, input) {
4676
4791
  const recursiveWorkspace = session.mode === "recursive-workspace";
4677
4792
  const mode = session.mode;
4678
4793
  const executionMode = normalizeExecutionMode(input);
4679
- const explicitResumeRunId = helpers.context.workflow?.resumeRunId ?? null;
4794
+ const explicitResumeRunId = helpers.context.workflow?.resumeRunId ?? input.resumeRunId ?? null;
4680
4795
  const autoResumeRun = executionMode === "execute" && !explicitResumeRunId ? findAutoResumableSaveRun(root, branch) : null;
4681
4796
  rejectImplicitWorkflowResume("save", autoResumeRun);
4682
4797
  const planAutoResumeRun = null;
@@ -4841,7 +4956,10 @@ async function workflowSave(helpers, input) {
4841
4956
  resumable: true
4842
4957
  }] : []
4843
4958
  ],
4844
- autoResumeRun ? {
4959
+ explicitResumeRunId ? {
4960
+ ...helpers.context,
4961
+ workflow: { ...helpers.context.workflow ?? {}, resumeRunId: explicitResumeRunId }
4962
+ } : autoResumeRun ? {
4845
4963
  ...helpers.context,
4846
4964
  workflow: {
4847
4965
  ...helpers.context.workflow ?? {},
@@ -5127,7 +5245,7 @@ async function workflowClose(helpers, input) {
5127
5245
  const root = workspaceRoot(tenantRoot);
5128
5246
  const executionMode = normalizeExecutionMode(input);
5129
5247
  const session = resolveTreeseedWorkflowSession(root);
5130
- const explicitResumeRunId = helpers.context.workflow?.resumeRunId ?? null;
5248
+ const explicitResumeRunId = helpers.context.workflow?.resumeRunId ?? input.resumeRunId ?? null;
5131
5249
  const autoResumeRun = executionMode === "execute" && !explicitResumeRunId ? findAutoResumableTaskRun(root, "close", session.branchName) : null;
5132
5250
  rejectImplicitWorkflowResume("close", autoResumeRun);
5133
5251
  const planAutoResumeRun = null;
@@ -5214,7 +5332,13 @@ async function workflowClose(helpers, input) {
5214
5332
  { id: "workspace-link", description: "Restore local workspace links", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
5215
5333
  ...isManagedWorkflowWorktree(root) ? [{ id: "worktree-cleanup", description: "Remove managed workflow worktree", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: false }] : []
5216
5334
  ],
5217
- autoResumeRun ? {
5335
+ explicitResumeRunId ? {
5336
+ ...helpers.context,
5337
+ workflow: {
5338
+ ...helpers.context.workflow ?? {},
5339
+ resumeRunId: explicitResumeRunId
5340
+ }
5341
+ } : autoResumeRun ? {
5218
5342
  ...helpers.context,
5219
5343
  workflow: {
5220
5344
  ...helpers.context.workflow ?? {},
@@ -5611,7 +5735,7 @@ async function workflowStage(helpers, input) {
5611
5735
  const root = workspaceRoot(tenantRoot);
5612
5736
  const executionMode = normalizeExecutionMode(input);
5613
5737
  const session = resolveTreeseedWorkflowSession(root);
5614
- const explicitResumeRunId = helpers.context.workflow?.resumeRunId ?? null;
5738
+ const explicitResumeRunId = helpers.context.workflow?.resumeRunId ?? input.resumeRunId ?? null;
5615
5739
  const rawAutoResumeRun = executionMode === "execute" && !explicitResumeRunId ? findAutoResumableTaskRun(root, "stage", session.branchName) : null;
5616
5740
  rejectImplicitWorkflowResume("stage", rawAutoResumeRun);
5617
5741
  const autoResumeRun = rawAutoResumeRun?.steps.some((step) => step.id === "preflight") ? rawAutoResumeRun : null;
@@ -6026,7 +6150,9 @@ async function workflowRelease(helpers, input) {
6026
6150
  const rootRepo = createWorkspaceRootRepoReport(root);
6027
6151
  const packageReports = createWorkspacePackageReports(root);
6028
6152
  const releaseHelperRepos = checkedOutReleaseHelperRepos(root);
6029
- 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;
6030
6156
  const autoResumeRun = executionMode === "execute" && !explicitResumeRunId && input.fresh !== true ? findAutoResumableReleaseRun(root, session.branchName, rootRepo, packageReports, { archiveStale: false }) : null;
6031
6157
  const planAutoResumeRun = executionMode === "plan" && input.fresh !== true ? findAutoResumableReleaseRun(root, session.branchName, rootRepo, packageReports) : null;
6032
6158
  const effectiveInput = autoResumeRun ? {
@@ -6037,7 +6163,7 @@ async function workflowRelease(helpers, input) {
6037
6163
  const level = effectiveInput.bump ?? "patch";
6038
6164
  const ciMode = normalizeCiMode(effectiveInput.ciMode, "release");
6039
6165
  const packageSelection = session.packageSelection;
6040
- const plannedRelease = buildReleasePlanSnapshot({
6166
+ const plannedRelease = recordedReleasePlan ?? buildReleasePlanSnapshot({
6041
6167
  root,
6042
6168
  mode: session.mode,
6043
6169
  level,
@@ -6049,12 +6175,14 @@ async function workflowRelease(helpers, input) {
6049
6175
  blockers: []
6050
6176
  });
6051
6177
  const selectedPackageNames = releasePlanPackageSelection(plannedRelease.packageSelection).selected;
6052
- const blockers = collectReleasePlanBlockers(session, session.mode, selectedPackageNames, {
6178
+ const blockers = explicitResumeJournal ? [] : collectReleasePlanBlockers(session, session.mode, selectedPackageNames, {
6053
6179
  level,
6054
6180
  repairVersionLine: effectiveInput.repairVersionLine === true
6055
6181
  });
6056
- blockers.push(...collectReleaseHelperRepoBlockers(root));
6057
- blockers.push(...stageCandidateAttestationBlockers(root));
6182
+ if (!explicitResumeJournal) {
6183
+ blockers.push(...collectReleaseHelperRepoBlockers(root));
6184
+ blockers.push(...stageCandidateAttestationBlockers(root));
6185
+ }
6058
6186
  const selectedVersions = releasePlanVersionMap(plannedRelease.plannedVersions);
6059
6187
  const releaseImageVersions = productionReleaseImageRefVersions(root, selectedVersions);
6060
6188
  const releaseImageRefs = productionReleaseImageRefEnv(releaseImageVersions);
@@ -6160,7 +6288,13 @@ ${blockers.join("\n")}`, {
6160
6288
  { id: "release-back-merge", description: "Back-merge production release history into staging", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
6161
6289
  { id: "workspace-link", description: "Restore local workspace links after release", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true }
6162
6290
  ],
6163
- autoResumeRun ? {
6291
+ explicitResumeRunId ? {
6292
+ ...helpers.context,
6293
+ workflow: {
6294
+ ...helpers.context.workflow ?? {},
6295
+ resumeRunId: explicitResumeRunId
6296
+ }
6297
+ } : autoResumeRun ? {
6164
6298
  ...helpers.context,
6165
6299
  workflow: {
6166
6300
  ...helpers.context.workflow ?? {},
@@ -6238,6 +6372,9 @@ ${rendered}`);
6238
6372
  if (packageName !== "@treeseed/sdk" && pendingPackageReleases.has("@treeseed/sdk") && !dependencies.includes("@treeseed/sdk")) {
6239
6373
  dependencies.push("@treeseed/sdk");
6240
6374
  }
6375
+ if (packageName === "@treeseed/api" && pendingPackageReleases.has("@treeseed/cli") && !dependencies.includes("@treeseed/cli")) {
6376
+ dependencies.push("@treeseed/cli");
6377
+ }
6241
6378
  return [packageName, dependencies];
6242
6379
  }));
6243
6380
  while (pendingPackageReleases.size > 0) {
@@ -6251,6 +6388,26 @@ ${rendered}`);
6251
6388
  const pkg = packageRepoByName.get(packageName);
6252
6389
  const version = selectedVersions.get(pkg.name);
6253
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
+ }
6254
6411
  const metadata = await prepareAdapterReleaseMetadata(root, pkg, version);
6255
6412
  const changelog = updateReleaseChangelog(pkg.dir, {
6256
6413
  version,
@@ -6483,7 +6640,8 @@ async function workflowResume(helpers, input) {
6483
6640
  );
6484
6641
  const classification = classifyWorkflowRunJournal(journal, {
6485
6642
  currentBranch: session.branchName,
6486
- currentHeads
6643
+ currentHeads,
6644
+ acceptedReleaseHeads: acceptedPublishedReleaseHeads(root, journal, currentHeads)
6487
6645
  });
6488
6646
  if (classification.state !== "resumable") {
6489
6647
  workflowError("resume", "resume_unavailable", `Run ${runId} is ${classification.state} and is not safe to resume.`, {
@@ -6513,7 +6671,10 @@ async function workflowResume(helpers, input) {
6513
6671
  case "stage":
6514
6672
  return workflowStage(resumedHelpers, journal.input);
6515
6673
  case "release":
6516
- return workflowRelease(resumedHelpers, journal.input);
6674
+ return workflowRelease(resumedHelpers, {
6675
+ ...journal.input,
6676
+ resumeRunId: runId
6677
+ });
6517
6678
  case "destroy":
6518
6679
  return workflowDestroy(resumedHelpers, journal.input);
6519
6680
  default:
@@ -113,6 +113,7 @@ export declare function updateWorkflowRunJournal(root: string, runId: string, up
113
113
  export declare function classifyWorkflowRunJournal(journal: TreeseedWorkflowRunJournal, options?: {
114
114
  currentBranch?: string | null;
115
115
  currentHeads?: Record<string, string | null | undefined>;
116
+ acceptedReleaseHeads?: Record<string, string | null | undefined>;
116
117
  now?: string;
117
118
  }): TreeseedWorkflowRunClassification;
118
119
  export declare function classifyWorkflowRunJournals(root: string, options?: Parameters<typeof classifyWorkflowRunJournal>[1]): {
@@ -406,6 +406,8 @@ function expectedPackageHeadAfterReleaseGate(journal, packageName) {
406
406
  const data = releaseStepData(journal, `release-${packageName}`);
407
407
  const backMerge = stringRecord(data?.backMerge);
408
408
  if (typeof backMerge?.commitSha === "string") return backMerge.commitSha;
409
+ const commit = stringRecord(data?.commit);
410
+ if (typeof commit?.commitSha === "string") return commit.commitSha;
409
411
  if (typeof data?.commitSha === "string") return data.commitSha;
410
412
  return null;
411
413
  }
@@ -512,9 +514,9 @@ function classifyWorkflowRunJournal(journal, options = {}) {
512
514
  }
513
515
  for (const name of selectedReleasePackageNames(releasePlan)) {
514
516
  const currentHead = options.currentHeads[name];
515
- const plannedHead = journalReleasePlanHead(releasePlan, name);
516
- if (currentHead && plannedHead && currentHead !== plannedHead) {
517
- reasons.push(`${name} head changed from ${plannedHead} to ${currentHead}`);
517
+ const expectedHead = expectedPackageHeadAfterReleaseGate(journal, name) ?? options.acceptedReleaseHeads?.[name] ?? journalReleasePlanHead(releasePlan, name);
518
+ if (currentHead && expectedHead && currentHead !== expectedHead) {
519
+ reasons.push(`${name} head changed from ${expectedHead} to ${currentHead}`);
518
520
  }
519
521
  }
520
522
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@treeseed/sdk",
3
- "version": "0.12.61",
3
+ "version": "0.12.62",
4
4
  "description": "Shared Treeseed SDK for content-backed and D1-backed object models.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {