name: Backport CommonCore on: pull_request: types: [closed] branches: - main permissions: contents: 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:-}" if [[ -n "$body" ]]; then curl --fail --silent --show-error \ -X "$method" \ -H "Authorization: token ${GITEA_TOKEN}" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ --data "$body" \ "${GITHUB_API_URL}${path}" else curl --fail --silent --show-error \ -X "$method" \ -H "Authorization: token ${GITEA_TOKEN}" \ -H "Accept: application/json" \ "${GITHUB_API_URL}${path}" fi } repo_path="/repos/${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")" 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 config --global http.extraHeader "Authorization: token ${GITEA_TOKEN}" git fetch origin '+refs/heads/*:refs/remotes/origin/*' for commit in "${source_commits[@]}"; do git cat-file -e "${commit}^{commit}" done 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 cherry_pick_args=(-x) if [[ "$(git rev-list --parents -n 1 "$commit" | wc -w)" -gt 2 ]]; then cherry_pick_args=(-x -m 1) fi if git cherry-pick "${cherry_pick_args[@]}" "$commit"; then continue fi if [[ -z "$(git status --porcelain)" ]]; then echo "Cherry-pick of ${commit} produced no changes; skipping it." git cherry-pick --skip || true continue fi git cherry-pick --abort || true failures+=("${target_branch}: cherry-pick of ${commit} failed") break done if [[ "${failures[*]:-}" == *"${target_branch}:"* ]]; then git checkout main continue fi if git diff --quiet "origin/${target_branch}" HEAD; then echo "${target_branch} already contains the change; no PR needed." git checkout main continue fi 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