@treeseed/sdk 0.6.36 → 0.6.38

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.
@@ -57,7 +57,6 @@ import {
57
57
  headCommit,
58
58
  listTaskBranches,
59
59
  mergeBranchIntoTarget,
60
- mergeStagingIntoMain,
61
60
  prepareReleaseBranches,
62
61
  PRODUCTION_BRANCH,
63
62
  pushBranch,
@@ -68,7 +67,7 @@ import {
68
67
  squashMergeBranchIntoStaging,
69
68
  syncBranchWithOrigin
70
69
  } from "../operations/services/git-workflow.js";
71
- import { getGitHubAutomationMode, resolveGitHubRepositorySlug } from "../operations/services/github-automation.js";
70
+ import { resolveGitHubRepositorySlug } from "../operations/services/github-automation.js";
72
71
  import {
73
72
  formatGitHubActionsGateFailure,
74
73
  inspectGitHubActionsVerification,
@@ -78,6 +77,11 @@ import {
78
77
  import {
79
78
  runReleaseCandidateGate
80
79
  } from "../operations/services/release-candidate.js";
80
+ import {
81
+ collectReleaseHistoryCommits,
82
+ renderAdministrativeCommitMessage,
83
+ upsertReleaseChangelog
84
+ } from "../operations/services/release-history.js";
81
85
  import { loadCliDeployConfig, packageScriptPath, resolveWranglerBin } from "../operations/services/runtime-tools.js";
82
86
  import { runTenantDeployPreflight, runWorkspaceReleasePreflight, runWorkspaceSavePreflight } from "../operations/services/save-deploy-preflight.js";
83
87
  import { collectCliPreflight } from "../operations/services/workspace-preflight.js";
@@ -338,9 +342,6 @@ function shouldDispatchSwitchToManagedWorktree(root, input, env) {
338
342
  return !isManagedWorkflowWorktree(root) && effectiveWorkflowWorktreeMode(input.worktreeMode, env) === "on";
339
343
  }
340
344
  function assertHostedGitHubWorkflowAuthReady(operation, root) {
341
- if (getGitHubAutomationMode() === "stub") {
342
- return null;
343
- }
344
345
  const tools = collectTreeseedToolStatus({
345
346
  tenantRoot: root,
346
347
  env: process.env
@@ -372,7 +373,7 @@ function assertHostedGitHubWorkflowAuthReady(operation, root) {
372
373
  }
373
374
  async function waitForWorkflowGates(operation, gates, ciMode, options = {}) {
374
375
  if (ciMode === "off" || process.env.TREESEED_STAGE_WAIT_MODE === "skip") {
375
- return gates.map((gate) => skippedGitHubActionsGate(gate, ciMode === "off" ? "disabled" : "stubbed"));
376
+ return gates.map((gate) => skippedGitHubActionsGate(gate, "disabled"));
376
377
  }
377
378
  if (gates.length === 0) {
378
379
  return [];
@@ -405,7 +406,9 @@ async function waitForWorkflowGates(operation, gates, ciMode, options = {}) {
405
406
  ...result,
406
407
  workflow: String(result.workflow ?? gate.workflow),
407
408
  branch: String(result.branch ?? gate.branch),
408
- headSha: String(result.headSha ?? gate.headSha)
409
+ headSha: String(result.headSha ?? gate.headSha),
410
+ timeoutSeconds: gate.timeoutSeconds ?? null,
411
+ cached: false
409
412
  };
410
413
  if (normalized.status === "completed" && normalized.conclusion !== "success") {
411
414
  workflowError(operation, "github_workflow_failed", formatGitHubActionsGateFailure(gate, normalized), {
@@ -419,13 +422,21 @@ async function waitForWorkflowGates(operation, gates, ciMode, options = {}) {
419
422
  }
420
423
  return results;
421
424
  }
425
+ const RELEASE_DEPLOY_GATE_TIMEOUT_SECONDS = 45 * 60;
426
+ function releaseDeployGate(gate) {
427
+ return {
428
+ ...gate,
429
+ timeoutSeconds: gate.timeoutSeconds ?? RELEASE_DEPLOY_GATE_TIMEOUT_SECONDS
430
+ };
431
+ }
422
432
  function recordHostedDeploymentStatesFromRootGates(root, rootRelease, workflowGates) {
423
433
  const gates = Array.isArray(workflowGates) ? workflowGates.map((gate) => stringRecord(gate)).filter((gate) => Boolean(gate)) : [];
424
434
  const releaseRecord = stringRecord(rootRelease) ?? {};
425
435
  const reports = [];
436
+ const releaseTag = typeof releaseRecord.rootVersion === "string" ? releaseRecord.rootVersion : null;
426
437
  for (const target of [
427
438
  { scope: "staging", branch: STAGING_BRANCH, commit: releaseRecord.stagingCommit },
428
- { scope: "prod", branch: PRODUCTION_BRANCH, commit: releaseRecord.releasedCommit }
439
+ { scope: "prod", branch: releaseTag ?? PRODUCTION_BRANCH, commit: releaseRecord.releasedCommit }
429
440
  ]) {
430
441
  const gate = gates.find((candidate) => candidate.workflow === "deploy.yml" && candidate.branch === target.branch && candidate.status === "completed" && candidate.conclusion === "success");
431
442
  const timestamp = typeof gate?.updatedAt === "string" && gate.updatedAt.trim() ? gate.updatedAt : null;
@@ -452,10 +463,10 @@ function recordHostedDeploymentStatesFromRootGates(root, rootRelease, workflowGa
452
463
  return reports;
453
464
  }
454
465
  function ensureTreeseedCommandReadiness(root) {
455
- if (getGitHubAutomationMode() === "stub") {
466
+ if (process.env.TREESEED_COMMAND_READINESS_MODE === "skip") {
456
467
  return {
457
468
  status: "skipped",
458
- reason: "stubbed",
469
+ reason: "disabled",
459
470
  checks: [],
460
471
  missing: []
461
472
  };
@@ -705,13 +716,13 @@ function remoteTagCommit(repoDir, tagName) {
705
716
  const direct = output.split("\n").find((line) => line.endsWith(`refs/tags/${tagName}`));
706
717
  return (peeled ?? direct)?.split(/\s+/u)[0] ?? null;
707
718
  }
708
- function ensureReleaseTag(repoDir, tagName, commitSha) {
719
+ function ensureReleaseTag(repoDir, tagName, commitSha, message) {
709
720
  const localCommit = gitObjectCommit(repoDir, tagName);
710
721
  if (localCommit && localCommit !== commitSha) {
711
722
  throw new Error(`Release tag ${tagName} already exists locally at ${localCommit}, expected ${commitSha}.`);
712
723
  }
713
724
  if (!localCommit) {
714
- run("git", ["tag", "-a", tagName, commitSha, "-m", `release: ${tagName}`], { cwd: repoDir });
725
+ run("git", ["tag", "-a", tagName, commitSha, "-m", message ?? `release: ${tagName}`], { cwd: repoDir });
715
726
  }
716
727
  const remoteCommit = remoteTagCommit(repoDir, tagName);
717
728
  if (remoteCommit && remoteCommit !== commitSha) {
@@ -734,6 +745,51 @@ function commitAllIfChanged(repoDir, message) {
734
745
  run("git", ["commit", "-m", message], { cwd: repoDir });
735
746
  return { committed: true, commitSha: headCommit(repoDir) };
736
747
  }
748
+ function releaseHistoryCommits(repoDir, sourceRef = `origin/${PRODUCTION_BRANCH}`, targetRef = "HEAD") {
749
+ try {
750
+ return collectReleaseHistoryCommits(repoDir, sourceRef, targetRef);
751
+ } catch {
752
+ return [];
753
+ }
754
+ }
755
+ function versionLines(versions) {
756
+ return [...(versions ?? /* @__PURE__ */ new Map()).entries()].sort(([left], [right]) => left.localeCompare(right)).map(([name, version]) => `${name}: ${version}`);
757
+ }
758
+ function updateReleaseChangelog(repoDir, input) {
759
+ const sourceRef = input.sourceRef ?? `origin/${PRODUCTION_BRANCH}`;
760
+ const targetRef = input.targetRef ?? "HEAD";
761
+ const commits = input.commits ?? releaseHistoryCommits(repoDir, sourceRef, targetRef);
762
+ return upsertReleaseChangelog(repoDir, {
763
+ version: input.version,
764
+ sourceRef,
765
+ targetRef,
766
+ commits,
767
+ extraBullets: input.extraDependencyBullets?.length ? { Dependencies: input.extraDependencyBullets } : void 0
768
+ });
769
+ }
770
+ function releaseAdminMessage(input) {
771
+ return renderAdministrativeCommitMessage({
772
+ subject: input.subject,
773
+ version: input.version,
774
+ tagName: input.tagName,
775
+ sourceRef: input.sourceRef ?? STAGING_BRANCH,
776
+ targetRef: input.targetRef ?? PRODUCTION_BRANCH,
777
+ commits: input.commits ?? [],
778
+ changelog: input.changelog ?? null,
779
+ extraLines: input.extraLines
780
+ });
781
+ }
782
+ function completedJournalStepData(root, runId, stepId) {
783
+ const journal = readWorkflowRunJournal(root, runId);
784
+ return stringRecord(journal?.steps.find((step) => step.id === stepId && step.status === "completed")?.data);
785
+ }
786
+ function shouldResumeReleaseAtRootGates(root, runId) {
787
+ const journal = readWorkflowRunJournal(root, runId);
788
+ if (!journal || journal.command !== "release") return false;
789
+ const rootStep = journal.steps.find((step) => step.id === "release-root");
790
+ const gateStep = journal.steps.find((step) => step.id === "release-root-gates");
791
+ return rootStep?.status === "completed" && gateStep?.status !== "completed";
792
+ }
737
793
  function createNextSteps(steps) {
738
794
  return steps.map(renderWorkflowStep);
739
795
  }
@@ -1368,9 +1424,6 @@ function failWorkflowRun(root, runId, error, recovery) {
1368
1424
  releaseWorkflowLock(root, runId);
1369
1425
  }
1370
1426
  function validatePackageReleaseWorkflows(root, packageNames) {
1371
- if (process.env.TREESEED_GITHUB_AUTOMATION_MODE === "stub") {
1372
- return;
1373
- }
1374
1427
  const missing = checkedOutWorkspacePackageRepos(root).filter((pkg) => packageNames.includes(pkg.name)).filter((pkg) => !existsSync(resolve(pkg.dir, ".github", "workflows", "publish.yml"))).map((pkg) => pkg.name);
1375
1428
  if (missing.length > 0) {
1376
1429
  workflowError("release", "workflow_contract_missing", `Treeseed release requires .github/workflows/publish.yml in: ${missing.join(", ")}.`, {
@@ -1381,7 +1434,7 @@ function validatePackageReleaseWorkflows(root, packageNames) {
1381
1434
  }
1382
1435
  }
1383
1436
  function validateStagingWorkflowContracts(root) {
1384
- if (process.env.TREESEED_GITHUB_AUTOMATION_MODE === "stub" || process.env.TREESEED_STAGE_WAIT_MODE === "skip") {
1437
+ if (process.env.TREESEED_STAGE_WAIT_MODE === "skip") {
1385
1438
  return;
1386
1439
  }
1387
1440
  const missing = [];
@@ -1397,14 +1450,31 @@ function validateStagingWorkflowContracts(root) {
1397
1450
  }
1398
1451
  }
1399
1452
  function shouldSkipReleaseInstall() {
1400
- return process.env.TREESEED_GITHUB_AUTOMATION_MODE === "stub" || process.env.TREESEED_SAVE_NPM_INSTALL_MODE === "skip";
1453
+ return process.env.TREESEED_SAVE_NPM_INSTALL_MODE === "skip";
1401
1454
  }
1402
1455
  function runReleaseNpmInstall(repoDir, options = {}) {
1403
1456
  if (shouldSkipReleaseInstall()) {
1404
- return { status: "skipped", reason: "stubbed" };
1457
+ return { status: "skipped", reason: "disabled" };
1458
+ }
1459
+ 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"];
1460
+ const result = spawnSync("npm", args, {
1461
+ cwd: repoDir,
1462
+ env: {
1463
+ ...process.env,
1464
+ npm_config_audit: "false",
1465
+ npm_config_fund: "false"
1466
+ },
1467
+ stdio: "pipe",
1468
+ encoding: "utf8"
1469
+ });
1470
+ if (result.status !== 0) {
1471
+ const detail = [
1472
+ result.error?.message,
1473
+ result.stderr?.trim(),
1474
+ result.stdout?.trim()
1475
+ ].filter(Boolean).join("\n");
1476
+ throw new Error(detail || `npm ${args.join(" ")} failed`);
1405
1477
  }
1406
- const args = repoDir === options.workspaceRoot ? ["install", "--package-lock-only", "--ignore-scripts"] : ["install", "--package-lock-only", "--ignore-scripts", "--workspaces=false"];
1407
- run("npm", args, { cwd: repoDir });
1408
1478
  return { status: "completed", reason: null };
1409
1479
  }
1410
1480
  function pathIsWithin(parent, candidate) {
@@ -1422,7 +1492,7 @@ function assertNoInternalDevReferencesForRepo(root, repoDir, packageNames) {
1422
1492
  throw new Error(`Stable release still contains internal Git/dev dependency references.
1423
1493
  ${rendered}`);
1424
1494
  }
1425
- function backMergeProductionIntoStaging(repoDir, repoName) {
1495
+ function backMergeProductionIntoStaging(repoDir, repoName, message) {
1426
1496
  syncBranchWithOrigin(repoDir, PRODUCTION_BRANCH);
1427
1497
  syncBranchWithOrigin(repoDir, STAGING_BRANCH);
1428
1498
  checkoutBranch(repoDir, STAGING_BRANCH);
@@ -1439,7 +1509,7 @@ function backMergeProductionIntoStaging(repoDir, repoName) {
1439
1509
  } catch {
1440
1510
  }
1441
1511
  try {
1442
- run("git", ["merge", "--no-ff", `origin/${PRODUCTION_BRANCH}`, "-m", `release: back-merge ${PRODUCTION_BRANCH} into ${STAGING_BRANCH}`], { cwd: repoDir });
1512
+ run("git", ["merge", "--no-ff", `origin/${PRODUCTION_BRANCH}`, "-m", message ?? `release: back-merge ${PRODUCTION_BRANCH} into ${STAGING_BRANCH}`], { cwd: repoDir });
1443
1513
  } catch (error) {
1444
1514
  const report = collectMergeConflictReport(repoDir);
1445
1515
  throw new TreeseedWorkflowError("release", "merge_conflict", formatMergeConflictReport(report, repoDir, STAGING_BRANCH), {
@@ -1457,14 +1527,32 @@ function backMergeProductionIntoStaging(repoDir, repoName) {
1457
1527
  commitSha: headCommit(repoDir)
1458
1528
  };
1459
1529
  }
1460
- function backMergeRootProductionIntoStaging(root, syncPackageStagingHeads) {
1530
+ function backMergeRootProductionIntoStaging(root, syncPackageStagingHeads, options = {}) {
1461
1531
  const gitRoot = repoRoot(root);
1462
- const backMerge = backMergeProductionIntoStaging(gitRoot, "@treeseed/market");
1532
+ const commits = releaseHistoryCommits(gitRoot, STAGING_BRANCH, `origin/${PRODUCTION_BRANCH}`);
1533
+ const backMerge = backMergeProductionIntoStaging(gitRoot, "@treeseed/market", releaseAdminMessage({
1534
+ subject: `release: back-merge ${PRODUCTION_BRANCH} into ${STAGING_BRANCH}`,
1535
+ version: options.version,
1536
+ sourceRef: PRODUCTION_BRANCH,
1537
+ targetRef: STAGING_BRANCH,
1538
+ commits,
1539
+ changelog: options.changelog ?? null,
1540
+ extraLines: versionLines(options.selectedVersions).map((line) => `Released package ${line}`)
1541
+ }));
1463
1542
  if (!syncPackageStagingHeads) {
1464
1543
  return backMerge;
1465
1544
  }
1466
1545
  syncAllCheckedOutPackageRepos(root, STAGING_BRANCH);
1467
- const pointerSync = commitAllIfChanged(gitRoot, "release: sync package staging heads");
1546
+ const pointerCommits = releaseHistoryCommits(gitRoot, `origin/${STAGING_BRANCH}`, "HEAD");
1547
+ const pointerSync = commitAllIfChanged(gitRoot, releaseAdminMessage({
1548
+ subject: "release: sync package staging heads",
1549
+ version: options.version,
1550
+ sourceRef: "package staging heads",
1551
+ targetRef: STAGING_BRANCH,
1552
+ commits: pointerCommits,
1553
+ changelog: options.changelog ?? null,
1554
+ extraLines: versionLines(options.selectedVersions).map((line) => `Staging package ${line}`)
1555
+ }));
1468
1556
  if (pointerSync.committed) {
1469
1557
  pushBranch(gitRoot, STAGING_BRANCH);
1470
1558
  }
@@ -1590,8 +1678,8 @@ function collectReleasePlanBlockers(session, mode, selectedPackageNames) {
1590
1678
  }
1591
1679
  return blockers;
1592
1680
  }
1593
- function assertReleaseGitHubAutomationReady(root, selectedPackageNames) {
1594
- if (process.env.TREESEED_GITHUB_AUTOMATION_MODE === "stub") {
1681
+ function assertReleaseGitHubAutomationReady(root, selectedPackageNames, ciMode) {
1682
+ if (ciMode === "off") {
1595
1683
  return;
1596
1684
  }
1597
1685
  assertHostedGitHubWorkflowAuthReady("release", root);
@@ -3494,7 +3582,10 @@ async function workflowRelease(helpers, input) {
3494
3582
  const freshPreparation = freshRelease && executionMode === "execute" ? prepareFreshReleaseRun(root, session.branchName, rootRepo, packageReports) : { archived: [], blockers: [] };
3495
3583
  const autoResumeRun = executionMode === "execute" && !explicitResumeRunId && !freshRelease ? findAutoResumableReleaseRun(root, session.branchName, rootRepo, packageReports) : null;
3496
3584
  const planAutoResumeRun = executionMode === "plan" && input.fresh !== true ? findAutoResumableReleaseRun(root, session.branchName, rootRepo, packageReports) : null;
3497
- const effectiveInput = autoResumeRun ? autoResumeRun.input : input;
3585
+ const effectiveInput = autoResumeRun ? {
3586
+ ...autoResumeRun.input,
3587
+ ciMode: input.ciMode ?? autoResumeRun.input.ciMode
3588
+ } : input;
3498
3589
  const level = effectiveInput.bump ?? "patch";
3499
3590
  const ciMode = normalizeCiMode(effectiveInput.ciMode, "release");
3500
3591
  const isResume = Boolean(explicitResumeRunId || autoResumeRun);
@@ -3576,6 +3667,10 @@ async function workflowRelease(helpers, input) {
3576
3667
  if (autoResumeRun) {
3577
3668
  helpers.write(`[workflow][resume] Resuming interrupted release ${autoResumeRun.runId} on ${STAGING_BRANCH}.`);
3578
3669
  }
3670
+ const resumeAtRootGates = workflowRun.resumed && shouldResumeReleaseAtRootGates(root, workflowRun.runId);
3671
+ if (resumeAtRootGates) {
3672
+ helpers.write(`[workflow][resume] Resuming release ${workflowRun.runId} directly at production deploy gates.`);
3673
+ }
3579
3674
  let releaseCleanupSnapshot = null;
3580
3675
  try {
3581
3676
  const releasePlan = await executeJournalStep(root, workflowRun.runId, "release-plan", () => plannedRelease);
@@ -3584,31 +3679,68 @@ async function workflowRelease(helpers, input) {
3584
3679
  const effectiveVersions = releasePlanVersionMap(releasePlan.plannedVersions);
3585
3680
  const rootVersion = String(releasePlan.rootVersion);
3586
3681
  applyTreeseedEnvironmentToProcess({ tenantRoot: root, scope: "staging", override: true });
3587
- assertReleaseGitHubAutomationReady(root, effectiveSelectedPackageNames);
3588
- const releaseCandidate = await executeJournalStep(root, workflowRun.runId, "release-candidate", () => runReleaseCandidateForPlan("release", root, releasePlan, { allowReuse: true }));
3589
- if (!isResume) {
3682
+ assertReleaseGitHubAutomationReady(root, effectiveSelectedPackageNames, ciMode);
3683
+ const releaseCandidate = resumeAtRootGates ? completedJournalStepData(root, workflowRun.runId, "release-candidate") : await executeJournalStep(root, workflowRun.runId, "release-candidate", () => runReleaseCandidateForPlan("release", root, releasePlan, { allowReuse: true }));
3684
+ if (!resumeAtRootGates && !isResume) {
3590
3685
  assertSessionBranchSafety("release", session, { requireCleanPackages: true, requireCurrentBranch: true });
3591
3686
  assertCleanWorktree(root);
3592
3687
  }
3593
- prepareReleaseBranches(root);
3594
- ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
3595
- runWorkspaceReleasePreflight({ cwd: root });
3596
- await executeJournalStep(root, workflowRun.runId, "workspace-unlink", () => unlinkWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"), { rerunCompleted: true });
3688
+ if (!resumeAtRootGates) {
3689
+ prepareReleaseBranches(root);
3690
+ ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
3691
+ runWorkspaceReleasePreflight({ cwd: root });
3692
+ await executeJournalStep(root, workflowRun.runId, "workspace-unlink", () => unlinkWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"), { rerunCompleted: true });
3693
+ }
3597
3694
  if (mode === "root-only") {
3598
3695
  const rootRelease2 = await executeJournalStep(root, workflowRun.runId, "release-root", () => {
3599
3696
  setRootPackageJsonVersion(root, rootVersion);
3600
3697
  run("git", ["checkout", STAGING_BRANCH], { cwd: gitRoot });
3601
- commitAllIfChanged(gitRoot, `release: ${level} bump`);
3698
+ const rootCommitsBeforeChangelog = releaseHistoryCommits(gitRoot);
3699
+ const changelog = updateReleaseChangelog(gitRoot, {
3700
+ version: rootVersion,
3701
+ commits: rootCommitsBeforeChangelog,
3702
+ extraDependencyBullets: [`Release @treeseed/market ${rootVersion}.`]
3703
+ });
3704
+ commitAllIfChanged(gitRoot, releaseAdminMessage({
3705
+ subject: `release: ${level} bump`,
3706
+ version: rootVersion,
3707
+ tagName: rootVersion,
3708
+ commits: rootCommitsBeforeChangelog,
3709
+ changelog
3710
+ }));
3602
3711
  pushBranch(gitRoot, STAGING_BRANCH);
3603
3712
  const stagingCommit = headCommit(gitRoot);
3604
- const released = mergeStagingIntoMain(root);
3605
- const tag = ensureReleaseTag(gitRoot, rootVersion, released.commitSha);
3713
+ const rootCommits = releaseHistoryCommits(gitRoot);
3714
+ const released = mergeBranchIntoTarget(root, {
3715
+ sourceBranch: STAGING_BRANCH,
3716
+ targetBranch: PRODUCTION_BRANCH,
3717
+ message: releaseAdminMessage({
3718
+ subject: `release: ${STAGING_BRANCH} -> ${PRODUCTION_BRANCH}`,
3719
+ version: rootVersion,
3720
+ tagName: rootVersion,
3721
+ commits: rootCommits,
3722
+ changelog
3723
+ }),
3724
+ pushTarget: true
3725
+ });
3726
+ const tag = ensureReleaseTag(gitRoot, rootVersion, released.commitSha, releaseAdminMessage({
3727
+ subject: `release: ${rootVersion}`,
3728
+ version: rootVersion,
3729
+ tagName: rootVersion,
3730
+ commits: rootCommits,
3731
+ changelog
3732
+ }));
3606
3733
  syncBranchWithOrigin(gitRoot, STAGING_BRANCH);
3607
3734
  return {
3608
3735
  rootVersion,
3609
3736
  stagingCommit,
3610
3737
  releasedCommit: released.commitSha,
3611
- tag
3738
+ tag,
3739
+ changelog,
3740
+ adminCommitSummary: {
3741
+ commitCount: rootCommits.length,
3742
+ notableCommits: rootCommits.slice(0, 12)
3743
+ }
3612
3744
  };
3613
3745
  });
3614
3746
  rootRepo.committed = true;
@@ -3618,48 +3750,23 @@ async function workflowRelease(helpers, input) {
3618
3750
  rootRepo.commitSha = String(rootRelease2?.releasedCommit ?? headCommit(gitRoot));
3619
3751
  rootRepo.tagName = String(rootRelease2?.rootVersion ?? "");
3620
3752
  const rootWorkflowGateResult2 = await executeJournalStep(root, workflowRun.runId, "release-root-gates", () => waitForWorkflowGates("release", [
3621
- {
3622
- name: rootRepo.name,
3623
- repoPath: rootRepo.path,
3624
- workflow: "verify.yml",
3625
- branch: STAGING_BRANCH,
3626
- headSha: String(rootRelease2?.stagingCommit ?? "")
3627
- },
3628
- {
3753
+ releaseDeployGate({
3629
3754
  name: rootRepo.name,
3630
3755
  repoPath: rootRepo.path,
3631
3756
  workflow: "deploy.yml",
3632
- branch: STAGING_BRANCH,
3633
- headSha: String(rootRelease2?.stagingCommit ?? "")
3634
- },
3635
- {
3636
- name: rootRepo.name,
3637
- repoPath: rootRepo.path,
3638
- workflow: "verify.yml",
3639
3757
  branch: rootVersion,
3640
3758
  headSha: String(rootRelease2?.releasedCommit ?? rootRepo.commitSha ?? "")
3641
- },
3642
- {
3643
- name: rootRepo.name,
3644
- repoPath: rootRepo.path,
3645
- workflow: "verify.yml",
3646
- branch: PRODUCTION_BRANCH,
3647
- headSha: String(rootRelease2?.releasedCommit ?? rootRepo.commitSha ?? "")
3648
- },
3649
- {
3650
- name: rootRepo.name,
3651
- repoPath: rootRepo.path,
3652
- workflow: "deploy.yml",
3653
- branch: PRODUCTION_BRANCH,
3654
- headSha: String(rootRelease2?.releasedCommit ?? rootRepo.commitSha ?? "")
3655
- }
3759
+ })
3656
3760
  ].filter((gate) => gate.headSha), ciMode, {
3657
3761
  root,
3658
3762
  runId: workflowRun.runId,
3659
3763
  onProgress: (line, stream) => helpers.write(line, stream)
3660
3764
  }).then((workflowGates) => ({ workflowGates })));
3661
3765
  const hostedDeploymentState2 = recordHostedDeploymentStatesFromRootGates(root, rootRelease2, rootWorkflowGateResult2?.workflowGates);
3662
- const releaseBackMerge2 = await executeJournalStep(root, workflowRun.runId, "release-back-merge", () => backMergeRootProductionIntoStaging(root, false));
3766
+ const releaseBackMerge2 = await executeJournalStep(root, workflowRun.runId, "release-back-merge", () => backMergeRootProductionIntoStaging(root, false, {
3767
+ version: rootVersion,
3768
+ changelog: rootRelease2?.changelog ?? null
3769
+ }));
3663
3770
  const workspaceLinks2 = ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
3664
3771
  const payload2 = {
3665
3772
  mode,
@@ -3698,13 +3805,15 @@ async function workflowRelease(helpers, input) {
3698
3805
  ])
3699
3806
  });
3700
3807
  }
3701
- validatePackageReleaseWorkflows(root, effectivePackageSelection.selected);
3702
- for (const pkg of checkedOutWorkspacePackageRepos(root)) {
3703
- if (effectiveSelectedPackageNames.has(pkg.name)) {
3704
- prepareReleaseBranches(pkg.dir);
3808
+ if (!resumeAtRootGates) {
3809
+ validatePackageReleaseWorkflows(root, effectivePackageSelection.selected);
3810
+ for (const pkg of checkedOutWorkspacePackageRepos(root)) {
3811
+ if (effectiveSelectedPackageNames.has(pkg.name)) {
3812
+ prepareReleaseBranches(pkg.dir);
3813
+ }
3705
3814
  }
3706
3815
  }
3707
- releaseCleanupSnapshot = collectReleaseCleanupSnapshot(root, effectiveSelectedPackageNames);
3816
+ releaseCleanupSnapshot = resumeAtRootGates ? null : collectReleaseCleanupSnapshot(root, effectiveSelectedPackageNames);
3708
3817
  const metadata = await executeJournalStep(root, workflowRun.runId, "prepare-release-metadata", () => {
3709
3818
  const releasedPackageDevTags2 = Object.fromEntries(
3710
3819
  checkedOutWorkspacePackageRepos(root).filter((pkg) => effectiveSelectedPackageNames.has(pkg.name)).map((pkg) => {
@@ -3724,7 +3833,7 @@ async function workflowRelease(helpers, input) {
3724
3833
  replacedDevReferences: replacedDevReferences2,
3725
3834
  releaseInstalls: releaseInstalls2
3726
3835
  };
3727
- }, { rerunCompleted: workflowRun.resumed });
3836
+ }, { rerunCompleted: workflowRun.resumed && !resumeAtRootGates });
3728
3837
  const replacedDevReferences = Array.isArray(metadata?.replacedDevReferences) ? metadata.replacedDevReferences : [];
3729
3838
  const releaseInstalls = Array.isArray(metadata?.releaseInstalls) ? metadata.releaseInstalls : [];
3730
3839
  const releasedPackageDevTags = new Map(Object.entries(metadata?.releasedPackageDevTags ?? {}).map(([name, version]) => [name, String(version)]));
@@ -3739,24 +3848,52 @@ async function workflowRelease(helpers, input) {
3739
3848
  }
3740
3849
  const releasedPackage = await executeJournalStep(root, workflowRun.runId, `release-${report.name}`, async () => {
3741
3850
  checkoutBranch(pkg.dir, STAGING_BRANCH);
3851
+ const tagName = String(effectiveVersions.get(pkg.name));
3742
3852
  releaseInstalls.push({
3743
3853
  name: pkg.name,
3744
3854
  ...runReleaseNpmInstall(pkg.dir, { workspaceRoot: root })
3745
3855
  });
3746
3856
  assertNoInternalDevReferencesForRepo(root, pkg.dir, effectiveSelectedPackageNames);
3857
+ const packageCommitsBeforeChangelog = releaseHistoryCommits(pkg.dir);
3858
+ const changelog = updateReleaseChangelog(pkg.dir, {
3859
+ version: tagName,
3860
+ commits: packageCommitsBeforeChangelog,
3861
+ extraDependencyBullets: [`Release ${pkg.name} ${tagName}.`]
3862
+ });
3747
3863
  if (hasMeaningfulChanges(pkg.dir)) {
3748
3864
  run("git", ["add", "-A"], { cwd: pkg.dir });
3749
- run("git", ["commit", "-m", `release: ${effectiveVersions.get(pkg.name)}`], { cwd: pkg.dir });
3865
+ run("git", ["commit", "-m", releaseAdminMessage({
3866
+ subject: `release: ${tagName}`,
3867
+ version: tagName,
3868
+ tagName,
3869
+ commits: packageCommitsBeforeChangelog,
3870
+ changelog,
3871
+ extraLines: [`Package: ${pkg.name}`]
3872
+ })], { cwd: pkg.dir });
3750
3873
  }
3751
3874
  pushBranch(pkg.dir, STAGING_BRANCH);
3875
+ const packageCommits = releaseHistoryCommits(pkg.dir);
3752
3876
  const mergeResult = mergeBranchIntoTarget(pkg.dir, {
3753
3877
  sourceBranch: STAGING_BRANCH,
3754
3878
  targetBranch: PRODUCTION_BRANCH,
3755
- message: `release: ${STAGING_BRANCH} -> ${PRODUCTION_BRANCH}`,
3879
+ message: releaseAdminMessage({
3880
+ subject: `release: ${STAGING_BRANCH} -> ${PRODUCTION_BRANCH}`,
3881
+ version: tagName,
3882
+ tagName,
3883
+ commits: packageCommits,
3884
+ changelog,
3885
+ extraLines: [`Package: ${pkg.name}`]
3886
+ }),
3756
3887
  pushTarget: true
3757
3888
  });
3758
- const tagName = String(effectiveVersions.get(pkg.name));
3759
- const tag = ensureReleaseTag(pkg.dir, tagName, mergeResult.commitSha);
3889
+ const tag = ensureReleaseTag(pkg.dir, tagName, mergeResult.commitSha, releaseAdminMessage({
3890
+ subject: `release: ${tagName}`,
3891
+ version: tagName,
3892
+ tagName,
3893
+ commits: packageCommits,
3894
+ changelog,
3895
+ extraLines: [`Package: ${pkg.name}`]
3896
+ }));
3760
3897
  const workflowGates = await waitForWorkflowGates("release", [
3761
3898
  {
3762
3899
  name: pkg.name,
@@ -3764,20 +3901,6 @@ async function workflowRelease(helpers, input) {
3764
3901
  workflow: "publish.yml",
3765
3902
  headSha: mergeResult.commitSha,
3766
3903
  branch: tagName
3767
- },
3768
- {
3769
- name: pkg.name,
3770
- repoPath: pkg.dir,
3771
- workflow: "verify.yml",
3772
- headSha: mergeResult.commitSha,
3773
- branch: tagName
3774
- },
3775
- {
3776
- name: pkg.name,
3777
- repoPath: pkg.dir,
3778
- workflow: "verify.yml",
3779
- headSha: mergeResult.commitSha,
3780
- branch: PRODUCTION_BRANCH
3781
3904
  }
3782
3905
  ], ciMode, {
3783
3906
  root,
@@ -3786,12 +3909,26 @@ async function workflowRelease(helpers, input) {
3786
3909
  });
3787
3910
  const publish = workflowGates.find((gate) => gate.workflow === "publish.yml") ?? workflowGates[0] ?? null;
3788
3911
  assertReleaseGitHubWorkflowSucceeded(pkg.name, publish);
3789
- const backMerge = backMergeProductionIntoStaging(pkg.dir, pkg.name);
3912
+ const backMerge = backMergeProductionIntoStaging(pkg.dir, pkg.name, releaseAdminMessage({
3913
+ subject: `release: back-merge ${PRODUCTION_BRANCH} into ${STAGING_BRANCH}`,
3914
+ version: tagName,
3915
+ tagName,
3916
+ sourceRef: PRODUCTION_BRANCH,
3917
+ targetRef: STAGING_BRANCH,
3918
+ commits: packageCommits,
3919
+ changelog,
3920
+ extraLines: [`Package: ${pkg.name}`]
3921
+ }));
3790
3922
  syncBranchWithOrigin(pkg.dir, STAGING_BRANCH);
3791
3923
  return {
3792
3924
  commitSha: mergeResult.commitSha,
3793
3925
  tagName,
3794
3926
  tag,
3927
+ changelog,
3928
+ adminCommitSummary: {
3929
+ commitCount: packageCommits.length,
3930
+ notableCommits: packageCommits.slice(0, 12)
3931
+ },
3795
3932
  publish,
3796
3933
  workflowGates,
3797
3934
  backMerge
@@ -3805,6 +3942,8 @@ async function workflowRelease(helpers, input) {
3805
3942
  report.publishWait = releasedPackage?.publish ?? null;
3806
3943
  report.workflowGates = Array.isArray(releasedPackage?.workflowGates) ? releasedPackage.workflowGates : [];
3807
3944
  report.backMerge = releasedPackage?.backMerge ?? null;
3945
+ report.changelog = releasedPackage?.changelog ?? null;
3946
+ report.adminCommitSummary = releasedPackage?.adminCommitSummary ?? null;
3808
3947
  report.branch = STAGING_BRANCH;
3809
3948
  publishWait.push({
3810
3949
  name: report.name,
@@ -3815,16 +3954,41 @@ async function workflowRelease(helpers, input) {
3815
3954
  const rootRelease = await executeJournalStep(root, workflowRun.runId, "release-root", () => {
3816
3955
  setRootPackageJsonVersion(root, rootVersion);
3817
3956
  run("git", ["checkout", STAGING_BRANCH], { cwd: gitRoot });
3818
- commitAllIfChanged(gitRoot, `release: ${level} bump`);
3957
+ const rootCommitsBeforeChangelog = releaseHistoryCommits(gitRoot);
3958
+ const changelog = updateReleaseChangelog(gitRoot, {
3959
+ version: rootVersion,
3960
+ commits: rootCommitsBeforeChangelog,
3961
+ extraDependencyBullets: [
3962
+ `Release @treeseed/market ${rootVersion}.`,
3963
+ ...versionLines(effectiveVersions).map((line) => `Release package ${line}.`)
3964
+ ]
3965
+ });
3966
+ commitAllIfChanged(gitRoot, releaseAdminMessage({
3967
+ subject: `release: ${level} bump`,
3968
+ version: rootVersion,
3969
+ tagName: rootVersion,
3970
+ commits: rootCommitsBeforeChangelog,
3971
+ changelog,
3972
+ extraLines: versionLines(effectiveVersions).map((line) => `Package ${line}`)
3973
+ }));
3819
3974
  pushBranch(gitRoot, STAGING_BRANCH);
3820
3975
  const stagingCommit = headCommit(gitRoot);
3976
+ const rootCommits = releaseHistoryCommits(gitRoot);
3821
3977
  let released;
3822
3978
  let submoduleReconciliation = null;
3979
+ const mergeMessage = releaseAdminMessage({
3980
+ subject: `release: ${STAGING_BRANCH} -> ${PRODUCTION_BRANCH}`,
3981
+ version: rootVersion,
3982
+ tagName: rootVersion,
3983
+ commits: rootCommits,
3984
+ changelog,
3985
+ extraLines: versionLines(effectiveVersions).map((line) => `Package ${line}`)
3986
+ });
3823
3987
  try {
3824
3988
  released = mergeBranchIntoTarget(root, {
3825
3989
  sourceBranch: STAGING_BRANCH,
3826
3990
  targetBranch: PRODUCTION_BRANCH,
3827
- message: `release: ${STAGING_BRANCH} -> ${PRODUCTION_BRANCH}`,
3991
+ message: mergeMessage,
3828
3992
  pushTarget: false,
3829
3993
  quietMerge: true
3830
3994
  });
@@ -3835,7 +3999,7 @@ async function workflowRelease(helpers, input) {
3835
3999
  }
3836
4000
  helpers.write(`[release][reconcile] Resolving generated package pointer reconciliation for ${reconciliation.entries.map((entry) => String(entry.path)).join(", ")}.`);
3837
4001
  submoduleReconciliation = reconciliation;
3838
- commitAllIfChanged(gitRoot, `release: ${STAGING_BRANCH} -> ${PRODUCTION_BRANCH}`);
4002
+ commitAllIfChanged(gitRoot, mergeMessage);
3839
4003
  released = { commitSha: headCommit(gitRoot) };
3840
4004
  }
3841
4005
  for (const pkg of checkedOutWorkspacePackageRepos(root)) {
@@ -3843,9 +4007,26 @@ async function workflowRelease(helpers, input) {
3843
4007
  syncBranchWithOrigin(pkg.dir, PRODUCTION_BRANCH);
3844
4008
  }
3845
4009
  }
3846
- commitAllIfChanged(gitRoot, "release: sync package main heads");
4010
+ const mainPointerCommits = releaseHistoryCommits(gitRoot, released.commitSha, "HEAD");
4011
+ commitAllIfChanged(gitRoot, releaseAdminMessage({
4012
+ subject: "release: sync package main heads",
4013
+ version: rootVersion,
4014
+ tagName: rootVersion,
4015
+ sourceRef: "package main heads",
4016
+ targetRef: PRODUCTION_BRANCH,
4017
+ commits: mainPointerCommits,
4018
+ changelog,
4019
+ extraLines: versionLines(effectiveVersions).map((line) => `Main package ${line}`)
4020
+ }));
3847
4021
  const releasedCommit = headCommit(gitRoot);
3848
- const tag = ensureReleaseTag(gitRoot, rootVersion, releasedCommit);
4022
+ const tag = ensureReleaseTag(gitRoot, rootVersion, releasedCommit, releaseAdminMessage({
4023
+ subject: `release: ${rootVersion}`,
4024
+ version: rootVersion,
4025
+ tagName: rootVersion,
4026
+ commits: rootCommits,
4027
+ changelog,
4028
+ extraLines: versionLines(effectiveVersions).map((line) => `Package ${line}`)
4029
+ }));
3849
4030
  run("git", ["push", "origin", PRODUCTION_BRANCH], { cwd: gitRoot });
3850
4031
  syncAllCheckedOutPackageRepos(root, STAGING_BRANCH);
3851
4032
  syncBranchWithOrigin(gitRoot, STAGING_BRANCH);
@@ -3855,6 +4036,11 @@ async function workflowRelease(helpers, input) {
3855
4036
  releasedCommit,
3856
4037
  mergeCommit: released.commitSha,
3857
4038
  tag,
4039
+ changelog,
4040
+ adminCommitSummary: {
4041
+ commitCount: rootCommits.length,
4042
+ notableCommits: rootCommits.slice(0, 12)
4043
+ },
3858
4044
  submoduleReconciliation
3859
4045
  };
3860
4046
  });
@@ -3865,48 +4051,24 @@ async function workflowRelease(helpers, input) {
3865
4051
  rootRepo.commitSha = String(rootRelease?.releasedCommit ?? headCommit(gitRoot));
3866
4052
  rootRepo.tagName = String(rootRelease?.rootVersion ?? "");
3867
4053
  const rootWorkflowGateResult = await executeJournalStep(root, workflowRun.runId, "release-root-gates", () => waitForWorkflowGates("release", [
3868
- {
3869
- name: rootRepo.name,
3870
- repoPath: rootRepo.path,
3871
- workflow: "verify.yml",
3872
- branch: STAGING_BRANCH,
3873
- headSha: String(rootRelease?.stagingCommit ?? "")
3874
- },
3875
- {
4054
+ releaseDeployGate({
3876
4055
  name: rootRepo.name,
3877
4056
  repoPath: rootRepo.path,
3878
4057
  workflow: "deploy.yml",
3879
- branch: STAGING_BRANCH,
3880
- headSha: String(rootRelease?.stagingCommit ?? "")
3881
- },
3882
- {
3883
- name: rootRepo.name,
3884
- repoPath: rootRepo.path,
3885
- workflow: "verify.yml",
3886
4058
  branch: rootVersion,
3887
4059
  headSha: String(rootRelease?.releasedCommit ?? rootRepo.commitSha ?? "")
3888
- },
3889
- {
3890
- name: rootRepo.name,
3891
- repoPath: rootRepo.path,
3892
- workflow: "verify.yml",
3893
- branch: PRODUCTION_BRANCH,
3894
- headSha: String(rootRelease?.releasedCommit ?? rootRepo.commitSha ?? "")
3895
- },
3896
- {
3897
- name: rootRepo.name,
3898
- repoPath: rootRepo.path,
3899
- workflow: "deploy.yml",
3900
- branch: PRODUCTION_BRANCH,
3901
- headSha: String(rootRelease?.releasedCommit ?? rootRepo.commitSha ?? "")
3902
- }
4060
+ })
3903
4061
  ].filter((gate) => gate.headSha), ciMode, {
3904
4062
  root,
3905
4063
  runId: workflowRun.runId,
3906
4064
  onProgress: (line, stream) => helpers.write(line, stream)
3907
4065
  }).then((workflowGates) => ({ workflowGates })));
3908
4066
  const hostedDeploymentState = recordHostedDeploymentStatesFromRootGates(root, rootRelease, rootWorkflowGateResult?.workflowGates);
3909
- const releaseBackMerge = await executeJournalStep(root, workflowRun.runId, "release-back-merge", () => backMergeRootProductionIntoStaging(root, true));
4067
+ const releaseBackMerge = await executeJournalStep(root, workflowRun.runId, "release-back-merge", () => backMergeRootProductionIntoStaging(root, true, {
4068
+ version: rootVersion,
4069
+ changelog: rootRelease?.changelog ?? null,
4070
+ selectedVersions: effectiveVersions
4071
+ }));
3910
4072
  const devTagCleanupMode = effectiveInput.devTagCleanup ?? "safe-after-release";
3911
4073
  const devTagCleanup = devTagCleanupMode === "off" ? (skipJournalStep(root, workflowRun.runId, "cleanup-dev-tags", { status: "skipped", reason: "disabled" }), { status: "skipped", reason: "disabled" }) : await executeJournalStep(root, workflowRun.runId, "cleanup-dev-tags", () => {
3912
4074
  const activeDevTags = collectActiveDevTagReferences(root);