From 8a9fc9982edb06493df0bf26ea2a089ab8f185eb Mon Sep 17 00:00:00 2001 From: ChristophShyper <45788587+ChristophShyper@users.noreply.github.com> Date: Sat, 23 May 2026 14:25:18 +0200 Subject: [PATCH 1/4] feat: add repository and repository_path inputs for enhanced configuration --- README.md | 55 +++++--- Taskfile.scripts.yml | 14 +- action.yml | 8 ++ entrypoint.sh | 81 +++++++++-- tests/unit/test_repository_configuration.sh | 133 +++++++++++++++++++ tests/unit/test_template_source_selection.sh | 42 ++++-- 6 files changed, 283 insertions(+), 50 deletions(-) create mode 100755 tests/unit/test_repository_configuration.sh diff --git a/README.md b/README.md index 089c413..094fb0f 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,12 @@ ## 🔗 Related Actions **Useful in combination with my other action [devops-infra/action-commit-push](https://github.com/devops-infra/action-commit-push).** +Both actions are compatible when you use `actions/checkout` with a custom `path`: +- set `repository_path` in `devops-infra/action-commit-push` +- set the same `repository_path` in `devops-infra/action-pull-request` + +This action isolates global Git config in a temporary file (via `GIT_CONFIG_GLOBAL`) to avoid modifying runner/user-level Git config. + ## 📊 Badges [ @@ -52,6 +58,8 @@ This action supports three tag levels for flexible versioning: uses: devops-infra/action-pull-request@v1.1.3 with: github_token: ${{ secrets.GITHUB_TOKEN }} + repository: devops-infra/action-pull-request + repository_path: . source_branch: development target_branch: master title: My pull request @@ -73,26 +81,28 @@ This action supports three tag levels for flexible versioning: ### 🔧 Input Parameters -| Input Variable | Required | Default | Description | -|-----------------|----------|-------------------------------|-------------------------------------------------------------------------------------------------------------------------| -| `github_token` | **Yes** | `""` | GitHub token `${{ secrets.GITHUB_TOKEN }}` | -| `source_branch` | No | *current branch* | Name of the source branch | -| `target_branch` | No | `master` | Name of the target branch. Change it if you use `main` | -| `title` | No | *subject of the first commit* | Pull request title | -| `template` | No | `""` | Template file location | -| `body` | No | *list of commits* | Pull request body | -| `reviewer` | No | `""` | Reviewer's username | -| `assignee` | No | `""` | Assignee's usernames | -| `label` | No | `""` | Labels to apply, comma separated | -| `milestone` | No | `""` | Milestone | -| `draft` | No | `false` | Whether to mark it as a draft | -| `old_string` | No | `""` | Old string for the replacement in the template | -| `new_string` | No | `""` | New string for the replacement in the template. If not specified, but `old_string` was, it will gather commits subjects | -| `get_diff` | No | `false` | Whether to replace predefined comments with differences between branches - see details below | -| `ignore_users` | No | `"dependabot"` | List of users to ignore, comma separated | -| `allow_no_diff` | No | `false` | Allows to continue on merge commits with no diffs | -| `max_body_bytes` | No | `65000` | Maximum PR body size in bytes before overflow is posted as managed PR comments | -| `max_diff_lines` | No | `0` | Maximum lines per generated diff section (`0` means unlimited) | +| Input Variable | Required | Default | Description | +|-------------------|----------|-------------------------------|-------------------------------------------------------------------------------------------------------------------------| +| `github_token` | **Yes** | `""` | GitHub token `${{ secrets.GITHUB_TOKEN }}` | +| `repository` | No | `${{ github.repository }}` | Target repository in `owner/name` format used for API calls and git remote auth | +| `repository_path` | No | `.` | Relative path under `GITHUB_WORKSPACE` to the checked-out repository | +| `source_branch` | No | *current branch* | Name of the source branch | +| `target_branch` | No | `master` | Name of the target branch. Change it if you use `main` | +| `title` | No | *subject of the first commit* | Pull request title | +| `template` | No | `""` | Template file location | +| `body` | No | *list of commits* | Pull request body | +| `reviewer` | No | `""` | Reviewer's username | +| `assignee` | No | `""` | Assignee's usernames | +| `label` | No | `""` | Labels to apply, comma separated | +| `milestone` | No | `""` | Milestone | +| `draft` | No | `false` | Whether to mark it as a draft | +| `old_string` | No | `""` | Old string for the replacement in the template | +| `new_string` | No | `""` | New string for the replacement in the template. If not specified, but `old_string` was, it will gather commits subjects | +| `get_diff` | No | `false` | Whether to replace predefined comments with differences between branches - see details below | +| `ignore_users` | No | `"dependabot"` | List of users to ignore, comma separated | +| `allow_no_diff` | No | `false` | Allows to continue on merge commits with no diffs | +| `max_body_bytes` | No | `65000` | Maximum PR body size in bytes before overflow is posted as managed PR comments | +| `max_diff_lines` | No | `0` | Maximum lines per generated diff section (`0` means unlimited) | ### 🔐 Required Workflow Permissions @@ -195,12 +205,15 @@ jobs: uses: actions/checkout@v5 with: fetch-depth: 0 + path: repo - name: Run the Action if: startsWith(github.ref, 'refs/heads/feature') uses: devops-infra/action-pull-request@v1.1.3 with: github_token: ${{ secrets.GITHUB_TOKEN }} + repository: ${{ github.repository }} + repository_path: repo title: ${{ github.event.commits[0].message }} assignee: ${{ github.actor }} label: automatic,feature @@ -236,7 +249,7 @@ jobs: - uses: devops-infra/action-pull-request@v1.1.3 id: Pin patch version - - uses: devops-infra/action-pull-request@v1.0 + - uses: devops-infra/action-pull-request@v1.1 id: Pin minor version - uses: devops-infra/action-pull-request@v1 diff --git a/Taskfile.scripts.yml b/Taskfile.scripts.yml index 64f67d0..1d9bf48 100644 --- a/Taskfile.scripts.yml +++ b/Taskfile.scripts.yml @@ -102,13 +102,13 @@ tasks: - echo Updating full version from {{.VERSION_FROM_ACTION_YML}} to {{.VERSION}} - echo Updating minor version from {{.MINOR_FROM_ACTION_YML}} to {{.VERSION_MINOR}} - echo Updating major version from {{.MAJOR_FROM_ACTION_YML}} to {{.VERSION_MAJOR}} - - "{{.SED}} -i 's#{{.DOCKER_NAME}}:{{.VERSION_FROM_ACTION_YML}}#{{.DOCKER_NAME}}:{{.VERSION}}#g' action.yml" - - "{{.SED}} -i 's#{{.DOCKER_NAME}}@{{.VERSION_FROM_ACTION_YML}}#{{.DOCKER_NAME}}@{{.VERSION}}#g' README.md" - - "{{.SED}} -i 's#{{.GITHUB_NAME}}@{{.VERSION_FROM_ACTION_YML}}#{{.GITHUB_NAME}}@{{.VERSION}}#g' README.md" - - "{{.SED}} -i 's#{{.DOCKER_NAME}}@{{.MINOR_FROM_ACTION_YML}}#{{.DOCKER_NAME}}@{{.VERSION_MINOR}}#g' README.md" - - "{{.SED}} -i 's#{{.GITHUB_NAME}}@{{.MINOR_FROM_ACTION_YML}}#{{.GITHUB_NAME}}@{{.VERSION_MINOR}}#g' README.md" - - "{{.SED}} -i 's#{{.DOCKER_NAME}}@{{.MAJOR_FROM_ACTION_YML}}#{{.DOCKER_NAME}}@{{.VERSION_MAJOR}}#g' README.md" - - "{{.SED}} -i 's#{{.GITHUB_NAME}}@{{.MAJOR_FROM_ACTION_YML}}#{{.GITHUB_NAME}}@{{.VERSION_MAJOR}}#g' README.md" + - "{{.SED}} -E -i 's#({{.DOCKER_NAME}}:)v[0-9]+\\.[0-9]+\\.[0-9]+#\\1{{.VERSION}}#g' action.yml" + - "{{.SED}} -E -i 's#({{.DOCKER_NAME}}@)v[0-9]+\\.[0-9]+\\.[0-9]+([^0-9.]|$)#\\1{{.VERSION}}\\2#g' README.md" + - "{{.SED}} -E -i 's#({{.GITHUB_NAME}}@)v[0-9]+\\.[0-9]+\\.[0-9]+([^0-9.]|$)#\\1{{.VERSION}}\\2#g' README.md" + - "{{.SED}} -E -i 's#({{.DOCKER_NAME}}@)v[0-9]+\\.[0-9]+([^0-9.]|$)#\\1{{.VERSION_MINOR}}\\2#g' README.md" + - "{{.SED}} -E -i 's#({{.GITHUB_NAME}}@)v[0-9]+\\.[0-9]+([^0-9.]|$)#\\1{{.VERSION_MINOR}}\\2#g' README.md" + - "{{.SED}} -E -i 's#({{.DOCKER_NAME}}@)v[0-9]+([^0-9.]|$)#\\1{{.VERSION_MAJOR}}\\2#g' README.md" + - "{{.SED}} -E -i 's#({{.GITHUB_NAME}}@)v[0-9]+([^0-9.]|$)#\\1{{.VERSION_MAJOR}}\\2#g' README.md" version:resolve-next: desc: Resolve next version from bump type and profile diff --git a/action.yml b/action.yml index 5b3d7f9..6ad28ee 100644 --- a/action.yml +++ b/action.yml @@ -5,6 +5,14 @@ inputs: github_token: description: GitHub token required: true + repository: + description: Repository in owner/name format used for API calls and git remote auth (defaults to current repository) + required: false + default: "" + repository_path: + description: Relative path under GITHUB_WORKSPACE to the checked-out repository (use when actions/checkout path is set) + required: false + default: "." source_branch: description: Name of the source branch required: false diff --git a/entrypoint.sh b/entrypoint.sh index 1475aaa..a30edec 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -18,6 +18,10 @@ OVERFLOW_CHUNK_PREFIX="/tmp/template-overflow-chunk" CHUNK_COUNT=0 MANAGED_COMMENT_START="" MANAGED_COMMENT_END="" +TARGET_REPOSITORY="" +REPOSITORY_PATH="" +WORKSPACE_DIR="" +REPO_DIR="" REPLACE_TEMPLATE_SCRIPT="/scripts/replace-template-diff.sh" if [[ ! -x "${REPLACE_TEMPLATE_SCRIPT}" ]]; then @@ -138,11 +142,21 @@ split_template_by_bytes() { python3 "${SPLIT_CONTENT_SCRIPT}" "${input_file}" "${main_output_file}" "${chunk_prefix}" "${max_main_bytes}" "${max_comment_bytes}" } +resolve_path() { + local input_path="$1" + python3 - "$input_path" <<'PY' +import pathlib +import sys + +print(pathlib.Path(sys.argv[1]).resolve(strict=False)) +PY +} + get_managed_comment_ids() { local pr_number="$1" local output_file="$2" - gh api "repos/${GITHUB_REPOSITORY}/issues/${pr_number}/comments" --paginate | jq -r \ + gh api "repos/${TARGET_REPOSITORY}/issues/${pr_number}/comments" --paginate | jq -r \ --arg start "${MANAGED_COMMENT_START}" \ --arg end "${MANAGED_COMMENT_END}" \ 'if type == "array" then .[] else . end @@ -172,16 +186,16 @@ reconcile_managed_comments() { if (( idx <= ${#existing_ids[@]} )); then local comment_id="${existing_ids[$((idx-1))]}" - gh api --method PATCH "repos/${GITHUB_REPOSITORY}/issues/comments/${comment_id}" --field "body=@${comment_file}" >/dev/null + gh api --method PATCH "repos/${TARGET_REPOSITORY}/issues/comments/${comment_id}" --field "body=@${comment_file}" >/dev/null else - gh api --method POST "repos/${GITHUB_REPOSITORY}/issues/${pr_number}/comments" --field "body=@${comment_file}" >/dev/null + gh api --method POST "repos/${TARGET_REPOSITORY}/issues/${pr_number}/comments" --field "body=@${comment_file}" >/dev/null fi done if (( ${#existing_ids[@]} > chunk_count )); then for ((idx=chunk_count+1; idx<=${#existing_ids[@]}; idx++)); do local stale_id="${existing_ids[$((idx-1))]}" - gh api --method DELETE "repos/${GITHUB_REPOSITORY}/issues/comments/${stale_id}" >/dev/null + gh api --method DELETE "repos/${TARGET_REPOSITORY}/issues/comments/${stale_id}" >/dev/null done fi } @@ -212,6 +226,8 @@ apply_body_limits() { } echo "Inputs:" +echo " repository: ${INPUT_REPOSITORY:-}" +echo " repository_path: ${INPUT_REPOSITORY_PATH:-.}" echo " source_branch: ${INPUT_SOURCE_BRANCH}" echo " target_branch: ${INPUT_TARGET_BRANCH}" echo " title: ${INPUT_TITLE}" @@ -264,15 +280,60 @@ if [[ -z "${INPUT_GITHUB_TOKEN}" ]]; then exit 1 fi +TARGET_REPOSITORY="${INPUT_REPOSITORY:-${GITHUB_REPOSITORY}}" +if [[ -z "${TARGET_REPOSITORY}" ]]; then + echo -e "\n[ERROR] Unable to resolve repository. Set input 'repository' or ensure GITHUB_REPOSITORY is available." >&2 + exit 1 +fi +if [[ ! "${TARGET_REPOSITORY}" =~ ^[^/]+/[^/]+$ ]]; then + echo -e "\n[ERROR] Input 'repository' must use owner/name format. Got: ${TARGET_REPOSITORY}" >&2 + exit 1 +fi + +REPOSITORY_PATH="${INPUT_REPOSITORY_PATH:-.}" +if [[ -z "${REPOSITORY_PATH}" ]]; then + REPOSITORY_PATH="." +fi +if [[ "${REPOSITORY_PATH}" == /* ]]; then + echo -e "\n[ERROR] Input 'repository_path' must be a relative path under GITHUB_WORKSPACE." >&2 + exit 1 +fi + +WORKSPACE_DIR="$(resolve_path "${GITHUB_WORKSPACE}")" +REPO_DIR="$(resolve_path "${GITHUB_WORKSPACE}/${REPOSITORY_PATH}")" +if [[ "${REPO_DIR}" != "${WORKSPACE_DIR}" && "${REPO_DIR}" != "${WORKSPACE_DIR}"/* ]]; then + echo -e "\n[ERROR] Input 'repository_path' resolves outside GITHUB_WORKSPACE." >&2 + exit 1 +fi +if [[ ! -d "${REPO_DIR}" ]]; then + echo -e "\n[ERROR] Repository path does not exist: ${REPO_DIR}" >&2 + exit 1 +fi + +if ! git -C "${REPO_DIR}" rev-parse --is-inside-work-tree >/dev/null 2>&1; then + echo -e "\n[ERROR] Path is not a git repository: ${REPO_DIR}" >&2 + exit 1 +fi + echo -e "\nSetting GitHub credentials..." +# Keep all global git config isolated to a temporary file +export GIT_CONFIG_GLOBAL +GIT_CONFIG_GLOBAL="$(mktemp /tmp/action-pull-request-git-config-XXXXXX)" +trap 'rm -f "${GIT_CONFIG_GLOBAL}"' EXIT + # Prevents issues with: fatal: unsafe repository ('/github/workspace' is owned by someone else) -git config --global --add safe.directory "${GITHUB_WORKSPACE}" +git config --global --add safe.directory "${WORKSPACE_DIR}" git config --global --add safe.directory /github/workspace -git remote set-url origin "https://${GITHUB_ACTOR}:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}" -git config --global user.name "${GITHUB_ACTOR}" -git config --global user.email "${GITHUB_ACTOR}@users.noreply.github.com" +git config --global --add safe.directory "${REPO_DIR}" +git -C "${REPO_DIR}" remote set-url origin "https://${GITHUB_ACTOR}:${GITHUB_TOKEN}@github.com/${TARGET_REPOSITORY}" +git -C "${REPO_DIR}" config user.name "${GITHUB_ACTOR}" +git -C "${REPO_DIR}" config user.email "${GITHUB_ACTOR}@users.noreply.github.com" # Needed for hub binary export GITHUB_USER="${GITHUB_ACTOR}" +echo "Repository: ${TARGET_REPOSITORY}" +echo "Repository path: ${REPO_DIR}" + +cd "${REPO_DIR}" echo -e "\nSetting branches..." SOURCE_BRANCH="${INPUT_SOURCE_BRANCH:-$(git symbolic-ref --short -q HEAD)}" @@ -339,7 +400,7 @@ else TEMPLATE="${INPUT_BODY}" else echo "Template source: existing pull request body" - TEMPLATE=$(hub api --method GET "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" | jq -r '.body') + TEMPLATE=$(hub api --method GET "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}" | jq -r '.body') fi fi @@ -466,7 +527,7 @@ if [[ -z "${PR_NUMBER}" ]]; then fi else echo -e "\nUpdating pull request" - COMMAND="hub api --method PATCH repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER} --field 'body=@/tmp/template'" + COMMAND="hub api --method PATCH repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER} --field 'body=@/tmp/template'" echo -e "Running: ${COMMAND}" URL=$(sh -c "${COMMAND} | jq -r '.html_url'") # shellcheck disable=SC2181 diff --git a/tests/unit/test_repository_configuration.sh b/tests/unit/test_repository_configuration.sh new file mode 100755 index 0000000..be28b97 --- /dev/null +++ b/tests/unit/test_repository_configuration.sh @@ -0,0 +1,133 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)" +SCRIPT_PATH="${SCRIPT_DIR}/../../entrypoint.sh" +TMP_DIR="$(mktemp -d)" +trap 'rm -rf "${TMP_DIR}"' EXIT + +assert_contains() { + local file_path="$1" + local expected="$2" + if ! grep -Fq -- "${expected}" "${file_path}"; then + echo "Assertion failed. Expected to find: ${expected}" >&2 + echo "----- FILE CONTENT -----" >&2 + cat "${file_path}" >&2 + exit 1 + fi +} + +WORKSPACE_DIR="${TMP_DIR}/workspace" +REPOSITORY_SUBPATH="repo" +REPO_DIR="${WORKSPACE_DIR}/${REPOSITORY_SUBPATH}" +EXPECTED_REPO_DIR="$(python3 - "${REPO_DIR}" <<'PY' +import pathlib +import sys + +print(pathlib.Path(sys.argv[1]).resolve(strict=False)) +PY +)" +mkdir -p "${REPO_DIR}" "${TMP_DIR}/home" "${TMP_DIR}/bin" + +git init --initial-branch=main "${REPO_DIR}" >/dev/null +pushd "${REPO_DIR}" >/dev/null +git config user.name "tester" +git config user.email "tester@example.com" +printf 'x\n' > README.md +git add README.md +git commit -m "init" >/dev/null +git branch develop +git update-ref refs/remotes/origin/main refs/heads/main +git update-ref refs/remotes/origin/develop refs/heads/develop +git remote add origin . +popd >/dev/null + +cat > "${TMP_DIR}/bin/git" <<'EOF' +#!/usr/bin/env bash +set -Eeuo pipefail + +REAL_GIT="${REAL_GIT:-/usr/bin/git}" +CALLS_LOG="${GIT_WRAPPER_CALLS_LOG:?}" +ENV_LOG="${GIT_WRAPPER_ENV_LOG:?}" + +printf '%s\n' "$*" >> "${CALLS_LOG}" +printf '%s\n' "${GIT_CONFIG_GLOBAL:-}" >> "${ENV_LOG}" + +if [[ "$#" -ge 2 && "$1" == "remote" && "$2" == "set-url" ]]; then + exit 0 +fi + +if [[ "$#" -ge 1 && "$1" == "fetch" ]]; then + exec "${REAL_GIT}" fetch . '+refs/heads/*:refs/heads/*' --update-head-ok +fi + +exec "${REAL_GIT}" "$@" +EOF +chmod +x "${TMP_DIR}/bin/git" + +LOG_FILE="${TMP_DIR}/run.log" +CALLS_LOG="${TMP_DIR}/git-calls.log" +ENV_LOG="${TMP_DIR}/git-env.log" + +set +e +( + cd "${REPO_DIR}" + PATH="${TMP_DIR}/bin:${PATH}" \ + REAL_GIT="$(command -v git)" \ + GIT_WRAPPER_CALLS_LOG="${CALLS_LOG}" \ + GIT_WRAPPER_ENV_LOG="${ENV_LOG}" \ + HOME="${TMP_DIR}/home" \ + GITHUB_ACTOR="ci-user" \ + GITHUB_TOKEN="token" \ + GITHUB_REPOSITORY="owner/workflow-repo" \ + GITHUB_WORKSPACE="${WORKSPACE_DIR}" \ + GITHUB_OUTPUT="${TMP_DIR}/output.txt" \ + INPUT_GITHUB_TOKEN="token" \ + INPUT_REPOSITORY="octo/demo" \ + INPUT_REPOSITORY_PATH="${REPOSITORY_SUBPATH}" \ + INPUT_SOURCE_BRANCH="develop" \ + INPUT_TARGET_BRANCH="main" \ + INPUT_TITLE="" \ + INPUT_TEMPLATE="" \ + INPUT_BODY="" \ + INPUT_REVIEWER="" \ + INPUT_ASSIGNEE="" \ + INPUT_LABEL="" \ + INPUT_MILESTONE="" \ + INPUT_DRAFT="false" \ + INPUT_GET_DIFF="false" \ + INPUT_OLD_STRING="" \ + INPUT_NEW_STRING="" \ + INPUT_IGNORE_USERS="dependabot" \ + INPUT_ALLOW_NO_DIFF="false" \ + INPUT_MAX_BODY_BYTES="65000" \ + INPUT_MAX_DIFF_LINES="0" \ + bash "${SCRIPT_PATH}" >"${LOG_FILE}" 2>&1 +) +STATUS="$?" +set -e + +if [[ "${STATUS}" != "0" ]]; then + echo "Expected successful execution with repository/repository_path inputs" >&2 + cat "${LOG_FILE}" >&2 + exit 1 +fi + +assert_contains "${LOG_FILE}" "Repository: octo/demo" +assert_contains "${LOG_FILE}" "Repository path: ${EXPECTED_REPO_DIR}" +assert_contains "${CALLS_LOG}" "remote set-url origin https://ci-user:token@github.com/octo/demo" + +if [[ -f "${TMP_DIR}/home/.gitconfig" ]]; then + echo "Expected HOME git config to stay untouched" >&2 + cat "${TMP_DIR}/home/.gitconfig" >&2 + exit 1 +fi + +if ! grep -Eq '^/tmp/action-pull-request-git-config-' "${ENV_LOG}"; then + echo "Expected isolated GIT_CONFIG_GLOBAL path in git wrapper env log" >&2 + cat "${ENV_LOG}" >&2 + exit 1 +fi + +echo "Repository configuration test passed." diff --git a/tests/unit/test_template_source_selection.sh b/tests/unit/test_template_source_selection.sh index 4e3eaf2..46386c7 100755 --- a/tests/unit/test_template_source_selection.sh +++ b/tests/unit/test_template_source_selection.sh @@ -19,56 +19,73 @@ assert_contains() { } mkdir -p "${TMP_DIR}/bin" +mkdir -p "${TMP_DIR}/repo" cat > "${TMP_DIR}/bin/git" <<'EOF' #!/usr/bin/env bash set -Eeuo pipefail -if [[ "$#" -ge 2 && "$1" == "config" && "$2" == "--global" ]]; then +args=("$@") +if [[ "${#args[@]}" -ge 2 && "${args[0]}" == "-C" ]]; then + args=("${args[@]:2}") +fi + +if [[ "${#args[@]}" -ge 2 && "${args[0]}" == "config" && "${args[1]}" == "--global" ]]; then + exit 0 +fi + +if [[ "${#args[@]}" -ge 1 && "${args[0]}" == "config" ]]; then + exit 0 +fi + +if [[ "${#args[@]}" -ge 2 && "${args[0]}" == "remote" && "${args[1]}" == "set-url" ]]; then exit 0 fi -if [[ "$#" -ge 2 && "$1" == "remote" && "$2" == "set-url" ]]; then +if [[ "${#args[@]}" -ge 1 && "${args[0]}" == "fetch" ]]; then exit 0 fi -if [[ "$#" -ge 1 && "$1" == "fetch" ]]; then +if [[ "${#args[@]}" -ge 2 && "${args[0]}" == "rev-parse" && "${args[1]}" == "--is-inside-work-tree" ]]; then + echo "true" exit 0 fi -if [[ "$#" -ge 2 && "$1" == "show-ref" ]]; then - if [[ "${@: -1}" == "refs/remotes/origin/develop" || "${@: -1}" == "refs/remotes/origin/release/MAPL-v3" ]]; then +if [[ "${#args[@]}" -ge 2 && "${args[0]}" == "show-ref" ]]; then + last_arg="${args[$((${#args[@]} - 1))]}" + if [[ "${last_arg}" == "refs/remotes/origin/develop" || "${last_arg}" == "refs/remotes/origin/release/MAPL-v3" ]]; then exit 0 fi exit 1 fi -if [[ "$#" -ge 2 && "$1" == "rev-parse" ]]; then - if [[ "${@: -1}" == "origin/develop" ]]; then +if [[ "${#args[@]}" -ge 2 && "${args[0]}" == "rev-parse" ]]; then + last_arg="${args[$((${#args[@]} - 1))]}" + if [[ "${last_arg}" == "origin/develop" ]]; then echo "bbb222" exit 0 fi - if [[ "${@: -1}" == "origin/release/MAPL-v3" ]]; then + if [[ "${last_arg}" == "origin/release/MAPL-v3" ]]; then echo "aaa111" exit 0 fi fi -if [[ "$#" -ge 2 && "$1" == "diff" && "$2" == "--quiet" ]]; then +if [[ "${#args[@]}" -ge 2 && "${args[0]}" == "diff" && "${args[1]}" == "--quiet" ]]; then exit 1 fi -if [[ "$#" -ge 1 && "$1" == "diff" ]]; then +if [[ "${#args[@]}" -ge 1 && "${args[0]}" == "diff" ]]; then echo "M README.md" exit 0 fi -if [[ "$#" -ge 1 && "$1" == "log" ]]; then +if [[ "${#args[@]}" -ge 1 && "${args[0]}" == "log" ]]; then echo "stub log" exit 0 fi -if [[ "$#" -ge 2 && "$1" == "symbolic-ref" ]]; then +if [[ "${#args[@]}" -ge 2 && "${args[0]}" == "symbolic-ref" ]]; then echo "develop" exit 0 fi @@ -135,6 +152,7 @@ GITHUB_REPOSITORY="owner/repo" \ GITHUB_WORKSPACE="${TMP_DIR}" \ GITHUB_OUTPUT="${TMP_DIR}/output.txt" \ INPUT_GITHUB_TOKEN="token" \ +INPUT_REPOSITORY_PATH="repo" \ INPUT_SOURCE_BRANCH="develop" \ INPUT_TARGET_BRANCH="release/MAPL-v3" \ INPUT_TITLE="" \ From 42456d19df1637d6571c27f5e6619a966dd75d12 Mon Sep 17 00:00:00 2001 From: ChristophShyper <45788587+ChristophShyper@users.noreply.github.com> Date: Sat, 23 May 2026 16:21:28 +0200 Subject: [PATCH 2/4] fix: enhance repository validation in entrypoint script and add tests --- entrypoint.sh | 41 ++++++++-------- tests/unit/test_repository_validation.sh | 61 ++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 20 deletions(-) create mode 100755 tests/unit/test_repository_validation.sh diff --git a/entrypoint.sh b/entrypoint.sh index a30edec..84917cc 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -285,10 +285,14 @@ if [[ -z "${TARGET_REPOSITORY}" ]]; then echo -e "\n[ERROR] Unable to resolve repository. Set input 'repository' or ensure GITHUB_REPOSITORY is available." >&2 exit 1 fi -if [[ ! "${TARGET_REPOSITORY}" =~ ^[^/]+/[^/]+$ ]]; then +if [[ ! "${TARGET_REPOSITORY}" =~ ^[A-Za-z0-9]([A-Za-z0-9-]{0,38})/[A-Za-z0-9._-]+$ ]]; then echo -e "\n[ERROR] Input 'repository' must use owner/name format. Got: ${TARGET_REPOSITORY}" >&2 exit 1 fi +if [[ "${TARGET_REPOSITORY}" == *".."* || "${TARGET_REPOSITORY}" == */.* || "${TARGET_REPOSITORY}" == */*. || "${TARGET_REPOSITORY}" == *"/."* ]]; then + echo -e "\n[ERROR] Input 'repository' contains unsupported characters. Got: ${TARGET_REPOSITORY}" >&2 + exit 1 +fi REPOSITORY_PATH="${INPUT_REPOSITORY_PATH:-.}" if [[ -z "${REPOSITORY_PATH}" ]]; then @@ -310,12 +314,6 @@ if [[ ! -d "${REPO_DIR}" ]]; then exit 1 fi -if ! git -C "${REPO_DIR}" rev-parse --is-inside-work-tree >/dev/null 2>&1; then - echo -e "\n[ERROR] Path is not a git repository: ${REPO_DIR}" >&2 - exit 1 -fi - -echo -e "\nSetting GitHub credentials..." # Keep all global git config isolated to a temporary file export GIT_CONFIG_GLOBAL GIT_CONFIG_GLOBAL="$(mktemp /tmp/action-pull-request-git-config-XXXXXX)" @@ -325,6 +323,13 @@ trap 'rm -f "${GIT_CONFIG_GLOBAL}"' EXIT git config --global --add safe.directory "${WORKSPACE_DIR}" git config --global --add safe.directory /github/workspace git config --global --add safe.directory "${REPO_DIR}" + +if ! git -C "${REPO_DIR}" rev-parse --is-inside-work-tree >/dev/null 2>&1; then + echo -e "\n[ERROR] Path is not a git repository: ${REPO_DIR}" >&2 + exit 1 +fi + +echo -e "\nSetting GitHub credentials..." git -C "${REPO_DIR}" remote set-url origin "https://${GITHUB_ACTOR}:${GITHUB_TOKEN}@github.com/${TARGET_REPOSITORY}" git -C "${REPO_DIR}" config user.name "${GITHUB_ACTOR}" git -C "${REPO_DIR}" config user.email "${GITHUB_ACTOR}@users.noreply.github.com" @@ -479,19 +484,18 @@ if [[ -z "${PR_NUMBER}" ]]; then else TITLE=$(git log -1 --pretty=%s | head -1) fi - ARG_LIST=() - ARG_LIST+=("-F /tmp/template") + ARG_LIST=("-F" "/tmp/template") if [[ -n "${INPUT_REVIEWER}" ]]; then - ARG_LIST+=("-r \"${INPUT_REVIEWER}\"") + ARG_LIST+=("-r" "${INPUT_REVIEWER}") fi if [[ -n "${INPUT_ASSIGNEE}" ]]; then - ARG_LIST+=("-a \"${INPUT_ASSIGNEE}\"") + ARG_LIST+=("-a" "${INPUT_ASSIGNEE}") fi if [[ -n "${INPUT_LABEL}" ]]; then - ARG_LIST+=("-l \"${INPUT_LABEL}\"") + ARG_LIST+=("-l" "${INPUT_LABEL}") fi if [[ -n "${INPUT_MILESTONE}" ]]; then - ARG_LIST+=("-M \"${INPUT_MILESTONE}\"") + ARG_LIST+=("-M" "${INPUT_MILESTONE}") fi if [[ "${INPUT_DRAFT}" == "true" ]]; then ARG_LIST+=("-d") @@ -515,10 +519,8 @@ if [[ -z "${PR_NUMBER}" ]]; then echo -e "\n${TEMPLATE}" >> /tmp/template echo -e "\nTemplate:" cat /tmp/template - # shellcheck disable=SC2016,SC2124 - COMMAND="hub pull-request -b ${TARGET_BRANCH} -h ${SOURCE_BRANCH} --no-edit ${ARG_LIST[@]}" - echo -e "\nRunning: ${COMMAND}" - URL=$(sh -c "${COMMAND}") + echo -e "\nRunning: hub pull-request -b ${TARGET_BRANCH} -h ${SOURCE_BRANCH} --no-edit ..." + URL=$(hub pull-request -b "${TARGET_BRANCH}" -h "${SOURCE_BRANCH}" --no-edit "${ARG_LIST[@]}") # shellcheck disable=SC2181 if [[ "$?" != "0" ]]; then RET_CODE=1; fi PR_NUMBER=$(gh pr view --json number -q .number "${URL}") @@ -527,9 +529,8 @@ if [[ -z "${PR_NUMBER}" ]]; then fi else echo -e "\nUpdating pull request" - COMMAND="hub api --method PATCH repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER} --field 'body=@/tmp/template'" - echo -e "Running: ${COMMAND}" - URL=$(sh -c "${COMMAND} | jq -r '.html_url'") + echo -e "Running: hub api --method PATCH repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER} --field body=@/tmp/template" + URL=$(hub api --method PATCH "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}" --field "body=@/tmp/template" | jq -r '.html_url') # shellcheck disable=SC2181 if [[ "$?" != "0" ]]; then RET_CODE=1; fi if (( CHUNK_COUNT > 0 )); then diff --git a/tests/unit/test_repository_validation.sh b/tests/unit/test_repository_validation.sh new file mode 100755 index 0000000..38315b8 --- /dev/null +++ b/tests/unit/test_repository_validation.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)" +SCRIPT_PATH="${SCRIPT_DIR}/../../entrypoint.sh" +TMP_DIR="$(mktemp -d)" +trap 'rm -rf "${TMP_DIR}"' EXIT + +assert_contains() { + local file_path="$1" + local expected="$2" + if ! grep -Fq -- "${expected}" "${file_path}"; then + echo "Assertion failed. Expected to find: ${expected}" >&2 + echo "----- FILE CONTENT -----" >&2 + cat "${file_path}" >&2 + exit 1 + fi +} + +LOG_FILE="${TMP_DIR}/run.log" + +set +e +GITHUB_ACTOR="ci-user" \ +GITHUB_TOKEN="token" \ +GITHUB_REPOSITORY="owner/repo" \ +GITHUB_WORKSPACE="${TMP_DIR}" \ +GITHUB_OUTPUT="${TMP_DIR}/output.txt" \ +INPUT_GITHUB_TOKEN="token" \ +INPUT_REPOSITORY="owner/repo;touch /tmp/pwned" \ +INPUT_REPOSITORY_PATH="." \ +INPUT_SOURCE_BRANCH="develop" \ +INPUT_TARGET_BRANCH="main" \ +INPUT_TITLE="" \ +INPUT_TEMPLATE="" \ +INPUT_BODY="" \ +INPUT_REVIEWER="" \ +INPUT_ASSIGNEE="" \ +INPUT_LABEL="" \ +INPUT_MILESTONE="" \ +INPUT_DRAFT="false" \ +INPUT_GET_DIFF="false" \ +INPUT_OLD_STRING="" \ +INPUT_NEW_STRING="" \ +INPUT_IGNORE_USERS="dependabot" \ +INPUT_ALLOW_NO_DIFF="false" \ +INPUT_MAX_BODY_BYTES="65000" \ +INPUT_MAX_DIFF_LINES="0" \ +bash "${SCRIPT_PATH}" >"${LOG_FILE}" 2>&1 +STATUS="$?" +set -e + +if [[ "${STATUS}" == "0" ]]; then + echo "Expected non-zero exit code for invalid repository input" >&2 + cat "${LOG_FILE}" >&2 + exit 1 +fi + +assert_contains "${LOG_FILE}" "Input 'repository' must use owner/name format" + +echo "Repository validation test passed." From dc75c12630616364a48405dd5e0ea5370c199723 Mon Sep 17 00:00:00 2001 From: ChristophShyper <45788587+ChristophShyper@users.noreply.github.com> Date: Sat, 23 May 2026 16:47:15 +0200 Subject: [PATCH 3/4] test: add validation for repository names containing dot in tests --- entrypoint.sh | 4 --- tests/unit/test_repository_validation.sh | 40 ++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/entrypoint.sh b/entrypoint.sh index 84917cc..cbb8554 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -289,10 +289,6 @@ if [[ ! "${TARGET_REPOSITORY}" =~ ^[A-Za-z0-9]([A-Za-z0-9-]{0,38})/[A-Za-z0-9._- echo -e "\n[ERROR] Input 'repository' must use owner/name format. Got: ${TARGET_REPOSITORY}" >&2 exit 1 fi -if [[ "${TARGET_REPOSITORY}" == *".."* || "${TARGET_REPOSITORY}" == */.* || "${TARGET_REPOSITORY}" == */*. || "${TARGET_REPOSITORY}" == *"/."* ]]; then - echo -e "\n[ERROR] Input 'repository' contains unsupported characters. Got: ${TARGET_REPOSITORY}" >&2 - exit 1 -fi REPOSITORY_PATH="${INPUT_REPOSITORY_PATH:-.}" if [[ -z "${REPOSITORY_PATH}" ]]; then diff --git a/tests/unit/test_repository_validation.sh b/tests/unit/test_repository_validation.sh index 38315b8..08eadbc 100755 --- a/tests/unit/test_repository_validation.sh +++ b/tests/unit/test_repository_validation.sh @@ -58,4 +58,44 @@ fi assert_contains "${LOG_FILE}" "Input 'repository' must use owner/name format" +LOG_FILE_DOT_REPO="${TMP_DIR}/run-dot-repo.log" + +set +e +GITHUB_ACTOR="ci-user" \ +GITHUB_TOKEN="token" \ +GITHUB_REPOSITORY="owner/repo" \ +GITHUB_WORKSPACE="${TMP_DIR}" \ +GITHUB_OUTPUT="${TMP_DIR}/output-dot-repo.txt" \ +INPUT_GITHUB_TOKEN="token" \ +INPUT_REPOSITORY="owner/.github" \ +INPUT_REPOSITORY_PATH="." \ +INPUT_SOURCE_BRANCH="develop" \ +INPUT_TARGET_BRANCH="main" \ +INPUT_TITLE="" \ +INPUT_TEMPLATE="" \ +INPUT_BODY="" \ +INPUT_REVIEWER="" \ +INPUT_ASSIGNEE="" \ +INPUT_LABEL="" \ +INPUT_MILESTONE="" \ +INPUT_DRAFT="false" \ +INPUT_GET_DIFF="false" \ +INPUT_OLD_STRING="" \ +INPUT_NEW_STRING="" \ +INPUT_IGNORE_USERS="ci-user" \ +INPUT_ALLOW_NO_DIFF="false" \ +INPUT_MAX_BODY_BYTES="65000" \ +INPUT_MAX_DIFF_LINES="0" \ +bash "${SCRIPT_PATH}" >"${LOG_FILE_DOT_REPO}" 2>&1 +STATUS_DOT_REPO="$?" +set -e + +if [[ "${STATUS_DOT_REPO}" != "0" ]]; then + echo "Expected successful execution for owner/.github repository" >&2 + cat "${LOG_FILE_DOT_REPO}" >&2 + exit 1 +fi + +assert_contains "${LOG_FILE_DOT_REPO}" "User ci-user is ignored. Skipping." + echo "Repository validation test passed." From 0a9fff482f547d6776974282783a9b19c2f31385 Mon Sep 17 00:00:00 2001 From: ChristophShyper <45788587+ChristophShyper@users.noreply.github.com> Date: Sat, 23 May 2026 16:59:59 +0200 Subject: [PATCH 4/4] feat: update entrypoint and test scripts for improved GitHub token handling and repository validation --- entrypoint.sh | 2 +- tests/unit/test_repository_validation.sh | 96 +++++++++++++++++------- 2 files changed, 69 insertions(+), 29 deletions(-) diff --git a/entrypoint.sh b/entrypoint.sh index cbb8554..6a1a7c7 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -326,7 +326,7 @@ if ! git -C "${REPO_DIR}" rev-parse --is-inside-work-tree >/dev/null 2>&1; then fi echo -e "\nSetting GitHub credentials..." -git -C "${REPO_DIR}" remote set-url origin "https://${GITHUB_ACTOR}:${GITHUB_TOKEN}@github.com/${TARGET_REPOSITORY}" +git -C "${REPO_DIR}" remote set-url origin "https://${GITHUB_ACTOR}:${INPUT_GITHUB_TOKEN}@github.com/${TARGET_REPOSITORY}" git -C "${REPO_DIR}" config user.name "${GITHUB_ACTOR}" git -C "${REPO_DIR}" config user.email "${GITHUB_ACTOR}@users.noreply.github.com" # Needed for hub binary diff --git a/tests/unit/test_repository_validation.sh b/tests/unit/test_repository_validation.sh index 08eadbc..ea87c15 100755 --- a/tests/unit/test_repository_validation.sh +++ b/tests/unit/test_repository_validation.sh @@ -18,13 +18,48 @@ assert_contains() { fi } +WORKSPACE_DIR="${TMP_DIR}/workspace" +REPO_DIR="${WORKSPACE_DIR}/repo" +mkdir -p "${REPO_DIR}" "${TMP_DIR}/bin" + +git init --initial-branch=main "${REPO_DIR}" >/dev/null +pushd "${REPO_DIR}" >/dev/null +git config user.name "tester" +git config user.email "tester@example.com" +printf 'x\n' > README.md +git add README.md +git commit -m "init" >/dev/null +git branch develop +git update-ref refs/remotes/origin/main refs/heads/main +git update-ref refs/remotes/origin/develop refs/heads/develop +git remote add origin . +popd >/dev/null + +cat > "${TMP_DIR}/bin/git" <<'EOF' +#!/usr/bin/env bash +set -Eeuo pipefail + +REAL_GIT="${REAL_GIT:-/usr/bin/git}" + +if [[ "$#" -ge 2 && "$1" == "remote" && "$2" == "set-url" ]]; then + exit 0 +fi + +if [[ "$#" -ge 1 && "$1" == "fetch" ]]; then + exec "${REAL_GIT}" fetch . '+refs/heads/*:refs/heads/*' --update-head-ok +fi + +exec "${REAL_GIT}" "$@" +EOF +chmod +x "${TMP_DIR}/bin/git" + LOG_FILE="${TMP_DIR}/run.log" set +e GITHUB_ACTOR="ci-user" \ GITHUB_TOKEN="token" \ GITHUB_REPOSITORY="owner/repo" \ -GITHUB_WORKSPACE="${TMP_DIR}" \ +GITHUB_WORKSPACE="${WORKSPACE_DIR}" \ GITHUB_OUTPUT="${TMP_DIR}/output.txt" \ INPUT_GITHUB_TOKEN="token" \ INPUT_REPOSITORY="owner/repo;touch /tmp/pwned" \ @@ -61,32 +96,37 @@ assert_contains "${LOG_FILE}" "Input 'repository' must use owner/name format" LOG_FILE_DOT_REPO="${TMP_DIR}/run-dot-repo.log" set +e -GITHUB_ACTOR="ci-user" \ -GITHUB_TOKEN="token" \ -GITHUB_REPOSITORY="owner/repo" \ -GITHUB_WORKSPACE="${TMP_DIR}" \ -GITHUB_OUTPUT="${TMP_DIR}/output-dot-repo.txt" \ -INPUT_GITHUB_TOKEN="token" \ -INPUT_REPOSITORY="owner/.github" \ -INPUT_REPOSITORY_PATH="." \ -INPUT_SOURCE_BRANCH="develop" \ -INPUT_TARGET_BRANCH="main" \ -INPUT_TITLE="" \ -INPUT_TEMPLATE="" \ -INPUT_BODY="" \ -INPUT_REVIEWER="" \ -INPUT_ASSIGNEE="" \ -INPUT_LABEL="" \ -INPUT_MILESTONE="" \ -INPUT_DRAFT="false" \ -INPUT_GET_DIFF="false" \ -INPUT_OLD_STRING="" \ -INPUT_NEW_STRING="" \ -INPUT_IGNORE_USERS="ci-user" \ -INPUT_ALLOW_NO_DIFF="false" \ -INPUT_MAX_BODY_BYTES="65000" \ -INPUT_MAX_DIFF_LINES="0" \ -bash "${SCRIPT_PATH}" >"${LOG_FILE_DOT_REPO}" 2>&1 +( + cd "${REPO_DIR}" + PATH="${TMP_DIR}/bin:${PATH}" \ + REAL_GIT="$(command -v git)" \ + GITHUB_ACTOR="ci-user" \ + GITHUB_TOKEN="token" \ + GITHUB_REPOSITORY="owner/repo" \ + GITHUB_WORKSPACE="${WORKSPACE_DIR}" \ + GITHUB_OUTPUT="${TMP_DIR}/output-dot-repo.txt" \ + INPUT_GITHUB_TOKEN="token" \ + INPUT_REPOSITORY="owner/.github" \ + INPUT_REPOSITORY_PATH="repo" \ + INPUT_SOURCE_BRANCH="develop" \ + INPUT_TARGET_BRANCH="main" \ + INPUT_TITLE="" \ + INPUT_TEMPLATE="" \ + INPUT_BODY="" \ + INPUT_REVIEWER="" \ + INPUT_ASSIGNEE="" \ + INPUT_LABEL="" \ + INPUT_MILESTONE="" \ + INPUT_DRAFT="false" \ + INPUT_GET_DIFF="false" \ + INPUT_OLD_STRING="" \ + INPUT_NEW_STRING="" \ + INPUT_IGNORE_USERS="dependabot" \ + INPUT_ALLOW_NO_DIFF="false" \ + INPUT_MAX_BODY_BYTES="65000" \ + INPUT_MAX_DIFF_LINES="0" \ + bash "${SCRIPT_PATH}" >"${LOG_FILE_DOT_REPO}" 2>&1 +) STATUS_DOT_REPO="$?" set -e @@ -96,6 +136,6 @@ if [[ "${STATUS_DOT_REPO}" != "0" ]]; then exit 1 fi -assert_contains "${LOG_FILE_DOT_REPO}" "User ci-user is ignored. Skipping." +assert_contains "${LOG_FILE_DOT_REPO}" "Repository: owner/.github" echo "Repository validation test passed."