Merge main into branches (scheduled + manual) #6
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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." |