From 9af7fb4e247abb48964fa87c663c45928c7bc6de Mon Sep 17 00:00:00 2001 From: Bert Belder Date: Thu, 1 Oct 2026 09:49:16 -0700 Subject: [PATCH 1/4] feat(create): accept --build-timeout with an s, m or h suffix `--build-timeout` now takes `10m` or `600s` as well as a bare number, which keeps meaning minutes so existing scripts are unaffected. The value must still come out as one of the available steps (5 to 30 minutes). --- AGENTS.md | 4 ++-- deploy/create/flow.ts | 20 ++++++++++++++++++++ deploy/create/mod.ts | 20 ++++++++++++-------- tests/build_timeout.test.ts | 13 +++++++++++++ 4 files changed, 47 insertions(+), 10 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index dc14715..784b475 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -128,8 +128,8 @@ to run non-interactively. - static: `--static-dir `, `--single-page-app` - `--framework-preset `, `--install-command`, `--build-command`, `--pre-deploy-command`, `--app-directory ` -- `--region `, `--build-timeout `, - `--build-memory-limit ` +- `--region `, `--build-timeout ` (minutes, or e.g. + `10m`/`600s`), `--build-memory-limit ` ### `env` diff --git a/deploy/create/flow.ts b/deploy/create/flow.ts index e707e33..56e2a78 100644 --- a/deploy/create/flow.ts +++ b/deploy/create/flow.ts @@ -22,6 +22,26 @@ export const AVAILABLE_BUILD_TIMEOUTS = [5, 10, 15, 20, 25, 30]; export const AVAILABLE_BUILD_MEMORY_LIMITS = [1024, 2048, 3072, 4096]; export const REGIONS = ["us", "eu", "global"]; +const DURATION_UNIT_MINUTES: Record = { + s: 1 / 60, + m: 1, + h: 60, +}; + +/** + * Parses `--build-timeout`: a whole number of minutes (`10`, as the flag has + * always taken), or a whole number with an `s`, `m` or `h` suffix (`"600s"`, + * `"10m"`). Returns the timeout in minutes if it is one of the available + * steps, and null otherwise. + */ +export function parseBuildTimeoutFlag(value: string): number | null { + const match = /^([1-9][0-9]*)([smh]?)$/.exec(value); + if (match === null) return null; + const minutes = Number(match[1]) * + (match[2] ? DURATION_UNIT_MINUTES[match[2]] : 1); + return AVAILABLE_BUILD_TIMEOUTS.includes(minutes) ? minutes : null; +} + /** * The build timeout a detected build config asks for (deno.json * `deploy.buildTimeout`), as the largest available step not above it or diff --git a/deploy/create/mod.ts b/deploy/create/mod.ts index a9fd3ed..8425738 100644 --- a/deploy/create/mod.ts +++ b/deploy/create/mod.ts @@ -6,6 +6,7 @@ import { AVAILABLE_BUILD_TIMEOUTS, createFlow, detectedBuildTimeoutForOrg, + parseBuildTimeoutFlag, REGIONS, renderBuildConfig, } from "./flow.ts"; @@ -126,21 +127,24 @@ export const createCommand = new Command() "For static configuration", ) .option( - "--build-timeout ", - `The build timeout in minutes. One of ${ + "--build-timeout ", + `The build timeout, as minutes or with an s, m or h suffix (e.g. 10, 10m, 600s). One of ${ AVAILABLE_BUILD_TIMEOUTS.join(", ") - }. Defaults to deploy.buildTimeout from a detected deno.json, or ${ + } minutes. Defaults to deploy.buildTimeout from a detected deno.json, or ${ AVAILABLE_BUILD_TIMEOUTS[0] - }`, + } minutes`, { - value(value: number) { - if (AVAILABLE_BUILD_TIMEOUTS.includes(value)) { - return value; + value(value: string) { + const minutes = parseBuildTimeoutFlag(value); + if (minutes !== null) { + return minutes; } else { throw new ValidationError( `Invalid build timeout: ${value}. Valid values are ${ AVAILABLE_BUILD_TIMEOUTS.join(", ") - }.`, + } minutes, e.g. ${AVAILABLE_BUILD_TIMEOUTS[1]} or ${ + AVAILABLE_BUILD_TIMEOUTS[1] + }m.`, ); } }, diff --git a/tests/build_timeout.test.ts b/tests/build_timeout.test.ts index c60ebee..f423148 100644 --- a/tests/build_timeout.test.ts +++ b/tests/build_timeout.test.ts @@ -3,6 +3,7 @@ import type { TRPCClient } from "../auth.ts"; import { detectedBuildTimeout, detectedBuildTimeoutForOrg, + parseBuildTimeoutFlag, } from "../deploy/create/flow.ts"; Deno.test("detectedBuildTimeout: undefined without a deno.json build timeout", () => { @@ -87,3 +88,15 @@ Deno.test("detectedBuildTimeoutForOrg: leaves an unknown org uncapped for the co 20, ); }); + +Deno.test("parseBuildTimeoutFlag: minutes, or a duration with a unit suffix", () => { + assertEquals(parseBuildTimeoutFlag("10"), 10); + assertEquals(parseBuildTimeoutFlag("10m"), 10); + assertEquals(parseBuildTimeoutFlag("600s"), 10); + assertEquals(parseBuildTimeoutFlag("30m"), 30); + for ( + const value of ["7", "90s", "1h", "0", "10min", "1.5m", "", "-5", "05"] + ) { + assertEquals(parseBuildTimeoutFlag(value), null, value); + } +}); From 6686488bdba1d5f3b1e3b7497a82826916b88d1a Mon Sep 17 00:00:00 2001 From: Bert Belder Date: Thu, 1 Oct 2026 09:52:23 -0700 Subject: [PATCH 2/4] fix(create): keep converting a unitless --build-timeout as a number --- deploy/create/flow.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/deploy/create/flow.ts b/deploy/create/flow.ts index 56e2a78..c8a6190 100644 --- a/deploy/create/flow.ts +++ b/deploy/create/flow.ts @@ -35,10 +35,12 @@ const DURATION_UNIT_MINUTES: Record = { * steps, and null otherwise. */ export function parseBuildTimeoutFlag(value: string): number | null { - const match = /^([1-9][0-9]*)([smh]?)$/.exec(value); - if (match === null) return null; - const minutes = Number(match[1]) * - (match[2] ? DURATION_UNIT_MINUTES[match[2]] : 1); + const match = /^([1-9][0-9]*)([smh])$/.exec(value); + // Without a unit, convert as the flag's former numeric type did, so that + // spellings such as `10.0` keep working. + const minutes = match === null + ? Number(value) + : Number(match[1]) * DURATION_UNIT_MINUTES[match[2]]; return AVAILABLE_BUILD_TIMEOUTS.includes(minutes) ? minutes : null; } From 346fd633f41b4316a2df6801e538d7d82df333f9 Mon Sep 17 00:00:00 2001 From: Bert Belder Date: Thu, 1 Oct 2026 09:55:05 -0700 Subject: [PATCH 3/4] test(create): expect unitless --build-timeout spellings to convert as numbers --- tests/build_timeout.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/build_timeout.test.ts b/tests/build_timeout.test.ts index f423148..384c93c 100644 --- a/tests/build_timeout.test.ts +++ b/tests/build_timeout.test.ts @@ -94,8 +94,11 @@ Deno.test("parseBuildTimeoutFlag: minutes, or a duration with a unit suffix", () assertEquals(parseBuildTimeoutFlag("10m"), 10); assertEquals(parseBuildTimeoutFlag("600s"), 10); assertEquals(parseBuildTimeoutFlag("30m"), 30); + // A unitless value converts like the flag's former numeric type. + assertEquals(parseBuildTimeoutFlag("10.0"), 10); + assertEquals(parseBuildTimeoutFlag("05"), 5); for ( - const value of ["7", "90s", "1h", "0", "10min", "1.5m", "", "-5", "05"] + const value of ["7", "90s", "1h", "0", "10min", "1.5m", "", "-5", "05m"] ) { assertEquals(parseBuildTimeoutFlag(value), null, value); } From ad2222951d0acdf199f8a62accdc86cfc6f2b83f Mon Sep 17 00:00:00 2001 From: Bert Belder Date: Thu, 1 Oct 2026 09:57:11 -0700 Subject: [PATCH 4/4] docs(create): don't advertise an hour suffix for --build-timeout --- deploy/create/flow.ts | 6 +++--- deploy/create/mod.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/deploy/create/flow.ts b/deploy/create/flow.ts index c8a6190..612dbd1 100644 --- a/deploy/create/flow.ts +++ b/deploy/create/flow.ts @@ -29,10 +29,10 @@ const DURATION_UNIT_MINUTES: Record = { }; /** - * Parses `--build-timeout`: a whole number of minutes (`10`, as the flag has - * always taken), or a whole number with an `s`, `m` or `h` suffix (`"600s"`, + * Parses `--build-timeout`: a number of minutes (`10`, as the flag has always + * taken), or a whole number with a unit suffix as in deno.json (`"600s"`, * `"10m"`). Returns the timeout in minutes if it is one of the available - * steps, and null otherwise. + * steps (5 to 30 minutes), and null otherwise. */ export function parseBuildTimeoutFlag(value: string): number | null { const match = /^([1-9][0-9]*)([smh])$/.exec(value); diff --git a/deploy/create/mod.ts b/deploy/create/mod.ts index 8425738..54e8216 100644 --- a/deploy/create/mod.ts +++ b/deploy/create/mod.ts @@ -128,7 +128,7 @@ export const createCommand = new Command() ) .option( "--build-timeout ", - `The build timeout, as minutes or with an s, m or h suffix (e.g. 10, 10m, 600s). One of ${ + `The build timeout, as minutes or with a unit suffix (e.g. 10, 10m, 600s). One of ${ AVAILABLE_BUILD_TIMEOUTS.join(", ") } minutes. Defaults to deploy.buildTimeout from a detected deno.json, or ${ AVAILABLE_BUILD_TIMEOUTS[0]