edsger 0.48.0 → 0.48.1

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.
@@ -10,6 +10,7 @@
10
10
  * template_snapshot + tag and no clone error, we short-circuit without
11
11
  * re-cloning. Pass `{ force: true }` to regenerate anyway.
12
12
  */
13
+ import { type RunSheet } from '../../api/run-sheets.js';
13
14
  export interface RunSheetOptions {
14
15
  releaseId: string;
15
16
  force?: boolean;
@@ -27,6 +28,18 @@ export interface RunSheetResult {
27
28
  commitsTruncated?: boolean;
28
29
  isDraft?: boolean;
29
30
  }
31
+ /**
32
+ * Draft releases have no stored `diff_summary` / `diff_stats` (those are
33
+ * only written by smoke-test against a cut tag). Compute a lightweight
34
+ * substitute on the fly so draft templates that reference `{{diff_summary}}`
35
+ * or `{{diff_stats.*}}` still render something useful.
36
+ *
37
+ * Exported for unit tests.
38
+ */
39
+ export declare function fetchDraftDiff(owner: string, repo: string, base: string | null, head: string, token: string): Promise<{
40
+ summary: string | null;
41
+ stats: Record<string, unknown> | null;
42
+ }>;
30
43
  /**
31
44
  * Atomic-create a `.lock` file alongside the workspace dir. Returns
32
45
  * true if we acquired the lock, false if another CLI instance holds a
@@ -51,4 +64,6 @@ export declare function releaseFileLock(lockPath: string): void;
51
64
  export declare function pruneStaleRunSheetWorkspaces(workspaceRoot: string, now?: number, maxAgeMs?: number): {
52
65
  removed: string[];
53
66
  };
67
+ /** Exported for unit tests; not part of the public CLI surface. */
68
+ export declare function runSheetIsFresh(existing: RunSheet | null, template: string, tag: string): boolean;
54
69
  export declare function runRunSheet(options: RunSheetOptions): Promise<RunSheetResult>;
@@ -19,7 +19,7 @@ import { getRelease } from '../../api/releases.js';
19
19
  import { getRunSheetByRelease, upsertRunSheet, } from '../../api/run-sheets.js';
20
20
  import { logError, logInfo, logSuccess, logWarning, } from '../../utils/logger.js';
21
21
  import { cloneFeatureRepo, ensureWorkspaceDir, getFeatureRepoPath, syncRepoToRef, } from '../../workspace/workspace-manager.js';
22
- import { fetchAllCompareCommits, getDefaultBranchHead, } from '../release-sync/github.js';
22
+ import { fetchAllCompareCommits, fetchCompare, getDefaultBranchHead, summariseStats, } from '../release-sync/github.js';
23
23
  import { isSafeGitRef, renderTemplate } from './render.js';
24
24
  async function fetchCommitsBetween(owner, repo, base, head, token) {
25
25
  if (!base) {
@@ -48,6 +48,30 @@ async function fetchCommitsBetween(owner, repo, base, head, token) {
48
48
  return { text: '', list: [], truncated: false, error: message };
49
49
  }
50
50
  }
51
+ /**
52
+ * Draft releases have no stored `diff_summary` / `diff_stats` (those are
53
+ * only written by smoke-test against a cut tag). Compute a lightweight
54
+ * substitute on the fly so draft templates that reference `{{diff_summary}}`
55
+ * or `{{diff_stats.*}}` still render something useful.
56
+ *
57
+ * Exported for unit tests.
58
+ */
59
+ export async function fetchDraftDiff(owner, repo, base, head, token) {
60
+ if (!base) {
61
+ return { summary: null, stats: null };
62
+ }
63
+ try {
64
+ const compare = await fetchCompare(owner, repo, base, head, token);
65
+ const stats = summariseStats(compare);
66
+ const summary = `Draft diff from ${base} to ${head}: ${stats.total_commits} commit(s), ${stats.files_changed} file(s) changed (+${stats.additions}/-${stats.deletions}).`;
67
+ return { summary, stats: stats };
68
+ }
69
+ catch (err) {
70
+ const message = err instanceof Error ? err.message : String(err);
71
+ logWarning(`Could not compute draft diff summary: ${message}`);
72
+ return { summary: null, stats: null };
73
+ }
74
+ }
51
75
  // Stale locks (e.g. left behind by a crashed or SIGKILLed CLI) are
52
76
  // considered abandoned after this many ms.
53
77
  const LOCK_STALE_MS = 15 * 60 * 1000;
@@ -205,7 +229,8 @@ function computeInputHash(parts) {
205
229
  h.update(`commits:${parts.commitShas.join(',')}\n`);
206
230
  return h.digest('hex');
207
231
  }
208
- function runSheetIsFresh(existing, template, tag) {
232
+ /** Exported for unit tests; not part of the public CLI surface. */
233
+ export function runSheetIsFresh(existing, template, tag) {
209
234
  if (!existing || !existing.content) {
210
235
  return false;
211
236
  }
@@ -219,6 +244,12 @@ function runSheetIsFresh(existing, template, tag) {
219
244
  if (meta.clone_error) {
220
245
  return false;
221
246
  }
247
+ // Drafts are rendered from the default branch HEAD, which moves. Always
248
+ // regenerate so `{{commits}}` / `{{diff_summary}}` reflect the current
249
+ // branch tip rather than a stale snapshot.
250
+ if (meta.is_draft) {
251
+ return false;
252
+ }
222
253
  return true;
223
254
  }
224
255
  // eslint-disable-next-line complexity -- orchestration with cache, clone, lock, render
@@ -289,6 +320,10 @@ export async function runRunSheet(options) {
289
320
  // both the checkout and the commits compare.
290
321
  let isDraft = false;
291
322
  let draftBaseRef = null;
323
+ // Draft-only: live-computed diff stats/summary (DB columns are empty
324
+ // until smoke-test runs against a real tag).
325
+ let draftDiffSummary = null;
326
+ let draftDiffStats = null;
292
327
  if (repoConfigured && gh.owner && gh.repo && gh.token) {
293
328
  const { owner, repo, token } = gh;
294
329
  // Serialise concurrent CLI invocations for the *same release* by
@@ -320,24 +355,47 @@ export async function runRunSheet(options) {
320
355
  // we don't want to inherit that bug here.)
321
356
  const { repoPath } = cloneFeatureRepo(workspaceRoot, workspaceName, owner, repo, token);
322
357
  repoDir = repoPath;
323
- try {
324
- syncRepoToRef(repoPath, { tag: release.tag }, token);
325
- }
326
- catch (err) {
327
- const tagErr = err instanceof Error ? err.message : String(err);
328
- logWarning(`Tag ${release.tag} not found generating draft run sheet from default branch.`);
358
+ // SNAPSHOT releases (those discovered via `release-sync`'s snapshot
359
+ // detection) have no GitHub release object backing them, so
360
+ // `published_at` is null. Use that as a free local signal to skip
361
+ // the tag-checkout entirely — no extra GitHub round-trip, no
362
+ // misleading "Failed to checkout refs/tags/..." cloneError. For
363
+ // real releases (`published_at` set) we still attempt the tag and
364
+ // fall back if the checkout itself fails (deleted tag, network).
365
+ const expectDraft = release.published_at == null;
366
+ if (expectDraft) {
367
+ logInfo(`Tag ${release.tag} not yet cut (no published_at) — generating draft run sheet from default branch.`);
329
368
  try {
330
369
  const { branch } = await getDefaultBranchHead(owner, repo, token);
331
370
  syncRepoToRef(repoPath, { branch }, token);
332
371
  isDraft = true;
333
372
  draftBaseRef = branch;
334
- cloneError = `Tag ${release.tag} not found; fell back to default branch ${branch}. (${tagErr})`;
335
373
  }
336
374
  catch (fallbackErr) {
337
- cloneError = `Could not checkout tag ${release.tag} or fall back to default branch: ${fallbackErr instanceof Error ? fallbackErr.message : String(fallbackErr)}`;
375
+ cloneError = `Could not check out default branch for draft run sheet: ${fallbackErr instanceof Error ? fallbackErr.message : String(fallbackErr)}`;
338
376
  logWarning(cloneError);
339
377
  }
340
378
  }
379
+ else {
380
+ try {
381
+ syncRepoToRef(repoPath, { tag: release.tag }, token);
382
+ }
383
+ catch (err) {
384
+ const tagErr = err instanceof Error ? err.message : String(err);
385
+ logWarning(`Tag ${release.tag} checkout failed — falling back to default branch.`);
386
+ try {
387
+ const { branch } = await getDefaultBranchHead(owner, repo, token);
388
+ syncRepoToRef(repoPath, { branch }, token);
389
+ isDraft = true;
390
+ draftBaseRef = branch;
391
+ cloneError = `Tag ${release.tag} could not be checked out; fell back to default branch ${branch}. (${tagErr})`;
392
+ }
393
+ catch (fallbackErr) {
394
+ cloneError = `Could not checkout tag ${release.tag} or fall back to default branch: ${fallbackErr instanceof Error ? fallbackErr.message : String(fallbackErr)}`;
395
+ logWarning(cloneError);
396
+ }
397
+ }
398
+ }
341
399
  }
342
400
  catch (err) {
343
401
  cloneError = err instanceof Error ? err.message : String(err);
@@ -349,6 +407,11 @@ export async function runRunSheet(options) {
349
407
  commitsList = c.list;
350
408
  commitsTruncated = c.truncated;
351
409
  commitsError = c.error ?? null;
410
+ if (isDraft) {
411
+ const d = await fetchDraftDiff(owner, repo, release.previous_tag, head, token);
412
+ draftDiffSummary = d.summary;
413
+ draftDiffStats = d.stats;
414
+ }
352
415
  }
353
416
  else {
354
417
  cloneError = 'Product is not linked to a GitHub repository';
@@ -367,8 +430,12 @@ export async function runRunSheet(options) {
367
430
  published_at: release.published_at,
368
431
  previous_tag: release.previous_tag,
369
432
  previous_published_at: release.previous_published_at,
370
- diff_summary: release.diff_summary,
371
- diff_stats: release.diff_stats ?? {},
433
+ diff_summary: isDraft
434
+ ? (draftDiffSummary ?? release.diff_summary)
435
+ : release.diff_summary,
436
+ diff_stats: isDraft
437
+ ? (draftDiffStats ?? release.diff_stats ?? {})
438
+ : (release.diff_stats ?? {}),
372
439
  }, repoDir, commits, draftNotice, commitsList);
373
440
  const inputHash = computeInputHash({
374
441
  template,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edsger",
3
- "version": "0.48.0",
3
+ "version": "0.48.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "edsger": "dist/index.js"