Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions .github/scripts/auto_resolve_ignored_conflicts.sh
Original file line number Diff line number Diff line change
@@ -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 <base_branch>
#
# 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."
Comment on lines +83 to +88
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The commit message lists all files in the IGNORED_CONFLICT_FILES array, regardless of which files actually had conflicts and were resolved. This could be misleading. Consider only listing the files that were actually resolved in this specific merge, similar to how the workflow inline script does it.

Copilot uses AI. Check for mistakes.
echo "✓ Merge conflict resolution committed"
exit 0
Comment on lines +80 to +90
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the merge fails but no unmerged files are found (RESOLVED_COUNT=0, REMAINING_CONFLICTS empty), the script attempts to commit at line 83. Since no files were staged, git commit will fail with "nothing to commit", and due to set -e at line 13, the script will exit with an error code. Consider checking if RESOLVED_COUNT > 0 before attempting to commit, or handling the edge case where a merge conflict is reported but no conflicted files are detected.

Copilot uses AI. Check for mistakes.
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
Original file line number Diff line number Diff line change
Expand Up @@ -314,14 +314,109 @@ 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 }}
Comment on lines +331 to +334
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition checks vars.WORKFLOWS_APP_ID but the preflight job at line 266 checks secrets.WORKFLOWS_APP_ID, and line 423 passes secrets.WORKFLOWS_APP_ID to the reusable workflow. This inconsistency could cause the app token generation to be skipped when WORKFLOWS_APP_ID is stored as a secret (the recommended approach per line 15 in the file header). Consider using secrets.WORKFLOWS_APP_ID here for consistency, or clarify if the App ID should be stored in vars vs secrets.

Suggested change
if: ${{ vars.WORKFLOWS_APP_ID != '' }}
uses: actions/create-github-app-token@v2
with:
app-id: ${{ vars.WORKFLOWS_APP_ID }}
if: ${{ secrets.WORKFLOWS_APP_ID != '' }}
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.WORKFLOWS_APP_ID }}

Copilot uses AI. Check for mistakes.
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")
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The file list here contains only 3 files, but the standalone script at .github/scripts/auto_resolve_ignored_conflicts.sh contains 6 files (including "coverage-trend-history.ndjson", "metrics-history.ndjson", and "residual-trend-history.ndjson"). These lists should be consistent to ensure the same behavior across different usage contexts.

Suggested change
IGNORED_FILES=("pr_body.md" "ci/autofix/history.json" "keepalive-metrics.ndjson")
IGNORED_FILES=("pr_body.md" "ci/autofix/history.json" "keepalive-metrics.ndjson" "coverage-trend-history.ndjson" "metrics-history.ndjson" "residual-trend-history.ndjson")

Copilot uses AI. Check for mistakes.
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
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the merge fails but no unmerged files are detected by git diff --name-only --diff-filter=U (REMAINING=0, RESOLVED=0), neither the commit branch (line 397) nor the abort branch (line 401) executes. This leaves the repository in a conflicted merge state without cleanup. Consider adding an else clause to handle this edge case, likely by aborting the merge with git merge --abort.

Suggested change
git merge --abort || true
git merge --abort || true
else
echo "⚠ Merge did not complete successfully, but no conflicts were detected (resolved=$RESOLVED, remaining=$REMAINING). Aborting merge to clean up state."
git merge --abort || true

Copilot uses AI. Check for mistakes.
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)
needs:
- 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' ||
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition only checks for 'skipped' result, not 'failure'. If the resolve-trivial-conflicts job fails (e.g., git push fails at line 399), the run-codex job will not execute because the condition doesn't account for the 'failure' result. Consider updating the condition to also handle job failures, for example: needs.resolve-trivial-conflicts.result == 'skipped' || needs.resolve-trivial-conflicts.result == 'failure' || needs.resolve-trivial-conflicts.outputs.remaining_conflicts != '0'

Suggested change
(needs.resolve-trivial-conflicts.result == 'skipped' ||
(needs.resolve-trivial-conflicts.result == 'skipped' ||
needs.resolve-trivial-conflicts.result == 'failure' ||

Copilot uses AI. Check for mistakes.
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 }}
Expand All @@ -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
Expand Down
Loading