@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.
@@ -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) {
@@ -420,8 +420,8 @@ function createDeprecatedTaskTag(repoDir, branchName, message) {
420
420
  return { tagName, head };
421
421
  }
422
422
  function waitForStagingAutomation(repoDir) {
423
- if (process.env.TREESEED_STAGE_WAIT_MODE === "skip" || process.env.TREESEED_GITHUB_AUTOMATION_MODE === "stub") {
424
- return { status: "skipped", reason: "stubbed" };
423
+ if (process.env.TREESEED_STAGE_WAIT_MODE === "skip") {
424
+ return { status: "skipped", reason: "disabled" };
425
425
  }
426
426
  try {
427
427
  const gh = resolveTreeseedToolBinary("gh");
@@ -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
  }
@@ -22,7 +22,7 @@ export interface TreeseedGitHubRepositoryTarget {
22
22
  visibility: 'private' | 'public' | 'internal';
23
23
  source: 'config' | 'origin' | 'default';
24
24
  }
25
- export declare function getGitHubAutomationMode(): "stub" | "real";
25
+ export declare function getGitHubAutomationMode(): string;
26
26
  export declare function parseGitHubRepositoryFromRemote(remoteUrl: any): string | null;
27
27
  export declare function resolveGitHubRepositorySlug(tenantRoot: any): string;
28
28
  export declare function maybeResolveGitHubRepositorySlug(tenantRoot: any): string | null;
@@ -51,17 +51,7 @@ export declare function ensureGitHubBootstrapRepository(tenantRoot: string, { va
51
51
  pushed: boolean;
52
52
  mode: string;
53
53
  }>;
54
- export declare function createGitHubRepository(input: any): Promise<import("./github-api.ts").GitHubRepositorySummary | {
55
- visibility: any;
56
- defaultBranch: string;
57
- mode: string;
58
- slug: string;
59
- owner: string;
60
- name: string;
61
- sshUrl: string;
62
- httpsUrl: string;
63
- url: string;
64
- }>;
54
+ export declare function createGitHubRepository(input: any): Promise<import("./github-api.ts").GitHubRepositorySummary>;
65
55
  export declare function initializeGitHubRepositoryWorkingTree(cwd: any, repository: any, { defaultBranch, createStaging, commitMessage, remoteName, push, }?: {
66
56
  defaultBranch?: string | undefined;
67
57
  createStaging?: boolean | undefined;
@@ -74,14 +64,6 @@ export declare function initializeGitHubRepositoryWorkingTree(cwd: any, reposito
74
64
  defaultBranch: string;
75
65
  stagingBranch: string | null;
76
66
  pushed: boolean;
77
- mode: string;
78
- } | {
79
- repository: any;
80
- remoteName: string;
81
- defaultBranch: string;
82
- stagingBranch: string | null;
83
- pushed: boolean;
84
- mode?: undefined;
85
67
  };
86
68
  export declare function resolveGitRepositoryRoot(tenantRoot: any): any;
87
69
  export declare function requiredGitHubEnvironment(tenantRoot: any, { scope, purpose }?: {
@@ -99,38 +81,20 @@ export declare function renderHostedProjectWorkflow({ workingDirectory }: {
99
81
  workingDirectory: any;
100
82
  }): string;
101
83
  export declare function ensureDeployWorkflow(tenantRoot: any): {
102
- workflowPath: string;
103
- changed: boolean;
104
- workingDirectory: string;
105
- mode: string;
106
- } | {
107
84
  workingDirectory: string;
108
85
  workflowPath: string;
109
86
  changed: boolean;
110
- mode?: undefined;
111
87
  };
112
88
  export declare function ensureHostedProjectWorkflow(tenantRoot: any): {
113
- workflowPath: string;
114
- changed: boolean;
115
- workingDirectory: string;
116
- mode: string;
117
- } | {
118
89
  workingDirectory: string;
119
90
  workflowPath: string;
120
91
  changed: boolean;
121
- mode?: undefined;
122
92
  };
123
- export declare function ensureStandardizedGitHubWorkflows(tenantRoot: any): ({
124
- workflowPath: string;
125
- changed: boolean;
126
- workingDirectory: string;
127
- mode: string;
128
- } | {
93
+ export declare function ensureStandardizedGitHubWorkflows(tenantRoot: any): {
129
94
  workingDirectory: string;
130
95
  workflowPath: string;
131
96
  changed: boolean;
132
- mode?: undefined;
133
- })[];
97
+ }[];
134
98
  export declare function listGitHubSecretNames(repository: any, tenantRoot: any): Promise<Set<string>>;
135
99
  export declare function listGitHubVariableNames(repository: any, tenantRoot: any): Promise<Set<string>>;
136
100
  export declare function formatMissingSecretsReport(repository: any, missingSecrets: any, reason?: string): string;
@@ -139,9 +103,6 @@ export declare function ensureGitHubSecrets(tenantRoot: any, { dryRun }?: {
139
103
  }): Promise<{
140
104
  existing: never[];
141
105
  created: never[];
142
- } | {
143
- existing: never[];
144
- created: never[];
145
106
  } | {
146
107
  existing: string[];
147
108
  created: string[];
@@ -152,18 +113,6 @@ export declare function ensureGitHubEnvironment(tenantRoot: any, { dryRun, scope
152
113
  purpose?: string | undefined;
153
114
  valuesOverlay?: {} | undefined;
154
115
  }): Promise<{
155
- repository: string | null;
156
- secrets: {
157
- existing: never[];
158
- created: never[];
159
- };
160
- variables: {
161
- existing: never[];
162
- created: never[];
163
- };
164
- skipped: string;
165
- mode: string;
166
- } | {
167
116
  repository: null;
168
117
  secrets: {
169
118
  existing: never[];
@@ -174,7 +123,6 @@ export declare function ensureGitHubEnvironment(tenantRoot: any, { dryRun, scope
174
123
  created: never[];
175
124
  };
176
125
  skipped: string;
177
- mode?: undefined;
178
126
  } | {
179
127
  repository: string;
180
128
  secrets: {
@@ -186,7 +134,6 @@ export declare function ensureGitHubEnvironment(tenantRoot: any, { dryRun, scope
186
134
  created: string[];
187
135
  };
188
136
  skipped?: undefined;
189
- mode?: undefined;
190
137
  }>;
191
138
  export declare function ensureGitHubDeployAutomation(tenantRoot: any, { dryRun, valuesOverlay }?: {
192
139
  dryRun?: boolean | undefined;
@@ -194,33 +141,18 @@ export declare function ensureGitHubDeployAutomation(tenantRoot: any, { dryRun,
194
141
  }): Promise<{
195
142
  mode: string;
196
143
  workflow: {
197
- workflowPath: string;
198
- changed: boolean;
199
- workingDirectory: string;
200
- mode: string;
201
- } | {
202
144
  workingDirectory: string;
203
145
  workflowPath: string;
204
146
  changed: boolean;
205
- mode?: undefined;
206
147
  };
207
- workflows: ({
208
- workflowPath: string;
209
- changed: boolean;
210
- workingDirectory: string;
211
- mode: string;
212
- } | {
148
+ workflows: {
213
149
  workingDirectory: string;
214
150
  workflowPath: string;
215
151
  changed: boolean;
216
- mode?: undefined;
217
- })[];
152
+ }[];
218
153
  secrets: {
219
154
  existing: never[];
220
155
  created: never[];
221
- } | {
222
- existing: never[];
223
- created: never[];
224
156
  } | {
225
157
  existing: string[];
226
158
  created: string[];
@@ -228,26 +160,11 @@ export declare function ensureGitHubDeployAutomation(tenantRoot: any, { dryRun,
228
160
  variables: {
229
161
  existing: never[];
230
162
  created: never[];
231
- } | {
232
- existing: never[];
233
- created: never[];
234
163
  } | {
235
164
  existing: string[];
236
165
  created: string[];
237
166
  };
238
167
  environment: {
239
- repository: string | null;
240
- secrets: {
241
- existing: never[];
242
- created: never[];
243
- };
244
- variables: {
245
- existing: never[];
246
- created: never[];
247
- };
248
- skipped: string;
249
- mode: string;
250
- } | {
251
168
  repository: null;
252
169
  secrets: {
253
170
  existing: never[];
@@ -258,7 +175,6 @@ export declare function ensureGitHubDeployAutomation(tenantRoot: any, { dryRun,
258
175
  created: never[];
259
176
  };
260
177
  skipped: string;
261
- mode?: undefined;
262
178
  } | {
263
179
  repository: string;
264
180
  secrets: {
@@ -270,7 +186,6 @@ export declare function ensureGitHubDeployAutomation(tenantRoot: any, { dryRun,
270
186
  created: string[];
271
187
  };
272
188
  skipped?: undefined;
273
- mode?: undefined;
274
189
  };
275
190
  }>;
276
191
  export declare function waitForGitHubWorkflowCompletion(tenantRoot: any, { repository, workflow, headSha, branch, timeoutSeconds, pollSeconds, onProgress, }?: {
@@ -290,11 +205,4 @@ export declare function waitForGitHubWorkflowCompletion(tenantRoot: any, { repos
290
205
  url: string | null;
291
206
  jobs: import("./github-api.ts").GitHubWorkflowJobSummary[];
292
207
  failedJobs: import("./github-api.ts").GitHubWorkflowJobSummary[];
293
- } | {
294
- status: string;
295
- reason: string;
296
- repository: any;
297
- workflow: string;
298
- headSha: any;
299
- branch: any;
300
208
  }>;
@@ -22,10 +22,7 @@ function slugifySegment(value, fallback = "project") {
22
22
  return String(value ?? "").trim().toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-").slice(0, 96) || fallback;
23
23
  }
24
24
  function getGitHubAutomationMode() {
25
- return process.env.TREESEED_GITHUB_AUTOMATION_MODE === "stub" ? "stub" : "real";
26
- }
27
- function isGitHubAutomationStubbed() {
28
- return getGitHubAutomationMode() === "stub";
25
+ return "real";
29
26
  }
30
27
  function parseGitHubRepositoryFromRemote(remoteUrl) {
31
28
  if (!remoteUrl) {
@@ -170,17 +167,6 @@ async function ensureGitHubBootstrapRepository(tenantRoot, {
170
167
  const remotes = resolveGitHubRemoteUrls(target.owner, target.name);
171
168
  const slug = remotes.slug;
172
169
  onProgress?.(`[local][github][repo] Preparing ${slug} from ${target.source}...`);
173
- if (isGitHubAutomationStubbed()) {
174
- onProgress?.(`[local][github][repo] Stubbed GitHub automation; repository ${slug} not changed.`);
175
- return {
176
- repository: slug,
177
- target,
178
- created: false,
179
- remote: { changed: false, previous: null, next: remotes.sshUrl },
180
- pushed: false,
181
- mode: "stub"
182
- };
183
- }
184
170
  const client = createGitHubApiClient({
185
171
  env: {
186
172
  GH_TOKEN: configuredValue(values, "GH_TOKEN") || configuredValue(values, "GITHUB_TOKEN"),
@@ -217,14 +203,6 @@ async function ensureGitHubBootstrapRepository(tenantRoot, {
217
203
  async function createGitHubRepository(input) {
218
204
  const visibility = input.visibility ?? "private";
219
205
  const remotes = resolveGitHubRemoteUrls(input.owner, input.name);
220
- if (isGitHubAutomationStubbed()) {
221
- return {
222
- ...remotes,
223
- visibility,
224
- defaultBranch: "main",
225
- mode: "stub"
226
- };
227
- }
228
206
  return await ensureGitHubRepository({
229
207
  owner: remotes.owner,
230
208
  name: remotes.name,
@@ -243,16 +221,6 @@ function initializeGitHubRepositoryWorkingTree(cwd, repository, {
243
221
  remoteName = "origin",
244
222
  push = true
245
223
  } = {}) {
246
- if (isGitHubAutomationStubbed()) {
247
- return {
248
- repository,
249
- remoteName,
250
- defaultBranch,
251
- stagingBranch: createStaging ? "staging" : null,
252
- pushed: false,
253
- mode: "stub"
254
- };
255
- }
256
224
  runGit(["init", "-b", defaultBranch], { cwd, allowFailure: true });
257
225
  ensureGitIdentity(cwd);
258
226
  const currentRemote = runGit(["remote", "get-url", remoteName], { cwd, allowFailure: true }).stdout?.trim() ?? "";
@@ -348,14 +316,6 @@ function ensureWorkflowFile(tenantRoot, fileName, expected) {
348
316
  return { workflowPath, changed: true };
349
317
  }
350
318
  function ensureDeployWorkflow(tenantRoot) {
351
- if (isGitHubAutomationStubbed()) {
352
- return {
353
- workflowPath: resolve(tenantRoot, ".github", "workflows", "deploy.yml"),
354
- changed: false,
355
- workingDirectory: ".",
356
- mode: "stub"
357
- };
358
- }
359
319
  const repositoryRoot = resolveGitRepositoryRoot(tenantRoot);
360
320
  const workingDirectory = relative(repositoryRoot, tenantRoot).replaceAll("\\", "/") || ".";
361
321
  const expected = renderDeployWorkflow({ workingDirectory });
@@ -365,14 +325,6 @@ function ensureDeployWorkflow(tenantRoot) {
365
325
  };
366
326
  }
367
327
  function ensureHostedProjectWorkflow(tenantRoot) {
368
- if (isGitHubAutomationStubbed()) {
369
- return {
370
- workflowPath: resolve(tenantRoot, ".github", "workflows", "hosted-project.yml"),
371
- changed: false,
372
- workingDirectory: ".",
373
- mode: "stub"
374
- };
375
- }
376
328
  const repositoryRoot = resolveGitRepositoryRoot(tenantRoot);
377
329
  const workingDirectory = relative(repositoryRoot, tenantRoot).replaceAll("\\", "/") || ".";
378
330
  const expected = renderHostedProjectWorkflow({ workingDirectory });
@@ -419,21 +371,6 @@ function nonEmptyValues(values = {}) {
419
371
  );
420
372
  }
421
373
  async function ensureGitHubEnvironment(tenantRoot, { dryRun = false, scope = "prod", purpose = "save", valuesOverlay = {} } = {}) {
422
- if (isGitHubAutomationStubbed()) {
423
- return {
424
- repository: maybeResolveGitHubRepositorySlug(tenantRoot),
425
- secrets: {
426
- existing: [],
427
- created: []
428
- },
429
- variables: {
430
- existing: [],
431
- created: []
432
- },
433
- skipped: "stubbed",
434
- mode: "stub"
435
- };
436
- }
437
374
  const repository = maybeResolveGitHubRepositorySlug(tenantRoot);
438
375
  if (!repository) {
439
376
  if (dryRun) {
@@ -518,16 +455,6 @@ async function waitForGitHubWorkflowCompletion(tenantRoot, {
518
455
  pollSeconds = 5,
519
456
  onProgress
520
457
  } = {}) {
521
- if (isGitHubAutomationStubbed()) {
522
- return {
523
- status: "skipped",
524
- reason: "stubbed",
525
- repository: repository ?? maybeResolveGitHubRepositorySlug(tenantRoot),
526
- workflow,
527
- headSha: headSha ?? null,
528
- branch: branch ?? null
529
- };
530
- }
531
458
  const repo = repository ?? resolveGitHubRepositorySlug(tenantRoot);
532
459
  return await waitForGitHubWorkflowRunCompletion(repo, {
533
460
  client: createGitHubApiClient(),
@@ -4,7 +4,7 @@ import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, write
4
4
  import { tmpdir } from "node:os";
5
5
  import { dirname, join, relative, resolve } from "node:path";
6
6
  import { isTreeseedEnvironmentEntryRelevant, isTreeseedEnvironmentEntryRequired } from "../../platform/environment.js";
7
- import { getGitHubAutomationMode, maybeResolveGitHubRepositorySlug } from "./github-automation.js";
7
+ import { maybeResolveGitHubRepositorySlug } from "./github-automation.js";
8
8
  import { createGitHubApiClient, listGitHubEnvironmentSecretNames, listGitHubEnvironmentVariableNames } from "./github-api.js";
9
9
  import { collectInternalDevReferenceIssues } from "./package-reference-policy.js";
10
10
  import { collectTreeseedEnvironmentContext, resolveTreeseedMachineEnvironmentValues, validateTreeseedCommandEnvironment } from "./config-runtime.js";
@@ -123,9 +123,6 @@ function packageReadinessChecks(root, selectedPackageNames, failures) {
123
123
  if (selectedPackageNames.length === 0) {
124
124
  return { name: "package-release-readiness", status: "skipped", detail: "No packages are selected for this release." };
125
125
  }
126
- if (getGitHubAutomationMode() === "stub") {
127
- return { name: "package-release-readiness", status: "skipped", detail: "GitHub automation is stubbed." };
128
- }
129
126
  const selected = new Set(selectedPackageNames);
130
127
  const packages = workspacePackages(root).filter((pkg) => selected.has(pkg.name));
131
128
  for (const pkg of packages) {
@@ -266,8 +263,8 @@ function buildRehearsalWorkspacePackageArtifacts(root) {
266
263
  }
267
264
  }
268
265
  function runProductionDependencyRehearsal(root, plannedVersions, selectedPackageNames, failures) {
269
- if (getGitHubAutomationMode() === "stub" || process.env.TREESEED_RELEASE_CANDIDATE_REHEARSAL_MODE === "skip") {
270
- return "Skipped clean install rehearsal in stub/skip mode.";
266
+ if (process.env.TREESEED_RELEASE_CANDIDATE_REHEARSAL_MODE === "skip") {
267
+ return "Skipped clean install rehearsal by request.";
271
268
  }
272
269
  const selectedPackageSet = new Set(selectedPackageNames);
273
270
  let tempParent = null;
@@ -379,9 +376,6 @@ function localConfigCheck(root, scope, failures) {
379
376
  }
380
377
  }
381
378
  async function githubRemoteConfigCheck(root, scope, failures) {
382
- if (getGitHubAutomationMode() === "stub") {
383
- return;
384
- }
385
379
  const repository = maybeResolveGitHubRepositorySlug(root);
386
380
  if (!repository) {
387
381
  addFailure(failures, {
@@ -490,8 +484,8 @@ function providerResourceIdentifierCheck(root, scope, failures) {
490
484
  }
491
485
  }
492
486
  async function configParityChecks(root, failures) {
493
- if (getGitHubAutomationMode() === "stub") {
494
- return { name: "config-parity", status: "skipped", detail: "GitHub automation is stubbed." };
487
+ if (process.env.TREESEED_RELEASE_CANDIDATE_CONFIG_PARITY_MODE === "skip") {
488
+ return { name: "config-parity", status: "skipped", detail: "Remote config parity skipped by request." };
495
489
  }
496
490
  const before = failures.length;
497
491
  localConfigCheck(root, "staging", failures);
@@ -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
+ };