Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/06_agent_handoff.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
50 changes: 50 additions & 0 deletions .github/workflows/trunk-safe-check-test.yml
Original file line number Diff line number Diff line change
@@ -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
88 changes: 73 additions & 15 deletions .github/workflows/trunk.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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"
24 changes: 24 additions & 0 deletions runbooks/security-incident-response.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
127 changes: 127 additions & 0 deletions scripts/test-trunk-safe-check.sh
Original file line number Diff line number Diff line change
@@ -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'
Loading
Loading