|
| 1 | +#!/usr/bin/env bash |
| 2 | +set -euo pipefail |
| 3 | + |
| 4 | +base_ref="${INPUT_BASE_REF:-main}" |
| 5 | +pull_request_number="${INPUT_PULL_REQUEST_NUMBER:-}" |
| 6 | +allowed_conflicts=$'CHANGELOG.md\n.github/wiki' |
| 7 | +resolved_count=0 |
| 8 | +skipped_count=0 |
| 9 | +failed_count=0 |
| 10 | +summary_file="${GITHUB_STEP_SUMMARY:-}" |
| 11 | + |
| 12 | +append_summary() { |
| 13 | + local message="$1" |
| 14 | + |
| 15 | + if [ -n "${summary_file}" ]; then |
| 16 | + printf '%s\n' "${message}" >> "${summary_file}" |
| 17 | + else |
| 18 | + printf '%s\n' "${message}" |
| 19 | + fi |
| 20 | +} |
| 21 | + |
| 22 | +collect_pull_requests() { |
| 23 | + if [ -n "${pull_request_number}" ]; then |
| 24 | + gh pr view "${pull_request_number}" \ |
| 25 | + --json number,title,url,baseRefName,headRefName,headRepositoryOwner,isCrossRepository,mergeable |
| 26 | + |
| 27 | + return |
| 28 | + fi |
| 29 | + |
| 30 | + gh pr list \ |
| 31 | + --state open \ |
| 32 | + --base "${base_ref}" \ |
| 33 | + --json number,title,url,baseRefName,headRefName,headRepositoryOwner,isCrossRepository,mergeable |
| 34 | +} |
| 35 | + |
| 36 | +repository_url() { |
| 37 | + php -r ' |
| 38 | + $composer = json_decode((string) file_get_contents("composer.json"), true); |
| 39 | + $support = is_array($composer) ? ($composer["support"] ?? []) : []; |
| 40 | + $source = is_array($support) ? ($support["source"] ?? null) : null; |
| 41 | + echo is_string($source) && "" !== $source ? $source : "https://github.com/" . getenv("GITHUB_REPOSITORY"); |
| 42 | + ' |
| 43 | +} |
| 44 | + |
| 45 | +is_allowed_conflict_scope() { |
| 46 | + local conflicts="$1" |
| 47 | + |
| 48 | + while IFS= read -r file; do |
| 49 | + if [ -z "${file}" ]; then |
| 50 | + continue |
| 51 | + fi |
| 52 | + |
| 53 | + if ! grep -Fx --quiet -- "${file}" <<< "${allowed_conflicts}"; then |
| 54 | + return 1 |
| 55 | + fi |
| 56 | + done <<< "${conflicts}" |
| 57 | + |
| 58 | + return 0 |
| 59 | +} |
| 60 | + |
| 61 | +resolve_pull_request() { |
| 62 | + local number="$1" |
| 63 | + local title="$2" |
| 64 | + local url="$3" |
| 65 | + local head_ref="$4" |
| 66 | + local head_owner="$5" |
| 67 | + local cross_repository="$6" |
| 68 | + local pr_base_ref="$7" |
| 69 | + local mergeable="$8" |
| 70 | + |
| 71 | + append_summary "- PR #${number}: inspecting ${url}" |
| 72 | + |
| 73 | + if [ "${pr_base_ref}" != "${base_ref}" ]; then |
| 74 | + append_summary " - skipped: base branch is \`${pr_base_ref}\`, expected \`${base_ref}\`" |
| 75 | + skipped_count=$((skipped_count + 1)) |
| 76 | + |
| 77 | + return |
| 78 | + fi |
| 79 | + |
| 80 | + if [ "${cross_repository}" = "true" ] || [ "${head_owner}" != "${GITHUB_REPOSITORY_OWNER}" ]; then |
| 81 | + append_summary " - skipped: pull request branch is outside this repository" |
| 82 | + skipped_count=$((skipped_count + 1)) |
| 83 | + |
| 84 | + return |
| 85 | + fi |
| 86 | + |
| 87 | + if [ "${mergeable}" = "MERGEABLE" ]; then |
| 88 | + append_summary " - skipped: GitHub currently reports the pull request as mergeable" |
| 89 | + skipped_count=$((skipped_count + 1)) |
| 90 | + |
| 91 | + return |
| 92 | + fi |
| 93 | + |
| 94 | + local workdir |
| 95 | + workdir="$(mktemp -d)" |
| 96 | + trap 'rm -rf "${workdir}"' RETURN |
| 97 | + |
| 98 | + git clone --no-tags "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "${workdir}/repo" >/dev/null 2>&1 |
| 99 | + git -C "${workdir}/repo" config user.name "github-actions[bot]" |
| 100 | + git -C "${workdir}/repo" config user.email "41898282+github-actions[bot]@users.noreply.github.com" |
| 101 | + git -C "${workdir}/repo" fetch --no-tags origin \ |
| 102 | + "+refs/heads/${base_ref}:refs/remotes/origin/${base_ref}" \ |
| 103 | + "+refs/heads/${head_ref}:refs/remotes/origin/${head_ref}" >/dev/null 2>&1 |
| 104 | + git -C "${workdir}/repo" switch -C "${head_ref}" "refs/remotes/origin/${head_ref}" >/dev/null 2>&1 |
| 105 | + |
| 106 | + if git -C "${workdir}/repo" merge --no-commit --no-ff "refs/remotes/origin/${base_ref}" >/dev/null 2>&1; then |
| 107 | + git -C "${workdir}/repo" merge --abort >/dev/null 2>&1 || true |
| 108 | + append_summary " - skipped: merge succeeds cleanly when checked locally" |
| 109 | + skipped_count=$((skipped_count + 1)) |
| 110 | + |
| 111 | + return |
| 112 | + fi |
| 113 | + |
| 114 | + local conflicts |
| 115 | + conflicts="$(git -C "${workdir}/repo" diff --name-only --diff-filter=U)" |
| 116 | + |
| 117 | + if [ -z "${conflicts}" ]; then |
| 118 | + git -C "${workdir}/repo" merge --abort >/dev/null 2>&1 || true |
| 119 | + append_summary " - skipped: merge failed but no unmerged files were reported" |
| 120 | + skipped_count=$((skipped_count + 1)) |
| 121 | + |
| 122 | + return |
| 123 | + fi |
| 124 | + |
| 125 | + if ! is_allowed_conflict_scope "${conflicts}"; then |
| 126 | + git -C "${workdir}/repo" merge --abort >/dev/null 2>&1 || true |
| 127 | + append_summary " - skipped: conflict scope requires manual review" |
| 128 | + append_summary "$(printf '%s\n' "${conflicts}" | sed 's/^/ - `/; s/$/`/')" |
| 129 | + skipped_count=$((skipped_count + 1)) |
| 130 | + |
| 131 | + return |
| 132 | + fi |
| 133 | + |
| 134 | + if grep -Fx --quiet -- ".github/wiki" <<< "${conflicts}"; then |
| 135 | + git -C "${workdir}/repo" checkout --ours -- .github/wiki |
| 136 | + git -C "${workdir}/repo" add .github/wiki |
| 137 | + fi |
| 138 | + |
| 139 | + if grep -Fx --quiet -- "CHANGELOG.md" <<< "${conflicts}"; then |
| 140 | + # During `git merge base into PR`, stage 2 is the PR side and stage 3 is the base branch side. |
| 141 | + git -C "${workdir}/repo" show :2:CHANGELOG.md > "${workdir}/CHANGELOG.ours.md" |
| 142 | + git -C "${workdir}/repo" show :3:CHANGELOG.md > "${workdir}/CHANGELOG.theirs.md" |
| 143 | + ( |
| 144 | + cd "${workdir}/repo" |
| 145 | + php "${DEV_TOOLS_CONFLICT_RESOLVER}" \ |
| 146 | + --target="${workdir}/CHANGELOG.theirs.md" \ |
| 147 | + --source="${workdir}/CHANGELOG.ours.md" \ |
| 148 | + --output="CHANGELOG.md" \ |
| 149 | + --repository-url="$(repository_url)" |
| 150 | + ) |
| 151 | + git -C "${workdir}/repo" add CHANGELOG.md |
| 152 | + fi |
| 153 | + |
| 154 | + if [ -n "$(git -C "${workdir}/repo" diff --name-only --diff-filter=U)" ]; then |
| 155 | + git -C "${workdir}/repo" merge --abort >/dev/null 2>&1 || true |
| 156 | + append_summary " - failed: predictable files were handled, but unmerged paths remain" |
| 157 | + failed_count=$((failed_count + 1)) |
| 158 | + |
| 159 | + return |
| 160 | + fi |
| 161 | + |
| 162 | + git -C "${workdir}/repo" commit -m "Resolve predictable conflicts with ${base_ref}" >/dev/null 2>&1 |
| 163 | + git -C "${workdir}/repo" push origin "HEAD:${head_ref}" >/dev/null 2>&1 |
| 164 | + append_summary " - resolved: pushed an automatic conflict-resolution commit for \`${title}\`" |
| 165 | + resolved_count=$((resolved_count + 1)) |
| 166 | +} |
| 167 | + |
| 168 | +if [ -z "${GH_TOKEN:-}" ]; then |
| 169 | + echo "GH_TOKEN is required." >&2 |
| 170 | + |
| 171 | + exit 1 |
| 172 | +fi |
| 173 | + |
| 174 | +append_summary "## Predictable Conflict Resolution Summary" |
| 175 | +append_summary "" |
| 176 | +append_summary "- Base branch: \`${base_ref}\`" |
| 177 | + |
| 178 | +pull_requests="$(collect_pull_requests)" |
| 179 | + |
| 180 | +if [ "${pull_requests:0:1}" = "{" ]; then |
| 181 | + pull_requests="[${pull_requests}]" |
| 182 | +fi |
| 183 | + |
| 184 | +while IFS= read -r pull_request; do |
| 185 | + [ -n "${pull_request}" ] || continue |
| 186 | + |
| 187 | + resolve_pull_request \ |
| 188 | + "$(jq -r '.number' <<< "${pull_request}")" \ |
| 189 | + "$(jq -r '.title' <<< "${pull_request}")" \ |
| 190 | + "$(jq -r '.url' <<< "${pull_request}")" \ |
| 191 | + "$(jq -r '.headRefName' <<< "${pull_request}")" \ |
| 192 | + "$(jq -r '.headRepositoryOwner.login' <<< "${pull_request}")" \ |
| 193 | + "$(jq -r '.isCrossRepository' <<< "${pull_request}")" \ |
| 194 | + "$(jq -r '.baseRefName' <<< "${pull_request}")" \ |
| 195 | + "$(jq -r '.mergeable // "UNKNOWN"' <<< "${pull_request}")" |
| 196 | +done < <(jq -c '.[]' <<< "${pull_requests}") |
| 197 | + |
| 198 | +append_summary "" |
| 199 | +append_summary "- Resolved: ${resolved_count}" |
| 200 | +append_summary "- Skipped: ${skipped_count}" |
| 201 | +append_summary "- Failed: ${failed_count}" |
| 202 | + |
| 203 | +if [ "${failed_count}" -gt 0 ]; then |
| 204 | + exit 1 |
| 205 | +fi |
0 commit comments