diff --git a/README.md b/README.md index 98eaeab..1b5cf0c 100644 --- a/README.md +++ b/README.md @@ -112,9 +112,12 @@ The action follows this workflow: 2. **Stash changes** - Safely stashes any uncommitted changes (including untracked files) 3. **Sync with upstream** - Pulls latest changes using rebase to maintain linear history 4. **Restore changes** - Pops the stash to restore your changes + - **Automatic conflict resolution**: If conflicts occur between stashed changes and pulled changes, the action automatically resolves them by accepting your stashed changes (the newer modifications from this workflow) + - This ensures concurrent workflow changes are properly merged without leaving conflict markers 5. **Stage files** - Adds files matching the specified pattern -6. **Commit** - Creates a commit if there are staged changes -7. **Push with retry** - Attempts to push with automatic retry on failure: +6. **Safety check** - Verifies no conflict markers are present in staged files (additional safeguard) +7. **Commit** - Creates a commit if there are staged changes +8. **Push with retry** - Attempts to push with automatic retry on failure: - On push failure, rebases again and retries - Continues up to `max_retries` attempts - Handles race conditions from concurrent workflows @@ -297,6 +300,14 @@ This usually means concurrent changes occurred. The action automatically handles max_retries: 10 # Increase for high-concurrency scenarios ``` +### Conflict markers in committed files + +The action now includes automatic conflict resolution and a safety check to prevent committing conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`). If you see the error "Conflict markers detected in staged files!", this means: +1. A merge conflict occurred that couldn't be auto-resolved +2. The action prevented committing corrupted files +3. Review the workflow logs to understand the conflict +4. This is a safety feature to protect your repository from corrupted files + ### No commit created but expected changes Check that: diff --git a/action.yml b/action.yml index ae3dab1..6d3b365 100644 --- a/action.yml +++ b/action.yml @@ -84,15 +84,69 @@ runs: # Stash any uncommitted changes (including untracked files) echo "đŸ“Ļ Stashing current changes..." - git stash push -u -m "temp-stashed-changes" || true + STASH_RESULT=0 + git stash push -u -m "temp-stashed-changes" || STASH_RESULT=$? + + if [ $STASH_RESULT -ne 0 ]; then + # Check if it's because there's nothing to stash (expected) or a real error + if git diff --quiet && git diff --cached --quiet && [ -z "$(git ls-files --others --exclude-standard)" ]; then + echo "â„šī¸ Nothing to stash" + STASH_RESULT=0 + else + echo "::error::Failed to stash changes" + exit 1 + fi + fi # Pull latest changes with rebase echo "âŦ‡ī¸ Pulling latest changes with rebase..." git pull --rebase origin "$(git branch --show-current)" - # Pop the stash to restore changes + # Pop the stash to restore changes (if we stashed anything) echo "📂 Restoring stashed changes..." - git stash pop || true + if [ "$(git stash list | grep -c 'temp-stashed-changes')" -gt 0 ]; then + if ! git stash pop; then + echo "âš ī¸ Merge conflicts detected during stash pop" + + # Get list of conflicted files + CONFLICTED_FILES=$(git diff --name-only --diff-filter=U) + + if [ -n "$CONFLICTED_FILES" ]; then + echo "🔧 Resolving conflicts by accepting stashed changes..." + + # For each conflicted file, accept the stashed version + # In stash pop context: --theirs = stashed changes (what we want) + RESOLUTION_FAILED=0 + while IFS= read -r file; do + if [ -n "$file" ]; then + echo " - Resolving: $file" + if ! git checkout --theirs "$file"; then + echo "::error::Failed to resolve conflict in $file" + RESOLUTION_FAILED=1 + break + fi + fi + done <<< "$CONFLICTED_FILES" + + if [ $RESOLUTION_FAILED -ne 0 ]; then + # Drop the stash to clean up + git stash drop + exit 1 + fi + + # Drop the stash since we've successfully applied it + git stash drop + + echo "✅ Conflicts resolved" + else + echo "â„šī¸ No actual conflicts found, continuing..." + # Still need to drop the stash + git stash drop + fi + fi + else + echo "â„šī¸ No stash to restore" + fi # Stage files matching the pattern echo "➕ Staging files matching pattern: $FILE_PATTERN" @@ -106,6 +160,20 @@ runs: exit 0 fi + # Safety check: ensure no conflict markers are being committed + echo "🔍 Checking for conflict markers in staged files..." + if git diff --cached | grep -qE "^\+.*<<<<<<<|^\+.*=======|^\+.*>>>>>>>"; then + echo "::error::Conflict markers detected in staged files! Aborting commit." + echo "The following files contain unresolved conflicts:" + STAGED_FILES=$(git diff --cached --name-only) + while IFS= read -r file; do + if [ -n "$file" ] && grep -qE "<<<<<<<|=======|>>>>>>>" "$file" 2>/dev/null; then + echo " - $file" + fi + done <<< "$STAGED_FILES" + exit 1 + fi + # Create commit echo "💾 Creating commit..." git commit -m "$COMMIT_MESSAGE" @@ -127,7 +195,20 @@ runs: if [ $ATTEMPT -lt $MAX_RETRIES ]; then echo "âš ī¸ Push failed, rebasing and retrying..." - git pull --rebase origin "$(git branch --show-current)" + # Use theirs strategy to prefer our committed changes over remote changes during rebase + # In rebase context: theirs = our local commit (what we want to keep) + if ! git pull --rebase -X theirs origin "$(git branch --show-current)"; then + echo "::error::Rebase failed during retry despite conflict resolution strategy." + echo "::error::This indicates a complex conflict that cannot be auto-resolved." + + # Check if we're in a rebase state + if [ -d .git/rebase-merge ] || [ -d .git/rebase-apply ]; then + echo "Aborting rebase..." + git rebase --abort + fi + + exit 1 + fi fi ATTEMPT=$((ATTEMPT + 1))