diff --git a/.github/workflows/boolean_input_validation.yml b/.github/workflows/boolean_input_validation.yml new file mode 100644 index 0000000..63946f2 --- /dev/null +++ b/.github/workflows/boolean_input_validation.yml @@ -0,0 +1,24 @@ +name: Boolean input validation + +on: + pull_request: + paths: + - action.sh + - action.yml + - tests/boolean_input_validation_test.sh + - .github/workflows/boolean_input_validation.yml + push: + branches: + - main + paths: + - action.sh + - action.yml + - tests/boolean_input_validation_test.sh + - .github/workflows/boolean_input_validation.yml + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: tests/boolean_input_validation_test.sh diff --git a/action.sh b/action.sh index ea30192..2fe8cd7 100755 --- a/action.sh +++ b/action.sh @@ -164,6 +164,29 @@ do esac done +function validate_boolean_input { + local input_name=$1 + local input_value=$2 + case "${input_value}" in + true|false) + ;; + *) + echo "Invalid ${input_name}: expected 'true' or 'false'" >&2 + exit 1 + ;; + esac +} + +function validate_boolean_inputs { + maintenance_policy_terminate=${maintenance_policy_terminate:-false} + validate_boolean_input preemptible "${preemptible}" + validate_boolean_input ephemeral "${ephemeral}" + validate_boolean_input no_external_address "${no_external_address}" + validate_boolean_input actions_preinstalled "${actions_preinstalled}" + validate_boolean_input arm "${arm}" + validate_boolean_input maintenance_policy_terminate "${maintenance_policy_terminate}" +} + function gcloud_auth { # NOTE: when --project is specified, it updates the config echo ${service_account_key} | gcloud --project ${project_id} --quiet auth activate-service-account --key-file - &>/dev/null @@ -198,7 +221,7 @@ function start_vm { network_flag=$([[ ! -z "${network}" ]] && echo "--network=${network}" || echo "") subnet_flag=$([[ ! -z "${subnet}" ]] && echo "--subnet=${subnet}" || echo "") accelerator=$([[ ! -z "${accelerator}" ]] && echo "--accelerator=${accelerator} --maintenance-policy=TERMINATE" || echo "") - maintenance_policy_flag=$([[ -z "${maintenance_policy_terminate}" ]] || echo "--maintenance-policy=TERMINATE" ) + maintenance_policy_flag=$([[ "${maintenance_policy_terminate}" == "true" ]] && echo "--maintenance-policy=TERMINATE" || echo "") echo "The new GCE VM will be ${VM_ID}" @@ -252,7 +275,7 @@ function start_vm { nohup sh -c \"sleep 3d && ${shutdown_command}\" > /dev/null & " - if $actions_preinstalled ; then + if [[ "${actions_preinstalled}" == "true" ]]; then echo "✅ Startup script won't install GitHub Actions (pre-installed)" startup_script="${startup_prelude} cd /actions-runner @@ -268,7 +291,7 @@ function start_vm { fi fi echo "✅ Startup script will install GitHub Actions v$runner_ver" - if $arm ; then + if [[ "${arm}" == "true" ]]; then startup_script="${startup_prelude} mkdir /actions-runner cd /actions-runner @@ -352,6 +375,7 @@ function start_vm { } safety_on +validate_boolean_inputs case "$command" in start) start_vm diff --git a/action.yml b/action.yml index 83e7811..7ef9c56 100644 --- a/action.yml +++ b/action.yml @@ -99,7 +99,9 @@ inputs: default: false required: false maintenance_policy_terminate: - description: "Set Google maintenance policy to TERMINATE. This is required for GPUs as they do ot support live-migration." + description: "Whether to set the Google maintenance policy to TERMINATE. This is required for GPUs because they do not support live migration." + default: "false" + required: false boot_disk_type: description: "Boot disk type for the GCE instance (https://cloud.google.com/sdk/gcloud/reference/compute/disk-types/list)" required: false @@ -117,6 +119,13 @@ runs: using: "composite" steps: - id: gce-github-runner-script + env: + INPUT_ACTIONS_PREINSTALLED: ${{ inputs.actions_preinstalled }} + INPUT_ARM: ${{ inputs.arm }} + INPUT_EPHEMERAL: ${{ inputs.ephemeral }} + INPUT_MAINTENANCE_POLICY_TERMINATE: ${{ inputs.maintenance_policy_terminate }} + INPUT_NO_EXTERNAL_ADDRESS: ${{ inputs.no_external_address }} + INPUT_PREEMPTIBLE: ${{ inputs.preemptible }} run: > ${{ github.action_path }}/action.sh --command=start @@ -138,11 +147,11 @@ runs: --image=${{ inputs.image }} --image_family=${{ inputs.image_family }} --boot_disk_type=${{ inputs.boot_disk_type }} - --preemptible=${{ inputs.preemptible }} - --ephemeral=${{ inputs.ephemeral }} - --no_external_address=${{ inputs.no_external_address }} - --actions_preinstalled=${{ inputs.actions_preinstalled }} - --maintenance_policy_terminate=${{ inputs.maintenance_policy_terminate }} - --arm=${{ inputs.arm }} + --preemptible="${INPUT_PREEMPTIBLE}" + --ephemeral="${INPUT_EPHEMERAL}" + --no_external_address="${INPUT_NO_EXTERNAL_ADDRESS}" + --actions_preinstalled="${INPUT_ACTIONS_PREINSTALLED}" + --maintenance_policy_terminate="${INPUT_MAINTENANCE_POLICY_TERMINATE}" + --arm="${INPUT_ARM}" --min_cpu_platform='${{ inputs.min_cpu_platform }}' shell: bash diff --git a/tests/boolean_input_validation_test.sh b/tests/boolean_input_validation_test.sh new file mode 100755 index 0000000..5b78055 --- /dev/null +++ b/tests/boolean_input_validation_test.sh @@ -0,0 +1,140 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +tmp_dir=$(mktemp -d) +trap 'rm -rf "$tmp_dir"' EXIT +mkdir -p "$tmp_dir/bin" + +cat > "$tmp_dir/bin/curl" <<'EOF' +#!/usr/bin/env bash +printf '{"token":"runner-token"}\n' +EOF + +cat > "$tmp_dir/bin/gcloud" <<'EOF' +#!/usr/bin/env bash +printf '%s\n' "$*" >> "${GCLOUD_LOG}" +if [[ "$*" == *"compute instances describe"* ]]; then + printf '{"labels":{"gh_ready":"1"}}\n' +fi +EOF + +cat > "$tmp_dir/bin/marker-command" <<'EOF' +#!/usr/bin/env bash +touch "${MARKER_FILE}" +EOF +chmod +x "$tmp_dir/bin/curl" "$tmp_dir/bin/gcloud" "$tmp_dir/bin/marker-command" + +run_action() { + local preemptible=$1 + local ephemeral=$2 + local no_external_address=$3 + local actions_preinstalled=$4 + local arm=$5 + local maintenance_policy_terminate=$6 + rm -f "$tmp_dir/marker" + : > "$tmp_dir/gcloud.log" + set +e + output=$( + PATH="$tmp_dir/bin:$PATH" \ + GCLOUD_LOG="$tmp_dir/gcloud.log" \ + MARKER_FILE="$tmp_dir/marker" \ + GITHUB_REPOSITORY=example/repo \ + GITHUB_REPOSITORY_OWNER=example \ + GITHUB_RUN_ID=123 \ + GITHUB_RUN_ATTEMPT=1 \ + GITHUB_OUTPUT="$tmp_dir/output" \ + "$repo_root/action.sh" \ + --command=start \ + --token=test-token \ + --project_id= \ + --service_account_key= \ + --runner_ver=2.321.0 \ + --vm_name_prefix=test-runner \ + --machine_zone=us-east1-c \ + --machine_type=n1-standard-1 \ + --scopes=cloud-platform \ + --shutdown_timeout=30 \ + --preemptible="$preemptible" \ + --ephemeral="$ephemeral" \ + --no_external_address="$no_external_address" \ + --actions_preinstalled="$actions_preinstalled" \ + --arm="$arm" \ + --maintenance_policy_terminate="$maintenance_policy_terminate" \ + 2>&1 + ) + status=$? + set -e +} + +assert_rejected() { + local input_name=$1 + if [[ $status -eq 0 ]]; then + printf 'expected %s to be rejected, got success\n%s\n' "$input_name" "$output" >&2 + if [[ -e "$tmp_dir/marker" ]]; then + printf 'marker executable ran\n' >&2 + fi + exit 1 + fi + if [[ "$output" != *"Invalid ${input_name}"* ]]; then + printf 'missing invalid-input error for %s:\n%s\n' "$input_name" "$output" >&2 + exit 1 + fi + if [[ -e "$tmp_dir/marker" ]]; then + printf 'marker executable ran for rejected %s\n' "$input_name" >&2 + exit 1 + fi + if [[ -s "$tmp_dir/gcloud.log" ]]; then + printf 'gcloud ran for rejected %s\n' "$input_name" >&2 + exit 1 + fi +} + +run_action false false false marker-command false '' +assert_rejected actions_preinstalled + +run_action sometimes false false false false '' +assert_rejected preemptible + +run_action false '' false false false '' +assert_rejected ephemeral + +run_action false false TRUE false false '' +assert_rejected no_external_address + +run_action false false false false 'false ' '' +assert_rejected arm + +run_action false false false false false enabled +assert_rejected maintenance_policy_terminate + +run_action false false false false false '' +if [[ $status -ne 0 ]]; then + printf 'valid false/empty inputs failed:\n%s\n' "$output" >&2 + exit 1 +fi +if grep -q -- '--provisioning-model=SPOT\|--ephemeral\|--no-address\|--maintenance-policy=TERMINATE' "$tmp_dir/gcloud.log"; then + printf 'false Boolean inputs unexpectedly enabled flags\n' >&2 + exit 1 +fi + +run_action true true true true true true +if [[ $status -ne 0 ]]; then + printf 'valid true inputs failed:\n%s\n' "$output" >&2 + exit 1 +fi +for expected_flag in '--provisioning-model=SPOT' '--no-address' '--maintenance-policy=TERMINATE'; do + if ! grep -q -- "$expected_flag" "$tmp_dir/gcloud.log"; then + printf 'missing flag for true input: %s\n' "$expected_flag" >&2 + exit 1 + fi +done + +for input_name in preemptible ephemeral no_external_address actions_preinstalled arm maintenance_policy_terminate; do + if grep -Fq -- "--${input_name}=\${{ inputs.${input_name} }}" "$repo_root/action.yml"; then + printf '%s is still rendered directly into shell source\n' "$input_name" >&2 + exit 1 + fi +done + +printf 'Boolean input validation tests passed\n'