Skip to content

Commit bc361d8

Browse files
feat: warn on timestamp-like branch names, include rebase abort error in PR comment
Co-Authored-By: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com>
1 parent c156b63 commit bc361d8

3 files changed

Lines changed: 126 additions & 1 deletion

File tree

dist/index.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39329,6 +39329,7 @@ async function run() {
3932939329
if (!token) {
3933039330
throw new Error("GitHub token is required. Please provide a token with appropriate permissions.");
3933139331
}
39332+
warnIfDynamicBranchName(branch);
3933239333
if (updateFromSource) {
3933339334
await updateFromSourceSpec(token, branch, autoMerge);
3933439335
}
@@ -39519,6 +39520,28 @@ async function syncChanges(options) {
3951939520
throw new Error(`Failed to sync changes: ${error instanceof Error ? error.message : "Unknown error"}`);
3952039521
}
3952139522
}
39523+
// Warn if the branch name appears to contain a dynamic/timestamp component,
39524+
// which would prevent PR deduplication from working.
39525+
function warnIfDynamicBranchName(branch) {
39526+
// Match common timestamp patterns in branch names:
39527+
// - ISO-like dates: 2026-02-25, 2026-02-25T00-27-08
39528+
// - Unix timestamps: 1772467782 (10+ digits)
39529+
// - Date segments: 20260225, 202602
39530+
const timestampPatterns = [
39531+
/\d{4}-\d{2}-\d{2}/, // ISO date: 2026-02-25
39532+
/\d{4}-\d{2}-\d{2}T\d{2}/, // ISO datetime: 2026-02-25T00
39533+
/\d{10,}/, // Unix timestamp: 1772467782
39534+
/\d{8,}/, // Compact date: 20260225
39535+
];
39536+
for (const pattern of timestampPatterns) {
39537+
if (pattern.test(branch)) {
39538+
core.warning(`Branch name '${branch}' appears to contain a timestamp or date. ` +
39539+
`Using dynamic branch names means each run creates a new branch and a new PR. ` +
39540+
`For PR deduplication to work, use a stable branch name (e.g., 'update-api').`);
39541+
return;
39542+
}
39543+
}
39544+
}
3952239545
async function branchExists(owner, repo, branchName, octokit) {
3952339546
try {
3952439547
await octokit.rest.git.getRef({

src/sync.test.ts

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
44
const state = {
55
infoCalls: [] as string[],
66
setFailedCalls: [] as string[],
7+
warningCalls: [] as string[],
78
execCalls: [] as [string, string[] | undefined, unknown][], // [cmd, args, opts]
89
getInputImpl: (_name: string): string => "",
910
getBooleanInputImpl: (_name: string): boolean => false,
@@ -42,6 +43,9 @@ vi.mock("@actions/core", () => ({
4243
setFailed: vi.fn((msg: string) => {
4344
state.setFailedCalls.push(msg);
4445
}),
46+
warning: vi.fn((msg: string) => {
47+
state.warningCalls.push(msg);
48+
}),
4549
}));
4650

4751
vi.mock("@actions/github", () => ({
@@ -84,6 +88,7 @@ function setupMocks({
8488
// Reset shared state
8589
state.infoCalls = [];
8690
state.setFailedCalls = [];
91+
state.warningCalls = [];
8792
state.execCalls = [];
8893

8994
// Setup input implementations
@@ -153,6 +158,7 @@ beforeEach(() => {
153158
vi.clearAllMocks();
154159
state.infoCalls = [];
155160
state.setFailedCalls = [];
161+
state.warningCalls = [];
156162
state.execCalls = [];
157163
});
158164

@@ -323,10 +329,12 @@ describe("updateFromSourceSpec", () => {
323329
}),
324330
);
325331

326-
// Comment body should mention sync failed
332+
// Comment body should mention sync failed and include error details
327333
const commentCall = mockIssuesCreateComment.mock.calls[0][0];
328334
expect(commentCall.body).toContain("Sync failed");
329335
expect(commentCall.body).toContain("merge conflicts");
336+
expect(commentCall.body).toContain("Rebase error:");
337+
expect(commentCall.body).toContain("merge conflict");
330338

331339
// Action should still fail (not silently succeed)
332340
expect(state.setFailedCalls).toEqual(
@@ -362,6 +370,72 @@ describe("updateFromSourceSpec", () => {
362370
]),
363371
);
364372
});
373+
374+
it("should include rebase abort error in PR comment when abort fails", async () => {
375+
setupMocks({ hasChanges: true, existingPRNumber: 42 });
376+
state.execImpl = async (
377+
cmd: string,
378+
args?: string[],
379+
): Promise<number> => {
380+
if (
381+
cmd === "git" &&
382+
Array.isArray(args) &&
383+
(args.includes("push") ||
384+
(args.includes("pull") && args.includes("--rebase")))
385+
) {
386+
throw new Error("merge conflict");
387+
}
388+
if (
389+
cmd === "git" &&
390+
Array.isArray(args) &&
391+
args.includes("rebase") &&
392+
args.includes("--abort")
393+
) {
394+
throw new Error("no rebase in progress");
395+
}
396+
return 0;
397+
};
398+
await importAndRun();
399+
400+
const commentCall = mockIssuesCreateComment.mock.calls[0][0];
401+
expect(commentCall.body).toContain("Rebase error:");
402+
expect(commentCall.body).toContain("merge conflict");
403+
expect(commentCall.body).toContain("Rebase abort error:");
404+
expect(commentCall.body).toContain("no rebase in progress");
405+
});
406+
});
407+
408+
describe("dynamic branch name warning", () => {
409+
it("should warn when branch name contains an ISO date", async () => {
410+
setupMocks({ hasChanges: false });
411+
state.getInputImpl = (name: string): string => {
412+
const inputs: Record<string, string> = {
413+
token: "fake-token",
414+
branch: "update-openapi-spec-2026-02-25T00-27-08-474Z",
415+
auto_merge: "false",
416+
update_from_source: "true",
417+
};
418+
return inputs[name] || "";
419+
};
420+
await importAndRun();
421+
422+
expect(state.warningCalls).toEqual(
423+
expect.arrayContaining([
424+
expect.stringContaining("appears to contain a timestamp"),
425+
]),
426+
);
427+
});
428+
429+
it("should not warn for a stable branch name", async () => {
430+
setupMocks({ hasChanges: false });
431+
await importAndRun();
432+
433+
expect(state.warningCalls).not.toEqual(
434+
expect.arrayContaining([
435+
expect.stringContaining("appears to contain a timestamp"),
436+
]),
437+
);
438+
});
365439
});
366440

367441
describe("when auto_merge is true", () => {

src/sync.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ export async function run(): Promise<void> {
3636
);
3737
}
3838

39+
warnIfDynamicBranchName(branch);
40+
3941
if (updateFromSource) {
4042
await updateFromSourceSpec(token, branch, autoMerge);
4143
} else {
@@ -340,6 +342,32 @@ async function syncChanges(options: SyncOptions): Promise<void> {
340342
}
341343
}
342344

345+
// Warn if the branch name appears to contain a dynamic/timestamp component,
346+
// which would prevent PR deduplication from working.
347+
function warnIfDynamicBranchName(branch: string): void {
348+
// Match common timestamp patterns in branch names:
349+
// - ISO-like dates: 2026-02-25, 2026-02-25T00-27-08
350+
// - Unix timestamps: 1772467782 (10+ digits)
351+
// - Date segments: 20260225, 202602
352+
const timestampPatterns = [
353+
/\d{4}-\d{2}-\d{2}/, // ISO date: 2026-02-25
354+
/\d{4}-\d{2}-\d{2}T\d{2}/, // ISO datetime: 2026-02-25T00
355+
/\d{10,}/, // Unix timestamp: 1772467782
356+
/\d{8,}/, // Compact date: 20260225
357+
];
358+
359+
for (const pattern of timestampPatterns) {
360+
if (pattern.test(branch)) {
361+
core.warning(
362+
`Branch name '${branch}' appears to contain a timestamp or date. ` +
363+
`Using dynamic branch names means each run creates a new branch and a new PR. ` +
364+
`For PR deduplication to work, use a stable branch name (e.g., 'update-api').`,
365+
);
366+
return;
367+
}
368+
}
369+
}
370+
343371
async function branchExists(
344372
owner: string,
345373
repo: string,

0 commit comments

Comments
 (0)