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
55 changes: 34 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
[
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
14 changes: 7 additions & 7 deletions Taskfile.scripts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
104 changes: 81 additions & 23 deletions entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ OVERFLOW_CHUNK_PREFIX="/tmp/template-overflow-chunk"
CHUNK_COUNT=0
MANAGED_COMMENT_START="<!-- action-pull-request:managed-diff-chunk:start -->"
MANAGED_COMMENT_END="<!-- action-pull-request:managed-diff-chunk:end -->"
TARGET_REPOSITORY=""
REPOSITORY_PATH=""
WORKSPACE_DIR=""
REPO_DIR=""

REPLACE_TEMPLATE_SCRIPT="/scripts/replace-template-diff.sh"
if [[ ! -x "${REPLACE_TEMPLATE_SCRIPT}" ]]; then
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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}"
Expand Down Expand Up @@ -264,15 +280,61 @@ if [[ -z "${INPUT_GITHUB_TOKEN}" ]]; then
exit 1
fi

echo -e "\nSetting GitHub credentials..."
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}" =~ ^[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

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

# 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}"

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}:${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
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)}"
Expand Down Expand Up @@ -339,7 +401,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

Expand Down Expand Up @@ -418,19 +480,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")
Expand All @@ -454,10 +515,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}")
Expand All @@ -466,9 +525,8 @@ 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'"
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
Expand Down
Loading
Loading