diff --git a/.github/workflows/build-push.yaml b/.github/workflows/build-push.yaml index 0d3ed99..4443a0f 100644 --- a/.github/workflows/build-push.yaml +++ b/.github/workflows/build-push.yaml @@ -35,6 +35,11 @@ on: type: string default: "" description: "Additional non-secret, newline-separated Docker build arguments" + build-timeout-minutes: + required: false + type: number + default: 30 + description: "Bounded timeout for each native image build; use 5 through 120 minutes" runners: required: false type: string @@ -102,7 +107,7 @@ jobs: - ubuntu-24.04-arm name: build-${{ matrix.runner }} runs-on: ${{ matrix.runner }} - timeout-minutes: 30 + timeout-minutes: ${{ inputs.build-timeout-minutes }} outputs: image-tag: ${{ steps.record-tag.outputs.tag }} steps: @@ -112,6 +117,7 @@ jobs: ADDITIONAL_GAR_REGISTRY: ${{ inputs.additional-gar-registry }} ADDITIONAL_IMAGE_NAMES: ${{ inputs.additional-image-names }} BUILD_CONTEXT: ${{ inputs.context }} + BUILD_TIMEOUT_MINUTES: ${{ inputs.build-timeout-minutes }} CALLER_REF: ${{ inputs.ref }} CERTIFICATE_IDENTITY: ${{ inputs.certificate-identity }} DOCKER_FILE: ${{ inputs.file }} @@ -138,6 +144,11 @@ jobs: echo "The runners compatibility input must remain the fixed LibOps hosted-runner pair" >&2 exit 1 fi + if [[ ! "$BUILD_TIMEOUT_MINUTES" =~ ^[0-9]+$ ]] || + ((BUILD_TIMEOUT_MINUTES < 5 || BUILD_TIMEOUT_MINUTES > 120)); then + echo "Native build timeout must be an integer from 5 through 120 minutes" >&2 + exit 1 + fi validate_repo_path() { local value="$1" local label="$2" diff --git a/ci/github/build_push_contract_test.go b/ci/github/build_push_contract_test.go index 6e0c1a7..1fc6220 100644 --- a/ci/github/build_push_contract_test.go +++ b/ci/github/build_push_contract_test.go @@ -115,6 +115,24 @@ func TestPublisherRunnersCannotBeCallerControlled(t *testing.T) { } } +func TestNativeBuildTimeoutIsBoundedCallerInput(t *testing.T) { + workflow := workflowSource(t) + inputs := stepBlock(t, workflow, " inputs:\n", " secrets:\n") + buildHeader := stepBlock(t, workflow, "jobs:\n build:\n", " outputs:\n") + validation := stepBlock(t, workflow, " - name: validate input\n", " - uses: actions/checkout@") + + for _, required := range []string{ + "build-timeout-minutes:\n", + "type: number", + "default: 30", + } { + requireContains(t, inputs, required) + } + requireContains(t, buildHeader, "timeout-minutes: ${{ inputs.build-timeout-minutes }}") + requireContains(t, validation, "BUILD_TIMEOUT_MINUTES: ${{ inputs.build-timeout-minutes }}") + requireContains(t, validation, "BUILD_TIMEOUT_MINUTES < 5 || BUILD_TIMEOUT_MINUTES > 120") +} + func TestDefaultImageNameReceivesCanonicalValidation(t *testing.T) { workflow := workflowSource(t) validation := stepBlock(t, workflow, " - name: validate input\n", " - uses: actions/checkout@")