diff --git a/src/args.test.ts b/src/args.test.ts index 874a8d7..303b345 100644 --- a/src/args.test.ts +++ b/src/args.test.ts @@ -98,4 +98,31 @@ describe("parseCLIArgs", () => { const result = parseCLIArgs(["sync", "--name", "Release 1.2.0"]); expect(getCLIWarnings(result)).toEqual([]); }); + + it("defaults --timeout to 60 seconds", () => { + const result = parseCLIArgs([]); + expect(result.timeoutSeconds).toBe(60); + }); + + it("parses --timeout with space syntax", () => { + const result = parseCLIArgs(["--timeout", "120"]); + expect(result.timeoutSeconds).toBe(120); + }); + + it("parses --timeout with = syntax", () => { + const result = parseCLIArgs(["--timeout=30"]); + expect(result.timeoutSeconds).toBe(30); + }); + + it("throws on non-numeric --timeout", () => { + expect(() => parseCLIArgs(["--timeout", "abc"])).toThrow('Invalid --timeout value: "abc"'); + }); + + it("throws on zero --timeout", () => { + expect(() => parseCLIArgs(["--timeout", "0"])).toThrow('Invalid --timeout value: "0"'); + }); + + it("throws on negative --timeout", () => { + expect(() => parseCLIArgs(["--timeout=-5"])).toThrow('Invalid --timeout value: "-5"'); + }); }); diff --git a/src/args.ts b/src/args.ts index 224a89a..958f0e9 100644 --- a/src/args.ts +++ b/src/args.ts @@ -7,6 +7,7 @@ export type ParsedCLIArgs = { stageName?: string; includePaths: string[]; jsonOutput: boolean; + timeoutSeconds: number; }; export function parseCLIArgs(argv: string[]): ParsedCLIArgs { @@ -18,11 +19,22 @@ export function parseCLIArgs(argv: string[]): ParsedCLIArgs { stage: { type: "string" }, "include-paths": { type: "string" }, json: { type: "boolean", default: false }, + timeout: { type: "string" }, }, allowPositionals: true, strict: true, }); + const DEFAULT_TIMEOUT_SECONDS = 60; + let timeoutSeconds = DEFAULT_TIMEOUT_SECONDS; + if (values.timeout !== undefined) { + const parsed = Number(values.timeout); + if (Number.isNaN(parsed) || parsed <= 0) { + throw new Error(`Invalid --timeout value: "${values.timeout}". Must be a positive number of seconds.`); + } + timeoutSeconds = parsed; + } + return { command: positionals[0] || "sync", releaseName: values.name, @@ -35,6 +47,7 @@ export function parseCLIArgs(argv: string[]): ParsedCLIArgs { .filter((p) => p.length > 0) : [], jsonOutput: values.json ?? false, + timeoutSeconds, }; } diff --git a/src/index.ts b/src/index.ts index a78fb76..cb7c71e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -45,6 +45,7 @@ Options: --release-version= Release version identifier --stage= Deployment stage (required for update) --include-paths= Filter commits by file paths (comma-separated globs) + --timeout= Abort if the operation exceeds this duration (default: 60) --json Output result as JSON -v, --version Show version number -h, --help Show this help message @@ -77,7 +78,7 @@ try { console.error("Run linear-release --help for usage information."); process.exit(1); } -const { command, releaseName, releaseVersion, stageName, includePaths, jsonOutput } = parsedArgs; +const { command, releaseName, releaseVersion, stageName, includePaths, jsonOutput, timeoutSeconds } = parsedArgs; const cliWarnings = getCLIWarnings(parsedArgs); if (jsonOutput) { setStderr(true); @@ -602,7 +603,20 @@ async function main() { } } -main().catch((error) => { - console.error(`Error: ${error.message}`); +const timeoutMs = timeoutSeconds * 1000; +const timeout = setTimeout(() => { + console.error( + `Error: Operation timed out after ${timeoutSeconds}s. This may indicate a large repository or slow network. Use --timeout= to increase the limit.`, + ); process.exit(1); -}); +}, timeoutMs); +timeout.unref(); + +main() + .catch((error) => { + console.error(`Error: ${error.message}`); + process.exit(1); + }) + .finally(() => { + clearTimeout(timeout); + });