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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ This action supports three tag levels for flexible versioning:
assignee: octocat
label: enhancement
milestone: My milestone
project: Engineering Roadmap
draft: true
old_string: "<!-- Add your description here -->"
new_string: "** Automatic pull request**"
Expand All @@ -95,6 +96,7 @@ This action supports three tag levels for flexible versioning:
| `assignee` | No | `""` | Assignee's usernames |
| `label` | No | `""` | Labels to apply, comma separated |
| `milestone` | No | `""` | Milestone |
| `project` | No | `""` | GitHub Project title to add the pull request to |
| `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 |
Expand All @@ -119,6 +121,7 @@ permissions:
- `contents: read` is required to read repository state.
- `pull-requests: write` is required to create and update pull requests.
- `issues: write` is required when managed overflow comments are created, updated, or deleted (including cleanup on later runs).
- Project assignment via `project` requires a token/auth context that `gh` can use with project access.


### 📤 Output Parameters
Expand Down Expand Up @@ -217,6 +220,7 @@ jobs:
title: ${{ github.event.commits[0].message }}
assignee: ${{ github.actor }}
label: automatic,feature
project: Engineering Roadmap
template: .github/PULL_REQUEST_TEMPLATE/FEATURE.md
old_string: "**Write your description here**"
new_string: ${{ github.event.commits[0].message }}
Expand Down
4 changes: 4 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ inputs:
description: Milestone
required: false
default: ""
project:
description: GitHub Project title to add the pull request to
required: false
default: ""
draft:
description: Whether to mark it as a draft
required: false
Expand Down
44 changes: 44 additions & 0 deletions entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ TARGET_OWNER=""
REPOSITORY_PATH=""
WORKSPACE_DIR=""
REPO_DIR=""
PROJECT_VALUE=""

REPLACE_TEMPLATE_SCRIPT="/scripts/replace-template-diff.sh"
if [[ ! -x "${REPLACE_TEMPLATE_SCRIPT}" ]]; then
Expand Down Expand Up @@ -174,6 +175,7 @@ trim_whitespace() {
: "${INPUT_ASSIGNEE:=}"
: "${INPUT_LABEL:=}"
: "${INPUT_MILESTONE:=}"
: "${INPUT_PROJECT:=}"
: "${INPUT_DRAFT:=false}"
: "${INPUT_OLD_STRING:=}"
: "${INPUT_NEW_STRING:=}"
Expand Down Expand Up @@ -203,6 +205,42 @@ append_csv_arg() {
done
}

pr_has_project() {
local pr_number="$1"
local project_title="$2"
local project_titles=""

project_titles="$(
gh pr view "${pr_number}" --repo "${TARGET_REPOSITORY}" --json projectItems,projectCards --jq \
'[.projectItems[]?.project.title?, .projectCards[]?.project.name?, .projectCards[]?.project.title?]
| map(select(. != null))
| .[]'
)"

if grep -Fxq -- "${project_title}" <<< "${project_titles}"; then
return 0
fi

return 1
}

add_pr_to_project_if_needed() {
local pr_number="$1"
local project_title="$2"

if [[ -z "${project_title}" ]]; then
return 0
fi

if pr_has_project "${pr_number}" "${project_title}"; then
echo -e "\n[INFO] Pull request #${pr_number} is already assigned to project '${project_title}'."
return 0
fi

echo -e "\nAdding pull request #${pr_number} to project '${project_title}'"
gh pr edit "${pr_number}" --repo "${TARGET_REPOSITORY}" --add-project "${project_title}" >/dev/null
}

get_managed_comment_ids() {
local pr_number="$1"
local output_file="$2"
Expand Down Expand Up @@ -304,6 +342,7 @@ echo " reviewer: ${INPUT_REVIEWER}"
echo " assignee: ${INPUT_ASSIGNEE}"
echo " label: ${INPUT_LABEL}"
echo " milestone: ${INPUT_MILESTONE}"
echo " project: ${INPUT_PROJECT}"
echo " draft: ${INPUT_DRAFT}"
echo " get_diff: ${INPUT_GET_DIFF}"
echo " old_string: ${INPUT_OLD_STRING}"
Expand All @@ -315,6 +354,7 @@ echo " max_diff_lines: ${INPUT_MAX_DIFF_LINES}"

MAX_BODY_BYTES="${INPUT_MAX_BODY_BYTES:-65000}"
MAX_DIFF_LINES="${INPUT_MAX_DIFF_LINES:-0}"
PROJECT_VALUE="$(trim_whitespace "${INPUT_PROJECT}")"
validate_number_input "${MAX_BODY_BYTES}" "max_body_bytes"
validate_number_input "${MAX_DIFF_LINES}" "max_diff_lines"

Expand Down Expand Up @@ -574,6 +614,9 @@ if [[ -z "${PR_NUMBER}" ]]; then
if [[ -n "${milestone_value}" ]]; then
GH_CREATE_ARGS+=(--milestone "${milestone_value}")
fi
if [[ -n "${PROJECT_VALUE}" ]]; then
GH_CREATE_ARGS+=(--project "${PROJECT_VALUE}")
fi
if [[ "${INPUT_DRAFT}" == "true" ]]; then
GH_CREATE_ARGS+=(--draft)
fi
Expand All @@ -595,6 +638,7 @@ else
URL="$(gh api --method PATCH "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}" --field "body=@/tmp/template" --jq '.html_url')"
# shellcheck disable=SC2181
if [[ "$?" != "0" ]]; then RET_CODE=1; fi
add_pr_to_project_if_needed "${PR_NUMBER}" "${PROJECT_VALUE}"
if (( CHUNK_COUNT > 0 )); then
reconcile_managed_comments "${PR_NUMBER}" "${CHUNK_COUNT}"
else
Expand Down
16 changes: 16 additions & 0 deletions tests/unit/test_existing_pr_lookup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,20 @@ if [[ "$#" -ge 2 && "$1" == "pr" && "$2" == "create" ]]; then
exit 1
fi

if [[ "$#" -ge 2 && "$1" == "pr" && "$2" == "view" ]]; then
if [[ "${cmd}" == *"--json projectItems,projectCards"* ]]; then
exit 0
fi
fi

if [[ "$#" -ge 2 && "$1" == "pr" && "$2" == "edit" ]]; then
if [[ "${cmd}" != *"--repo owner/repo"* || "${cmd}" != *"--add-project Roadmap"* || "${cmd}" != *"123"* ]]; then
echo "Missing project add call in update mode" >&2
exit 1
fi
exit 0
fi

echo "Unsupported gh call: $*" >&2
exit 1
EOF
Expand Down Expand Up @@ -153,6 +167,7 @@ INPUT_REVIEWER="" \
INPUT_ASSIGNEE="" \
INPUT_LABEL="" \
INPUT_MILESTONE="" \
INPUT_PROJECT="Roadmap" \
INPUT_DRAFT="false" \
INPUT_GET_DIFF="false" \
INPUT_OLD_STRING="" \
Expand All @@ -172,6 +187,7 @@ if [[ "${STATUS}" != "0" ]]; then
fi

assert_contains "${LOG_FILE}" "Updating pull request"
assert_contains "${LOG_FILE}" "Adding pull request #123 to project 'Roadmap'"
assert_contains "${TMP_DIR}/output.txt" "url=https://example.test/pr/123"
assert_contains "${TMP_DIR}/output.txt" "pr_number=123"

Expand Down
198 changes: 198 additions & 0 deletions tests/unit/test_existing_pr_project_skip.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
#!/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
}

assert_not_contains() {
local file_path="$1"
local expected="$2"
if grep -Fq -- "${expected}" "${file_path}"; then
echo "Assertion failed. Expected not to find: ${expected}" >&2
echo "----- FILE CONTENT -----" >&2
cat "${file_path}" >&2
exit 1
fi
}

mkdir -p "${TMP_DIR}/bin"
mkdir -p "${TMP_DIR}/repo"

cat > "${TMP_DIR}/bin/git" <<'EOF'
#!/usr/bin/env bash
set -Eeuo pipefail

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 [[ "${#args[@]}" -ge 1 && "${args[0]}" == "fetch" ]]; then
exit 0
fi

if [[ "${#args[@]}" -ge 2 && "${args[0]}" == "rev-parse" && "${args[1]}" == "--is-inside-work-tree" ]]; then
echo "true"
exit 0
fi

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/main" ]]; then
exit 0
fi
exit 1
fi

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 [[ "${last_arg}" == "origin/main" ]]; then
echo "aaa111"
exit 0
fi
fi

if [[ "${#args[@]}" -ge 2 && "${args[0]}" == "diff" && "${args[1]}" == "--quiet" ]]; then
exit 1
fi

if [[ "${#args[@]}" -ge 1 && "${args[0]}" == "diff" ]]; then
echo "M README.md"
exit 0
fi

if [[ "${#args[@]}" -ge 1 && "${args[0]}" == "log" ]]; then
echo "stub log"
exit 0
fi

if [[ "${#args[@]}" -ge 2 && "${args[0]}" == "symbolic-ref" ]]; then
echo "develop"
exit 0
fi

echo "Unsupported git call: $*" >&2
exit 1
EOF

cat > "${TMP_DIR}/bin/gh" <<'EOF'
#!/usr/bin/env bash
set -Eeuo pipefail

cmd="$*"

if [[ "$#" -ge 1 && "$1" == "api" ]]; then
if [[ "${cmd}" == *"repos/owner/repo/pulls?state=open&base=main"* ]]; then
printf '%s\n' '[{"number":123,"head":{"ref":"develop","repo":{"full_name":"owner/repo"}}}]'
exit 0
fi
if [[ "${cmd}" == *"repos/owner/repo/pulls/123"* && "${cmd}" == *"--method GET"* ]]; then
echo "OLD BODY"
exit 0
fi
if [[ "${cmd}" == *"repos/owner/repo/pulls/123"* && "${cmd}" == *"--method PATCH"* ]]; then
echo "https://example.test/pr/123"
exit 0
fi
if [[ "${cmd}" == *"repos/owner/repo/issues/123/comments"* ]]; then
echo "[]"
exit 0
fi
fi

if [[ "$#" -ge 2 && "$1" == "pr" && "$2" == "view" ]]; then
if [[ "${cmd}" == *"--json projectItems,projectCards"* ]]; then
printf '%s\n' 'Roadmap'
exit 0
fi
fi

if [[ "$#" -ge 2 && "$1" == "pr" && "$2" == "edit" ]]; then
echo "gh pr edit should not be called when project is already assigned" >&2
exit 1
fi

echo "Unsupported gh call: $*" >&2
exit 1
EOF

cat > "${TMP_DIR}/template.md" <<'EOF'
## Template body from file
EOF

chmod +x "${TMP_DIR}/bin/git" "${TMP_DIR}/bin/gh"

LOG_FILE="${TMP_DIR}/run.log"
set +e
PATH="${TMP_DIR}/bin:${PATH}" \
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_PATH="repo" \
INPUT_SOURCE_BRANCH="develop" \
INPUT_TARGET_BRANCH="main" \
INPUT_TITLE="" \
INPUT_TEMPLATE="${TMP_DIR}/template.md" \
INPUT_BODY="" \
INPUT_REVIEWER="" \
INPUT_ASSIGNEE="" \
INPUT_LABEL="" \
INPUT_MILESTONE="" \
INPUT_PROJECT="Roadmap" \
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 in update mode when project already exists" >&2
cat "${LOG_FILE}" >&2
exit 1
fi

assert_contains "${LOG_FILE}" "Pull request #123 is already assigned to project 'Roadmap'."
assert_not_contains "${LOG_FILE}" "Adding pull request #123 to project 'Roadmap'"
assert_contains "${TMP_DIR}/output.txt" "url=https://example.test/pr/123"
assert_contains "${TMP_DIR}/output.txt" "pr_number=123"

echo "Existing PR project skip test passed."
1 change: 1 addition & 0 deletions tests/unit/test_optional_inputs_defaults.sh
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ if [[ "${STATUS}" != "0" ]]; then
fi

assert_contains "${LOG_FILE}" "template: "
assert_contains "${LOG_FILE}" "project: "
assert_contains "${LOG_FILE}" "Creating pull request"
assert_contains "${TMP_DIR}/output.txt" "pr_number=123"

Expand Down
5 changes: 5 additions & 0 deletions tests/unit/test_pr_create_with_gh.sh
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,10 @@ if [[ "$#" -ge 2 && "$1" == "pr" && "$2" == "create" ]]; then
echo "Missing milestone" >&2
exit 1
fi
if [[ "${cmd}" != *"--project Roadmap"* ]]; then
echo "Missing project" >&2
exit 1
fi
if [[ "${cmd}" != *"--draft"* ]]; then
echo "Missing draft flag" >&2
exit 1
Expand Down Expand Up @@ -184,6 +188,7 @@ INPUT_REVIEWER="alice,bob" \
INPUT_ASSIGNEE="assignee1,assignee2" \
INPUT_LABEL="bug,chore" \
INPUT_MILESTONE="Milestone-1" \
INPUT_PROJECT="Roadmap" \
INPUT_DRAFT="true" \
INPUT_GET_DIFF="false" \
INPUT_OLD_STRING="" \
Expand Down