From 2a49593d2fa58beb5baa505410d951b1c4fd287c Mon Sep 17 00:00:00 2001 From: Stefan Zweifel Date: Sat, 27 Jun 2026 17:15:03 +0200 Subject: [PATCH 01/11] feat: add _run_hook helper and eight hook inputs --- action.yml | 32 ++++++++++++++++++++++++++++++++ entrypoint.sh | 10 ++++++++++ tests/git-auto-commit.bats | 17 +++++++++++++++++ 3 files changed, 59 insertions(+) diff --git a/action.yml b/action.yml index 9b06340f..a0e3a858 100644 --- a/action.yml +++ b/action.yml @@ -84,6 +84,38 @@ inputs: description: Perform a clean git tag and push, without commiting anything required: false default: false + before_add: + description: Shell snippet to run before `git add`. + required: false + default: '' + after_add: + description: Shell snippet to run after `git add`. + required: false + default: '' + before_commit: + description: Shell snippet to run before `git commit`. + required: false + default: '' + after_commit: + description: Shell snippet to run after `git commit`. + required: false + default: '' + before_tag: + description: Shell snippet to run before `git tag` is created. + required: false + default: '' + after_tag: + description: Shell snippet to run after `git tag` is created. + required: false + default: '' + before_push: + description: Shell snippet to run before `git push`. + required: false + default: '' + after_push: + description: Shell snippet to run after `git push`. + required: false + default: '' disable_pull_request_target_trigger_warning: description: Suppress the security warning emitted when the action runs on a `pull_request_target` event. required: false diff --git a/entrypoint.sh b/entrypoint.sh index 320d43dc..3a436410 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -26,6 +26,16 @@ _log() { echo "::$level::$message"; } +_run_hook() { + local name=${1} + local snippet=${2} + + if [ -n "$snippet" ]; then + _log "debug" "Running $name hook"; + eval "$snippet" + fi +} + _main() { _check_if_git_is_available diff --git a/tests/git-auto-commit.bats b/tests/git-auto-commit.bats index 01803107..4435aad4 100644 --- a/tests/git-auto-commit.bats +++ b/tests/git-auto-commit.bats @@ -43,6 +43,14 @@ setup() { export INPUT_CREATE_BRANCH=false export INPUT_DISABLE_PULL_REQUEST_TARGET_TRIGGER_WARNING=false export INPUT_INTERNAL_GIT_BINARY=git + export INPUT_BEFORE_ADD="" + export INPUT_AFTER_ADD="" + export INPUT_BEFORE_COMMIT="" + export INPUT_AFTER_COMMIT="" + export INPUT_BEFORE_TAG="" + export INPUT_AFTER_TAG="" + export INPUT_BEFORE_PUSH="" + export INPUT_AFTER_PUSH="" # Unset the GitHub event name by default so tests do not pick up # pull_request_target from the environment of the shell running BATS. @@ -1572,3 +1580,12 @@ END assert_success refute_output --partial "::warning::git-auto-commit is running on a 'pull_request_target' event." } + +@test "It does not log a hook line when no hooks are set" { + touch "${FAKE_LOCAL_REPOSITORY}"/new-file-1.txt + + run git_auto_commit + + assert_success + refute_output --partial "::debug::Running" +} From 2798c4f7e47ab646d36703d6cfe94ea8e2681737 Mon Sep 17 00:00:00 2001 From: Stefan Zweifel Date: Sat, 27 Jun 2026 17:21:02 +0200 Subject: [PATCH 02/11] fix(_run_hook): default snippet to empty when arg unset Guards against set -u aborting with 'unbound variable' if _run_hook is ever called with only one argument. --- entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/entrypoint.sh b/entrypoint.sh index 3a436410..76392824 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -28,7 +28,7 @@ _log() { _run_hook() { local name=${1} - local snippet=${2} + local snippet=${2:-} if [ -n "$snippet" ]; then _log "debug" "Running $name hook"; From 0d14230b122d7f80dfbc31b5e15abcbe4ca156f6 Mon Sep 17 00:00:00 2001 From: Stefan Zweifel Date: Sat, 27 Jun 2026 17:26:03 +0200 Subject: [PATCH 03/11] feat: wire add/commit hooks into _main --- entrypoint.sh | 4 +++ tests/git-auto-commit.bats | 69 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/entrypoint.sh b/entrypoint.sh index 76392824..995463d7 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -58,14 +58,18 @@ _main() { _switch_to_branch + _run_hook "before_add" "$INPUT_BEFORE_ADD" _add_files + _run_hook "after_add" "$INPUT_AFTER_ADD" # Check dirty state of repo again using git-diff. # (git-diff detects better if CRLF of files changes and does NOT # proceed, if only CRLF changes are detected. See #241 and #265 # for more details.) if [ -n "$(git diff --staged)" ] || "$INPUT_SKIP_DIRTY_CHECK"; then + _run_hook "before_commit" "$INPUT_BEFORE_COMMIT" _local_commit + _run_hook "after_commit" "$INPUT_AFTER_COMMIT" _tag_commit diff --git a/tests/git-auto-commit.bats b/tests/git-auto-commit.bats index 4435aad4..e756c59f 100644 --- a/tests/git-auto-commit.bats +++ b/tests/git-auto-commit.bats @@ -1589,3 +1589,72 @@ END assert_success refute_output --partial "::debug::Running" } + +@test "It runs the before_add hook before git add" { + touch "${FAKE_LOCAL_REPOSITORY}"/new-file-1.txt + export INPUT_BEFORE_ADD="echo BEFORE_ADD_RAN > '${FAKE_LOCAL_REPOSITORY}/before-add-marker.txt'" + + run git_auto_commit + + assert_success + assert_line "::debug::Running before_add hook" + [ -f "${FAKE_LOCAL_REPOSITORY}/before-add-marker.txt" ] +} + +@test "It runs the after_add hook after git add" { + touch "${FAKE_LOCAL_REPOSITORY}"/new-file-1.txt + export INPUT_AFTER_ADD="echo AFTER_ADD_RAN > '${FAKE_LOCAL_REPOSITORY}/after-add-marker.txt'" + + run git_auto_commit + + assert_success + assert_line "::debug::Running after_add hook" + [ -f "${FAKE_LOCAL_REPOSITORY}/after-add-marker.txt" ] +} + +@test "It runs the before_commit hook before creating the commit" { + touch "${FAKE_LOCAL_REPOSITORY}"/new-file-1.txt + export INPUT_BEFORE_COMMIT="echo BEFORE_COMMIT_RAN > '${FAKE_LOCAL_REPOSITORY}/before-commit-marker.txt'" + + run git_auto_commit + + assert_success + assert_line "::debug::Running before_commit hook" + [ -f "${FAKE_LOCAL_REPOSITORY}/before-commit-marker.txt" ] + + # The marker was created after `git add` ran, so it is NOT in the commit. + run git show --name-only HEAD + refute_output --partial "before-commit-marker.txt" +} + +@test "It runs the after_commit hook after creating the commit" { + touch "${FAKE_LOCAL_REPOSITORY}"/new-file-1.txt + export INPUT_AFTER_COMMIT="git rev-parse HEAD > '${FAKE_LOCAL_REPOSITORY}/after-commit-sha.txt'" + + run git_auto_commit + + assert_success + assert_line "::debug::Running after_commit hook" + [ -f "${FAKE_LOCAL_REPOSITORY}/after-commit-sha.txt" ] + + # Sanity check: the SHA written is a valid commit hash + run cat "${FAKE_LOCAL_REPOSITORY}/after-commit-sha.txt" + assert_output --regexp '^[0-9a-f]{40}$' +} + +@test "It does not run add or commit hooks when the working tree is clean" { + export INPUT_BEFORE_ADD="echo SHOULD_NOT_RUN > '${FAKE_LOCAL_REPOSITORY}/before-add-marker.txt'" + export INPUT_AFTER_ADD="echo SHOULD_NOT_RUN > '${FAKE_LOCAL_REPOSITORY}/after-add-marker.txt'" + export INPUT_BEFORE_COMMIT="echo SHOULD_NOT_RUN > '${FAKE_LOCAL_REPOSITORY}/before-commit-marker.txt'" + export INPUT_AFTER_COMMIT="echo SHOULD_NOT_RUN > '${FAKE_LOCAL_REPOSITORY}/after-commit-marker.txt'" + + run git_auto_commit + + assert_success + assert_line "Working tree clean. Nothing to commit." + refute_output --partial "::debug::Running" + [ ! -f "${FAKE_LOCAL_REPOSITORY}/before-add-marker.txt" ] + [ ! -f "${FAKE_LOCAL_REPOSITORY}/after-add-marker.txt" ] + [ ! -f "${FAKE_LOCAL_REPOSITORY}/before-commit-marker.txt" ] + [ ! -f "${FAKE_LOCAL_REPOSITORY}/after-commit-marker.txt" ] +} From 2d7649d3dbcacf0227ac9bf2e78acd4345688a87 Mon Sep 17 00:00:00 2001 From: Stefan Zweifel Date: Sat, 27 Jun 2026 17:32:17 +0200 Subject: [PATCH 04/11] feat: wire tag and push hooks into _main --- entrypoint.sh | 32 ++++++++++++++-- tests/git-auto-commit.bats | 77 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 3 deletions(-) diff --git a/entrypoint.sh b/entrypoint.sh index 995463d7..13632784 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -50,8 +50,22 @@ _main() { if "$INPUT_CREATE_GIT_TAG_ONLY"; then _log "debug" "Create git tag only"; _set_github_output "create_git_tag_only" "true" - _tag_commit + + if [ -n "$INPUT_TAG_NAME" ] || [ -n "$INPUT_TAGGING_MESSAGE" ]; then + _run_hook "before_tag" "$INPUT_BEFORE_TAG" + _tag_commit + _run_hook "after_tag" "$INPUT_AFTER_TAG" + else + _tag_commit + fi + + if ! "$INPUT_SKIP_PUSH"; then + _run_hook "before_push" "$INPUT_BEFORE_PUSH" + fi _push_to_github + if ! "$INPUT_SKIP_PUSH"; then + _run_hook "after_push" "$INPUT_AFTER_PUSH" + fi elif _git_is_dirty || "$INPUT_SKIP_DIRTY_CHECK"; then _set_github_output "changes_detected" "true" @@ -71,9 +85,21 @@ _main() { _local_commit _run_hook "after_commit" "$INPUT_AFTER_COMMIT" - _tag_commit - + if [ -n "$INPUT_TAG_NAME" ] || [ -n "$INPUT_TAGGING_MESSAGE" ]; then + _run_hook "before_tag" "$INPUT_BEFORE_TAG" + _tag_commit + _run_hook "after_tag" "$INPUT_AFTER_TAG" + else + _tag_commit + fi + + if ! "$INPUT_SKIP_PUSH"; then + _run_hook "before_push" "$INPUT_BEFORE_PUSH" + fi _push_to_github + if ! "$INPUT_SKIP_PUSH"; then + _run_hook "after_push" "$INPUT_AFTER_PUSH" + fi else _set_github_output "changes_detected" "false" diff --git a/tests/git-auto-commit.bats b/tests/git-auto-commit.bats index e756c59f..7fd728dd 100644 --- a/tests/git-auto-commit.bats +++ b/tests/git-auto-commit.bats @@ -1658,3 +1658,80 @@ END [ ! -f "${FAKE_LOCAL_REPOSITORY}/before-commit-marker.txt" ] [ ! -f "${FAKE_LOCAL_REPOSITORY}/after-commit-marker.txt" ] } + +@test "It runs before_tag and after_tag hooks when creating a tag" { + INPUT_TAG_NAME="v1.0.0" + INPUT_TAGGING_MESSAGE="Release v1.0.0" + + touch "${FAKE_LOCAL_REPOSITORY}"/new-file-1.txt + export INPUT_BEFORE_TAG="echo BEFORE_TAG_RAN > '${FAKE_LOCAL_REPOSITORY}/before-tag-marker.txt'" + export INPUT_AFTER_TAG="echo AFTER_TAG_RAN > '${FAKE_LOCAL_REPOSITORY}/after-tag-marker.txt'" + + run git_auto_commit + + assert_success + assert_line "::debug::Running before_tag hook" + assert_line "::debug::Running after_tag hook" + [ -f "${FAKE_LOCAL_REPOSITORY}/before-tag-marker.txt" ] + [ -f "${FAKE_LOCAL_REPOSITORY}/after-tag-marker.txt" ] +} + +@test "It does not run tag hooks when no tag name or tagging message is set" { + touch "${FAKE_LOCAL_REPOSITORY}"/new-file-1.txt + export INPUT_BEFORE_TAG="echo SHOULD_NOT_RUN > '${FAKE_LOCAL_REPOSITORY}/before-tag-marker.txt'" + export INPUT_AFTER_TAG="echo SHOULD_NOT_RUN > '${FAKE_LOCAL_REPOSITORY}/after-tag-marker.txt'" + + run git_auto_commit + + assert_success + refute_line "::debug::Running before_tag hook" + refute_line "::debug::Running after_tag hook" + [ ! -f "${FAKE_LOCAL_REPOSITORY}/before-tag-marker.txt" ] + [ ! -f "${FAKE_LOCAL_REPOSITORY}/after-tag-marker.txt" ] +} + +@test "It runs tag hooks under create_git_tag_only mode" { + INPUT_CREATE_GIT_TAG_ONLY=true + INPUT_TAG_NAME="v1.0.0" + INPUT_TAGGING_MESSAGE="Release v1.0.0" + + export INPUT_BEFORE_TAG="echo BEFORE_TAG_RAN > '${FAKE_LOCAL_REPOSITORY}/before-tag-marker.txt'" + export INPUT_AFTER_TAG="echo AFTER_TAG_RAN > '${FAKE_LOCAL_REPOSITORY}/after-tag-marker.txt'" + + run git_auto_commit + + assert_success + assert_line "::debug::Running before_tag hook" + assert_line "::debug::Running after_tag hook" + [ -f "${FAKE_LOCAL_REPOSITORY}/before-tag-marker.txt" ] + [ -f "${FAKE_LOCAL_REPOSITORY}/after-tag-marker.txt" ] +} + +@test "It runs before_push and after_push hooks around git push" { + touch "${FAKE_LOCAL_REPOSITORY}"/new-file-1.txt + export INPUT_BEFORE_PUSH="echo BEFORE_PUSH_RAN > '${FAKE_LOCAL_REPOSITORY}/before-push-marker.txt'" + export INPUT_AFTER_PUSH="echo AFTER_PUSH_RAN > '${FAKE_LOCAL_REPOSITORY}/after-push-marker.txt'" + + run git_auto_commit + + assert_success + assert_line "::debug::Running before_push hook" + assert_line "::debug::Running after_push hook" + [ -f "${FAKE_LOCAL_REPOSITORY}/before-push-marker.txt" ] + [ -f "${FAKE_LOCAL_REPOSITORY}/after-push-marker.txt" ] +} + +@test "It does not run push hooks when skip_push is true" { + INPUT_SKIP_PUSH=true + touch "${FAKE_LOCAL_REPOSITORY}"/new-file-1.txt + export INPUT_BEFORE_PUSH="echo SHOULD_NOT_RUN > '${FAKE_LOCAL_REPOSITORY}/before-push-marker.txt'" + export INPUT_AFTER_PUSH="echo SHOULD_NOT_RUN > '${FAKE_LOCAL_REPOSITORY}/after-push-marker.txt'" + + run git_auto_commit + + assert_success + refute_line "::debug::Running before_push hook" + refute_line "::debug::Running after_push hook" + [ ! -f "${FAKE_LOCAL_REPOSITORY}/before-push-marker.txt" ] + [ ! -f "${FAKE_LOCAL_REPOSITORY}/after-push-marker.txt" ] +} From 0b8fc144b6f1f6466a36a538dc95f6cb75a14fa9 Mon Sep 17 00:00:00 2001 From: Stefan Zweifel Date: Sat, 27 Jun 2026 17:50:04 +0200 Subject: [PATCH 05/11] test: verify failing hook aborts the action --- tests/git-auto-commit.bats | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/git-auto-commit.bats b/tests/git-auto-commit.bats index 7fd728dd..a2f516d2 100644 --- a/tests/git-auto-commit.bats +++ b/tests/git-auto-commit.bats @@ -1735,3 +1735,18 @@ END [ ! -f "${FAKE_LOCAL_REPOSITORY}/before-push-marker.txt" ] [ ! -f "${FAKE_LOCAL_REPOSITORY}/after-push-marker.txt" ] } + +@test "A hook that exits non-zero aborts the action" { + touch "${FAKE_LOCAL_REPOSITORY}"/new-file-1.txt + export INPUT_BEFORE_COMMIT="exit 1" + + run git_auto_commit + + assert_failure + assert_line "::debug::Running before_commit hook" + + # Assert the action stopped before pushing — local and remote shas differ. + current_sha="$(git rev-parse --verify --short ${FAKE_DEFAULT_BRANCH})" + remote_sha="$(git rev-parse --verify --short origin/${FAKE_DEFAULT_BRANCH})" + refute [assert_equal $current_sha $remote_sha] +} From 1e1fbe5eb64c0f86bba9d12782fa2344e1ef0df2 Mon Sep 17 00:00:00 2001 From: Stefan Zweifel Date: Sat, 27 Jun 2026 17:52:31 +0200 Subject: [PATCH 06/11] docs: document hooks system in README --- README.md | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/README.md b/README.md index 85611c5f..7fe9b736 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,19 @@ The following is an extended example with all available options. # `pull_request_target` event. See the "Workflow should run in **base** repository" # section below for context before disabling this warning. disable_pull_request_target_trigger_warning: false + + # Optional. Shell snippets to run around each git operation. Each hook + # is evaluated in the same bash process as the action — `set -eu` is + # in effect, the working directory is your repository, and all + # `INPUT_*` env vars are visible. A non-zero exit aborts the action. + before_add: 'git fetch --unshallow' + after_add: '' + before_commit: '' + after_commit: '' + before_tag: '' + after_tag: '' + before_push: '' + after_push: '' ``` Please note that the Action depends on `bash`. If you're using the Action in a job in combination with a custom Docker container, make sure that `bash` is installed. @@ -215,6 +228,56 @@ You can use these outputs to trigger other Actions in your Workflow run based on run: echo "No Changes!" ``` +## Hooks + +git-auto-commit can run custom shell snippets around each git operation +it performs. This is useful when you need to prepare or clean up the +repository as part of the same step — for example, unshallowing a +shallow clone right before the commit is staged. + +Eight optional hooks are available: + +| Hook | Runs | +| ---- | ---- | +| `before_add` / `after_add` | around `git add` | +| `before_commit` / `after_commit` | around `git commit` | +| `before_tag` / `after_tag` | around `git tag` (only when a tag is being created) | +| `before_push` / `after_push` | around `git push` (skipped when `skip_push: true`) | + +Each hook is an inline shell snippet that runs in the same bash process +as the action. The working directory is your repository, and all +`INPUT_*` environment variables and standard GitHub Actions env vars are +visible to the snippet. + +### Example + +```yaml +- uses: stefanzweifel/git-auto-commit-action@v7 + with: + before_add: | + git fetch --unshallow +``` + +Multi-line snippets work via YAML's `|` block scalar: + +```yaml +- uses: stefanzweifel/git-auto-commit-action@v7 + with: + before_commit: | + echo "About to commit at $(date)" + ./scripts/prepare-commit.sh +``` + +### Notes + +- A hook only runs when its underlying step actually runs. For example, + `before_add`/`after_add` are skipped when the working tree is clean, + and `before_push`/`after_push` are skipped when `skip_push: true`. +- If a hook exits with a non-zero status, the action fails. Append + `|| true` to a snippet to ignore its failure. +- Hooks share environment with the action, so they can read action + inputs (e.g. `$INPUT_COMMIT_MESSAGE`) and write to `$GITHUB_OUTPUT`. + ## Limitations & Gotchas The goal of this Action is to be "the Action for committing files for the 80% use case". Therefore, you might run into issues if your Workflow falls into the not supported 20% portion. From 6ed7cfe05e6ebbf4e5a5aee1b6e29b1632dd467c Mon Sep 17 00:00:00 2001 From: Stefan Zweifel Date: Sat, 27 Jun 2026 18:04:52 +0200 Subject: [PATCH 07/11] test: tighten failing-hook assertion The previous `refute [assert_equal ...]` line was silently a no-op: `refute` invoked `[` (the test builtin) with literal arguments and always saw the non-zero exit, so the check never actually compared shas. Replace with a direct assertion that no commit_hash output was written, which is what 'aborted before committing' really means here. --- tests/git-auto-commit.bats | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/git-auto-commit.bats b/tests/git-auto-commit.bats index a2f516d2..fdb6ef1b 100644 --- a/tests/git-auto-commit.bats +++ b/tests/git-auto-commit.bats @@ -1745,8 +1745,7 @@ END assert_failure assert_line "::debug::Running before_commit hook" - # Assert the action stopped before pushing — local and remote shas differ. - current_sha="$(git rev-parse --verify --short ${FAKE_DEFAULT_BRANCH})" - remote_sha="$(git rev-parse --verify --short origin/${FAKE_DEFAULT_BRANCH})" - refute [assert_equal $current_sha $remote_sha] + # Assert the action aborted before committing: no commit_hash output was written. + run cat_github_output + refute_line -e "commit_hash=[0-9a-f]{40}$" } From e42dc5ca6843698dcfcef7dcca26cc5200cbda0d Mon Sep 17 00:00:00 2001 From: Stefan Zweifel Date: Sat, 27 Jun 2026 18:05:16 +0200 Subject: [PATCH 08/11] docs(hooks): note set -eu semantics for snippet authors --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 7fe9b736..8f243f0e 100644 --- a/README.md +++ b/README.md @@ -277,6 +277,8 @@ Multi-line snippets work via YAML's `|` block scalar: `|| true` to a snippet to ignore its failure. - Hooks share environment with the action, so they can read action inputs (e.g. `$INPUT_COMMIT_MESSAGE`) and write to `$GITHUB_OUTPUT`. +- Snippets run under `set -eu`. Referencing an unset variable aborts the + action; use `${VAR:-}` to default an optional variable to empty. ## Limitations & Gotchas From f5a9455cae3da4cf0675691207ed0b49d49fefeb Mon Sep 17 00:00:00 2001 From: Stefan Zweifel Date: Sun, 28 Jun 2026 08:34:33 +0200 Subject: [PATCH 09/11] refactor(hooks): rename action inputs to suffix with _hook `before_add` etc. could read as just another `add`-related option in the inputs list (alongside `add_options`, `commit_options`, `push_options`). Suffix all eight hook inputs with `_hook` to disambiguate them from the existing option-category inputs. Internal helper still calls `_run_hook "before_add" ...` so the `::debug::Running before_add hook` log line is unchanged. --- README.md | 32 +++++++++++----------- action.yml | 16 +++++------ entrypoint.sh | 24 ++++++++--------- tests/git-auto-commit.bats | 54 +++++++++++++++++++------------------- 4 files changed, 63 insertions(+), 63 deletions(-) diff --git a/README.md b/README.md index 8f243f0e..e815412c 100644 --- a/README.md +++ b/README.md @@ -146,14 +146,14 @@ The following is an extended example with all available options. # is evaluated in the same bash process as the action — `set -eu` is # in effect, the working directory is your repository, and all # `INPUT_*` env vars are visible. A non-zero exit aborts the action. - before_add: 'git fetch --unshallow' - after_add: '' - before_commit: '' - after_commit: '' - before_tag: '' - after_tag: '' - before_push: '' - after_push: '' + before_add_hook: 'git fetch --unshallow' + after_add_hook: '' + before_commit_hook: '' + after_commit_hook: '' + before_tag_hook: '' + after_tag_hook: '' + before_push_hook: '' + after_push_hook: '' ``` Please note that the Action depends on `bash`. If you're using the Action in a job in combination with a custom Docker container, make sure that `bash` is installed. @@ -239,10 +239,10 @@ Eight optional hooks are available: | Hook | Runs | | ---- | ---- | -| `before_add` / `after_add` | around `git add` | -| `before_commit` / `after_commit` | around `git commit` | -| `before_tag` / `after_tag` | around `git tag` (only when a tag is being created) | -| `before_push` / `after_push` | around `git push` (skipped when `skip_push: true`) | +| `before_add_hook` / `after_add_hook` | around `git add` | +| `before_commit_hook` / `after_commit_hook` | around `git commit` | +| `before_tag_hook` / `after_tag_hook` | around `git tag` (only when a tag is being created) | +| `before_push_hook` / `after_push_hook` | around `git push` (skipped when `skip_push: true`) | Each hook is an inline shell snippet that runs in the same bash process as the action. The working directory is your repository, and all @@ -254,7 +254,7 @@ visible to the snippet. ```yaml - uses: stefanzweifel/git-auto-commit-action@v7 with: - before_add: | + before_add_hook: | git fetch --unshallow ``` @@ -263,7 +263,7 @@ Multi-line snippets work via YAML's `|` block scalar: ```yaml - uses: stefanzweifel/git-auto-commit-action@v7 with: - before_commit: | + before_commit_hook: | echo "About to commit at $(date)" ./scripts/prepare-commit.sh ``` @@ -271,8 +271,8 @@ Multi-line snippets work via YAML's `|` block scalar: ### Notes - A hook only runs when its underlying step actually runs. For example, - `before_add`/`after_add` are skipped when the working tree is clean, - and `before_push`/`after_push` are skipped when `skip_push: true`. + `before_add_hook`/`after_add_hook` are skipped when the working tree is clean, + and `before_push_hook`/`after_push_hook` are skipped when `skip_push: true`. - If a hook exits with a non-zero status, the action fails. Append `|| true` to a snippet to ignore its failure. - Hooks share environment with the action, so they can read action diff --git a/action.yml b/action.yml index a0e3a858..4d00735f 100644 --- a/action.yml +++ b/action.yml @@ -84,35 +84,35 @@ inputs: description: Perform a clean git tag and push, without commiting anything required: false default: false - before_add: + before_add_hook: description: Shell snippet to run before `git add`. required: false default: '' - after_add: + after_add_hook: description: Shell snippet to run after `git add`. required: false default: '' - before_commit: + before_commit_hook: description: Shell snippet to run before `git commit`. required: false default: '' - after_commit: + after_commit_hook: description: Shell snippet to run after `git commit`. required: false default: '' - before_tag: + before_tag_hook: description: Shell snippet to run before `git tag` is created. required: false default: '' - after_tag: + after_tag_hook: description: Shell snippet to run after `git tag` is created. required: false default: '' - before_push: + before_push_hook: description: Shell snippet to run before `git push`. required: false default: '' - after_push: + after_push_hook: description: Shell snippet to run after `git push`. required: false default: '' diff --git a/entrypoint.sh b/entrypoint.sh index 13632784..c5e567b5 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -52,19 +52,19 @@ _main() { _set_github_output "create_git_tag_only" "true" if [ -n "$INPUT_TAG_NAME" ] || [ -n "$INPUT_TAGGING_MESSAGE" ]; then - _run_hook "before_tag" "$INPUT_BEFORE_TAG" + _run_hook "before_tag" "$INPUT_BEFORE_TAG_HOOK" _tag_commit - _run_hook "after_tag" "$INPUT_AFTER_TAG" + _run_hook "after_tag" "$INPUT_AFTER_TAG_HOOK" else _tag_commit fi if ! "$INPUT_SKIP_PUSH"; then - _run_hook "before_push" "$INPUT_BEFORE_PUSH" + _run_hook "before_push" "$INPUT_BEFORE_PUSH_HOOK" fi _push_to_github if ! "$INPUT_SKIP_PUSH"; then - _run_hook "after_push" "$INPUT_AFTER_PUSH" + _run_hook "after_push" "$INPUT_AFTER_PUSH_HOOK" fi elif _git_is_dirty || "$INPUT_SKIP_DIRTY_CHECK"; then @@ -72,33 +72,33 @@ _main() { _switch_to_branch - _run_hook "before_add" "$INPUT_BEFORE_ADD" + _run_hook "before_add" "$INPUT_BEFORE_ADD_HOOK" _add_files - _run_hook "after_add" "$INPUT_AFTER_ADD" + _run_hook "after_add" "$INPUT_AFTER_ADD_HOOK" # Check dirty state of repo again using git-diff. # (git-diff detects better if CRLF of files changes and does NOT # proceed, if only CRLF changes are detected. See #241 and #265 # for more details.) if [ -n "$(git diff --staged)" ] || "$INPUT_SKIP_DIRTY_CHECK"; then - _run_hook "before_commit" "$INPUT_BEFORE_COMMIT" + _run_hook "before_commit" "$INPUT_BEFORE_COMMIT_HOOK" _local_commit - _run_hook "after_commit" "$INPUT_AFTER_COMMIT" + _run_hook "after_commit" "$INPUT_AFTER_COMMIT_HOOK" if [ -n "$INPUT_TAG_NAME" ] || [ -n "$INPUT_TAGGING_MESSAGE" ]; then - _run_hook "before_tag" "$INPUT_BEFORE_TAG" + _run_hook "before_tag" "$INPUT_BEFORE_TAG_HOOK" _tag_commit - _run_hook "after_tag" "$INPUT_AFTER_TAG" + _run_hook "after_tag" "$INPUT_AFTER_TAG_HOOK" else _tag_commit fi if ! "$INPUT_SKIP_PUSH"; then - _run_hook "before_push" "$INPUT_BEFORE_PUSH" + _run_hook "before_push" "$INPUT_BEFORE_PUSH_HOOK" fi _push_to_github if ! "$INPUT_SKIP_PUSH"; then - _run_hook "after_push" "$INPUT_AFTER_PUSH" + _run_hook "after_push" "$INPUT_AFTER_PUSH_HOOK" fi else _set_github_output "changes_detected" "false" diff --git a/tests/git-auto-commit.bats b/tests/git-auto-commit.bats index fdb6ef1b..3572f574 100644 --- a/tests/git-auto-commit.bats +++ b/tests/git-auto-commit.bats @@ -43,14 +43,14 @@ setup() { export INPUT_CREATE_BRANCH=false export INPUT_DISABLE_PULL_REQUEST_TARGET_TRIGGER_WARNING=false export INPUT_INTERNAL_GIT_BINARY=git - export INPUT_BEFORE_ADD="" - export INPUT_AFTER_ADD="" - export INPUT_BEFORE_COMMIT="" - export INPUT_AFTER_COMMIT="" - export INPUT_BEFORE_TAG="" - export INPUT_AFTER_TAG="" - export INPUT_BEFORE_PUSH="" - export INPUT_AFTER_PUSH="" + export INPUT_BEFORE_ADD_HOOK="" + export INPUT_AFTER_ADD_HOOK="" + export INPUT_BEFORE_COMMIT_HOOK="" + export INPUT_AFTER_COMMIT_HOOK="" + export INPUT_BEFORE_TAG_HOOK="" + export INPUT_AFTER_TAG_HOOK="" + export INPUT_BEFORE_PUSH_HOOK="" + export INPUT_AFTER_PUSH_HOOK="" # Unset the GitHub event name by default so tests do not pick up # pull_request_target from the environment of the shell running BATS. @@ -1592,7 +1592,7 @@ END @test "It runs the before_add hook before git add" { touch "${FAKE_LOCAL_REPOSITORY}"/new-file-1.txt - export INPUT_BEFORE_ADD="echo BEFORE_ADD_RAN > '${FAKE_LOCAL_REPOSITORY}/before-add-marker.txt'" + export INPUT_BEFORE_ADD_HOOK="echo BEFORE_ADD_RAN > '${FAKE_LOCAL_REPOSITORY}/before-add-marker.txt'" run git_auto_commit @@ -1603,7 +1603,7 @@ END @test "It runs the after_add hook after git add" { touch "${FAKE_LOCAL_REPOSITORY}"/new-file-1.txt - export INPUT_AFTER_ADD="echo AFTER_ADD_RAN > '${FAKE_LOCAL_REPOSITORY}/after-add-marker.txt'" + export INPUT_AFTER_ADD_HOOK="echo AFTER_ADD_RAN > '${FAKE_LOCAL_REPOSITORY}/after-add-marker.txt'" run git_auto_commit @@ -1614,7 +1614,7 @@ END @test "It runs the before_commit hook before creating the commit" { touch "${FAKE_LOCAL_REPOSITORY}"/new-file-1.txt - export INPUT_BEFORE_COMMIT="echo BEFORE_COMMIT_RAN > '${FAKE_LOCAL_REPOSITORY}/before-commit-marker.txt'" + export INPUT_BEFORE_COMMIT_HOOK="echo BEFORE_COMMIT_RAN > '${FAKE_LOCAL_REPOSITORY}/before-commit-marker.txt'" run git_auto_commit @@ -1629,7 +1629,7 @@ END @test "It runs the after_commit hook after creating the commit" { touch "${FAKE_LOCAL_REPOSITORY}"/new-file-1.txt - export INPUT_AFTER_COMMIT="git rev-parse HEAD > '${FAKE_LOCAL_REPOSITORY}/after-commit-sha.txt'" + export INPUT_AFTER_COMMIT_HOOK="git rev-parse HEAD > '${FAKE_LOCAL_REPOSITORY}/after-commit-sha.txt'" run git_auto_commit @@ -1643,10 +1643,10 @@ END } @test "It does not run add or commit hooks when the working tree is clean" { - export INPUT_BEFORE_ADD="echo SHOULD_NOT_RUN > '${FAKE_LOCAL_REPOSITORY}/before-add-marker.txt'" - export INPUT_AFTER_ADD="echo SHOULD_NOT_RUN > '${FAKE_LOCAL_REPOSITORY}/after-add-marker.txt'" - export INPUT_BEFORE_COMMIT="echo SHOULD_NOT_RUN > '${FAKE_LOCAL_REPOSITORY}/before-commit-marker.txt'" - export INPUT_AFTER_COMMIT="echo SHOULD_NOT_RUN > '${FAKE_LOCAL_REPOSITORY}/after-commit-marker.txt'" + export INPUT_BEFORE_ADD_HOOK="echo SHOULD_NOT_RUN > '${FAKE_LOCAL_REPOSITORY}/before-add-marker.txt'" + export INPUT_AFTER_ADD_HOOK="echo SHOULD_NOT_RUN > '${FAKE_LOCAL_REPOSITORY}/after-add-marker.txt'" + export INPUT_BEFORE_COMMIT_HOOK="echo SHOULD_NOT_RUN > '${FAKE_LOCAL_REPOSITORY}/before-commit-marker.txt'" + export INPUT_AFTER_COMMIT_HOOK="echo SHOULD_NOT_RUN > '${FAKE_LOCAL_REPOSITORY}/after-commit-marker.txt'" run git_auto_commit @@ -1664,8 +1664,8 @@ END INPUT_TAGGING_MESSAGE="Release v1.0.0" touch "${FAKE_LOCAL_REPOSITORY}"/new-file-1.txt - export INPUT_BEFORE_TAG="echo BEFORE_TAG_RAN > '${FAKE_LOCAL_REPOSITORY}/before-tag-marker.txt'" - export INPUT_AFTER_TAG="echo AFTER_TAG_RAN > '${FAKE_LOCAL_REPOSITORY}/after-tag-marker.txt'" + export INPUT_BEFORE_TAG_HOOK="echo BEFORE_TAG_RAN > '${FAKE_LOCAL_REPOSITORY}/before-tag-marker.txt'" + export INPUT_AFTER_TAG_HOOK="echo AFTER_TAG_RAN > '${FAKE_LOCAL_REPOSITORY}/after-tag-marker.txt'" run git_auto_commit @@ -1678,8 +1678,8 @@ END @test "It does not run tag hooks when no tag name or tagging message is set" { touch "${FAKE_LOCAL_REPOSITORY}"/new-file-1.txt - export INPUT_BEFORE_TAG="echo SHOULD_NOT_RUN > '${FAKE_LOCAL_REPOSITORY}/before-tag-marker.txt'" - export INPUT_AFTER_TAG="echo SHOULD_NOT_RUN > '${FAKE_LOCAL_REPOSITORY}/after-tag-marker.txt'" + export INPUT_BEFORE_TAG_HOOK="echo SHOULD_NOT_RUN > '${FAKE_LOCAL_REPOSITORY}/before-tag-marker.txt'" + export INPUT_AFTER_TAG_HOOK="echo SHOULD_NOT_RUN > '${FAKE_LOCAL_REPOSITORY}/after-tag-marker.txt'" run git_auto_commit @@ -1695,8 +1695,8 @@ END INPUT_TAG_NAME="v1.0.0" INPUT_TAGGING_MESSAGE="Release v1.0.0" - export INPUT_BEFORE_TAG="echo BEFORE_TAG_RAN > '${FAKE_LOCAL_REPOSITORY}/before-tag-marker.txt'" - export INPUT_AFTER_TAG="echo AFTER_TAG_RAN > '${FAKE_LOCAL_REPOSITORY}/after-tag-marker.txt'" + export INPUT_BEFORE_TAG_HOOK="echo BEFORE_TAG_RAN > '${FAKE_LOCAL_REPOSITORY}/before-tag-marker.txt'" + export INPUT_AFTER_TAG_HOOK="echo AFTER_TAG_RAN > '${FAKE_LOCAL_REPOSITORY}/after-tag-marker.txt'" run git_auto_commit @@ -1709,8 +1709,8 @@ END @test "It runs before_push and after_push hooks around git push" { touch "${FAKE_LOCAL_REPOSITORY}"/new-file-1.txt - export INPUT_BEFORE_PUSH="echo BEFORE_PUSH_RAN > '${FAKE_LOCAL_REPOSITORY}/before-push-marker.txt'" - export INPUT_AFTER_PUSH="echo AFTER_PUSH_RAN > '${FAKE_LOCAL_REPOSITORY}/after-push-marker.txt'" + export INPUT_BEFORE_PUSH_HOOK="echo BEFORE_PUSH_RAN > '${FAKE_LOCAL_REPOSITORY}/before-push-marker.txt'" + export INPUT_AFTER_PUSH_HOOK="echo AFTER_PUSH_RAN > '${FAKE_LOCAL_REPOSITORY}/after-push-marker.txt'" run git_auto_commit @@ -1724,8 +1724,8 @@ END @test "It does not run push hooks when skip_push is true" { INPUT_SKIP_PUSH=true touch "${FAKE_LOCAL_REPOSITORY}"/new-file-1.txt - export INPUT_BEFORE_PUSH="echo SHOULD_NOT_RUN > '${FAKE_LOCAL_REPOSITORY}/before-push-marker.txt'" - export INPUT_AFTER_PUSH="echo SHOULD_NOT_RUN > '${FAKE_LOCAL_REPOSITORY}/after-push-marker.txt'" + export INPUT_BEFORE_PUSH_HOOK="echo SHOULD_NOT_RUN > '${FAKE_LOCAL_REPOSITORY}/before-push-marker.txt'" + export INPUT_AFTER_PUSH_HOOK="echo SHOULD_NOT_RUN > '${FAKE_LOCAL_REPOSITORY}/after-push-marker.txt'" run git_auto_commit @@ -1738,7 +1738,7 @@ END @test "A hook that exits non-zero aborts the action" { touch "${FAKE_LOCAL_REPOSITORY}"/new-file-1.txt - export INPUT_BEFORE_COMMIT="exit 1" + export INPUT_BEFORE_COMMIT_HOOK="exit 1" run git_auto_commit From cb97bc91c2a291755a52897fff55ef2c45a377ff Mon Sep 17 00:00:00 2001 From: Stefan Zweifel Date: Sun, 28 Jun 2026 10:07:43 +0200 Subject: [PATCH 10/11] refactor(hooks): include _hook suffix in event identifier The first arg to `_run_hook` (used as the event name in the debug log) also gets the `_hook` suffix so the log line matches the action input name exactly. Drop the redundant trailing 'hook' word from the log message, so it now reads e.g. '::debug::Running before_add_hook' instead of '::debug::Running before_add hook'. --- entrypoint.sh | 26 +++++++++++++------------- tests/git-auto-commit.bats | 30 +++++++++++++++--------------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/entrypoint.sh b/entrypoint.sh index c5e567b5..3c8ddd06 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -31,7 +31,7 @@ _run_hook() { local snippet=${2:-} if [ -n "$snippet" ]; then - _log "debug" "Running $name hook"; + _log "debug" "Running $name"; eval "$snippet" fi } @@ -52,19 +52,19 @@ _main() { _set_github_output "create_git_tag_only" "true" if [ -n "$INPUT_TAG_NAME" ] || [ -n "$INPUT_TAGGING_MESSAGE" ]; then - _run_hook "before_tag" "$INPUT_BEFORE_TAG_HOOK" + _run_hook "before_tag_hook" "$INPUT_BEFORE_TAG_HOOK" _tag_commit - _run_hook "after_tag" "$INPUT_AFTER_TAG_HOOK" + _run_hook "after_tag_hook" "$INPUT_AFTER_TAG_HOOK" else _tag_commit fi if ! "$INPUT_SKIP_PUSH"; then - _run_hook "before_push" "$INPUT_BEFORE_PUSH_HOOK" + _run_hook "before_push_hook" "$INPUT_BEFORE_PUSH_HOOK" fi _push_to_github if ! "$INPUT_SKIP_PUSH"; then - _run_hook "after_push" "$INPUT_AFTER_PUSH_HOOK" + _run_hook "after_push_hook" "$INPUT_AFTER_PUSH_HOOK" fi elif _git_is_dirty || "$INPUT_SKIP_DIRTY_CHECK"; then @@ -72,33 +72,33 @@ _main() { _switch_to_branch - _run_hook "before_add" "$INPUT_BEFORE_ADD_HOOK" + _run_hook "before_add_hook" "$INPUT_BEFORE_ADD_HOOK" _add_files - _run_hook "after_add" "$INPUT_AFTER_ADD_HOOK" + _run_hook "after_add_hook" "$INPUT_AFTER_ADD_HOOK" # Check dirty state of repo again using git-diff. # (git-diff detects better if CRLF of files changes and does NOT # proceed, if only CRLF changes are detected. See #241 and #265 # for more details.) if [ -n "$(git diff --staged)" ] || "$INPUT_SKIP_DIRTY_CHECK"; then - _run_hook "before_commit" "$INPUT_BEFORE_COMMIT_HOOK" + _run_hook "before_commit_hook" "$INPUT_BEFORE_COMMIT_HOOK" _local_commit - _run_hook "after_commit" "$INPUT_AFTER_COMMIT_HOOK" + _run_hook "after_commit_hook" "$INPUT_AFTER_COMMIT_HOOK" if [ -n "$INPUT_TAG_NAME" ] || [ -n "$INPUT_TAGGING_MESSAGE" ]; then - _run_hook "before_tag" "$INPUT_BEFORE_TAG_HOOK" + _run_hook "before_tag_hook" "$INPUT_BEFORE_TAG_HOOK" _tag_commit - _run_hook "after_tag" "$INPUT_AFTER_TAG_HOOK" + _run_hook "after_tag_hook" "$INPUT_AFTER_TAG_HOOK" else _tag_commit fi if ! "$INPUT_SKIP_PUSH"; then - _run_hook "before_push" "$INPUT_BEFORE_PUSH_HOOK" + _run_hook "before_push_hook" "$INPUT_BEFORE_PUSH_HOOK" fi _push_to_github if ! "$INPUT_SKIP_PUSH"; then - _run_hook "after_push" "$INPUT_AFTER_PUSH_HOOK" + _run_hook "after_push_hook" "$INPUT_AFTER_PUSH_HOOK" fi else _set_github_output "changes_detected" "false" diff --git a/tests/git-auto-commit.bats b/tests/git-auto-commit.bats index 3572f574..562e5cea 100644 --- a/tests/git-auto-commit.bats +++ b/tests/git-auto-commit.bats @@ -1597,7 +1597,7 @@ END run git_auto_commit assert_success - assert_line "::debug::Running before_add hook" + assert_line "::debug::Running before_add_hook" [ -f "${FAKE_LOCAL_REPOSITORY}/before-add-marker.txt" ] } @@ -1608,7 +1608,7 @@ END run git_auto_commit assert_success - assert_line "::debug::Running after_add hook" + assert_line "::debug::Running after_add_hook" [ -f "${FAKE_LOCAL_REPOSITORY}/after-add-marker.txt" ] } @@ -1619,7 +1619,7 @@ END run git_auto_commit assert_success - assert_line "::debug::Running before_commit hook" + assert_line "::debug::Running before_commit_hook" [ -f "${FAKE_LOCAL_REPOSITORY}/before-commit-marker.txt" ] # The marker was created after `git add` ran, so it is NOT in the commit. @@ -1634,7 +1634,7 @@ END run git_auto_commit assert_success - assert_line "::debug::Running after_commit hook" + assert_line "::debug::Running after_commit_hook" [ -f "${FAKE_LOCAL_REPOSITORY}/after-commit-sha.txt" ] # Sanity check: the SHA written is a valid commit hash @@ -1670,8 +1670,8 @@ END run git_auto_commit assert_success - assert_line "::debug::Running before_tag hook" - assert_line "::debug::Running after_tag hook" + assert_line "::debug::Running before_tag_hook" + assert_line "::debug::Running after_tag_hook" [ -f "${FAKE_LOCAL_REPOSITORY}/before-tag-marker.txt" ] [ -f "${FAKE_LOCAL_REPOSITORY}/after-tag-marker.txt" ] } @@ -1684,8 +1684,8 @@ END run git_auto_commit assert_success - refute_line "::debug::Running before_tag hook" - refute_line "::debug::Running after_tag hook" + refute_line "::debug::Running before_tag_hook" + refute_line "::debug::Running after_tag_hook" [ ! -f "${FAKE_LOCAL_REPOSITORY}/before-tag-marker.txt" ] [ ! -f "${FAKE_LOCAL_REPOSITORY}/after-tag-marker.txt" ] } @@ -1701,8 +1701,8 @@ END run git_auto_commit assert_success - assert_line "::debug::Running before_tag hook" - assert_line "::debug::Running after_tag hook" + assert_line "::debug::Running before_tag_hook" + assert_line "::debug::Running after_tag_hook" [ -f "${FAKE_LOCAL_REPOSITORY}/before-tag-marker.txt" ] [ -f "${FAKE_LOCAL_REPOSITORY}/after-tag-marker.txt" ] } @@ -1715,8 +1715,8 @@ END run git_auto_commit assert_success - assert_line "::debug::Running before_push hook" - assert_line "::debug::Running after_push hook" + assert_line "::debug::Running before_push_hook" + assert_line "::debug::Running after_push_hook" [ -f "${FAKE_LOCAL_REPOSITORY}/before-push-marker.txt" ] [ -f "${FAKE_LOCAL_REPOSITORY}/after-push-marker.txt" ] } @@ -1730,8 +1730,8 @@ END run git_auto_commit assert_success - refute_line "::debug::Running before_push hook" - refute_line "::debug::Running after_push hook" + refute_line "::debug::Running before_push_hook" + refute_line "::debug::Running after_push_hook" [ ! -f "${FAKE_LOCAL_REPOSITORY}/before-push-marker.txt" ] [ ! -f "${FAKE_LOCAL_REPOSITORY}/after-push-marker.txt" ] } @@ -1743,7 +1743,7 @@ END run git_auto_commit assert_failure - assert_line "::debug::Running before_commit hook" + assert_line "::debug::Running before_commit_hook" # Assert the action aborted before committing: no commit_hash output was written. run cat_github_output From 79a7b0c86cd018ee3c04801a96e796b79c17d9b6 Mon Sep 17 00:00:00 2001 From: Stefan Zweifel Date: Sun, 28 Jun 2026 10:46:03 +0200 Subject: [PATCH 11/11] docs(hooks): add notes about security implications of hooks --- README.md | 31 +++++++++++++++++++++++++++++++ tests/git-auto-commit.bats | 13 +++++++++++++ 2 files changed, 44 insertions(+) diff --git a/README.md b/README.md index e815412c..34371900 100644 --- a/README.md +++ b/README.md @@ -280,6 +280,35 @@ Multi-line snippets work via YAML's `|` block scalar: - Snippets run under `set -eu`. Referencing an unset variable aborts the action; use `${VAR:-}` to default an optional variable to empty. +### Security + +Hook snippets are evaluated as shell code in the same process as the +action. Treat them as you would any `run:` step. + +> [!CAUTION] +> **Do not combine hooks with the `pull_request_target` event when the +> snippet references attacker-controlled GitHub context.** Fields like +> `${{ github.event.pull_request.title }}`, `${{ github.event.pull_request.body }}`, +> `${{ github.head_ref }}`, and commit messages from a fork are +> interpolated into the snippet **before** bash sees it. A malicious PR +> can inject shell commands that run on your runner with access to your +> repository secrets. See the [`pull_request_target` section](#workflow-should-run-in-base-repository) +> for the broader risk and [GitHub's script-injection guidance](https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#understanding-the-risk-of-script-injections). + +If you need values from PR-controlled context inside a hook, pass them +via an intermediate env var rather than interpolating them directly into +the snippet: + +```yaml +- uses: stefanzweifel/git-auto-commit-action@v7 + env: + PR_TITLE: ${{ github.event.pull_request.title }} + with: + before_commit_hook: | + # $PR_TITLE is read as data, not evaluated as code + echo "PR: $PR_TITLE" +``` + ## Limitations & Gotchas The goal of this Action is to be "the Action for committing files for the 80% use case". Therefore, you might run into issues if your Workflow falls into the not supported 20% portion. @@ -433,6 +462,8 @@ However, there are a couple of ways to use this Action in Workflows that should > > To remind users of this risk, git-auto-commit emits a warning annotation whenever it detects it is running on a `pull_request_target` event. > If you have evaluated the risk and want to silence the warning, set the `disable_pull_request_target_trigger_warning` input to `true`. +> +> **Extra caution if you also use [hooks](#hooks):** hook snippets are evaluated as shell code. Interpolating attacker-controlled fields (PR title/body, branch name, fork commit messages, etc.) directly into a hook input on a `pull_request_target` workflow lets a malicious PR run arbitrary commands on your runner with access to your secrets. Pass such values through an `env:` block and reference them as `$VARS` inside the snippet — see the [Security note in the Hooks section](#security) for an example. The workflow below runs whenever a commit is pushed to the `main`-branch or when activity on a pull request happens, by listening to the [`pull_request_target`](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target) event. diff --git a/tests/git-auto-commit.bats b/tests/git-auto-commit.bats index 562e5cea..6b0c3100 100644 --- a/tests/git-auto-commit.bats +++ b/tests/git-auto-commit.bats @@ -1749,3 +1749,16 @@ END run cat_github_output refute_line -e "commit_hash=[0-9a-f]{40}$" } + +@test "A before_commit hook can mutate INPUT_COMMIT_MESSAGE and the change is reflected in the commit" { + touch "${FAKE_LOCAL_REPOSITORY}"/new-file-1.txt + export INPUT_BEFORE_COMMIT_HOOK="INPUT_COMMIT_MESSAGE='message rewritten by hook'" + + run git_auto_commit + + assert_success + assert_line "::debug::Running before_commit_hook" + + run git -C "${FAKE_LOCAL_REPOSITORY}" log -1 --pretty=%B + assert_line "message rewritten by hook" +}