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
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'
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
-
Force‑push (
git push --force) – This overwrote the remotecontent/folder, deleting markdown that had been generated by the previous run. -
Manual conflict resolution in the workflow – I added a
git mergestep, but left the default merge strategy. The merge left many<<<<<<<markers inside the markdown files, breaking downstream parsers. -
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
2. Configure Git
- name: Set up Git
run: |
git config user.email "ci@vibecoding.com"
git config user.name "VibeCoding CI"
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
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"
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
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,
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
Top comments (0)