Skip to content

Merge main into branches (scheduled + manual) #6

Merge main into branches (scheduled + manual)

Merge main into branches (scheduled + manual) #6

name: Merge main into branches (scheduled + manual)
on:
schedule:
- cron: '0 21 * * *' # daily at 02:00 PKT (21:00 UTC previous day)
workflow_dispatch:
inputs:
targets:
description: 'Optional: comma-separated list of target branches (overrides auto-detect). If empty, all remote branches except `main` will be targeted.'
required: false
default: ''
permissions:
contents: write
issues: write
pull-requests: write
jobs:
merge:
runs-on: ubuntu-latest
steps:
- name: Checkout (full)
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Ensure required tools
run: |
set -euo pipefail
PACKAGES=""
if ! command -v jq >/dev/null 2>&1; then
PACKAGES="$PACKAGES jq"
fi
if ! command -v curl >/dev/null 2>&1; then
PACKAGES="$PACKAGES curl"
fi
if ! command -v git-lfs >/dev/null 2>&1; then
PACKAGES="$PACKAGES git-lfs"
fi
if [ -n "$PACKAGES" ]; then
sudo apt-get update
sudo apt-get install -y $PACKAGES
fi
if command -v git-lfs >/dev/null 2>&1; then
git lfs install --local || true
fi
- name: Git config user (default)
run: |
git config user.name "github-actions[bot]"
git config user.email "actions@github.com"
- name: Merge main into targets
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
TARGETS_INPUT: ${{ github.event.inputs.targets || '' }}
ASSIGNEE: "usman-pirzada"
run: |
set -euo pipefail
# helper for safe GitHub API POSTs — prints only safe fields
gh_api_post() {
url="$1"
payload="$2"
resp="$(mktemp)"
http_code="$(curl -s -o "$resp" -w '%{http_code}' -X POST -H "Authorization: token $GITHUB_TOKEN" -H "Accept: application/vnd.github+json" "$url" -d "$payload")"
html_url="$(jq -r '.html_url // empty' "$resp" 2>/dev/null || true)"
number="$(jq -r '.number // empty' "$resp" 2>/dev/null || true)"
message="$(jq -r '.message // empty' "$resp" 2>/dev/null || true)"
errors="$(jq -c '.errors // empty' "$resp" 2>/dev/null || true)"
printf '%s|%s|%s|%s|%s\n' "$http_code" "$html_url" "$number" "$message" "$errors"
rm -f "$resp"
}
# prepare targets list (manual override or auto-detect all remote branches except main and git-bot/*)
git fetch --prune origin '+refs/heads/*:refs/remotes/origin/*'
if [ -n "${TARGETS_INPUT:-}" ]; then
IFS=',' read -ra TARGETS <<< "$TARGETS_INPUT"
else
mapfile -t TARGETS < <(git for-each-ref --format='%(refname:short)' refs/remotes/origin \
| sed 's#^origin/##' \
| grep -v -x 'main' \
| grep -v -x 'HEAD' \
| grep -v -E '^git-bot/' || true)
fi
MAIN_REF="origin/main"
git fetch origin main || true
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
git config user.name "${{ github.actor }}"
git config user.email "${{ github.actor }}@users.noreply.github.com"
else
git config user.name "github-actions[bot]"
git config user.email "actions@github.com"
fi
TMP_SUFFIX="${GITHUB_RUN_ID:-manual}-$(date +%s)"
: > conflicts_report.txt
for raw_branch in "${TARGETS[@]}"; do
branch="$(echo "$raw_branch" | xargs)"
if [ -z "$branch" ]; then
continue
fi
if [ "$branch" = "main" ]; then
continue
fi
echo "=== Processing branch: $branch ==="
if ! git ls-remote --exit-code --heads origin "$branch" >/dev/null 2>&1; then
echo "origin/$branch not found; skipping."
continue
fi
git fetch origin "$branch"
git checkout -B "$branch" "origin/$branch"
if git merge --ff-only "$MAIN_REF" 2>/dev/null; then
echo "Fast-forwarded $branch to $MAIN_REF (no merge commit required). Attempting to push..."
if git push origin "$branch"; then
echo "Pushed $branch successfully."
else
echo "Push rejected (protected branch or policy). Creating a temporary branch and opening a PR."
tmp="git-bot/merge-main-into-${branch}-${TMP_SUFFIX}"
git checkout -b "$tmp"
git push origin "$tmp"
pr_title="Automated: merge main → $branch"
pr_body="This PR was created automatically by a scheduled/manual workflow to merge \`main\` into \`$branch\`.\n\nPlease review, run CI, and merge as appropriate."
repo="$REPO"
payload="$(jq -n --arg t "$pr_title" --arg b "$pr_body" --arg head "$tmp" --arg base "$branch" '{title: $t, body: $b, head: $head, base: $base}')"
result="$(gh_api_post "https://api.github.com/repos/$repo/pulls" "$payload")"
IFS='|' read -r pr_http pr_url pr_number pr_message pr_errors <<< "$result"
if [[ "$pr_http" =~ ^2 ]]; then
if [ -n "$pr_url" ]; then
echo "PR created: $pr_url"
else
echo "PR created but URL not returned."
fi
else
echo "Failed to create PR (HTTP $pr_http). Message: ${pr_message:-'(no message)'} Errors: ${pr_errors:-'(none)'}"
fi
git checkout "$branch"
git branch -D "$tmp" || true
fi
else
if git merge --no-ff -m "Merge origin/main into $branch [Automated]" "$MAIN_REF"; then
echo "Merge commit created for $branch — attempting to push..."
if git push origin "$branch"; then
echo "Pushed merged $branch successfully."
else
echo "Push rejected after merge (protected branch or policy). Creating a temporary branch and opening a PR."
tmp="git-bot/merge-main-into-${branch}-${TMP_SUFFIX}"
git checkout -b "$tmp"
git push origin "$tmp"
pr_title="Automated: merge main → $branch"
pr_body="This PR was created automatically by a scheduled/manual workflow to merge \`main\` into \`$branch\`.\n\nPlease review, run CI, and merge as appropriate."
repo="$REPO"
payload="$(jq -n --arg t "$pr_title" --arg b "$pr_body" --arg head "$tmp" --arg base "$branch" '{title: $t, body: $b, head: $head, base: $base}')"
result="$(gh_api_post "https://api.github.com/repos/$repo/pulls" "$payload")"
IFS='|' read -r pr_http pr_url pr_number pr_message pr_errors <<< "$result"
if [[ "$pr_http" =~ ^2 ]]; then
if [ -n "$pr_url" ]; then
echo "PR created: $pr_url"
else
echo "PR created but URL not returned."
fi
else
echo "Failed to create PR (HTTP $pr_http). Message: ${pr_message:-'(no message)'} Errors: ${pr_errors:-'(none)'}"
fi
git checkout "$branch"
git branch -D "$tmp" || true
fi
else
conflicts="$(git diff --name-only --diff-filter=U || true)"
if [ -z "$conflicts" ]; then
conflicts="$(git ls-files -u | awk '{print $4}' | sort -u || true)"
fi
if [ -z "$conflicts" ]; then
conflicts="(no files listed by diff --name-only; showing git status)\n$(git status --porcelain || true)\n\nDetailed unmerged entries:\n$(git ls-files -u || true)"
fi
git merge --abort || true
echo "Merge conflict for $branch. Conflicting files:"
echo "$conflicts"
printf "Branch: %s\nConflicting files:\n%s\n\n" "$branch" "$conflicts" >> conflicts_report.txt
issue_title="Merge conflict: main -> $branch"
issue_body="Automated attempt to merge \`main\` into \`$branch\` failed with conflicts.\n\n**Conflicting files / details:**\n\`\`\`\n$conflicts\n\`\`\`\nPlease resolve in branch \`$branch\`."
repo="$REPO"
payload="$(jq -n --arg t "$issue_title" --arg b "$issue_body" --arg a "$ASSIGNEE" '{title: $t, body: $b, assignees: [$a]}')"
result="$(gh_api_post "https://api.github.com/repos/$repo/issues" "$payload")"
IFS='|' read -r iss_http iss_url iss_number iss_message iss_errors <<< "$result"
if [[ "$iss_http" =~ ^2 || "$iss_http" =~ ^3 ]]; then
if [ -n "$iss_number" ]; then
echo "Issue created: $repo/issues/$iss_number"
assignees="$(curl -s -H "Authorization: token $GITHUB_TOKEN" "https://api.github.com/repos/$repo/issues/$iss_number" | jq -r '.assignees | map(.login) | join(",")' 2>/dev/null || true)"
if echo "$assignees" | grep -q "$ASSIGNEE"; then
echo "Assignee $ASSIGNEE successfully added to issue #$iss_number."
else
comment_body="Attempted to assign @$ASSIGNEE but the assignment did not succeed. @${ASSIGNEE}, please take a look at this conflict in \`$branch\`."
comment_payload="$(jq -n --arg b "$comment_body" '{body: $b}')"
comment_result="$(gh_api_post "https://api.github.com/repos/$repo/issues/$iss_number/comments" "$comment_payload")"
IFS='|' read -r c_http c_url c_num c_msg c_err <<< "$comment_result"
if [[ "$c_http" =~ ^2 ]]; then
echo "Posted a mention comment to notify $ASSIGNEE."
else
echo "Failed to post mention comment (HTTP $c_http). Message: ${c_msg:-'(no message)'}"
fi
fi
else
echo "Issue created but number not returned; message: ${iss_message:-'(no message)'}"
fi
else
echo "Failed to create issue (HTTP $iss_http). Message: ${iss_message:-'(no message)'} Errors: ${iss_errors:-'(none)'}"
fi
echo "Created/attempted issue for $branch; continuing to next branch."
fi
fi
sleep 2
done
echo "=== Summary of conflicts (if any) ==="
if [ -s conflicts_report.txt ]; then
echo "Conflicts found and issues created. conflicts_report.txt contains a summary."
cat conflicts_report.txt
else
echo "No conflicts detected."
fi
- name: Final status
run: echo "Bulk merge job completed."