@treeseed/sdk 0.6.37 → 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.
@@ -162,7 +162,7 @@ function remoteBranchExists(repoDir, branchName) {
162
162
  }
163
163
  }
164
164
  function fetchOrigin(repoDir) {
165
- runGit(["fetch", "origin"], { cwd: repoDir });
165
+ runGit(["fetch", "origin"], { cwd: repoDir, capture: true });
166
166
  }
167
167
  function ensureLocalBranchTracking(repoDir, branchName) {
168
168
  if (branchExists(repoDir, branchName)) {
@@ -175,7 +175,7 @@ function ensureLocalBranchTracking(repoDir, branchName) {
175
175
  runGit(["checkout", "--orphan", branchName], { cwd: repoDir });
176
176
  }
177
177
  function checkoutBranch(repoDir, branchName) {
178
- runGit(["checkout", branchName], { cwd: repoDir });
178
+ runGit(["checkout", branchName], { cwd: repoDir, capture: true });
179
179
  }
180
180
  function checkoutTaskBranchFromStaging(cwd, branchName, { createIfMissing = true, pushIfCreated = false } = {}) {
181
181
  const repoDir = assertCleanWorktree(cwd);
@@ -244,7 +244,7 @@ function syncBranchWithOrigin(repoDir, branchName) {
244
244
  checkoutBranch(repoDir, branchName);
245
245
  }
246
246
  if (remoteBranchExists(repoDir, branchName)) {
247
- runGit(["merge", "--ff-only", `origin/${branchName}`], { cwd: repoDir });
247
+ runGit(["merge", "--ff-only", `origin/${branchName}`], { cwd: repoDir, capture: true });
248
248
  }
249
249
  }
250
250
  function checkoutDetachedOriginBranch(repoDir, branchName) {
@@ -16,6 +16,8 @@ export type GitHubActionsWorkflowGate = {
16
16
  workflow: string;
17
17
  branch: string;
18
18
  headSha: string;
19
+ timeoutSeconds?: number;
20
+ pollSeconds?: number;
19
21
  };
20
22
  export type GitHubActionsWorkflowJobStep = {
21
23
  name: string;
@@ -116,6 +118,8 @@ export declare function skippedGitHubActionsGate(gate: GitHubActionsWorkflowGate
116
118
  url: null;
117
119
  createdAt: null;
118
120
  updatedAt: null;
121
+ timeoutSeconds: number | null;
122
+ cached: boolean;
119
123
  };
120
124
  export declare function formatGitHubActionsGateFailure(gate: GitHubActionsWorkflowGate, result: Record<string, unknown>): string;
121
125
  export declare function createGitHubActionsGateProgressReporter(gate: GitHubActionsWorkflowGate, options?: {
@@ -408,7 +408,9 @@ function skippedGitHubActionsGate(gate, reason) {
408
408
  runId: null,
409
409
  url: null,
410
410
  createdAt: null,
411
- updatedAt: null
411
+ updatedAt: null,
412
+ timeoutSeconds: gate.timeoutSeconds ?? null,
413
+ cached: false
412
414
  };
413
415
  }
414
416
  function formatGitHubActionsGateFailure(gate, result) {
@@ -537,8 +539,8 @@ async function waitForGitHubActionsGate(gate, options = {}) {
537
539
  workflow: gate.workflow,
538
540
  headSha: gate.headSha,
539
541
  branch: gate.branch,
540
- timeoutSeconds: options.timeoutSeconds,
541
- pollSeconds: options.pollSeconds,
542
+ timeoutSeconds: gate.timeoutSeconds ?? options.timeoutSeconds,
543
+ pollSeconds: gate.pollSeconds ?? options.pollSeconds,
542
544
  onProgress: reportProgress
543
545
  });
544
546
  }
@@ -0,0 +1,59 @@
1
+ export type ReleaseHistoryCommit = {
2
+ sha: string;
3
+ subject: string;
4
+ body: string;
5
+ };
6
+ export type ReleaseHistorySection = 'Added' | 'Changed' | 'Fixed' | 'Infrastructure' | 'Tests' | 'Dependencies';
7
+ export type ReleaseHistorySummary = {
8
+ version: string;
9
+ date: string;
10
+ sourceRef: string;
11
+ targetRef: string;
12
+ commitCount: number;
13
+ sections: Record<ReleaseHistorySection, string[]>;
14
+ notableCommits: ReleaseHistoryCommit[];
15
+ changelogPath: string;
16
+ changelogUpdated: boolean;
17
+ entry: string;
18
+ };
19
+ export declare function collectReleaseHistoryCommits(repoDir: string, sourceRef: string, targetRef: string, options?: {
20
+ maxCommits?: number;
21
+ }): ReleaseHistoryCommit[];
22
+ export declare function renderReleaseChangelogEntry(input: {
23
+ version: string;
24
+ date?: string;
25
+ commits: ReleaseHistoryCommit[];
26
+ extraBullets?: Partial<Record<ReleaseHistorySection, string[]>>;
27
+ }): {
28
+ date: string;
29
+ sections: Record<ReleaseHistorySection, string[]>;
30
+ entry: string;
31
+ };
32
+ export declare function upsertReleaseChangelog(repoDir: string, input: {
33
+ version: string;
34
+ sourceRef: string;
35
+ targetRef: string;
36
+ commits: ReleaseHistoryCommit[];
37
+ extraBullets?: Partial<Record<ReleaseHistorySection, string[]>>;
38
+ }): {
39
+ version: string;
40
+ date: string;
41
+ sourceRef: string;
42
+ targetRef: string;
43
+ commitCount: number;
44
+ sections: Record<ReleaseHistorySection, string[]>;
45
+ notableCommits: ReleaseHistoryCommit[];
46
+ changelogPath: string;
47
+ changelogUpdated: boolean;
48
+ entry: string;
49
+ };
50
+ export declare function renderAdministrativeCommitMessage(input: {
51
+ subject: string;
52
+ version?: string | null;
53
+ tagName?: string | null;
54
+ sourceRef: string;
55
+ targetRef: string;
56
+ commits: ReleaseHistoryCommit[];
57
+ changelog?: ReleaseHistorySummary | null;
58
+ extraLines?: string[];
59
+ }): string;
@@ -0,0 +1,159 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { spawnSync } from "node:child_process";
4
+ const SECTION_ORDER = [
5
+ "Added",
6
+ "Changed",
7
+ "Fixed",
8
+ "Infrastructure",
9
+ "Tests",
10
+ "Dependencies"
11
+ ];
12
+ function runGit(repoDir, args) {
13
+ const result = spawnSync("git", args, {
14
+ cwd: repoDir,
15
+ stdio: "pipe",
16
+ encoding: "utf8"
17
+ });
18
+ if (result.status !== 0) {
19
+ throw new Error(result.stderr?.trim() || result.stdout?.trim() || `git ${args.join(" ")} failed`);
20
+ }
21
+ return result.stdout;
22
+ }
23
+ function shortSha(value) {
24
+ return value.slice(0, 12);
25
+ }
26
+ function cleanLine(value) {
27
+ return value.replace(/\s+/gu, " ").trim();
28
+ }
29
+ function bulletText(commit) {
30
+ const subject = cleanLine(commit.subject);
31
+ return subject ? `${subject} (${shortSha(commit.sha)})` : shortSha(commit.sha);
32
+ }
33
+ function sectionForCommit(commit) {
34
+ const value = `${commit.subject}
35
+ ${commit.body}`.toLowerCase();
36
+ if (/^(feat|add)(\(|:)/u.test(value) || /\badded?\b/u.test(value)) return "Added";
37
+ if (/^(fix|hotfix)(\(|:)/u.test(value) || /\bfix(e[ds])?\b|\bbug\b/u.test(value)) return "Fixed";
38
+ if (/^(test)(\(|:)/u.test(value) || /\btest(s|ing)?\b|\bverify\b/u.test(value)) return "Tests";
39
+ if (/^(deps?|build)(\(|:)/u.test(value) || /\bdependenc(y|ies)\b|\blockfile\b|\bpackage pointer\b/u.test(value)) return "Dependencies";
40
+ if (/^(ci|chore|release)(\(|:)/u.test(value) || /\bdeploy\b|\bworkflow\b|\brelease\b|\bsubmodule\b/u.test(value)) return "Infrastructure";
41
+ return "Changed";
42
+ }
43
+ function uniqueSectionBullets(commits) {
44
+ const sections = Object.fromEntries(SECTION_ORDER.map((section) => [section, []]));
45
+ const seen = /* @__PURE__ */ new Set();
46
+ for (const commit of commits) {
47
+ const bullet = bulletText(commit);
48
+ const key = bullet.toLowerCase();
49
+ if (seen.has(key)) continue;
50
+ seen.add(key);
51
+ sections[sectionForCommit(commit)].push(bullet);
52
+ }
53
+ return sections;
54
+ }
55
+ function collectReleaseHistoryCommits(repoDir, sourceRef, targetRef, options = {}) {
56
+ const maxCommits = options.maxCommits ?? 80;
57
+ const output = runGit(repoDir, [
58
+ "log",
59
+ "--no-merges",
60
+ `--max-count=${maxCommits}`,
61
+ "--format=%H%x1f%s%x1f%b%x1e",
62
+ `${sourceRef}..${targetRef}`
63
+ ]);
64
+ return output.split("").map((entry) => entry.trim()).filter(Boolean).map((entry) => {
65
+ const [sha = "", subject = "", body = ""] = entry.split("");
66
+ return { sha: sha.trim(), subject: subject.trim(), body: body.trim() };
67
+ }).filter((commit) => commit.sha.length > 0);
68
+ }
69
+ function renderReleaseChangelogEntry(input) {
70
+ const date = input.date ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
71
+ const sections = uniqueSectionBullets(input.commits);
72
+ for (const [section, bullets] of Object.entries(input.extraBullets ?? {})) {
73
+ for (const bullet of bullets ?? []) {
74
+ const normalized = cleanLine(bullet);
75
+ if (normalized) sections[section].push(normalized);
76
+ }
77
+ }
78
+ const lines = [`## [${input.version}] - ${date}`, ""];
79
+ let wroteSection = false;
80
+ for (const section of SECTION_ORDER) {
81
+ const bullets = sections[section];
82
+ if (bullets.length === 0) continue;
83
+ wroteSection = true;
84
+ lines.push(`### ${section}`, "");
85
+ for (const bullet of bullets.slice(0, 20)) {
86
+ lines.push(`- ${bullet}`);
87
+ }
88
+ if (bullets.length > 20) {
89
+ lines.push(`- ${bullets.length - 20} additional change${bullets.length - 20 === 1 ? "" : "s"} omitted from this summary.`);
90
+ }
91
+ lines.push("");
92
+ }
93
+ if (!wroteSection) {
94
+ lines.push("### Changed", "", "- Release metadata and deployment history updated.", "");
95
+ }
96
+ return {
97
+ date,
98
+ sections,
99
+ entry: lines.join("\n").trimEnd()
100
+ };
101
+ }
102
+ function upsertReleaseChangelog(repoDir, input) {
103
+ const rendered = renderReleaseChangelogEntry(input);
104
+ const changelogPath = resolve(repoDir, "CHANGELOG.md");
105
+ const current = existsSync(changelogPath) ? readFileSync(changelogPath, "utf8") : "";
106
+ const title = "# Changelog";
107
+ const withoutExisting = current.replace(new RegExp(`^# Changelog\\s*\\n+## \\[${input.version.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")}\\][\\s\\S]*?(?=\\n## \\[|$)`, "u"), `${title}
108
+
109
+ `).trim();
110
+ const body = withoutExisting.startsWith(title) ? withoutExisting.slice(title.length).trim() : withoutExisting.trim();
111
+ const next = `${title}
112
+
113
+ ${rendered.entry}${body ? `
114
+
115
+ ${body}` : ""}
116
+ `;
117
+ const changed = current !== next;
118
+ if (changed) {
119
+ writeFileSync(changelogPath, next, "utf8");
120
+ }
121
+ return {
122
+ version: input.version,
123
+ date: rendered.date,
124
+ sourceRef: input.sourceRef,
125
+ targetRef: input.targetRef,
126
+ commitCount: input.commits.length,
127
+ sections: rendered.sections,
128
+ notableCommits: input.commits.slice(0, 12),
129
+ changelogPath,
130
+ changelogUpdated: changed,
131
+ entry: rendered.entry
132
+ };
133
+ }
134
+ function renderAdministrativeCommitMessage(input) {
135
+ const lines = [
136
+ input.subject,
137
+ "",
138
+ "Release summary:",
139
+ input.version ? `- Version: ${input.version}` : null,
140
+ input.tagName ? `- Tag: ${input.tagName}` : null,
141
+ `- Source: ${input.sourceRef}`,
142
+ `- Target: ${input.targetRef}`,
143
+ `- Promoted commits: ${input.commits.length}`,
144
+ ...(input.extraLines ?? []).map((line) => `- ${line}`),
145
+ "",
146
+ "Notable changes:",
147
+ ...input.commits.length > 0 ? input.commits.slice(0, 12).map((commit) => `- ${bulletText(commit)}`) : ["- Release metadata and package pointers updated."],
148
+ input.commits.length > 12 ? `- ${input.commits.length - 12} additional promoted commit${input.commits.length - 12 === 1 ? "" : "s"} omitted from this summary.` : null,
149
+ input.changelog ? "" : null,
150
+ input.changelog ? "See CHANGELOG.md for the release history entry." : null
151
+ ].filter((line) => line !== null);
152
+ return lines.join("\n");
153
+ }
154
+ export {
155
+ collectReleaseHistoryCommits,
156
+ renderAdministrativeCommitMessage,
157
+ renderReleaseChangelogEntry,
158
+ upsertReleaseChangelog
159
+ };
@@ -1,11 +1,40 @@
1
- import { resolveAstroBin, createProductionBuildEnv, packageScriptPath, runNodeBinary, runNodeScript } from '../operations/services/runtime-tools.js';
1
+ import { spawnSync } from 'node:child_process';
2
+ import { createBuildWarningSummary, formatAllowedBuildWarnings } from '../operations/services/build-warning-policy.js';
3
+ import { resolveAstroBin, createProductionBuildEnv, packageScriptPath, runNodeScript } from '../operations/services/runtime-tools.js';
4
+ function runFilteredNodeBinary(binPath, args, options) {
5
+ const result = spawnSync(process.execPath, [binPath, ...args], {
6
+ cwd: options.cwd,
7
+ env: { ...process.env, ...options.env },
8
+ stdio: 'pipe',
9
+ encoding: 'utf8',
10
+ });
11
+ const warningSummary = createBuildWarningSummary();
12
+ const emitFiltered = (text, stream) => {
13
+ for (const line of text.split(/\r?\n/u)) {
14
+ if (!line)
15
+ continue;
16
+ const classified = warningSummary.record(line);
17
+ if (classified.kind === 'allowed')
18
+ continue;
19
+ stream.write(`${line}\n`);
20
+ }
21
+ };
22
+ emitFiltered(result.stdout ?? '', process.stdout);
23
+ emitFiltered(result.stderr ?? '', process.stderr);
24
+ for (const line of formatAllowedBuildWarnings(warningSummary.allowedWarnings)) {
25
+ process.stdout.write(`${line}\n`);
26
+ }
27
+ if (result.status !== 0) {
28
+ process.exit(result.status ?? 1);
29
+ }
30
+ }
2
31
  process.env.TREESEED_LOCAL_DEV_MODE = process.env.TREESEED_LOCAL_DEV_MODE ?? 'cloudflare';
3
32
  const publishedRuntime = process.env.TREESEED_CONTENT_SERVING_MODE === 'published_runtime';
4
33
  runNodeScript(packageScriptPath('patch-starlight-content-path'), [], { cwd: process.cwd() });
5
34
  if (!publishedRuntime) {
6
35
  runNodeScript(packageScriptPath('aggregate-book'), [], { cwd: process.cwd() });
7
36
  }
8
- runNodeBinary(resolveAstroBin(), ['build'], {
37
+ runFilteredNodeBinary(resolveAstroBin(), ['build'], {
9
38
  cwd: process.cwd(),
10
39
  env: createProductionBuildEnv({
11
40
  TREESEED_LOCAL_DEV_MODE: process.env.TREESEED_LOCAL_DEV_MODE,
@@ -44,6 +44,8 @@ type WorkflowRepoReport = {
44
44
  publishWait: Record<string, unknown> | null;
45
45
  workflowGates: Array<Record<string, unknown>>;
46
46
  backMerge: Record<string, unknown> | null;
47
+ changelog?: Record<string, unknown> | null;
48
+ adminCommitSummary?: Record<string, unknown> | null;
47
49
  };
48
50
  export declare function workflowStatus(helpers: WorkflowOperationHelpers, input?: TreeseedWorkflowStatusOptions): Promise<TreeseedWorkflowResult<import("../workflow-state.ts").TreeseedWorkflowState>>;
49
51
  export declare function workflowCi(helpers: WorkflowOperationHelpers, input?: TreeseedCiInput): Promise<TreeseedWorkflowResult<TreeseedCiResult>>;
@@ -417,6 +419,8 @@ export declare function workflowSave(helpers: WorkflowOperationHelpers, input: T
417
419
  url: null;
418
420
  createdAt: null;
419
421
  updatedAt: null;
422
+ timeoutSeconds: number | null;
423
+ cached: boolean;
420
424
  }[];
421
425
  releaseCandidate: ReleaseCandidateReport | null;
422
426
  } & {
@@ -557,6 +561,8 @@ export declare function workflowClose(helpers: WorkflowOperationHelpers, input:
557
561
  url: null;
558
562
  createdAt: null;
559
563
  updatedAt: null;
564
+ timeoutSeconds: number | null;
565
+ cached: boolean;
560
566
  }[];
561
567
  releaseCandidate: ReleaseCandidateReport | null;
562
568
  } & {
@@ -738,6 +744,8 @@ export declare function workflowStage(helpers: WorkflowOperationHelpers, input:
738
744
  url: null;
739
745
  createdAt: null;
740
746
  updatedAt: null;
747
+ timeoutSeconds: number | null;
748
+ cached: boolean;
741
749
  }[];
742
750
  releaseCandidate: ReleaseCandidateReport | null;
743
751
  } & {
@@ -874,7 +882,7 @@ export declare function workflowRelease(helpers: WorkflowOperationHelpers, input
874
882
  publishWait: never[];
875
883
  repos: never[];
876
884
  rootRepo: WorkflowRepoReport;
877
- releaseCandidate: ReleaseCandidateReport;
885
+ releaseCandidate: ReleaseCandidateReport | null;
878
886
  releaseBackMerge: {
879
887
  status: string;
880
888
  merged: boolean;
@@ -914,6 +922,8 @@ export declare function workflowRelease(helpers: WorkflowOperationHelpers, input
914
922
  url: null;
915
923
  createdAt: null;
916
924
  updatedAt: null;
925
+ timeoutSeconds: number | null;
926
+ cached: boolean;
917
927
  }[];
918
928
  } & {
919
929
  finalState?: WorkflowStatePayload;
@@ -959,7 +969,7 @@ export declare function workflowRelease(helpers: WorkflowOperationHelpers, input
959
969
  publishWait: Record<string, unknown>[];
960
970
  repos: WorkflowRepoReport[];
961
971
  rootRepo: WorkflowRepoReport;
962
- releaseCandidate: ReleaseCandidateReport;
972
+ releaseCandidate: ReleaseCandidateReport | null;
963
973
  releaseBackMerge: {
964
974
  status: string;
965
975
  merged: boolean;
@@ -999,6 +1009,8 @@ export declare function workflowRelease(helpers: WorkflowOperationHelpers, input
999
1009
  url: null;
1000
1010
  createdAt: null;
1001
1011
  updatedAt: null;
1012
+ timeoutSeconds: number | null;
1013
+ cached: boolean;
1002
1014
  })[];
1003
1015
  } & {
1004
1016
  finalState?: WorkflowStatePayload;
@@ -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,
@@ -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";
@@ -402,7 +406,9 @@ async function waitForWorkflowGates(operation, gates, ciMode, options = {}) {
402
406
  ...result,
403
407
  workflow: String(result.workflow ?? gate.workflow),
404
408
  branch: String(result.branch ?? gate.branch),
405
- headSha: String(result.headSha ?? gate.headSha)
409
+ headSha: String(result.headSha ?? gate.headSha),
410
+ timeoutSeconds: gate.timeoutSeconds ?? null,
411
+ cached: false
406
412
  };
407
413
  if (normalized.status === "completed" && normalized.conclusion !== "success") {
408
414
  workflowError(operation, "github_workflow_failed", formatGitHubActionsGateFailure(gate, normalized), {
@@ -416,13 +422,21 @@ async function waitForWorkflowGates(operation, gates, ciMode, options = {}) {
416
422
  }
417
423
  return results;
418
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
+ }
419
432
  function recordHostedDeploymentStatesFromRootGates(root, rootRelease, workflowGates) {
420
433
  const gates = Array.isArray(workflowGates) ? workflowGates.map((gate) => stringRecord(gate)).filter((gate) => Boolean(gate)) : [];
421
434
  const releaseRecord = stringRecord(rootRelease) ?? {};
422
435
  const reports = [];
436
+ const releaseTag = typeof releaseRecord.rootVersion === "string" ? releaseRecord.rootVersion : null;
423
437
  for (const target of [
424
438
  { scope: "staging", branch: STAGING_BRANCH, commit: releaseRecord.stagingCommit },
425
- { scope: "prod", branch: PRODUCTION_BRANCH, commit: releaseRecord.releasedCommit }
439
+ { scope: "prod", branch: releaseTag ?? PRODUCTION_BRANCH, commit: releaseRecord.releasedCommit }
426
440
  ]) {
427
441
  const gate = gates.find((candidate) => candidate.workflow === "deploy.yml" && candidate.branch === target.branch && candidate.status === "completed" && candidate.conclusion === "success");
428
442
  const timestamp = typeof gate?.updatedAt === "string" && gate.updatedAt.trim() ? gate.updatedAt : null;
@@ -702,13 +716,13 @@ function remoteTagCommit(repoDir, tagName) {
702
716
  const direct = output.split("\n").find((line) => line.endsWith(`refs/tags/${tagName}`));
703
717
  return (peeled ?? direct)?.split(/\s+/u)[0] ?? null;
704
718
  }
705
- function ensureReleaseTag(repoDir, tagName, commitSha) {
719
+ function ensureReleaseTag(repoDir, tagName, commitSha, message) {
706
720
  const localCommit = gitObjectCommit(repoDir, tagName);
707
721
  if (localCommit && localCommit !== commitSha) {
708
722
  throw new Error(`Release tag ${tagName} already exists locally at ${localCommit}, expected ${commitSha}.`);
709
723
  }
710
724
  if (!localCommit) {
711
- run("git", ["tag", "-a", tagName, commitSha, "-m", `release: ${tagName}`], { cwd: repoDir });
725
+ run("git", ["tag", "-a", tagName, commitSha, "-m", message ?? `release: ${tagName}`], { cwd: repoDir });
712
726
  }
713
727
  const remoteCommit = remoteTagCommit(repoDir, tagName);
714
728
  if (remoteCommit && remoteCommit !== commitSha) {
@@ -731,6 +745,51 @@ function commitAllIfChanged(repoDir, message) {
731
745
  run("git", ["commit", "-m", message], { cwd: repoDir });
732
746
  return { committed: true, commitSha: headCommit(repoDir) };
733
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
+ }
734
793
  function createNextSteps(steps) {
735
794
  return steps.map(renderWorkflowStep);
736
795
  }
@@ -1397,8 +1456,25 @@ function runReleaseNpmInstall(repoDir, options = {}) {
1397
1456
  if (shouldSkipReleaseInstall()) {
1398
1457
  return { status: "skipped", reason: "disabled" };
1399
1458
  }
1400
- const args = repoDir === options.workspaceRoot ? ["install", "--package-lock-only", "--ignore-scripts"] : ["install", "--package-lock-only", "--ignore-scripts", "--workspaces=false"];
1401
- run("npm", args, { cwd: repoDir });
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`);
1477
+ }
1402
1478
  return { status: "completed", reason: null };
1403
1479
  }
1404
1480
  function pathIsWithin(parent, candidate) {
@@ -1416,7 +1492,7 @@ function assertNoInternalDevReferencesForRepo(root, repoDir, packageNames) {
1416
1492
  throw new Error(`Stable release still contains internal Git/dev dependency references.
1417
1493
  ${rendered}`);
1418
1494
  }
1419
- function backMergeProductionIntoStaging(repoDir, repoName) {
1495
+ function backMergeProductionIntoStaging(repoDir, repoName, message) {
1420
1496
  syncBranchWithOrigin(repoDir, PRODUCTION_BRANCH);
1421
1497
  syncBranchWithOrigin(repoDir, STAGING_BRANCH);
1422
1498
  checkoutBranch(repoDir, STAGING_BRANCH);
@@ -1433,7 +1509,7 @@ function backMergeProductionIntoStaging(repoDir, repoName) {
1433
1509
  } catch {
1434
1510
  }
1435
1511
  try {
1436
- 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 });
1437
1513
  } catch (error) {
1438
1514
  const report = collectMergeConflictReport(repoDir);
1439
1515
  throw new TreeseedWorkflowError("release", "merge_conflict", formatMergeConflictReport(report, repoDir, STAGING_BRANCH), {
@@ -1451,14 +1527,32 @@ function backMergeProductionIntoStaging(repoDir, repoName) {
1451
1527
  commitSha: headCommit(repoDir)
1452
1528
  };
1453
1529
  }
1454
- function backMergeRootProductionIntoStaging(root, syncPackageStagingHeads) {
1530
+ function backMergeRootProductionIntoStaging(root, syncPackageStagingHeads, options = {}) {
1455
1531
  const gitRoot = repoRoot(root);
1456
- 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
+ }));
1457
1542
  if (!syncPackageStagingHeads) {
1458
1543
  return backMerge;
1459
1544
  }
1460
1545
  syncAllCheckedOutPackageRepos(root, STAGING_BRANCH);
1461
- 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
+ }));
1462
1556
  if (pointerSync.committed) {
1463
1557
  pushBranch(gitRoot, STAGING_BRANCH);
1464
1558
  }
@@ -3573,6 +3667,10 @@ async function workflowRelease(helpers, input) {
3573
3667
  if (autoResumeRun) {
3574
3668
  helpers.write(`[workflow][resume] Resuming interrupted release ${autoResumeRun.runId} on ${STAGING_BRANCH}.`);
3575
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
+ }
3576
3674
  let releaseCleanupSnapshot = null;
3577
3675
  try {
3578
3676
  const releasePlan = await executeJournalStep(root, workflowRun.runId, "release-plan", () => plannedRelease);
@@ -3582,30 +3680,67 @@ async function workflowRelease(helpers, input) {
3582
3680
  const rootVersion = String(releasePlan.rootVersion);
3583
3681
  applyTreeseedEnvironmentToProcess({ tenantRoot: root, scope: "staging", override: true });
3584
3682
  assertReleaseGitHubAutomationReady(root, effectiveSelectedPackageNames, ciMode);
3585
- const releaseCandidate = await executeJournalStep(root, workflowRun.runId, "release-candidate", () => runReleaseCandidateForPlan("release", root, releasePlan, { allowReuse: true }));
3586
- if (!isResume) {
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) {
3587
3685
  assertSessionBranchSafety("release", session, { requireCleanPackages: true, requireCurrentBranch: true });
3588
3686
  assertCleanWorktree(root);
3589
3687
  }
3590
- prepareReleaseBranches(root);
3591
- ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
3592
- runWorkspaceReleasePreflight({ cwd: root });
3593
- 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
+ }
3594
3694
  if (mode === "root-only") {
3595
3695
  const rootRelease2 = await executeJournalStep(root, workflowRun.runId, "release-root", () => {
3596
3696
  setRootPackageJsonVersion(root, rootVersion);
3597
3697
  run("git", ["checkout", STAGING_BRANCH], { cwd: gitRoot });
3598
- 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
+ }));
3599
3711
  pushBranch(gitRoot, STAGING_BRANCH);
3600
3712
  const stagingCommit = headCommit(gitRoot);
3601
- const released = mergeStagingIntoMain(root);
3602
- 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
+ }));
3603
3733
  syncBranchWithOrigin(gitRoot, STAGING_BRANCH);
3604
3734
  return {
3605
3735
  rootVersion,
3606
3736
  stagingCommit,
3607
3737
  releasedCommit: released.commitSha,
3608
- tag
3738
+ tag,
3739
+ changelog,
3740
+ adminCommitSummary: {
3741
+ commitCount: rootCommits.length,
3742
+ notableCommits: rootCommits.slice(0, 12)
3743
+ }
3609
3744
  };
3610
3745
  });
3611
3746
  rootRepo.committed = true;
@@ -3615,48 +3750,23 @@ async function workflowRelease(helpers, input) {
3615
3750
  rootRepo.commitSha = String(rootRelease2?.releasedCommit ?? headCommit(gitRoot));
3616
3751
  rootRepo.tagName = String(rootRelease2?.rootVersion ?? "");
3617
3752
  const rootWorkflowGateResult2 = await executeJournalStep(root, workflowRun.runId, "release-root-gates", () => waitForWorkflowGates("release", [
3618
- {
3619
- name: rootRepo.name,
3620
- repoPath: rootRepo.path,
3621
- workflow: "verify.yml",
3622
- branch: STAGING_BRANCH,
3623
- headSha: String(rootRelease2?.stagingCommit ?? "")
3624
- },
3625
- {
3753
+ releaseDeployGate({
3626
3754
  name: rootRepo.name,
3627
3755
  repoPath: rootRepo.path,
3628
3756
  workflow: "deploy.yml",
3629
- branch: STAGING_BRANCH,
3630
- headSha: String(rootRelease2?.stagingCommit ?? "")
3631
- },
3632
- {
3633
- name: rootRepo.name,
3634
- repoPath: rootRepo.path,
3635
- workflow: "verify.yml",
3636
3757
  branch: rootVersion,
3637
3758
  headSha: String(rootRelease2?.releasedCommit ?? rootRepo.commitSha ?? "")
3638
- },
3639
- {
3640
- name: rootRepo.name,
3641
- repoPath: rootRepo.path,
3642
- workflow: "verify.yml",
3643
- branch: PRODUCTION_BRANCH,
3644
- headSha: String(rootRelease2?.releasedCommit ?? rootRepo.commitSha ?? "")
3645
- },
3646
- {
3647
- name: rootRepo.name,
3648
- repoPath: rootRepo.path,
3649
- workflow: "deploy.yml",
3650
- branch: PRODUCTION_BRANCH,
3651
- headSha: String(rootRelease2?.releasedCommit ?? rootRepo.commitSha ?? "")
3652
- }
3759
+ })
3653
3760
  ].filter((gate) => gate.headSha), ciMode, {
3654
3761
  root,
3655
3762
  runId: workflowRun.runId,
3656
3763
  onProgress: (line, stream) => helpers.write(line, stream)
3657
3764
  }).then((workflowGates) => ({ workflowGates })));
3658
3765
  const hostedDeploymentState2 = recordHostedDeploymentStatesFromRootGates(root, rootRelease2, rootWorkflowGateResult2?.workflowGates);
3659
- 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
+ }));
3660
3770
  const workspaceLinks2 = ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
3661
3771
  const payload2 = {
3662
3772
  mode,
@@ -3695,13 +3805,15 @@ async function workflowRelease(helpers, input) {
3695
3805
  ])
3696
3806
  });
3697
3807
  }
3698
- validatePackageReleaseWorkflows(root, effectivePackageSelection.selected);
3699
- for (const pkg of checkedOutWorkspacePackageRepos(root)) {
3700
- if (effectiveSelectedPackageNames.has(pkg.name)) {
3701
- 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
+ }
3702
3814
  }
3703
3815
  }
3704
- releaseCleanupSnapshot = collectReleaseCleanupSnapshot(root, effectiveSelectedPackageNames);
3816
+ releaseCleanupSnapshot = resumeAtRootGates ? null : collectReleaseCleanupSnapshot(root, effectiveSelectedPackageNames);
3705
3817
  const metadata = await executeJournalStep(root, workflowRun.runId, "prepare-release-metadata", () => {
3706
3818
  const releasedPackageDevTags2 = Object.fromEntries(
3707
3819
  checkedOutWorkspacePackageRepos(root).filter((pkg) => effectiveSelectedPackageNames.has(pkg.name)).map((pkg) => {
@@ -3721,7 +3833,7 @@ async function workflowRelease(helpers, input) {
3721
3833
  replacedDevReferences: replacedDevReferences2,
3722
3834
  releaseInstalls: releaseInstalls2
3723
3835
  };
3724
- }, { rerunCompleted: workflowRun.resumed });
3836
+ }, { rerunCompleted: workflowRun.resumed && !resumeAtRootGates });
3725
3837
  const replacedDevReferences = Array.isArray(metadata?.replacedDevReferences) ? metadata.replacedDevReferences : [];
3726
3838
  const releaseInstalls = Array.isArray(metadata?.releaseInstalls) ? metadata.releaseInstalls : [];
3727
3839
  const releasedPackageDevTags = new Map(Object.entries(metadata?.releasedPackageDevTags ?? {}).map(([name, version]) => [name, String(version)]));
@@ -3736,24 +3848,52 @@ async function workflowRelease(helpers, input) {
3736
3848
  }
3737
3849
  const releasedPackage = await executeJournalStep(root, workflowRun.runId, `release-${report.name}`, async () => {
3738
3850
  checkoutBranch(pkg.dir, STAGING_BRANCH);
3851
+ const tagName = String(effectiveVersions.get(pkg.name));
3739
3852
  releaseInstalls.push({
3740
3853
  name: pkg.name,
3741
3854
  ...runReleaseNpmInstall(pkg.dir, { workspaceRoot: root })
3742
3855
  });
3743
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
+ });
3744
3863
  if (hasMeaningfulChanges(pkg.dir)) {
3745
3864
  run("git", ["add", "-A"], { cwd: pkg.dir });
3746
- 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 });
3747
3873
  }
3748
3874
  pushBranch(pkg.dir, STAGING_BRANCH);
3875
+ const packageCommits = releaseHistoryCommits(pkg.dir);
3749
3876
  const mergeResult = mergeBranchIntoTarget(pkg.dir, {
3750
3877
  sourceBranch: STAGING_BRANCH,
3751
3878
  targetBranch: PRODUCTION_BRANCH,
3752
- 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
+ }),
3753
3887
  pushTarget: true
3754
3888
  });
3755
- const tagName = String(effectiveVersions.get(pkg.name));
3756
- 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
+ }));
3757
3897
  const workflowGates = await waitForWorkflowGates("release", [
3758
3898
  {
3759
3899
  name: pkg.name,
@@ -3761,20 +3901,6 @@ async function workflowRelease(helpers, input) {
3761
3901
  workflow: "publish.yml",
3762
3902
  headSha: mergeResult.commitSha,
3763
3903
  branch: tagName
3764
- },
3765
- {
3766
- name: pkg.name,
3767
- repoPath: pkg.dir,
3768
- workflow: "verify.yml",
3769
- headSha: mergeResult.commitSha,
3770
- branch: tagName
3771
- },
3772
- {
3773
- name: pkg.name,
3774
- repoPath: pkg.dir,
3775
- workflow: "verify.yml",
3776
- headSha: mergeResult.commitSha,
3777
- branch: PRODUCTION_BRANCH
3778
3904
  }
3779
3905
  ], ciMode, {
3780
3906
  root,
@@ -3783,12 +3909,26 @@ async function workflowRelease(helpers, input) {
3783
3909
  });
3784
3910
  const publish = workflowGates.find((gate) => gate.workflow === "publish.yml") ?? workflowGates[0] ?? null;
3785
3911
  assertReleaseGitHubWorkflowSucceeded(pkg.name, publish);
3786
- 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
+ }));
3787
3922
  syncBranchWithOrigin(pkg.dir, STAGING_BRANCH);
3788
3923
  return {
3789
3924
  commitSha: mergeResult.commitSha,
3790
3925
  tagName,
3791
3926
  tag,
3927
+ changelog,
3928
+ adminCommitSummary: {
3929
+ commitCount: packageCommits.length,
3930
+ notableCommits: packageCommits.slice(0, 12)
3931
+ },
3792
3932
  publish,
3793
3933
  workflowGates,
3794
3934
  backMerge
@@ -3802,6 +3942,8 @@ async function workflowRelease(helpers, input) {
3802
3942
  report.publishWait = releasedPackage?.publish ?? null;
3803
3943
  report.workflowGates = Array.isArray(releasedPackage?.workflowGates) ? releasedPackage.workflowGates : [];
3804
3944
  report.backMerge = releasedPackage?.backMerge ?? null;
3945
+ report.changelog = releasedPackage?.changelog ?? null;
3946
+ report.adminCommitSummary = releasedPackage?.adminCommitSummary ?? null;
3805
3947
  report.branch = STAGING_BRANCH;
3806
3948
  publishWait.push({
3807
3949
  name: report.name,
@@ -3812,16 +3954,41 @@ async function workflowRelease(helpers, input) {
3812
3954
  const rootRelease = await executeJournalStep(root, workflowRun.runId, "release-root", () => {
3813
3955
  setRootPackageJsonVersion(root, rootVersion);
3814
3956
  run("git", ["checkout", STAGING_BRANCH], { cwd: gitRoot });
3815
- 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
+ }));
3816
3974
  pushBranch(gitRoot, STAGING_BRANCH);
3817
3975
  const stagingCommit = headCommit(gitRoot);
3976
+ const rootCommits = releaseHistoryCommits(gitRoot);
3818
3977
  let released;
3819
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
+ });
3820
3987
  try {
3821
3988
  released = mergeBranchIntoTarget(root, {
3822
3989
  sourceBranch: STAGING_BRANCH,
3823
3990
  targetBranch: PRODUCTION_BRANCH,
3824
- message: `release: ${STAGING_BRANCH} -> ${PRODUCTION_BRANCH}`,
3991
+ message: mergeMessage,
3825
3992
  pushTarget: false,
3826
3993
  quietMerge: true
3827
3994
  });
@@ -3832,7 +3999,7 @@ async function workflowRelease(helpers, input) {
3832
3999
  }
3833
4000
  helpers.write(`[release][reconcile] Resolving generated package pointer reconciliation for ${reconciliation.entries.map((entry) => String(entry.path)).join(", ")}.`);
3834
4001
  submoduleReconciliation = reconciliation;
3835
- commitAllIfChanged(gitRoot, `release: ${STAGING_BRANCH} -> ${PRODUCTION_BRANCH}`);
4002
+ commitAllIfChanged(gitRoot, mergeMessage);
3836
4003
  released = { commitSha: headCommit(gitRoot) };
3837
4004
  }
3838
4005
  for (const pkg of checkedOutWorkspacePackageRepos(root)) {
@@ -3840,9 +4007,26 @@ async function workflowRelease(helpers, input) {
3840
4007
  syncBranchWithOrigin(pkg.dir, PRODUCTION_BRANCH);
3841
4008
  }
3842
4009
  }
3843
- 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
+ }));
3844
4021
  const releasedCommit = headCommit(gitRoot);
3845
- 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
+ }));
3846
4030
  run("git", ["push", "origin", PRODUCTION_BRANCH], { cwd: gitRoot });
3847
4031
  syncAllCheckedOutPackageRepos(root, STAGING_BRANCH);
3848
4032
  syncBranchWithOrigin(gitRoot, STAGING_BRANCH);
@@ -3852,6 +4036,11 @@ async function workflowRelease(helpers, input) {
3852
4036
  releasedCommit,
3853
4037
  mergeCommit: released.commitSha,
3854
4038
  tag,
4039
+ changelog,
4040
+ adminCommitSummary: {
4041
+ commitCount: rootCommits.length,
4042
+ notableCommits: rootCommits.slice(0, 12)
4043
+ },
3855
4044
  submoduleReconciliation
3856
4045
  };
3857
4046
  });
@@ -3862,48 +4051,24 @@ async function workflowRelease(helpers, input) {
3862
4051
  rootRepo.commitSha = String(rootRelease?.releasedCommit ?? headCommit(gitRoot));
3863
4052
  rootRepo.tagName = String(rootRelease?.rootVersion ?? "");
3864
4053
  const rootWorkflowGateResult = await executeJournalStep(root, workflowRun.runId, "release-root-gates", () => waitForWorkflowGates("release", [
3865
- {
3866
- name: rootRepo.name,
3867
- repoPath: rootRepo.path,
3868
- workflow: "verify.yml",
3869
- branch: STAGING_BRANCH,
3870
- headSha: String(rootRelease?.stagingCommit ?? "")
3871
- },
3872
- {
4054
+ releaseDeployGate({
3873
4055
  name: rootRepo.name,
3874
4056
  repoPath: rootRepo.path,
3875
4057
  workflow: "deploy.yml",
3876
- branch: STAGING_BRANCH,
3877
- headSha: String(rootRelease?.stagingCommit ?? "")
3878
- },
3879
- {
3880
- name: rootRepo.name,
3881
- repoPath: rootRepo.path,
3882
- workflow: "verify.yml",
3883
4058
  branch: rootVersion,
3884
4059
  headSha: String(rootRelease?.releasedCommit ?? rootRepo.commitSha ?? "")
3885
- },
3886
- {
3887
- name: rootRepo.name,
3888
- repoPath: rootRepo.path,
3889
- workflow: "verify.yml",
3890
- branch: PRODUCTION_BRANCH,
3891
- headSha: String(rootRelease?.releasedCommit ?? rootRepo.commitSha ?? "")
3892
- },
3893
- {
3894
- name: rootRepo.name,
3895
- repoPath: rootRepo.path,
3896
- workflow: "deploy.yml",
3897
- branch: PRODUCTION_BRANCH,
3898
- headSha: String(rootRelease?.releasedCommit ?? rootRepo.commitSha ?? "")
3899
- }
4060
+ })
3900
4061
  ].filter((gate) => gate.headSha), ciMode, {
3901
4062
  root,
3902
4063
  runId: workflowRun.runId,
3903
4064
  onProgress: (line, stream) => helpers.write(line, stream)
3904
4065
  }).then((workflowGates) => ({ workflowGates })));
3905
4066
  const hostedDeploymentState = recordHostedDeploymentStatesFromRootGates(root, rootRelease, rootWorkflowGateResult?.workflowGates);
3906
- 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
+ }));
3907
4072
  const devTagCleanupMode = effectiveInput.devTagCleanup ?? "safe-after-release";
3908
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", () => {
3909
4074
  const activeDevTags = collectActiveDevTagReferences(root);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@treeseed/sdk",
3
- "version": "0.6.37",
3
+ "version": "0.6.38",
4
4
  "description": "Shared Treeseed SDK for content-backed and D1-backed object models.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {
@@ -4,9 +4,8 @@ on:
4
4
  push:
5
5
  branches:
6
6
  - staging
7
- - main
8
7
  tags:
9
- - 'v*'
8
+ - '*.*.*'
10
9
  workflow_dispatch:
11
10
  inputs:
12
11
  environment:
@@ -75,12 +74,13 @@ jobs:
75
74
 
76
75
  if [[ "${ref_type}" == "tag" ]]; then
77
76
  scope="prod"
78
- release_tag="true"
79
- compare_ref="$(git rev-list --parents -n 1 "${head_sha}" | awk '{print $2}')"
80
- elif [[ "${ref_name}" == "main" ]]; then
81
- scope="prod"
82
- release_tag="false"
83
- compare_ref="${before_sha}"
77
+ if [[ "${ref_name}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
78
+ release_tag="true"
79
+ compare_ref="$(git rev-list --parents -n 1 "${head_sha}" | awk '{print $2}')"
80
+ else
81
+ release_tag="false"
82
+ compare_ref=""
83
+ fi
84
84
  else
85
85
  scope="staging"
86
86
  release_tag="false"
@@ -94,7 +94,10 @@ jobs:
94
94
  code_changed="false"
95
95
  content_changed="false"
96
96
 
97
- if [[ -n "${compare_ref}" ]]; then
97
+ if [[ "${ref_type}" == "tag" && "${release_tag}" != "true" ]]; then
98
+ code_changed="false"
99
+ content_changed="false"
100
+ elif [[ -n "${compare_ref}" ]]; then
98
101
  while IFS= read -r path; do
99
102
  [[ -z "${path}" ]] && continue
100
103
  case "${path}" in