name: Backport CommonCore on: pull_request: types: [closed] branches: - main permissions: contents: write issues: write pull-requests: write jobs: backport: name: Backport CommonCore changes runs-on: ubuntu-latest if: ${{ github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'main' }} steps: - name: Checkout sources uses: actions/checkout@v6 with: ref: main fetch-depth: 0 token: ${{ secrets.GITEA_TOKEN }} - name: Create version branch backports shell: bash env: GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} run: | set -euo pipefail api() { local method="$1" local path="$2" local body="${3:-}" local response_file local status response_file="$(mktemp)" if [[ -n "$body" ]]; then status="$(curl --silent --show-error \ -X "$method" \ -H "Authorization: token ${GITEA_TOKEN}" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ --data "$body" \ --output "$response_file" \ --write-out "%{http_code}" \ "${GITHUB_API_URL}${path}")" else status="$(curl --silent --show-error \ -X "$method" \ -H "Authorization: token ${GITEA_TOKEN}" \ -H "Accept: application/json" \ --output "$response_file" \ --write-out "%{http_code}" \ "${GITHUB_API_URL}${path}")" fi if [[ "$status" -lt 200 || "$status" -ge 300 ]]; then echo "Gitea API ${method} ${path} failed with HTTP ${status}" >&2 cat "$response_file" >&2 rm -f "$response_file" return 1 fi cat "$response_file" rm -f "$response_file" } repo_path="/repos/${GITHUB_REPOSITORY}" repo_owner="${GITHUB_REPOSITORY%%/*}" pr_number="$(jq -r '.number' "$GITHUB_EVENT_PATH")" pr_title="$(jq -r '.pull_request.title // ""' "$GITHUB_EVENT_PATH")" event_commit="$(jq -r '.pull_request.merge_commit_sha // .pull_request.merged_commit_id // .commit_id // env.GITHUB_SHA' "$GITHUB_EVENT_PATH")" if jq -e '.pull_request.labels[]? | select(.name == "no-backport")' "$GITHUB_EVENT_PATH" >/dev/null; then echo "PR #${pr_number} has label no-backport; skipping automatic backport." exit 0 fi commoncore_files=() page=1 while true; do response="$(api GET "${repo_path}/pulls/${pr_number}/files?page=${page}&limit=50")" count="$(jq 'length' <<< "$response")" while IFS= read -r file; do [[ -n "$file" ]] && commoncore_files+=("$file") done < <(jq -r '.[] | (.filename // .path // .name // "") | select(startswith("CommonCore/"))' <<< "$response") [[ "$count" -lt 50 ]] && break page=$((page + 1)) done if [[ "${#commoncore_files[@]}" -eq 0 ]]; then echo "PR #${pr_number} did not change CommonCore/; nothing to backport." exit 0 fi echo "PR #${pr_number} changed CommonCore files:" printf ' - %s\n' "${commoncore_files[@]}" pr_commits=() page=1 while true; do response="$(api GET "${repo_path}/pulls/${pr_number}/commits?page=${page}&limit=50")" count="$(jq 'length' <<< "$response")" while IFS= read -r commit; do [[ -n "$commit" ]] && pr_commits+=("$commit") done < <(jq -r '.[] | .sha // .id // empty' <<< "$response") [[ "$count" -lt 50 ]] && break page=$((page + 1)) done source_commits=("$event_commit") if [[ "${#pr_commits[@]}" -gt 1 ]]; then for commit in "${pr_commits[@]}"; do if [[ "$commit" == "$event_commit" ]]; then source_commits=("${pr_commits[@]}") break fi done fi echo "Using source commit plan:" printf ' - %s\n' "${source_commits[@]}" git config --global user.name "SteamWar Backport Bot" git config --global user.email "backport-bot@steamwar.de" git fetch origin '+refs/heads/*:refs/remotes/origin/*' for commit in "${source_commits[@]}"; do git cat-file -e "${commit}^{commit}" done apply_commoncore_change() { local commit="$1" local parent local patch_file if [[ "$(git rev-list --parents -n 1 "$commit" | wc -w)" -gt 2 ]]; then parent="${commit}^1" else parent="${commit}^" fi patch_file="$(mktemp)" git diff --binary "$parent" "$commit" -- CommonCore/ > "$patch_file" if [[ ! -s "$patch_file" ]]; then echo "Commit ${commit} has no CommonCore/ diff; skipping it." rm -f "$patch_file" return 0 fi if git apply --reverse --check "$patch_file" >/dev/null 2>&1; then echo "CommonCore/ diff from ${commit} is already present; skipping it." rm -f "$patch_file" return 0 fi if git apply --3way --index "$patch_file"; then rm -f "$patch_file" return 0 fi rm -f "$patch_file" return 1 } mapfile -t version_branches < <( git for-each-ref --format='%(refname:short)' refs/remotes/origin/version | sed 's#^origin/##' | sort -u ) if [[ "${#version_branches[@]}" -eq 0 ]]; then echo "No origin/version/* branches found; nothing to backport." exit 0 fi failures=() for target_branch in "${version_branches[@]}"; do safe_target="$(sed -E 's#[^A-Za-z0-9._-]+#-#g; s#^-+##; s#-+$##' <<< "$target_branch")" head_branch="backport-pr-${pr_number}-to-${safe_target}" existing_pr="$(api GET "${repo_path}/pulls?state=open&base_branch=$(jq -rn --arg value "$target_branch" '$value|@uri')&limit=50" | jq -r --arg head "$head_branch" '.[] | select(.head.ref == $head) | .number' | head -n 1)" if [[ -n "$existing_pr" ]]; then echo "Backport PR #${existing_pr} already exists for ${target_branch}; leaving it unchanged." continue fi echo "Creating ${head_branch} from ${target_branch}" git checkout -B "$head_branch" "origin/${target_branch}" for commit in "${source_commits[@]}"; do if apply_commoncore_change "$commit"; then continue fi echo "Failed to apply CommonCore/ diff from ${commit} to ${target_branch}" >&2 exit 1 done if git diff --cached --quiet; then echo "${target_branch} already contains the CommonCore/ change; no PR needed." git checkout main continue fi git commit -m "Backport CommonCore changes from #${pr_number}" \ -m "Source PR: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/pulls/${pr_number}" \ -m "Only CommonCore/ changes are included in this backport." git push origin "HEAD:refs/heads/${head_branch}" --force-with-lease if [[ "${#source_commits[@]}" -eq 1 ]]; then source_text="Source commit: \`${source_commits[0]}\`" else source_text="Source commits:" for commit in "${source_commits[@]}"; do source_text="${source_text}"$'\n'"- \`${commit}\`" done fi body="$(cat </dev/null echo "Created backport PR from ${head_branch} to ${target_branch}." git checkout main done if [[ "${#failures[@]}" -gt 0 ]]; then echo "Backport failures:" printf ' - %s\n' "${failures[@]}" exit 1 fi