The problem
We maintain a repo with two long-lived branches, master and main, that need to stay in sync. The manual routine was: checkout locally, pull both, merge, hope nothing conflicts, push. Every time. Repetitive, and one distracted moment away from a messy merge commit or a conflict resolution you didn't mean to make.
A co-maintainer on this repo set up a GitHub Action to handle it instead. I just liked it enough to write about it.
The workflow
name: Manual Fast-Forward Master to Main
on:
workflow_dispatch:
jobs:
merge-branches:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout Code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Configure Git Identity
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Attempt Fast-Forward Merge
id: git_merge
run: |
git fetch origin main
git fetch origin master
git checkout main
if ! git merge origin/master --ff-only; then
echo "::error::Cannot fast-forward! Histories have diverged or conflicts exist. Aborting."
exit 1
fi
- name: Push Changes
run: |
git push origin main
workflow_dispatch means this only runs when someone manually clicks "Run workflow" in the Actions tab, no accidental triggers on push or schedule. Click it, and the job:
Checks out the full history (fetch-depth: 0 needed so the merge actually has both branches' history to compare)
Checks out main
Tries git merge origin/master --ff-only
Pushes if it worked
Why --ff-only is the part worth understanding
A regular git merge will happily create a merge commit, or ask you to resolve conflicts, or in some configs auto-resolve things in ways you didn't explicitly review. --ff-only refuses all of that. It only allows the merge if main hasn't diverged from master at all, meaning main can just "fast-forward" its pointer forward to match, with no new commit and no ambiguity about what changed.
If the branches have diverged, the flag makes git fail instead of guessing. That's the whole safety net. The workflow catches that failure explicitly (if ! git merge ... ; then echo "::error::..."; exit 1; fi) so it shows up loud and clear in the Actions log instead of silently pushing something wrong.
That's the actual lesson here: automating a merge is easy. Automating it so that it fails safely instead of doing something unintended, that's the part worth copying into your own workflows.
Is this novel?
No, and worth being upfront about that. workflow_dispatch has existed since GitHub introduced it in 2020, and fast-forward-only merges are just core git behavior. This isn't a new invention, it's a small, well-known pattern for anyone who's automated branch syncing or release promotion with Actions.
But it's a good example of a broader habit worth building: find the repetitive manual git step you don't fully trust yourself to do carefully every single time, and hand it to a machine that does the exact same safe thing, consistently, with a paper trail.
Top comments (0)