diff --git a/.github/ISSUE_TEMPLATE/06_agent_handoff.yml b/.github/ISSUE_TEMPLATE/06_agent_handoff.yml index 7f0c129e0..50260743e 100644 --- a/.github/ISSUE_TEMPLATE/06_agent_handoff.yml +++ b/.github/ISSUE_TEMPLATE/06_agent_handoff.yml @@ -52,7 +52,7 @@ body: label: Verification description: Commands, checks, CI runs, or review performed. Say "Not run" if nothing was run. placeholder: | - - trunk check --no-fix: passed + - scripts/trunk-safe-check.sh -- check --no-fix: passed - CI: pending validations: required: true diff --git a/.github/workflows/trunk-safe-check-test.yml b/.github/workflows/trunk-safe-check-test.yml new file mode 100644 index 000000000..f7cde1d48 --- /dev/null +++ b/.github/workflows/trunk-safe-check-test.yml @@ -0,0 +1,50 @@ +--- +name: Trunk Safe Check Tests + +on: + push: + paths: + - "scripts/trunk-safe-check.sh" + - "scripts/trunk-safe-ci.sh" + - "scripts/test-trunk-safe-check.sh" + - "scripts/test-trunk-safe-ci.sh" + - ".github/workflows/trunk.yml" + - ".github/workflows/trunk-safe-check-test.yml" + pull_request: + paths: + - "scripts/trunk-safe-check.sh" + - "scripts/trunk-safe-ci.sh" + - "scripts/test-trunk-safe-check.sh" + - "scripts/test-trunk-safe-ci.sh" + - ".github/workflows/trunk.yml" + - ".github/workflows/trunk-safe-check-test.yml" + workflow_dispatch: {} + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + trunk-safe-check-test: + name: Trunk Safe Check Tests + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + + - name: Check shell syntax + shell: bash + run: | + sh -n scripts/trunk-safe-check.sh scripts/test-trunk-safe-check.sh + bash -n scripts/trunk-safe-ci.sh scripts/test-trunk-safe-ci.sh + + - name: Test isolated Trunk execution + run: sh scripts/test-trunk-safe-check.sh + + - name: Test CI check-mode translation + run: bash scripts/test-trunk-safe-ci.sh diff --git a/.github/workflows/trunk.yml b/.github/workflows/trunk.yml index b8731197f..207d82c4a 100644 --- a/.github/workflows/trunk.yml +++ b/.github/workflows/trunk.yml @@ -16,20 +16,22 @@ on: type: string default: "" cache: - description: Cache Trunk downloads and linter artifacts on ephemeral runners. + description: > + Deprecated compatibility input. Safe Trunk runtime state is always + disposable and is not cached between jobs. required: false type: boolean default: true check-mode: description: > - Trunk check mode. Leave empty to let trunk-action infer the correct + Trunk check mode. Leave empty to infer the correct mode for pull_request, push, schedule, and merge queue events. Set to 'all' only when the caller intentionally wants a full-repository scan. required: false type: string default: "" debug: - description: Enable trunk-action debug mode. + description: Report the selected safe check mode without printing environment data. required: false type: boolean default: false @@ -49,7 +51,7 @@ on: type: string default: "" setup-deps: - description: Let trunk-action run .trunk/setup-ci dependency setup. + description: Run the caller repository's .trunk/setup-ci action when present. required: false type: boolean default: false @@ -124,28 +126,84 @@ jobs: name: Trunk Code Quality runs-on: ${{ inputs['runs-on'] }} permissions: - actions: read - checks: write contents: read steps: - name: Check out repository uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 0 + persist-credentials: false - name: Prepare repository dependencies if: ${{ inputs['setup-command'] != '' || inputs.get_dependencies != '' }} shell: bash run: ${{ inputs['setup-command'] != '' && inputs['setup-command'] || inputs.get_dependencies }} - - name: Run Trunk Code Quality - uses: trunk-io/trunk-action@04ba50e7658c81db7356da96657e6e77f220bfa3 # v1.3.1 + # GitHub exposes the called workflow identity through job.workflow_*. + # Serialize the context because actionlint 1.7.12 does not model those + # current properties yet, then validate the values before checkout. + - name: Resolve called workflow source + id: workflow-source + shell: bash + env: + JOB_CONTEXT: ${{ toJSON(job) }} + run: | + repository=$(jq -r '.workflow_repository // empty' <<<"$JOB_CONTEXT") + sha=$(jq -r '.workflow_sha // empty' <<<"$JOB_CONTEXT") + [[ $repository =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]] + [[ $sha =~ ^[0-9a-f]{40}$ ]] + printf 'repository=%s\n' "$repository" >>"$GITHUB_OUTPUT" + printf 'sha=%s\n' "$sha" >>"$GITHUB_OUTPUT" + + - name: Check out safe Trunk tooling + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + repository: ${{ steps.workflow-source.outputs.repository }} + ref: ${{ steps.workflow-source.outputs.sha }} + path: .z-shell-workflow + persist-credentials: false + sparse-checkout: | + scripts/trunk-safe-check.sh + scripts/trunk-safe-ci.sh + sparse-checkout-cone-mode: false + + - name: Install safe Trunk tooling + shell: bash + run: | + install -d -m 700 "$RUNNER_TEMP/z-shell-trunk-safe" + install -m 700 .z-shell-workflow/scripts/trunk-safe-check.sh \ + "$RUNNER_TEMP/z-shell-trunk-safe/trunk-safe-check.sh" + install -m 700 .z-shell-workflow/scripts/trunk-safe-ci.sh \ + "$RUNNER_TEMP/z-shell-trunk-safe/trunk-safe-ci.sh" + rm -rf -- .z-shell-workflow + + - name: Set up Trunk launcher + uses: trunk-io/trunk-action/setup@04ba50e7658c81db7356da96657e6e77f220bfa3 # v1.3.1 with: - arguments: ${{ inputs.arguments }} - cache: ${{ inputs.cache }} - check-mode: ${{ inputs['check-mode'] }} - debug: ${{ inputs.debug }} - post-init: ${{ inputs['post-init'] }} - setup-deps: ${{ inputs['setup-deps'] }} - timeout-seconds: ${{ inputs['timeout-seconds'] }} trunk-path: ${{ inputs['trunk-path'] != '' && inputs['trunk-path'] || inputs.trunk_path }} + + - name: Set up Trunk dependencies + if: ${{ inputs['setup-deps'] && hashFiles('.trunk/setup-ci/action.yaml') != '' }} + uses: ./.trunk/setup-ci + + - name: Run post-init commands + if: ${{ inputs['post-init'] != '' }} + shell: bash + run: ${{ inputs['post-init'] }} + + - name: Run Trunk Code Quality safely + shell: bash + env: + TRUNK_PATH: ${{ env.TRUNK_PATH }} + TRUNK_SAFE_AFTER: ${{ github.event.after }} + TRUNK_SAFE_ARGUMENTS: ${{ inputs.arguments }} + TRUNK_SAFE_BEFORE: ${{ github.event.before }} + TRUNK_SAFE_CHECK_MODE: ${{ inputs['check-mode'] }} + TRUNK_SAFE_DEBUG: ${{ inputs.debug }} + TRUNK_SAFE_EVENT_NAME: ${{ github.event_name }} + TRUNK_SAFE_PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} + TRUNK_SAFE_PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} + TRUNK_SAFE_REF_NAME: ${{ github.ref_name }} + TRUNK_SAFE_TIMEOUT_SECONDS: ${{ inputs['timeout-seconds'] }} + TRUNK_SAFE_WRAPPER: ${{ runner.temp }}/z-shell-trunk-safe/trunk-safe-check.sh + run: "$RUNNER_TEMP/z-shell-trunk-safe/trunk-safe-ci.sh" diff --git a/runbooks/security-incident-response.md b/runbooks/security-incident-response.md index 649dab33b..7f5768a32 100644 --- a/runbooks/security-incident-response.md +++ b/runbooks/security-incident-response.md @@ -66,12 +66,36 @@ ephemeral notes): - the fix and any mitigation used - one concrete follow-up action to prevent recurrence (file a tracker issue) +## Safe local lint diagnostics + +Run Trunk through the repository wrapper so the linter process receives only a +documented minimal environment and disposable runtime directories: + +```sh +scripts/trunk-safe-check.sh -- check --no-fix +``` + +The wrapper forwards only `CI`, `HOME`, `LANG`, `LC_ALL`, `NO_COLOR`, `PATH`, +`TERM`, `TMPDIR`, `TRUNK_CACHE`, `TRUNK_LAUNCHER_QUIET`, and the XDG cache, +config, and data locations. Directory values other than `PATH` are generated +inside a private temporary runtime directory. It does not forward GitHub +tokens, credentials, proxy settings, or unrelated caller variables. Do not put +secrets in command-line arguments. + +If Trunk reports an internal tool-execution failure, the wrapper suppresses the +verbose diagnostic and deletes the runtime directory. Never print or attach a +raw Trunk failure report. If an earlier run exposed a credential-bearing value, +rotate that credential through its owning system and keep the incident details +in the access-controlled record. + ## Anti-patterns - discussing exploit details on a public thread - silent fixes with no reporter coordination or credit - skipping the post-incident review for a Critical incident - leaving severity untriaged past the SLA +- invoking Trunk directly from a credential-bearing environment +- printing or attaching raw internal Trunk failure diagnostics ## See also diff --git a/scripts/test-trunk-safe-check.sh b/scripts/test-trunk-safe-check.sh new file mode 100755 index 000000000..0987b1b93 --- /dev/null +++ b/scripts/test-trunk-safe-check.sh @@ -0,0 +1,127 @@ +#!/usr/bin/env sh +set -eu + +ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) +SCRIPT=$ROOT/scripts/trunk-safe-check.sh +TMPDIR=${TMPDIR:-/tmp} +TEST_TMP=$(mktemp -d "$TMPDIR/trunk-safe-check-test.XXXXXX") +trap 'rm -rf -- "$TEST_TMP"' EXIT HUP INT TERM + +fail() { + printf 'FAIL: %s\n' "$*" >&2 + exit 1 +} + +assert_contains() { + needle=$1 + file=$2 + grep -Fq -- "$needle" "$file" || fail "expected $file to contain: $needle" +} + +assert_not_contains() { + needle=$1 + file=$2 + if grep -Fq -- "$needle" "$file"; then + fail "did not expect $file to contain: $needle" + fi +} + +assert_runtime_clean() { + for entry in "$TEST_TMP/runtime"/z-shell-trunk.*; do + [ ! -e "$entry" ] || fail "wrapper left a runtime directory behind" + done +} + +FAKE_TRUNK=$TEST_TMP/fake-trunk +cat >"$FAKE_TRUNK" <<'EOF' +#!/usr/bin/env sh +set -eu + +capture_env= +capture_args= +exit_status=0 +internal_failure=false + +while [ "$#" -gt 0 ]; do + case $1 in + --capture-env) + capture_env=$2 + shift 2 + ;; + --capture-args) + capture_args=$2 + shift 2 + ;; + --exit) + exit_status=$2 + shift 2 + ;; + --internal-failure) + internal_failure=true + shift + ;; + *) + [ -n "$capture_args" ] && printf '%s\n' "$1" >>"$capture_args" + shift + ;; + esac +done + +[ -z "$capture_env" ] || env | LC_ALL=C sort >"$capture_env" +if [ "$internal_failure" = true ]; then + printf 'failed tool execution\n' >&2 + printf 'RAW_DIAGNOSTIC_MARKER\n' >&2 +else + printf 'fake trunk completed\n' +fi +exit "$exit_status" +EOF +chmod +x "$FAKE_TRUNK" + +mkdir -p "$TEST_TMP/runtime" +OUT=$TEST_TMP/out +ERR=$TEST_TMP/err +CAPTURED_ENV=$TEST_TMP/environment +CAPTURED_ARGS=$TEST_TMP/arguments +SENTINEL_VALUE=sentinel-must-not-reach-trunk + +CI=caller-controlled-value LEAK_SENTINEL=$SENTINEL_VALUE TMPDIR=$TEST_TMP/runtime \ + "$SCRIPT" --trunk-path "$FAKE_TRUNK" -- \ + --capture-env "$CAPTURED_ENV" \ + --capture-args "$CAPTURED_ARGS" \ + check "path with spaces" >"$OUT" 2>"$ERR" + +assert_contains "fake trunk completed" "$OUT" +assert_not_contains "$SENTINEL_VALUE" "$CAPTURED_ENV" +assert_not_contains "LEAK_SENTINEL=" "$CAPTURED_ENV" +assert_contains "CI=true" "$CAPTURED_ENV" +assert_not_contains "caller-controlled-value" "$CAPTURED_ENV" +assert_contains "HOME=$TEST_TMP/runtime/z-shell-trunk." "$CAPTURED_ENV" +assert_contains "TRUNK_CACHE=$TEST_TMP/runtime/z-shell-trunk." "$CAPTURED_ENV" +assert_contains "check" "$CAPTURED_ARGS" +assert_contains "path with spaces" "$CAPTURED_ARGS" + +assert_runtime_clean + +set +e +TMPDIR=$TEST_TMP/runtime "$SCRIPT" --trunk-path "$FAKE_TRUNK" -- \ + --exit 17 >"$OUT" 2>"$ERR" +status=$? +set -e +[ "$status" -eq 17 ] || fail "expected exit 17, got $status" + +set +e +TMPDIR=$TEST_TMP/runtime "$SCRIPT" --trunk-path "$FAKE_TRUNK" -- \ + --internal-failure --exit 23 >"$OUT" 2>"$ERR" +status=$? +set -e +[ "$status" -eq 23 ] || fail "expected exit 23, got $status" +assert_contains "verbose diagnostics were suppressed" "$ERR" +assert_not_contains "RAW_DIAGNOSTIC_MARKER" "$OUT" +assert_not_contains "RAW_DIAGNOSTIC_MARKER" "$ERR" + +assert_runtime_clean + +printf 'ok - Trunk environment is isolated\n' +printf 'ok - Trunk arguments and exit status are preserved\n' +printf 'ok - internal failure diagnostics are suppressed\n' diff --git a/scripts/test-trunk-safe-ci.sh b/scripts/test-trunk-safe-ci.sh new file mode 100755 index 000000000..70c3d1574 --- /dev/null +++ b/scripts/test-trunk-safe-ci.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT=$(cd -- "$(dirname -- "$0")/.." && pwd) +SCRIPT=$ROOT/scripts/trunk-safe-ci.sh +TEST_TMP=$(mktemp -d "${TMPDIR:-/tmp}/trunk-safe-ci-test.XXXXXX") +trap 'rm -rf -- "$TEST_TMP"' EXIT HUP INT TERM + +fail() { + printf 'FAIL: %s\n' "$*" >&2 + exit 1 +} + +assert_contains() { + local needle=$1 + local file=$2 + grep -Fxq -- "$needle" "$file" || fail "expected $file to contain an argument: $needle" +} + +assert_not_contains() { + local needle=$1 + local file=$2 + if grep -Fxq -- "$needle" "$file"; then + fail "did not expect $file to contain an argument: $needle" + fi +} + +FAKE_WRAPPER=$TEST_TMP/fake-wrapper +cat >"$FAKE_WRAPPER" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +printf '%s\n' "$@" >"$CAPTURE_ARGS" +EOF +chmod +x "$FAKE_WRAPPER" + +CAPTURE_ARGS=$TEST_TMP/arguments +export CAPTURE_ARGS + +TRUNK_SAFE_WRAPPER=$FAKE_WRAPPER \ + TRUNK_PATH=/tmp/fake-trunk \ + TRUNK_SAFE_CHECK_MODE=all \ + TRUNK_SAFE_AFTER=after-sha \ + TRUNK_SAFE_ARGUMENTS='--no-progress --no-fix' \ + "$SCRIPT" + +assert_contains "--trunk-path" "$CAPTURE_ARGS" +assert_contains "/tmp/fake-trunk" "$CAPTURE_ARGS" +assert_contains "check" "$CAPTURE_ARGS" +assert_contains "--ci" "$CAPTURE_ARGS" +assert_contains "--all" "$CAPTURE_ARGS" +assert_contains "--github-commit" "$CAPTURE_ARGS" +assert_contains "after-sha" "$CAPTURE_ARGS" +assert_contains "--no-progress" "$CAPTURE_ARGS" +assert_contains "--no-fix" "$CAPTURE_ARGS" + +TRUNK_SAFE_WRAPPER=$FAKE_WRAPPER \ + TRUNK_PATH=/tmp/fake-trunk \ + TRUNK_SAFE_CHECK_MODE=pull_request \ + TRUNK_SAFE_REF_NAME=feature-branch \ + TRUNK_SAFE_PR_BASE_SHA=base-sha \ + TRUNK_SAFE_PR_HEAD_SHA=head-sha \ + "$SCRIPT" + +assert_contains "--upstream" "$CAPTURE_ARGS" +assert_contains "base-sha" "$CAPTURE_ARGS" +assert_contains "head-sha" "$CAPTURE_ARGS" +assert_not_contains "--all" "$CAPTURE_ARGS" + +rm -f "$CAPTURE_ARGS" +TRUNK_SAFE_WRAPPER=$FAKE_WRAPPER \ + TRUNK_PATH=/tmp/fake-trunk \ + TRUNK_SAFE_CHECK_MODE=none \ + "$SCRIPT" +[[ ! -e $CAPTURE_ARGS ]] || fail "none mode invoked the wrapper" + +set +e +TRUNK_SAFE_WRAPPER=$FAKE_WRAPPER \ + TRUNK_PATH=/tmp/fake-trunk \ + TRUNK_SAFE_CHECK_MODE=unsupported \ + "$SCRIPT" >"$TEST_TMP/out" 2>"$TEST_TMP/err" +status=$? +set -e +[[ $status -eq 2 ]] || fail "expected invalid mode exit 2, got $status" + +set +e +TRUNK_SAFE_WRAPPER=$FAKE_WRAPPER \ + TRUNK_PATH=/tmp/fake-trunk \ + TRUNK_SAFE_CHECK_MODE=all \ + TRUNK_SAFE_ARGUMENTS='--token=synthetic-secret' \ + "$SCRIPT" >"$TEST_TMP/out" 2>"$TEST_TMP/err" +status=$? +set -e +[[ $status -eq 2 ]] || fail "expected credential argument exit 2, got $status" +grep -Fq "credential-bearing Trunk arguments are not allowed" "$TEST_TMP/err" || + fail "credential argument rejection was not reported" + +printf 'ok - CI check modes preserve Trunk scope\n' +printf 'ok - disabled and invalid modes are handled safely\n' +printf 'ok - credential-bearing arguments are rejected\n' diff --git a/scripts/trunk-safe-check.sh b/scripts/trunk-safe-check.sh new file mode 100755 index 000000000..3bb07aa8e --- /dev/null +++ b/scripts/trunk-safe-check.sh @@ -0,0 +1,104 @@ +#!/usr/bin/env sh +set -eu + +usage() { + cat <<'EOF' +Usage: scripts/trunk-safe-check.sh [--trunk-path PATH] -- ARG... + +Run Trunk with a minimal environment and disposable runtime directories. +The arguments after -- are passed to Trunk unchanged. +EOF +} + +fail() { + printf 'trunk-safe-check: %s\n' "$*" >&2 + exit 2 +} + +trunk_path= +while [ "$#" -gt 0 ]; do + case $1 in + --trunk-path) + [ "$#" -ge 2 ] || fail "--trunk-path requires a value" + trunk_path=$2 + shift 2 + ;; + --help | -h) + usage + exit 0 + ;; + --) + shift + break + ;; + *) + fail "unknown option: $1" + ;; + esac +done + +[ "$#" -gt 0 ] || fail "missing Trunk arguments after --" + +if [ -z "$trunk_path" ]; then + trunk_path=$(command -v trunk 2>/dev/null || true) +fi +[ -n "$trunk_path" ] || fail "trunk executable not found" +[ -x "$trunk_path" ] || fail "trunk executable is not executable: $trunk_path" + +case $trunk_path in +/*) ;; +*) trunk_path=$(CDPATH='' cd -- "$(dirname -- "$trunk_path")" && pwd)/$(basename -- "$trunk_path") ;; +esac + +runtime_parent=${TMPDIR:-/tmp} +[ -d "$runtime_parent" ] || fail "temporary directory does not exist: $runtime_parent" + +umask 077 +runtime_dir=$(mktemp -d "$runtime_parent/z-shell-trunk.XXXXXX") +# Invoked through the signal and exit trap below. +# shellcheck disable=SC2329 +cleanup() { + rm -rf -- "$runtime_dir" +} +trap cleanup EXIT HUP INT TERM + +mkdir -p \ + "$runtime_dir/cache" \ + "$runtime_dir/config" \ + "$runtime_dir/data" \ + "$runtime_dir/home" \ + "$runtime_dir/tmp" + +stdout_file=$runtime_dir/stdout +stderr_file=$runtime_dir/stderr +ci_value= +[ -z "${CI-}" ] || ci_value=true + +set +e +env -i \ + CI="$ci_value" \ + HOME="$runtime_dir/home" \ + LANG=C \ + LC_ALL=C \ + NO_COLOR=1 \ + PATH="${PATH:-/usr/bin:/bin}" \ + TERM=dumb \ + TMPDIR="$runtime_dir/tmp" \ + TRUNK_CACHE="$runtime_dir/cache" \ + TRUNK_LAUNCHER_QUIET=false \ + XDG_CACHE_HOME="$runtime_dir/cache" \ + XDG_CONFIG_HOME="$runtime_dir/config" \ + XDG_DATA_HOME="$runtime_dir/data" \ + "$trunk_path" "$@" >"$stdout_file" 2>"$stderr_file" +status=$? +set -e + +if [ "$status" -ne 0 ] && + grep -Eiq 'failed tool execution|failure\.ya?ml|internal (error|failure)' "$stdout_file" "$stderr_file"; then + printf 'Trunk failed internally with status %s; verbose diagnostics were suppressed.\n' "$status" >&2 +else + cat "$stdout_file" + cat "$stderr_file" >&2 +fi + +exit "$status" diff --git a/scripts/trunk-safe-ci.sh b/scripts/trunk-safe-ci.sh new file mode 100755 index 000000000..eca0f4710 --- /dev/null +++ b/scripts/trunk-safe-ci.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +set -euo pipefail + +fail() { + printf 'trunk-safe-ci: %s\n' "$*" >&2 + exit 2 +} + +: "${TRUNK_SAFE_WRAPPER:?TRUNK_SAFE_WRAPPER is required}" +: "${TRUNK_PATH:?TRUNK_PATH is required}" + +mode=${TRUNK_SAFE_CHECK_MODE-} +event_name=${TRUNK_SAFE_EVENT_NAME-} +ref_name=${TRUNK_SAFE_REF_NAME-} +before=${TRUNK_SAFE_BEFORE-} +after=${TRUNK_SAFE_AFTER-} +pr_base_sha=${TRUNK_SAFE_PR_BASE_SHA-} +pr_head_sha=${TRUNK_SAFE_PR_HEAD_SHA-} +timeout_seconds=${TRUNK_SAFE_TIMEOUT_SECONDS:-0} + +if [[ -z $mode ]]; then + case $event_name in + pull_request | pull_request_target) mode=pull_request ;; + push) mode=push ;; + schedule | workflow_dispatch) mode=all ;; + *) mode=none ;; + esac +fi + +case $mode in +all | none | populate_cache_only | pull_request | push | trunk_merge) ;; +*) fail "unsupported check mode: $mode" ;; +esac + +if [[ ${TRUNK_SAFE_DEBUG:-false} == true ]]; then + printf 'Selected safe Trunk check mode: %s\n' "$mode" +fi + +[[ $mode != none ]] || exit 0 + +args=(check --ci) +case $mode in +all | populate_cache_only) + args+=(--all) + [[ -z $after ]] || args+=(--github-commit "$after") + ;; +pull_request) + if [[ $ref_name == */merge ]]; then + upstream=$(git rev-parse HEAD^1) + commit=$(git rev-parse HEAD^2) + else + [[ -n $pr_base_sha ]] || fail "pull request base SHA is required" + [[ -n $pr_head_sha ]] || fail "pull request head SHA is required" + upstream=$pr_base_sha + commit=$pr_head_sha + fi + args+=(--upstream "$upstream" --github-commit "$commit") + ;; +push | trunk_merge) + if [[ $ref_name == gh-readonly-queue/* || $mode == trunk_merge ]]; then + upstream=$(git rev-parse HEAD^1) + elif [[ -z $before || $before == 0000000000000000000000000000000000000000 ]]; then + args+=(--all) + upstream= + else + upstream=$before + fi + [[ -z $upstream ]] || args+=(--upstream "$upstream") + [[ -z $after ]] || args+=(--github-commit "$after") + ;; +esac + +if [[ -n ${TRUNK_SAFE_ARGUMENTS-} ]]; then + read -r -a extra_args <<<"$TRUNK_SAFE_ARGUMENTS" + for arg in "${extra_args[@]}"; do + case $arg in + --token | --token=* | --github-token | --github-token=* | --trunk-token | --trunk-token=*) + fail "credential-bearing Trunk arguments are not allowed" + ;; + esac + done + args+=("${extra_args[@]}") +fi + +command=("$TRUNK_SAFE_WRAPPER" --trunk-path "$TRUNK_PATH" -- "${args[@]}") +if [[ $timeout_seconds == 0 ]]; then + "${command[@]}" +else + [[ $timeout_seconds =~ ^[0-9]+$ ]] || fail "timeout must be a non-negative integer" + timeout --preserve-status "$timeout_seconds" "${command[@]}" +fi