DEV Community

Roberto Luna
Roberto Luna

Posted on

Auto‑Resolving Content Folder Conflicts in GitHub Actions for the content‑automation Repo

Auto‑Resolving Content Folder Conflicts in GitHub Actions for the content‑automation Repo

TL;DR:

I added a “smart‑merge” step to the Bluesky daily workflow that automatically keeps the remote version of everything under content/ when a push conflict occurs, and I made the push non‑blocking. The CI now finishes even if the auto‑generated markdown files clash, keeping the repo in a consistent state.


The Problem

Our content‑automation repo generates a set of markdown files every day for Medium, Substack, Bluesky and Dev.to. The generation runs in a GitHub Actions workflow (.github/workflows/bluesky‑daily.yml). When two runs overlap (e.g., a daily run and a manual trigger) they both try to commit new files under content/2026/08/19/....

Git inevitably throws a merge conflict:

error: could not lock ref 'refs/heads/main'.
remote: error: refusing to merge unrelated histories
Enter fullscreen mode Exit fullscreen mode

or, after a git push:

! [remote rejected] main -> main (pre-receive hook declined)
error: failed to push some refs to 'git@github.com:myorg/content-automation.git'
Enter fullscreen mode Exit fullscreen mode

Because the workflow treated the push failure as a fatal error, the entire CI job aborted and no other downstream steps (like posting to Bluesky) ran. The result was a broken daily pipeline and a backlog of unresolved content.


What I Tried First

  1. Force‑push (git push --force) – This overwrote the remote content/ folder, deleting markdown that had been generated by the previous run.
  2. Manual conflict resolution in the workflow – I added a git merge step, but left the default merge strategy. The merge left many <<<<<<< markers inside the markdown files, breaking downstream parsers.
  3. Abort on push failure – The original workflow used set -e, so any non‑zero exit code stopped the job. This kept the repo clean but also prevented the rest of the pipeline from executing.

All of these approaches either lost data or halted the pipeline, which is unacceptable for a “build‑in‑public” project that needs to stay up‑to‑date.


The Implementation

1. Checkout with full history

# .github/workflows/bluesky-daily.yml
- uses: actions/checkout@v3
  with:
    fetch-depth: 0   # we need the full history to resolve merges
Enter fullscreen mode Exit fullscreen mode

2. Configure Git

- name: Set up Git
  run: |
    git config user.email "ci@vibecoding.com"
    git config user.name "VibeCoding CI"
Enter fullscreen mode Exit fullscreen mode

3. Auto‑resolve conflicts keeping the remote version

The key is to tell Git: for everything under content/, always prefer the remote side. We do that by checking out the remote version after the fetch and before committing.

- name: Pull remote changes and resolve conflicts
  run: |
    # Fetch the latest main branch
    git fetch origin main

    # Attempt a fast‑forward merge; if it fails we fall back to our strategy
    if ! git merge --ff-only origin/main; then
      echo "Fast‑forward failed – applying conflict resolution for content/*"

      # Checkout the remote version of the whole content folder
      git checkout origin/main -- content/

      # Stage the resolved files
      git add content/

      # Commit the resolution (use a deterministic message)
      git commit -m "ci: auto‑resolve content/ conflicts – keep remote"
    fi
Enter fullscreen mode Exit fullscreen mode

Why git checkout origin/main -- content/?

git checkout with a pathspec replaces the working‑tree copy of those paths with the version from the given commit (origin/main). This is equivalent to the theirs strategy but scoped only to the content/ directory, leaving any other changes (e.g., workflow updates) untouched.

4. Push without breaking the workflow

We wrap the push in a conditional that never fails the job:

- name: Push changes
  run: |
    git push origin main || echo "Push failed – continuing without aborting"
Enter fullscreen mode Exit fullscreen mode

The || ensures the step returns a success status (0) even if the remote rejects the push (e.g., because another workflow already pushed). This makes the push non‑blocking.

5. Update metadata.json atomically

During the same job we also toggle the medium_generated and substack_generated flags. Using jq guarantees a well‑formed JSON file:

- name: Mark generated outputs
  run: |
    jq '.medium_generated = true | .substack_generated = true' \
       content/2026/08/19/content-automation/metadata.json \
       > tmp.json && mv tmp.json content/2026/08/19/content-automation/metadata.json
    git add content/2026/08/19/content-automation/metadata.json
Enter fullscreen mode Exit fullscreen mode

The diff for metadata.json now looks like:

@@ -12,8 +12,8 @@
   "pull_requests": 0,
   "releases": 0,
   "closed_issues": 0,
-  "medium_generated": false,
-  "substack_generated": false,
+  "medium_generated": true,
+  "substack_generated": true,
Enter fullscreen mode Exit fullscreen mode

6. Full workflow snippet (relevant part)


yaml
name: Bluesky Daily

on:
  schedule:
    - cron: '0 6 * * *'   # 6 AM UTC

jobs:
  generate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0

      - name: Set up Node
        uses: actions/setup-node@v3
        with:
          node-version: '20'

      - name: Install deps
        run: npm ci

      - name: Run content generator
        run: npm run generate:daily

      # --- Conflict resolution block ---
      - name: Set up Git
        run: |
          git config user.email "ci@vibecoding.com"
          git config user.name "VibeCoding CI"

      - name: Pull remote changes and resolve conflicts
        run: |
          git fetch origin main
          if ! git merge --ff-only origin/main; then
            echo "Fast‑forward failed – applying conflict resolution for content/*"
            git checkout origin/main -- content/
            git add content/
            git commit -m "ci: auto‑resolve content/ conflicts – keep remote"
          fi

---

*Part of my [Build in Public](https://dev.to/zaerohell) series — sharing the real process of building SaaS projects from Playa del Carmen, México.*

*Repo: `zaerohell/content-automation` · 2026-08-20*

\#playadev #buildinpublic
Enter fullscreen mode Exit fullscreen mode

Top comments (0)