diff --git a/.dockerignore b/.dockerignore index 215e08c..6cc8f70 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,5 +4,8 @@ # Include !Dockerfile !entrypoint.sh +!scripts/ +!scripts/replace-template-diff.sh +!scripts/split_content_bytes.py !LICENSE !README.md diff --git a/Dockerfile b/Dockerfile index 3a7eb3f..2540f77 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,11 +5,12 @@ ENV DEBIAN_FRONTEND=noninteractive # Copy all needed files COPY entrypoint.sh / +COPY scripts/ /scripts/ # Install needed packages SHELL ["/bin/bash", "-euxo", "pipefail", "-c"] # hadolint ignore=DL3008 -RUN chmod +x /entrypoint.sh ;\ +RUN chmod +x /entrypoint.sh /scripts/replace-template-diff.sh /scripts/split_content_bytes.py ;\ apt-get update -y ;\ apt-get install --no-install-recommends -y \ curl \ @@ -25,7 +26,8 @@ RUN chmod +x /entrypoint.sh ;\ git \ gh \ hub \ - jq ;\ + jq \ + python3 ;\ apt-get clean ;\ rm -rf /var/lib/apt/lists/* diff --git a/README.md b/README.md index c15d712..c61c3d9 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,8 @@ This action supports three tag levels for flexible versioning: get_diff: true ignore_users: "dependabot" allow_no_diff: false + max_body_bytes: 65000 + max_diff_lines: 0 ``` @@ -89,6 +91,24 @@ This action supports three tag levels for flexible versioning: | `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 + +Set explicit job/workflow token permissions when using this action: + +```yaml +permissions: + contents: read + pull-requests: write + issues: write +``` + +- `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). ### 📤 Outputs Parameters @@ -107,6 +127,11 @@ Now this action will expect to have three types of comment blocks. Meaning anyth * `` and `` - show graph of commits in the pull request, with authors' info and time * `` and `` - show list of modified files +When the generated PR body exceeds `max_body_bytes`, the action keeps the main body within the configured size and publishes remaining content in managed PR comments. +Managed comments are updated/deleted on subsequent runs. + +Set `max_diff_lines` to cap each generated diff section before insertion. + If your template uses old comment strings it will try to adjust them in the pull request body to a new standard when pull request is created. It will not modify the template. **CAUTION**: Remember to not use default `fetch-depth` for [actions/checkout](https://github.com/actions/checkout) action. Rather set it to `0` - see example below. @@ -128,6 +153,12 @@ name: Run the Action on: push: branches-ignore: master + +permissions: + contents: read + pull-requests: write + issues: write + jobs: action-pull-request: runs-on: ubuntu-latest @@ -150,6 +181,12 @@ name: Run the Action on: push: branches-ignore: master + +permissions: + contents: read + pull-requests: write + issues: write + jobs: action-pull-request: runs-on: ubuntu-latest @@ -181,6 +218,12 @@ name: Run the Action on: push: branches-ignore: master + +permissions: + contents: read + pull-requests: write + issues: write + jobs: action-pull-request: runs-on: ubuntu-latest diff --git a/action.yml b/action.yml index 7462ed1..5f99434 100644 --- a/action.yml +++ b/action.yml @@ -65,6 +65,14 @@ inputs: description: Allows to continue on merge commits with no diffs required: false default: "false" + max_body_bytes: + description: Maximum PR body size in bytes before overflow is moved into managed comments + required: false + default: "65000" + max_diff_lines: + description: Maximum lines per generated diff section (0 means unlimited) + required: false + default: "0" outputs: url: description: Pull request URL diff --git a/entrypoint.sh b/entrypoint.sh index 090bf07..1475aaa 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -4,6 +4,212 @@ set -Eeuo pipefail # Return code RET_CODE=0 +GIT_LOG="" +GIT_SUMMARY="" +GIT_DIFF="" +SOURCE_COMPARE_REF="" +TARGET_COMPARE_REF="" +RESOLVED_BRANCH_REF="" +MAX_BODY_BYTES="" +MAX_DIFF_LINES="" +MAX_COMMENT_BODY_BYTES="" +OVERFLOW_MAIN_FILE="/tmp/template-overflow-main.md" +OVERFLOW_CHUNK_PREFIX="/tmp/template-overflow-chunk" +CHUNK_COUNT=0 +MANAGED_COMMENT_START="" +MANAGED_COMMENT_END="" + +REPLACE_TEMPLATE_SCRIPT="/scripts/replace-template-diff.sh" +if [[ ! -x "${REPLACE_TEMPLATE_SCRIPT}" ]]; then + REPLACE_TEMPLATE_SCRIPT="$(dirname "$0")/scripts/replace-template-diff.sh" +fi + +SPLIT_CONTENT_SCRIPT="/scripts/split_content_bytes.py" +if [[ ! -f "${SPLIT_CONTENT_SCRIPT}" ]]; then + SPLIT_CONTENT_SCRIPT="$(dirname "$0")/scripts/split_content_bytes.py" +fi + +get_git_log() { + if [[ -z "${GIT_LOG}" ]]; then + echo -e "\nListing new commits in the source branch..." + git log --graph --pretty=format:'%Cred%h%Creset - %Cblue%an%Creset - %Cgreen%cd%Creset %n%s %b' --abbrev-commit --date=format:'%Y-%m-%d %H:%M:%S' "${TARGET_COMPARE_REF}...${SOURCE_COMPARE_REF}" + GIT_LOG=$(git log --graph --pretty=format:'%Cred%h%Creset - %Cblue%an%Creset - %Cgreen%cd%Creset %n%s%n%b' --abbrev-commit --date=format:'%Y-%m-%d %H:%M:%S' --no-color "${TARGET_COMPARE_REF}...${SOURCE_COMPARE_REF}") + fi +} + +get_git_summary() { + if [[ -z "${GIT_SUMMARY}" ]]; then + echo -e "\n\nListing commits subjects in the source branch..." + git log --reverse --pretty=format:'%s' --abbrev-commit "${TARGET_COMPARE_REF}...${SOURCE_COMPARE_REF}" + GIT_SUMMARY=$(git log --reverse --pretty=format:'%s' --abbrev-commit "${TARGET_COMPARE_REF}...${SOURCE_COMPARE_REF}") + fi +} + +get_git_diff() { + if [[ -z "${GIT_DIFF}" ]]; then + echo -e "\n\nListing files modified in the source branch..." + git diff --compact-summary "${TARGET_COMPARE_REF}...${SOURCE_COMPARE_REF}" + GIT_DIFF=$(git diff --compact-summary --no-color "${TARGET_COMPARE_REF}...${SOURCE_COMPARE_REF}") + fi +} + +resolve_branch_ref() { + local branch_name="$1" + local remote_ref="refs/remotes/origin/${branch_name}" + local head_ref="refs/heads/${branch_name}" + + if git show-ref --verify --quiet "${remote_ref}"; then + RESOLVED_BRANCH_REF="origin/${branch_name}" + return 0 + fi + + if git show-ref --verify --quiet "${head_ref}"; then + RESOLVED_BRANCH_REF="${branch_name}" + return 0 + fi + + echo -e "\n[ERROR] Missing branch reference: ${branch_name}" >&2 + return 1 +} + +validate_number_input() { + local value="$1" + local input_name="$2" + + if [[ ! "${value}" =~ ^[0-9]+$ ]]; then + echo -e "\n[ERROR] Input '${input_name}' must be a non-negative integer. Got: ${value}" >&2 + exit 1 + fi +} + +apply_line_cap() { + local file_path="$1" + local max_lines="$2" + local section_name="$3" + + if [[ "${max_lines}" == "0" ]]; then + return 0 + fi + + python3 - "$file_path" "$max_lines" "$section_name" <<'PY' +import pathlib +import sys + +path = pathlib.Path(sys.argv[1]) +limit = int(sys.argv[2]) +section = sys.argv[3] +content = path.read_text(encoding="utf-8") +lines = content.splitlines() +if len(lines) <= limit: + raise SystemExit(0) +trimmed = lines[:limit] +removed = len(lines) - len(trimmed) +if limit == 1: + trimmed = [f"... truncated {removed} lines from {section} because max_diff_lines={limit} ..."] +else: + trimmed = lines[: limit - 1] + removed = len(lines) - len(trimmed) + trimmed.append(f"... truncated {removed} lines from {section} because max_diff_lines={limit} ...") +path.write_text("\n".join(trimmed) + "\n", encoding="utf-8") +PY +} + +write_chunk_comment_file() { + local chunk_body_file="$1" + local index="$2" + local total="$3" + local output_file="$4" + + { + printf '%s\n' "${MANAGED_COMMENT_START}" + printf '\n' "${index}" "${total}" + cat "${chunk_body_file}" + printf '\n%s\n' "${MANAGED_COMMENT_END}" + } > "${output_file}" +} + +split_template_by_bytes() { + local input_file="$1" + local main_output_file="$2" + local chunk_prefix="$3" + local max_main_bytes="$4" + local max_comment_bytes="$5" + + python3 "${SPLIT_CONTENT_SCRIPT}" "${input_file}" "${main_output_file}" "${chunk_prefix}" "${max_main_bytes}" "${max_comment_bytes}" +} + +get_managed_comment_ids() { + local pr_number="$1" + local output_file="$2" + + gh api "repos/${GITHUB_REPOSITORY}/issues/${pr_number}/comments" --paginate | jq -r \ + --arg start "${MANAGED_COMMENT_START}" \ + --arg end "${MANAGED_COMMENT_END}" \ + 'if type == "array" then .[] else . end + | select((.body // "" | contains($start)) and (.body // "" | contains($end))) + | .id' | sort -n > "${output_file}" +} + +reconcile_managed_comments() { + local pr_number="$1" + local chunk_count="$2" + + local ids_file="/tmp/managed-comment-ids.txt" + get_managed_comment_ids "${pr_number}" "${ids_file}" + + local -a existing_ids=() + while IFS= read -r line; do + if [[ -n "${line}" ]]; then + existing_ids+=("${line}") + fi + done < "${ids_file}" + + local idx + for ((idx=1; idx<=chunk_count; idx++)); do + local raw_chunk_file="${OVERFLOW_CHUNK_PREFIX}-${idx}.txt" + local comment_file="${OVERFLOW_CHUNK_PREFIX}-${idx}.comment.md" + write_chunk_comment_file "${raw_chunk_file}" "${idx}" "${chunk_count}" "${comment_file}" + + 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 + else + gh api --method POST "repos/${GITHUB_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 + done + fi +} + +apply_body_limits() { + local template_file="$1" + local max_body_bytes="$2" + local max_comment_bytes="$3" + + local template_size + template_size="$(wc -c < "${template_file}" | tr -d '[:space:]')" + if (( template_size <= max_body_bytes )); then + CHUNK_COUNT=0 + cp "${template_file}" "${OVERFLOW_MAIN_FILE}" + return 0 + fi + + echo -e "\n[INFO] PR body exceeds max_body_bytes=${max_body_bytes}. Splitting overflow into managed comments." + + local with_note_file="/tmp/template-with-note.md" + { + printf '_Note: Additional diff output is included in managed comments because body size exceeded max_body_bytes=%s._\n' "${max_body_bytes}" + printf '\n---\n\n' + cat "${template_file}" + } > "${with_note_file}" + + CHUNK_COUNT="$(split_template_by_bytes "${with_note_file}" "${OVERFLOW_MAIN_FILE}" "${OVERFLOW_CHUNK_PREFIX}" "${max_body_bytes}" "${max_comment_bytes}")" +} echo "Inputs:" echo " source_branch: ${INPUT_SOURCE_BRANCH}" @@ -21,6 +227,23 @@ echo " old_string: ${INPUT_OLD_STRING}" echo " new_string: ${INPUT_NEW_STRING}" echo " ignore_users: ${INPUT_IGNORE_USERS}" echo " allow_no_diff: ${INPUT_ALLOW_NO_DIFF}" +echo " max_body_bytes: ${INPUT_MAX_BODY_BYTES}" +echo " max_diff_lines: ${INPUT_MAX_DIFF_LINES}" + +MAX_BODY_BYTES="${INPUT_MAX_BODY_BYTES:-65000}" +MAX_DIFF_LINES="${INPUT_MAX_DIFF_LINES:-0}" +validate_number_input "${MAX_BODY_BYTES}" "max_body_bytes" +validate_number_input "${MAX_DIFF_LINES}" "max_diff_lines" + +if (( MAX_BODY_BYTES < 2048 )); then + echo -e "\n[ERROR] Input 'max_body_bytes' must be at least 2048. Got: ${MAX_BODY_BYTES}" >&2 + exit 1 +fi + +MAX_COMMENT_BODY_BYTES=$((MAX_BODY_BYTES - 512)) +if (( MAX_COMMENT_BODY_BYTES < 1024 )); then + MAX_COMMENT_BODY_BYTES=1024 +fi # Skip whole script to not cause errors IFS=',' read -r -a IGNORE_USERS <<< "${INPUT_IGNORE_USERS}" @@ -60,53 +283,64 @@ echo "Target branch: ${TARGET_BRANCH}" echo -e "\nUpdating all branches..." git fetch origin '+refs/heads/*:refs/heads/*' --update-head-ok +echo -e "\nValidating branches..." +if ! resolve_branch_ref "${SOURCE_BRANCH}"; then + exit 1 +fi +SOURCE_COMPARE_REF="${RESOLVED_BRANCH_REF}" + +if ! resolve_branch_ref "${TARGET_BRANCH}"; then + exit 1 +fi +TARGET_COMPARE_REF="${RESOLVED_BRANCH_REF}" + echo -e "\nComparing branches by revisions..." -if [[ $(git rev-parse --revs-only "${SOURCE_BRANCH}") == $(git rev-parse --revs-only "${TARGET_BRANCH}") ]]; then +if [[ $(git rev-parse --verify "${SOURCE_COMPARE_REF}") == $(git rev-parse --verify "${TARGET_COMPARE_REF}") ]]; then echo -e "\n[INFO] Both branches are the same. No action needed." exit 0 fi echo -e "\nComparing branches by diff..." -if [[ -z $(git diff "remotes/origin/${TARGET_BRANCH}...remotes/origin/${SOURCE_BRANCH}") ]]; then +if git diff --quiet "${TARGET_COMPARE_REF}...${SOURCE_COMPARE_REF}"; then if [[ "${INPUT_ALLOW_NO_DIFF}" == "true" ]]; then echo -e "\n[INFO] Both branches are the same. Continuing." else echo -e "\n[INFO] Both branches are the same. No action needed." exit 0 fi +else + DIFF_STATUS="$?" + if [[ "${DIFF_STATUS}" != "1" ]]; then + echo -e "\n[ERROR] Failed to compare branches by diff (git exit code: ${DIFF_STATUS})." + exit 1 + fi fi -# sed has problems with putting multi-line strings in the next steps, and later we use # for sed -# newline `\n` and hash `#` characters are replaced with some (hopefully) totally unlikely strings -# after insertions of git information into template those strings are replaced back by proper characters - -echo -e "\nListing new commits in the source branch..." -git log --graph --pretty=format:'%Cred%h%Creset - %Cblue%an%Creset - %Cgreen%cd%Creset %n%s %b' --abbrev-commit --date=format:'%Y-%m-%d %H:%M:%S' "origin/${TARGET_BRANCH}...origin/${SOURCE_BRANCH}" -GIT_LOG=$(git log --graph --pretty=format:'%Cred%h%Creset - %Cblue%an%Creset - %Cgreen%cd%Creset %n%s%n%b' --abbrev-commit --date=format:'%Y-%m-%d %H:%M:%S' --no-color "origin/${TARGET_BRANCH}...origin/${SOURCE_BRANCH}") -GIT_LOG=$(echo -e "${GIT_LOG}" | sed 's|#|^HaSz^|g' | sed ':a;N;$!ba; s/\n/^NowALiNiA^/g') - -echo -e "\n\nListing commits subjects in the source branch..." -git log --reverse --pretty=format:'%s' --abbrev-commit "origin/${TARGET_BRANCH}...origin/${SOURCE_BRANCH}" -GIT_SUMMARY=$(git log --reverse --pretty=format:'%s' --abbrev-commit "origin/${TARGET_BRANCH}...origin/${SOURCE_BRANCH}") -GIT_SUMMARY=$(echo -e "${GIT_SUMMARY}" | sed 's|#|^HaSz^|g' | sed ':a;N;$!ba; s/\n/^NowALiNiA^/g') - -echo -e "\n\nListing files modified in the source branch..." -git diff --compact-summary "origin/${TARGET_BRANCH}...origin/${SOURCE_BRANCH}" -GIT_DIFF=$(git diff --compact-summary --no-color "origin/${TARGET_BRANCH}...origin/${SOURCE_BRANCH}") -GIT_DIFF=$(echo -e "${GIT_DIFF}" | sed 's|#|^HaSz^|g' | sed ':a;N;$!ba; s/\n/^NowALiNiA^/g') - echo -e "\nSetting template..." PR_NUMBER=$(hub pr list --base "${TARGET_BRANCH}" --head "${SOURCE_BRANCH}" --format '%I') if [[ -z "${PR_NUMBER}" ]]; then if [[ -n "${INPUT_TEMPLATE}" ]]; then + echo "Template source: input template file" TEMPLATE=$(cat "${INPUT_TEMPLATE}") elif [[ -n "${INPUT_BODY}" ]]; then + echo "Template source: input body" TEMPLATE="${INPUT_BODY}" else + echo "Template source: generated git log" + get_git_log TEMPLATE="${GIT_LOG}" fi else - TEMPLATE=$(hub api --method GET "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" | jq -r '.body') + if [[ -n "${INPUT_TEMPLATE}" ]]; then + echo "Template source: input template file (update mode)" + TEMPLATE=$(cat "${INPUT_TEMPLATE}") + elif [[ -n "${INPUT_BODY}" ]]; then + echo "Template source: input body (update mode)" + 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') + fi fi if [[ -n "${INPUT_OLD_STRING}" ]]; then @@ -115,22 +349,67 @@ if [[ -n "${INPUT_OLD_STRING}" ]]; then if [[ -n "${INPUT_NEW_STRING}" ]]; then TEMPLATE=${TEMPLATE/${OLD_STRING}/${INPUT_NEW_STRING}} else + get_git_summary TEMPLATE=${TEMPLATE/${OLD_STRING}/${GIT_SUMMARY}} fi fi if [[ "${INPUT_GET_DIFF}" == "true" ]]; then echo -e "\nReplacing predefined fields with git information..." - # little hack to trick sed to work with multiline - # also backwards compatible with old replacement strings - TEMPLATE=$(echo -e "${TEMPLATE}" | sed ':a;N;$!ba; s#.*#\n'"${GIT_SUMMARY}"'\n#g') - TEMPLATE=$(echo -e "${TEMPLATE}" | sed ':a;N;$!ba; s##\n'"${GIT_LOG}"'\n#g') - TEMPLATE=$(echo -e "${TEMPLATE}" | sed ':a;N;$!ba; s#.*#\n'"${GIT_LOG}"'\n#g') - TEMPLATE=$(echo -e "${TEMPLATE}" | sed ':a;N;$!ba; s##\n'"${GIT_DIFF}"'\n#g') - TEMPLATE=$(echo -e "${TEMPLATE}" | sed ':a;N;$!ba; s#.*#\n'"${GIT_DIFF}"'\n#g') + REPLACE_SUMMARY="false" + REPLACE_COMMITS="false" + REPLACE_FILES="false" + + if [[ "${TEMPLATE}" == *""* && "${TEMPLATE}" == *""* ]]; then + REPLACE_SUMMARY="true" + fi + if [[ "${TEMPLATE}" == *""* || ( "${TEMPLATE}" == *""* && "${TEMPLATE}" == *""* ) ]]; then + REPLACE_COMMITS="true" + fi + if [[ "${TEMPLATE}" == *""* || ( "${TEMPLATE}" == *""* && "${TEMPLATE}" == *""* ) ]]; then + REPLACE_FILES="true" + fi + + echo "Detected diff markers: summary=${REPLACE_SUMMARY} commits=${REPLACE_COMMITS} files=${REPLACE_FILES}" + + if [[ "${REPLACE_SUMMARY}" == "true" || "${REPLACE_COMMITS}" == "true" || "${REPLACE_FILES}" == "true" ]]; then + TEMPLATE_WORK_FILE="/tmp/template-work.md" + SUMMARY_FILE="/tmp/template-summary.txt" + COMMITS_FILE="/tmp/template-commits.txt" + FILES_FILE="/tmp/template-files.txt" + + printf '%s' "${TEMPLATE}" > "${TEMPLATE_WORK_FILE}" + + if [[ "${REPLACE_SUMMARY}" == "true" ]]; then + get_git_summary + printf '%s' "${GIT_SUMMARY}" > "${SUMMARY_FILE}" + apply_line_cap "${SUMMARY_FILE}" "${MAX_DIFF_LINES}" "Diff summary" + fi + if [[ "${REPLACE_COMMITS}" == "true" ]]; then + get_git_log + printf '%s' "${GIT_LOG}" > "${COMMITS_FILE}" + apply_line_cap "${COMMITS_FILE}" "${MAX_DIFF_LINES}" "Diff commits" + fi + if [[ "${REPLACE_FILES}" == "true" ]]; then + get_git_diff + printf '%s' "${GIT_DIFF}" > "${FILES_FILE}" + apply_line_cap "${FILES_FILE}" "${MAX_DIFF_LINES}" "Diff files" + fi + + "${REPLACE_TEMPLATE_SCRIPT}" \ + --template "${TEMPLATE_WORK_FILE}" \ + --summary-file "${SUMMARY_FILE}" \ + --commits-file "${COMMITS_FILE}" \ + --files-file "${FILES_FILE}" \ + --replace-summary "${REPLACE_SUMMARY}" \ + --replace-commits "${REPLACE_COMMITS}" \ + --replace-files "${REPLACE_FILES}" + + TEMPLATE=$(cat "${TEMPLATE_WORK_FILE}") + else + echo -e "[INFO] No diff markers found in template body. Skipping get_diff replacements." + fi fi -#shellcheck disable=SC2016 -TEMPLATE=$(echo -e "${TEMPLATE}" | sed 's|\^HaSz\^|#|g' | sed '1h;2,$H;$!d;g; s|\^NowALiNiA\^|\n|g') if [[ -z "${PR_NUMBER}" ]]; then echo -e "\nSetting all arguments..." @@ -160,6 +439,15 @@ else echo -e "${TEMPLATE}" > /tmp/template fi +printf '%s' "${TEMPLATE}" > "/tmp/template-final.md" +apply_body_limits "/tmp/template-final.md" "${MAX_BODY_BYTES}" "${MAX_COMMENT_BODY_BYTES}" +TEMPLATE="$(cat "${OVERFLOW_MAIN_FILE}")" +printf '%s' "${TEMPLATE}" > /tmp/template + +FINAL_BODY_BYTES="$(wc -c < /tmp/template | tr -d '[:space:]')" +echo "Final main body size (bytes): ${FINAL_BODY_BYTES}" +echo "Managed overflow chunks: ${CHUNK_COUNT}" + if [[ -z "${PR_NUMBER}" ]]; then echo -e "\nCreating pull request" echo -e "${TITLE}" > /tmp/template @@ -173,6 +461,9 @@ if [[ -z "${PR_NUMBER}" ]]; then # shellcheck disable=SC2181 if [[ "$?" != "0" ]]; then RET_CODE=1; fi PR_NUMBER=$(gh pr view --json number -q .number "${URL}") + if (( CHUNK_COUNT > 0 )); then + reconcile_managed_comments "${PR_NUMBER}" "${CHUNK_COUNT}" + fi else echo -e "\nUpdating pull request" COMMAND="hub api --method PATCH repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER} --field 'body=@/tmp/template'" @@ -180,6 +471,11 @@ else URL=$(sh -c "${COMMAND} | jq -r '.html_url'") # shellcheck disable=SC2181 if [[ "$?" != "0" ]]; then RET_CODE=1; fi + if (( CHUNK_COUNT > 0 )); then + reconcile_managed_comments "${PR_NUMBER}" "${CHUNK_COUNT}" + else + reconcile_managed_comments "${PR_NUMBER}" "0" + fi fi # Finish diff --git a/scripts/replace-template-diff.sh b/scripts/replace-template-diff.sh new file mode 100755 index 0000000..49a83b5 --- /dev/null +++ b/scripts/replace-template-diff.sh @@ -0,0 +1,111 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +TEMPLATE_FILE="" +SUMMARY_FILE="" +COMMITS_FILE="" +FILES_FILE="" +REPLACE_SUMMARY="false" +REPLACE_COMMITS="false" +REPLACE_FILES="false" + +while [[ $# -gt 0 ]]; do + case "$1" in + --template) + TEMPLATE_FILE="$2" + shift 2 + ;; + --summary-file) + SUMMARY_FILE="$2" + shift 2 + ;; + --commits-file) + COMMITS_FILE="$2" + shift 2 + ;; + --files-file) + FILES_FILE="$2" + shift 2 + ;; + --replace-summary) + REPLACE_SUMMARY="$2" + shift 2 + ;; + --replace-commits) + REPLACE_COMMITS="$2" + shift 2 + ;; + --replace-files) + REPLACE_FILES="$2" + shift 2 + ;; + *) + echo "[ERROR] Unknown argument: $1" >&2 + exit 1 + ;; + esac +done + +if [[ -z "$TEMPLATE_FILE" ]]; then + echo "[ERROR] Missing required argument: --template" >&2 + exit 1 +fi + +if [[ ! -f "$TEMPLATE_FILE" ]]; then + echo "[ERROR] Template file does not exist: $TEMPLATE_FILE" >&2 + exit 1 +fi + +if [[ "$REPLACE_SUMMARY" == "true" && ( -z "$SUMMARY_FILE" || ! -f "$SUMMARY_FILE" ) ]]; then + echo "[ERROR] Summary replacement requested but file is missing" >&2 + exit 1 +fi + +if [[ "$REPLACE_COMMITS" == "true" && ( -z "$COMMITS_FILE" || ! -f "$COMMITS_FILE" ) ]]; then + echo "[ERROR] Commits replacement requested but file is missing" >&2 + exit 1 +fi + +if [[ "$REPLACE_FILES" == "true" && ( -z "$FILES_FILE" || ! -f "$FILES_FILE" ) ]]; then + echo "[ERROR] Files replacement requested but file is missing" >&2 + exit 1 +fi + +REPLACE_SUMMARY="$REPLACE_SUMMARY" \ +REPLACE_COMMITS="$REPLACE_COMMITS" \ +REPLACE_FILES="$REPLACE_FILES" \ +SUMMARY_FILE="$SUMMARY_FILE" \ +COMMITS_FILE="$COMMITS_FILE" \ +FILES_FILE="$FILES_FILE" \ +perl -0777 -i -pe ' + BEGIN { + sub read_file { + my ($path) = @_; + return q{} if !defined($path) || $path eq q{}; + open my $fh, q{<}, $path or die "Unable to read replacement file: $path\n"; + local $/; + my $content = <$fh>; + close $fh; + return defined($content) ? $content : q{}; + } + + $summary = $ENV{REPLACE_SUMMARY} eq q{true} ? read_file($ENV{SUMMARY_FILE}) : q{}; + $commits = $ENV{REPLACE_COMMITS} eq q{true} ? read_file($ENV{COMMITS_FILE}) : q{}; + $files = $ENV{REPLACE_FILES} eq q{true} ? read_file($ENV{FILES_FILE}) : q{}; + } + + if ($ENV{REPLACE_SUMMARY} eq q{true}) { + s{.*?}{"\n$summary\n"}gse; + } + + if ($ENV{REPLACE_COMMITS} eq q{true}) { + s{}{"\n$commits\n"}gse; + s{.*?}{"\n$commits\n"}gse; + } + + if ($ENV{REPLACE_FILES} eq q{true}) { + s{}{"\n$files\n"}gse; + s{.*?}{"\n$files\n"}gse; + } +' "$TEMPLATE_FILE" diff --git a/scripts/split_content_bytes.py b/scripts/split_content_bytes.py new file mode 100755 index 0000000..f01a808 --- /dev/null +++ b/scripts/split_content_bytes.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +"""Split UTF-8 text into byte-bounded main body and overflow chunks.""" + +from __future__ import annotations + +import pathlib +import sys + + +def take_prefix_by_bytes(text: str, max_bytes: int) -> tuple[str, str]: + """Return the largest UTF-8 prefix not exceeding max_bytes and the tail.""" + if max_bytes <= 0 or not text: + return "", text + + used = 0 + index = 0 + for i, char in enumerate(text): + size = len(char.encode("utf-8")) + if used + size > max_bytes: + break + used += size + index = i + 1 + return text[:index], text[index:] + + +def split_chunks(text: str, max_bytes: int) -> list[str]: + """Split text into UTF-8 chunks bounded by max_bytes.""" + chunks: list[str] = [] + remaining = text + + while remaining: + chunk, tail = take_prefix_by_bytes(remaining, max_bytes) + if not chunk: + # Fallback for invalid max_bytes/encoding edge cases. + chunk = remaining[:1] + tail = remaining[1:] + + if tail and "\n" in chunk: + cut_index = chunk.rfind("\n") + 1 + if cut_index > 0: + tail = chunk[cut_index:] + tail + chunk = chunk[:cut_index] + + chunks.append(chunk) + remaining = tail + + return chunks + + +def main() -> int: + """Parse arguments, split input content, and write chunk files.""" + if len(sys.argv) != 6: + print( + "Usage: split_content_bytes.py " + " ", + file=sys.stderr, + ) + return 1 + + input_file = pathlib.Path(sys.argv[1]) + main_output_file = pathlib.Path(sys.argv[2]) + chunk_prefix = sys.argv[3] + max_main_bytes = int(sys.argv[4]) + max_chunk_bytes = int(sys.argv[5]) + + if max_main_bytes <= 0 or max_chunk_bytes <= 0: + print( + "max_main_bytes and max_chunk_bytes must be greater than zero", + file=sys.stderr, + ) + return 1 + + content = input_file.read_text(encoding="utf-8") + + main_body, overflow = take_prefix_by_bytes(content, max_main_bytes) + main_output_file.write_text(main_body, encoding="utf-8") + + chunks = split_chunks(overflow, max_chunk_bytes) + for index, chunk in enumerate(chunks, start=1): + pathlib.Path(f"{chunk_prefix}-{index}.txt").write_text(chunk, encoding="utf-8") + + print(len(chunks)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/unit/test_branch_validation.sh b/tests/unit/test_branch_validation.sh new file mode 100755 index 0000000..67490b6 --- /dev/null +++ b/tests/unit/test_branch_validation.sh @@ -0,0 +1,95 @@ +#!/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 +} + +git init --initial-branch=main "${TMP_DIR}/repo" >/dev/null + +mkdir -p "${TMP_DIR}/bin" +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" + +pushd "${TMP_DIR}/repo" >/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/develop refs/heads/develop +git remote add origin . +popd >/dev/null + +LOG_FILE="${TMP_DIR}/run.log" +set +e +( + cd "${TMP_DIR}/repo" + PATH="${TMP_DIR}/bin:${PATH}" \ + REAL_GIT="$(command -v git)" \ + GITHUB_ACTOR="ci-user" \ + GITHUB_TOKEN="token" \ + GITHUB_REPOSITORY="owner/repo" \ + GITHUB_WORKSPACE="${TMP_DIR}/repo" \ + GITHUB_OUTPUT="${TMP_DIR}/output.txt" \ + INPUT_GITHUB_TOKEN="token" \ + INPUT_SOURCE_BRANCH="develop" \ + INPUT_TARGET_BRANCH="release/MAPL-v3" \ + 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 when target branch is missing" >&2 + cat "${LOG_FILE}" >&2 + exit 1 +fi + +assert_contains "${LOG_FILE}" "[ERROR] Missing branch reference: release/MAPL-v3" + +echo "Branch validation test passed." diff --git a/tests/unit/test_input_limits_validation.sh b/tests/unit/test_input_limits_validation.sh new file mode 100755 index 0000000..91560f6 --- /dev/null +++ b/tests/unit/test_input_limits_validation.sh @@ -0,0 +1,59 @@ +#!/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_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="invalid" \ +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 max_body_bytes" >&2 + cat "${LOG_FILE}" >&2 + exit 1 +fi + +assert_contains "${LOG_FILE}" "Input 'max_body_bytes' must be a non-negative integer" + +echo "Input limits validation test passed." diff --git a/tests/unit/test_replace_template_diff.sh b/tests/unit/test_replace_template_diff.sh new file mode 100755 index 0000000..0284839 --- /dev/null +++ b/tests/unit/test_replace_template_diff.sh @@ -0,0 +1,152 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +SCRIPT_PATH="$(dirname "$0")/../../scripts/replace-template-diff.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 not_expected="$2" + if grep -Fq -- "${not_expected}" "${file_path}"; then + echo "Assertion failed. Did not expect to find: ${not_expected}" >&2 + echo "----- FILE CONTENT -----" >&2 + cat "${file_path}" >&2 + exit 1 + fi +} + +run_replacement() { + local template_file="$1" + local replace_summary="$2" + local replace_commits="$3" + local replace_files="$4" + + "${SCRIPT_PATH}" \ + --template "${template_file}" \ + --summary-file "${TMP_DIR}/summary.txt" \ + --commits-file "${TMP_DIR}/commits.txt" \ + --files-file "${TMP_DIR}/files.txt" \ + --replace-summary "${replace_summary}" \ + --replace-commits "${replace_commits}" \ + --replace-files "${replace_files}" +} + +run_replacement_files_only() { + local template_file="$1" + + "${SCRIPT_PATH}" \ + --template "${template_file}" \ + --files-file "${TMP_DIR}/files.txt" \ + --replace-summary "false" \ + --replace-commits "false" \ + --replace-files "true" +} + +cat > "${TMP_DIR}/summary.txt" <<'EOF' +Summary line 1 +Summary line 2 +EOF + +cat > "${TMP_DIR}/commits.txt" <<'EOF' +abc123 - commit one +def456 - commit two +EOF + +cat > "${TMP_DIR}/files.txt" <<'EOF' +M README.md +A scripts/new.sh +EOF + +cat > "${TMP_DIR}/template-full.md" <<'EOF' +Body start + +old summary + + + +old files + +Body end +EOF + +run_replacement "${TMP_DIR}/template-full.md" "true" "true" "true" + +assert_contains "${TMP_DIR}/template-full.md" "Summary line 1" +assert_contains "${TMP_DIR}/template-full.md" "abc123 - commit one" +assert_contains "${TMP_DIR}/template-full.md" "M README.md" +assert_contains "${TMP_DIR}/template-full.md" "" +assert_contains "${TMP_DIR}/template-full.md" "" +assert_not_contains "${TMP_DIR}/template-full.md" "old summary" +assert_not_contains "${TMP_DIR}/template-full.md" "old files" + +cat > "${TMP_DIR}/template-commits-block.md" <<'EOF' +Body start + +legacy + +Body end +EOF + +run_replacement "${TMP_DIR}/template-commits-block.md" "false" "true" "false" + +assert_contains "${TMP_DIR}/template-commits-block.md" "def456 - commit two" +assert_not_contains "${TMP_DIR}/template-commits-block.md" "legacy" + +cat > "${TMP_DIR}/template-no-markers.md" <<'EOF' +No markers here. +EOF + +run_replacement "${TMP_DIR}/template-no-markers.md" "false" "false" "false" + +assert_contains "${TMP_DIR}/template-no-markers.md" "No markers here." + +cat > "${TMP_DIR}/template-files-only.md" <<'EOF' +Body start + +old files + +Body end +EOF + +run_replacement_files_only "${TMP_DIR}/template-files-only.md" + +assert_contains "${TMP_DIR}/template-files-only.md" "A scripts/new.sh" +assert_not_contains "${TMP_DIR}/template-files-only.md" "old files" + +python3 - <<'PY' > "${TMP_DIR}/commits-large.txt" +for i in range(15000): + print(f"{i:05d} - synthetic commit line with payload") +PY + +cat > "${TMP_DIR}/template-large.md" <<'EOF' +Large body start + +Large body end +EOF + +"${SCRIPT_PATH}" \ + --template "${TMP_DIR}/template-large.md" \ + --summary-file "${TMP_DIR}/summary.txt" \ + --commits-file "${TMP_DIR}/commits-large.txt" \ + --files-file "${TMP_DIR}/files.txt" \ + --replace-summary "false" \ + --replace-commits "true" \ + --replace-files "false" + +assert_contains "${TMP_DIR}/template-large.md" "14999 - synthetic commit line with payload" + +echo "All replace-template-diff tests passed." diff --git a/tests/unit/test_split_content_bytes.py b/tests/unit/test_split_content_bytes.py new file mode 100755 index 0000000..c31ad8c --- /dev/null +++ b/tests/unit/test_split_content_bytes.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +"""Unit tests for split_content_bytes helper script.""" + +from __future__ import annotations + +import pathlib +import subprocess +import tempfile + + +def read(path: pathlib.Path) -> str: + """Read UTF-8 text file content.""" + return path.read_text(encoding="utf-8") + + +def main() -> int: + """Run split-content helper validations.""" + script = ( + pathlib.Path(__file__).resolve().parents[2] + / "scripts" + / "split_content_bytes.py" + ) + + with tempfile.TemporaryDirectory() as tmp_dir: + tmp = pathlib.Path(tmp_dir) + source = tmp / "source.txt" + main_out = tmp / "main.txt" + prefix = str(tmp / "chunk") + + source.write_text( + "line-01\nline-02\nline-03\nline-04\nline-05\n", + encoding="utf-8", + ) + + result = subprocess.run( + [ + "python3", + str(script), + str(source), + str(main_out), + prefix, + "14", + "12", + ], + check=True, + capture_output=True, + text=True, + ) + + chunk_count = int(result.stdout.strip()) + assert chunk_count >= 1 + + combined = read(main_out) + for index in range(1, chunk_count + 1): + chunk_file = tmp / f"chunk-{index}.txt" + assert chunk_file.exists() + combined += read(chunk_file) + + assert combined == read(source) + assert len(read(main_out).encode("utf-8")) <= 14 + for index in range(1, chunk_count + 1): + chunk_file = tmp / f"chunk-{index}.txt" + assert len(read(chunk_file).encode("utf-8")) <= 12 + + print("split-content-bytes tests passed") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/unit/test_template_source_selection.sh b/tests/unit/test_template_source_selection.sh new file mode 100755 index 0000000..4e3eaf2 --- /dev/null +++ b/tests/unit/test_template_source_selection.sh @@ -0,0 +1,168 @@ +#!/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 +} + +mkdir -p "${TMP_DIR}/bin" + +cat > "${TMP_DIR}/bin/git" <<'EOF' +#!/usr/bin/env bash +set -Eeuo pipefail + +if [[ "$#" -ge 2 && "$1" == "config" && "$2" == "--global" ]]; then + exit 0 +fi + +if [[ "$#" -ge 2 && "$1" == "remote" && "$2" == "set-url" ]]; then + exit 0 +fi + +if [[ "$#" -ge 1 && "$1" == "fetch" ]]; then + exit 0 +fi + +if [[ "$#" -ge 2 && "$1" == "show-ref" ]]; then + if [[ "${@: -1}" == "refs/remotes/origin/develop" || "${@: -1}" == "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 + echo "bbb222" + exit 0 + fi + if [[ "${@: -1}" == "origin/release/MAPL-v3" ]]; then + echo "aaa111" + exit 0 + fi +fi + +if [[ "$#" -ge 2 && "$1" == "diff" && "$2" == "--quiet" ]]; then + exit 1 +fi + +if [[ "$#" -ge 1 && "$1" == "diff" ]]; then + echo "M README.md" + exit 0 +fi + +if [[ "$#" -ge 1 && "$1" == "log" ]]; then + echo "stub log" + exit 0 +fi + +if [[ "$#" -ge 2 && "$1" == "symbolic-ref" ]]; then + echo "develop" + exit 0 +fi + +echo "Unsupported git call: $*" >&2 +exit 1 +EOF + +cat > "${TMP_DIR}/bin/hub" <<'EOF' +#!/usr/bin/env bash +set -Eeuo pipefail + +if [[ "$#" -ge 2 && "$1" == "pr" && "$2" == "list" ]]; then + echo "123" + exit 0 +fi + +if [[ "$#" -ge 3 && "$1" == "api" && "$2" == "--method" && "$3" == "GET" ]]; then + echo '{"body":"OLD BODY WITHOUT MARKERS"}' + exit 0 +fi + +if [[ "$#" -ge 3 && "$1" == "api" && "$2" == "--method" && "$3" == "PATCH" ]]; then + echo '{"html_url":"https://example.test/pr/123"}' + exit 0 +fi + +echo "Unsupported hub call: $*" >&2 +exit 1 +EOF + +cat > "${TMP_DIR}/bin/gh" <<'EOF' +#!/usr/bin/env bash +set -Eeuo pipefail + +if [[ "$#" -ge 2 && "$1" == "api" ]]; then + if [[ "$2" == "repos/owner/repo/issues/123/comments" ]]; then + echo "[]" + exit 0 + fi + if [[ "$2" == "repos/owner/repo/issues/comments/"* ]]; then + exit 0 + fi +fi + +echo "Unsupported gh call: $*" >&2 +exit 1 +EOF + +cat > "${TMP_DIR}/template.md" <<'EOF' +## Template + + +EOF + +chmod +x "${TMP_DIR}/bin/git" "${TMP_DIR}/bin/hub" "${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_SOURCE_BRANCH="develop" \ +INPUT_TARGET_BRANCH="release/MAPL-v3" \ +INPUT_TITLE="" \ +INPUT_TEMPLATE="${TMP_DIR}/template.md" \ +INPUT_BODY="" \ +INPUT_REVIEWER="" \ +INPUT_ASSIGNEE="" \ +INPUT_LABEL="" \ +INPUT_MILESTONE="" \ +INPUT_DRAFT="false" \ +INPUT_GET_DIFF="true" \ +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" >&2 + cat "${LOG_FILE}" >&2 + exit 1 +fi + +assert_contains "${LOG_FILE}" "Template source: input template file (update mode)" +assert_contains "${LOG_FILE}" "Detected diff markers: summary=false commits=false files=true" + +echo "Template source selection test passed."