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
|
-
|
|
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
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
|
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:
|
|
371
|
-
|
|
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,
|