Skip to content

Commit 85757a4

Browse files
authored
Add --timeout flag to abort long-running operations (#15)
Large repositories with many commits could cause the CLI to hang indefinitely. This adds a --timeout flag (default 60s) that aborts with a clear error message if exceeded, preventing CI jobs from running forever and helping users identify performance issues. Co-authored-by: Tom Moor <380914+tommoor@users.noreply.github.com>
1 parent c2bdb69 commit 85757a4

3 files changed

Lines changed: 58 additions & 4 deletions

File tree

src/args.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,4 +98,31 @@ describe("parseCLIArgs", () => {
9898
const result = parseCLIArgs(["sync", "--name", "Release 1.2.0"]);
9999
expect(getCLIWarnings(result)).toEqual([]);
100100
});
101+
102+
it("defaults --timeout to 60 seconds", () => {
103+
const result = parseCLIArgs([]);
104+
expect(result.timeoutSeconds).toBe(60);
105+
});
106+
107+
it("parses --timeout with space syntax", () => {
108+
const result = parseCLIArgs(["--timeout", "120"]);
109+
expect(result.timeoutSeconds).toBe(120);
110+
});
111+
112+
it("parses --timeout with = syntax", () => {
113+
const result = parseCLIArgs(["--timeout=30"]);
114+
expect(result.timeoutSeconds).toBe(30);
115+
});
116+
117+
it("throws on non-numeric --timeout", () => {
118+
expect(() => parseCLIArgs(["--timeout", "abc"])).toThrow('Invalid --timeout value: "abc"');
119+
});
120+
121+
it("throws on zero --timeout", () => {
122+
expect(() => parseCLIArgs(["--timeout", "0"])).toThrow('Invalid --timeout value: "0"');
123+
});
124+
125+
it("throws on negative --timeout", () => {
126+
expect(() => parseCLIArgs(["--timeout=-5"])).toThrow('Invalid --timeout value: "-5"');
127+
});
101128
});

src/args.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export type ParsedCLIArgs = {
77
stageName?: string;
88
includePaths: string[];
99
jsonOutput: boolean;
10+
timeoutSeconds: number;
1011
};
1112

1213
export function parseCLIArgs(argv: string[]): ParsedCLIArgs {
@@ -18,11 +19,22 @@ export function parseCLIArgs(argv: string[]): ParsedCLIArgs {
1819
stage: { type: "string" },
1920
"include-paths": { type: "string" },
2021
json: { type: "boolean", default: false },
22+
timeout: { type: "string" },
2123
},
2224
allowPositionals: true,
2325
strict: true,
2426
});
2527

28+
const DEFAULT_TIMEOUT_SECONDS = 60;
29+
let timeoutSeconds = DEFAULT_TIMEOUT_SECONDS;
30+
if (values.timeout !== undefined) {
31+
const parsed = Number(values.timeout);
32+
if (Number.isNaN(parsed) || parsed <= 0) {
33+
throw new Error(`Invalid --timeout value: "${values.timeout}". Must be a positive number of seconds.`);
34+
}
35+
timeoutSeconds = parsed;
36+
}
37+
2638
return {
2739
command: positionals[0] || "sync",
2840
releaseName: values.name,
@@ -35,6 +47,7 @@ export function parseCLIArgs(argv: string[]): ParsedCLIArgs {
3547
.filter((p) => p.length > 0)
3648
: [],
3749
jsonOutput: values.json ?? false,
50+
timeoutSeconds,
3851
};
3952
}
4053

src/index.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ Options:
4545
--release-version=<version> Release version identifier
4646
--stage=<stage> Deployment stage (required for update)
4747
--include-paths=<paths> Filter commits by file paths (comma-separated globs)
48+
--timeout=<seconds> Abort if the operation exceeds this duration (default: 60)
4849
--json Output result as JSON
4950
-v, --version Show version number
5051
-h, --help Show this help message
@@ -77,7 +78,7 @@ try {
7778
console.error("Run linear-release --help for usage information.");
7879
process.exit(1);
7980
}
80-
const { command, releaseName, releaseVersion, stageName, includePaths, jsonOutput } = parsedArgs;
81+
const { command, releaseName, releaseVersion, stageName, includePaths, jsonOutput, timeoutSeconds } = parsedArgs;
8182
const cliWarnings = getCLIWarnings(parsedArgs);
8283
if (jsonOutput) {
8384
setStderr(true);
@@ -602,7 +603,20 @@ async function main() {
602603
}
603604
}
604605

605-
main().catch((error) => {
606-
console.error(`Error: ${error.message}`);
606+
const timeoutMs = timeoutSeconds * 1000;
607+
const timeout = setTimeout(() => {
608+
console.error(
609+
`Error: Operation timed out after ${timeoutSeconds}s. This may indicate a large repository or slow network. Use --timeout=<seconds> to increase the limit.`,
610+
);
607611
process.exit(1);
608-
});
612+
}, timeoutMs);
613+
timeout.unref();
614+
615+
main()
616+
.catch((error) => {
617+
console.error(`Error: ${error.message}`);
618+
process.exit(1);
619+
})
620+
.finally(() => {
621+
clearTimeout(timeout);
622+
});

0 commit comments

Comments
 (0)