diff --git a/.github/scripts/auto_resolve_ignored_conflicts.sh b/.github/scripts/auto_resolve_ignored_conflicts.sh new file mode 100755 index 000000000..6c5170504 --- /dev/null +++ b/.github/scripts/auto_resolve_ignored_conflicts.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash +# Auto-resolve conflicts in files that should use "ours" merge strategy +# These files are PR-specific and should keep the PR branch version when conflicts occur +# +# Usage: ./auto_resolve_ignored_conflicts.sh +# +# This script: +# 1. Fetches the latest base branch +# 2. Attempts to merge base into the current branch +# 3. For known PR-specific files, auto-resolves using --ours +# 4. Commits the resolution if successful + +set -e + +BASE_BRANCH="${1:-main}" + +# Files that should always keep the PR branch version (configured in .gitattributes with merge=ours) +# but need manual resolution since the git driver isn't configured +IGNORED_CONFLICT_FILES=( + "pr_body.md" + "ci/autofix/history.json" + "keepalive-metrics.ndjson" + "coverage-trend-history.ndjson" + "metrics-history.ndjson" + "residual-trend-history.ndjson" +) + +echo "=== Auto-Resolve Ignored Conflict Files ===" +echo "Base branch: $BASE_BRANCH" + +# Fetch latest base +echo "Fetching origin/$BASE_BRANCH..." +git fetch origin "$BASE_BRANCH" --quiet + +# Check if we're already up to date +if git merge-base --is-ancestor "origin/$BASE_BRANCH" HEAD 2>/dev/null; then + echo "✓ Branch is already up to date with $BASE_BRANCH" + exit 0 +fi + +# Try to merge - this may create conflicts +echo "Attempting merge from origin/$BASE_BRANCH..." +if git merge "origin/$BASE_BRANCH" --no-edit 2>/dev/null; then + echo "✓ Merge completed without conflicts" + exit 0 +fi + +# Merge failed - check for conflicts in ignored files +echo "Merge has conflicts. Checking for auto-resolvable files..." + +RESOLVED_COUNT=0 +REMAINING_CONFLICTS=() + +# Get list of unmerged files +while IFS= read -r conflict_file; do + [ -z "$conflict_file" ] && continue + + SHOULD_AUTO_RESOLVE=false + for ignored in "${IGNORED_CONFLICT_FILES[@]}"; do + if [[ "$conflict_file" == "$ignored" || "$conflict_file" == *"/$ignored" ]]; then + SHOULD_AUTO_RESOLVE=true + break + fi + done + + if $SHOULD_AUTO_RESOLVE; then + echo " → Auto-resolving: $conflict_file (keeping ours)" + git checkout --ours "$conflict_file" 2>/dev/null || true + git add "$conflict_file" 2>/dev/null || true + ((RESOLVED_COUNT++)) + else + REMAINING_CONFLICTS+=("$conflict_file") + fi +done < <(git diff --name-only --diff-filter=U 2>/dev/null) + +if [ "$RESOLVED_COUNT" -gt 0 ]; then + echo "✓ Auto-resolved $RESOLVED_COUNT file(s)" +fi + +if [ ${#REMAINING_CONFLICTS[@]} -eq 0 ]; then + # All conflicts resolved - commit + echo "All conflicts resolved. Committing..." + git commit -m "fix: auto-resolve PR-specific file conflicts with $BASE_BRANCH + +Files resolved using --ours strategy: +$(for f in "${IGNORED_CONFLICT_FILES[@]}"; do echo "- $f"; done) + +These files are PR-specific and should not inherit content from the base branch." + echo "✓ Merge conflict resolution committed" + exit 0 +else + echo "" + echo "⚠ Remaining conflicts require manual resolution:" + for f in "${REMAINING_CONFLICTS[@]}"; do + echo " - $f" + done + echo "" + echo "Run 'git status' to see conflict details." + exit 1 +fi diff --git a/templates/consumer-repo/.github/workflows/agents-keepalive-loop.yml b/templates/consumer-repo/.github/workflows/agents-keepalive-loop.yml index adf370952..b88908c62 100644 --- a/templates/consumer-repo/.github/workflows/agents-keepalive-loop.yml +++ b/templates/consumer-repo/.github/workflows/agents-keepalive-loop.yml @@ -314,6 +314,97 @@ jobs: }; await markAgentRunning({ github, context, core, inputs }); + # Auto-resolve trivial conflicts (pr_body.md, etc.) before running Codex + resolve-trivial-conflicts: + name: Auto-resolve PR-specific files + needs: + - evaluate + - preflight + if: needs.evaluate.outputs.action == 'conflict' + runs-on: ubuntu-latest + outputs: + resolved: ${{ steps.resolve.outputs.resolved }} + remaining_conflicts: ${{ steps.resolve.outputs.remaining_conflicts }} + steps: + - name: Generate GitHub App token + id: app-token + if: ${{ vars.WORKFLOWS_APP_ID != '' }} + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ vars.WORKFLOWS_APP_ID }} + private-key: ${{ secrets.WORKFLOWS_APP_PRIVATE_KEY }} + owner: ${{ github.repository_owner }} + + - name: Checkout PR branch + uses: actions/checkout@v6 + with: + token: ${{ steps.app-token.outputs.token || secrets.GITHUB_TOKEN }} + ref: ${{ needs.evaluate.outputs.pr_ref }} + fetch-depth: 0 + + - name: Auto-resolve trivial conflicts + id: resolve + run: | + PR_NUM="${{ needs.evaluate.outputs.pr_number }}" + BASE_BRANCH=$(gh pr view "$PR_NUM" --json baseRefName --jq '.baseRefName') + echo "Base branch: $BASE_BRANCH" + + # Configure git + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + # Fetch base branch + git fetch origin "$BASE_BRANCH" + + # Try merge + echo "Attempting merge from origin/$BASE_BRANCH..." + if git merge "origin/$BASE_BRANCH" --no-edit 2>/dev/null; then + echo "✓ Merge completed without conflicts" + echo "resolved=true" >> "$GITHUB_OUTPUT" + echo "remaining_conflicts=0" >> "$GITHUB_OUTPUT" + exit 0 + fi + + # Files that should always keep the PR branch version + IGNORED_FILES=("pr_body.md" "ci/autofix/history.json" "keepalive-metrics.ndjson") + RESOLVED=0 + REMAINING=0 + + # Get unmerged files + while IFS= read -r conflict_file; do + [ -z "$conflict_file" ] && continue + SHOULD_AUTO=false + for ignored in "${IGNORED_FILES[@]}"; do + if [[ "$conflict_file" == "$ignored" || "$conflict_file" == *"/$ignored" ]]; then + SHOULD_AUTO=true + break + fi + done + + if $SHOULD_AUTO; then + echo "Auto-resolving: $conflict_file (keeping ours)" + git checkout --ours "$conflict_file" 2>/dev/null || true + git add "$conflict_file" 2>/dev/null || true + ((RESOLVED++)) + else + ((REMAINING++)) + fi + done < <(git diff --name-only --diff-filter=U 2>/dev/null) + + echo "resolved=$RESOLVED" >> "$GITHUB_OUTPUT" + echo "remaining_conflicts=$REMAINING" >> "$GITHUB_OUTPUT" + + if [ "$REMAINING" -eq 0 ] && [ "$RESOLVED" -gt 0 ]; then + git commit -m "fix: auto-resolve PR-specific file conflicts with base branch" + git push + echo "✓ Conflict resolution committed and pushed" + elif [ "$REMAINING" -gt 0 ]; then + echo "⚠ $REMAINING file(s) still have conflicts - Codex will handle these" + git merge --abort || true + fi + env: + GH_TOKEN: ${{ steps.app-token.outputs.token || secrets.GITHUB_TOKEN }} + # Run Codex CLI for agent:codex PRs run-codex: name: Keepalive next task (Codex) @@ -321,7 +412,11 @@ jobs: - evaluate - preflight - mark-running - if: needs.evaluate.outputs.agent_type == 'codex' + - resolve-trivial-conflicts + if: | + needs.evaluate.outputs.agent_type == 'codex' && + (needs.resolve-trivial-conflicts.result == 'skipped' || + needs.resolve-trivial-conflicts.outputs.remaining_conflicts != '0') uses: stranske/Workflows/.github/workflows/reusable-codex-run.yml@main secrets: CODEX_AUTH_JSON: ${{ secrets.CODEX_AUTH_JSON }} @@ -341,6 +436,7 @@ jobs: needs: - evaluate - preflight + - resolve-trivial-conflicts - run-codex if: always() && needs.evaluate.outputs.pr_number != '' && needs.evaluate.outputs.pr_number != '0' runs-on: ubuntu-latest