# freebsd-automerge.sh — An automated upgrade assistant for FreeBSD
## Introduction
I would like to share a shell script I developed with the assistance of Claude
(Anthropic's AI) to automate the most tedious part of FreeBSD upgrades: resolving
merge conflicts in configuration files.
The script is called **freebsd-automerge.sh** and it handles the complete upgrade
workflow from start to finish, including conflict resolution, backup, and the
install/reboot cycle.
---
## The problem it solves
Anyone who has upgraded FreeBSD using `freebsd-update upgrade` knows the pain:
when configuration files have been locally modified, freebsd-update stops and
asks you to manually resolve conflicts one by one in an editor. On a system with
many customized files, this can mean dozens of interruptions requiring careful
manual editing.
The standard approach (mergemaster, etcupdate) works well but still requires
significant manual intervention. I wanted something fully automated that could
handle an upgrade — including a major version jump — with minimal human input.
---
## How it works
The core idea is a **3-way merge**:
```
base OLD (e.g. 14.3-RELEASE clean) → your customized file = YOUR changes
base OLD (e.g. 14.3-RELEASE clean) → base NEW (14.4-RELEASE) = FreeBSD changes
```
By comparing both sets of changes independently against the original clean base,
the script can automatically apply both your customizations and FreeBSD's updates
without conflicts — exactly the same mechanism git uses for merging branches.
The script operates in two distinct modes:
### EDITOR mode
The script registers itself as the `$EDITOR` for `freebsd-update`:
```sh
EDITOR=/root/freebsd-automerge.sh freebsd-update -r 14.4-RELEASE upgrade
```
When freebsd-update encounters a file it cannot merge automatically, instead of
opening nano it calls our script with the conflicted file path. The script then:
1. Extracts the clean OLD base version from `base.txz`
2. Extracts the clean NEW base version from `base.txz`
3. Uses `merge(1)` (from the base system) for the 3-way merge in-place
4. If residual conflicts remain, falls back to nano for manual resolution
5. Returns control to freebsd-update
### MAIN mode
The primary workflow:
1. Interactive menu querying the FreeBSD mirror in real time to show available
RELEASE, STABLE, RC, and BETA targets
2. Downloads `base.txz` for both OLD and NEW releases
3. Runs `freebsd-update upgrade` with itself as EDITOR
4. After upgrade, scans `/etc` and `/usr/share` for any remaining `<<<<<<<`
markers and resolves them with the same 3-way merge
5. Creates a centralized backup of all conflicted files with SHA256 manifest
and generates a `restore.sh` script for rollback
6. Runs `freebsd-update install` and handles the reboot cycle automatically,
including the two-pass install required for major version upgrades
---
## Key features
- **Zero configuration** — detects OLD and NEW versions automatically from
`uname -r` and `/var/db/freebsd-update/tag`
- **Live mirror query** — the target selection menu fetches available versions
directly from the FreeBSD mirror at runtime
- **Kernel/userland selection** — asks whether to upgrade both or userland only,
useful for systems running custom kernels (e.g. ARM SBCs with custom DRM drivers)
- **Auto-confirm mode** — `--auto-confirm` flag uses `expect(1)` to answer
freebsd-update's interactive prompts automatically
- **Major upgrade support** — handles the two-install, two-reboot cycle for
major version jumps (e.g. 14.x → 15.x) using a temporary `rc.d` service
that survives the first reboot
- **Safe by design** — full backup before touching anything, restore script
generated automatically, SHA256 integrity verification
---
## Current limitations and known bugs
I want to be transparent about what still needs work:
1. **STABLE branch detection** — the menu regex for `-STABLE` targets does not
always match the mirror directory listing correctly
2. **State recovery after interruption** — if the script is killed mid-upgrade,
resuming it does not correctly re-enter the freebsd-update merge phase;
it tries to call `freebsd-update install` prematurely
3. **grep -c arithmetic** — a subtle shell arithmetic bug produces
`[: 0 0: bad number` in some cases when counting conflict markers
4. **STABLE/CURRENT upgrade type** — `is_major_upgrade()` only compares
major version numbers; it does not distinguish STABLE or CURRENT targets
which may need different handling
5. **Terminal compatibility** — Unicode box-drawing characters in menus do not
render correctly in all FreeBSD terminal configurations; should fall back
to plain ASCII
---
## What I would love feedback on
This is where I genuinely need input from people who know FreeBSD internals
better than I do:
**1. Is using `$EDITOR` the right hook into freebsd-update?**
Is there a cleaner, more supported way to intercept freebsd-update's merge
phase? I am essentially abusing the EDITOR variable — it works, but it feels
fragile. Is there an official API or hook mechanism I am missing?
**2. The 3-way merge approach — is merge(1) the right tool?**
I am using `merge(1)` from the `rcs` package. Are there better alternatives
in the FreeBSD base system? `diff3(1)` is available but requires more
orchestration. `patch(1)` could work but loses context. Thoughts?
**3. Handling STABLE and CURRENT upgrades**
Upgrading to a -STABLE or -CURRENT branch is fundamentally different from
a RELEASE upgrade — freebsd-update behaves differently, and the base.txz
download path is different. Has anyone automated this reliably?
**4. The two-reboot major upgrade cycle**
I implemented this using a temporary `rc.d` service (`automerge_resume`)
that runs `freebsd-update install` after the first reboot and then removes
itself. It works, but it feels like a hack. Is there a cleaner pattern?
**5. pkg alignment after major upgrades**
After a major upgrade (e.g. 14.x → 15.x), all installed packages need to be
rebuilt or reinstalled. I currently just tell the user to run `pkg upgrade -f`
manually. Should this be integrated into the script? What is the safest
sequence?
---
## How to get it
The script is a single self-contained shell file with no external dependencies
beyond `merge(1)` (from `pkg install rcs`) and optionally `expect(1)` for
auto-confirm mode.
Usage:
```sh
# Show interactive target menu (queries mirror live)
sh freebsd-automerge.sh
# Filter menu to branch 14
sh freebsd-automerge.sh 14
# Direct upgrade, no menu
sh freebsd-automerge.sh 14.4-RELEASE
# Fully automated (requires: pkg install expect)
sh freebsd-automerge.sh --auto-confirm 14
```
---
## Credits
The script was conceived and directed by **ZioMario** (forums.freebsd.org user:
ZioMario), developed with the assistance of **Claude** (Anthropic AI).
The idea, architecture decisions, testing, and debugging were all driven by
ZioMario — Claude was used as an implementation tool.
All feedback, pull requests, and brutal criticism are welcome.
## Introduction
I would like to share a shell script I developed with the assistance of Claude
(Anthropic's AI) to automate the most tedious part of FreeBSD upgrades: resolving
merge conflicts in configuration files.
The script is called **freebsd-automerge.sh** and it handles the complete upgrade
workflow from start to finish, including conflict resolution, backup, and the
install/reboot cycle.
---
## The problem it solves
Anyone who has upgraded FreeBSD using `freebsd-update upgrade` knows the pain:
when configuration files have been locally modified, freebsd-update stops and
asks you to manually resolve conflicts one by one in an editor. On a system with
many customized files, this can mean dozens of interruptions requiring careful
manual editing.
The standard approach (mergemaster, etcupdate) works well but still requires
significant manual intervention. I wanted something fully automated that could
handle an upgrade — including a major version jump — with minimal human input.
---
## How it works
The core idea is a **3-way merge**:
```
base OLD (e.g. 14.3-RELEASE clean) → your customized file = YOUR changes
base OLD (e.g. 14.3-RELEASE clean) → base NEW (14.4-RELEASE) = FreeBSD changes
```
By comparing both sets of changes independently against the original clean base,
the script can automatically apply both your customizations and FreeBSD's updates
without conflicts — exactly the same mechanism git uses for merging branches.
The script operates in two distinct modes:
### EDITOR mode
The script registers itself as the `$EDITOR` for `freebsd-update`:
```sh
EDITOR=/root/freebsd-automerge.sh freebsd-update -r 14.4-RELEASE upgrade
```
When freebsd-update encounters a file it cannot merge automatically, instead of
opening nano it calls our script with the conflicted file path. The script then:
1. Extracts the clean OLD base version from `base.txz`
2. Extracts the clean NEW base version from `base.txz`
3. Uses `merge(1)` (from the base system) for the 3-way merge in-place
4. If residual conflicts remain, falls back to nano for manual resolution
5. Returns control to freebsd-update
### MAIN mode
The primary workflow:
1. Interactive menu querying the FreeBSD mirror in real time to show available
RELEASE, STABLE, RC, and BETA targets
2. Downloads `base.txz` for both OLD and NEW releases
3. Runs `freebsd-update upgrade` with itself as EDITOR
4. After upgrade, scans `/etc` and `/usr/share` for any remaining `<<<<<<<`
markers and resolves them with the same 3-way merge
5. Creates a centralized backup of all conflicted files with SHA256 manifest
and generates a `restore.sh` script for rollback
6. Runs `freebsd-update install` and handles the reboot cycle automatically,
including the two-pass install required for major version upgrades
---
## Key features
- **Zero configuration** — detects OLD and NEW versions automatically from
`uname -r` and `/var/db/freebsd-update/tag`
- **Live mirror query** — the target selection menu fetches available versions
directly from the FreeBSD mirror at runtime
- **Kernel/userland selection** — asks whether to upgrade both or userland only,
useful for systems running custom kernels (e.g. ARM SBCs with custom DRM drivers)
- **Auto-confirm mode** — `--auto-confirm` flag uses `expect(1)` to answer
freebsd-update's interactive prompts automatically
- **Major upgrade support** — handles the two-install, two-reboot cycle for
major version jumps (e.g. 14.x → 15.x) using a temporary `rc.d` service
that survives the first reboot
- **Safe by design** — full backup before touching anything, restore script
generated automatically, SHA256 integrity verification
---
## Current limitations and known bugs
I want to be transparent about what still needs work:
1. **STABLE branch detection** — the menu regex for `-STABLE` targets does not
always match the mirror directory listing correctly
2. **State recovery after interruption** — if the script is killed mid-upgrade,
resuming it does not correctly re-enter the freebsd-update merge phase;
it tries to call `freebsd-update install` prematurely
3. **grep -c arithmetic** — a subtle shell arithmetic bug produces
`[: 0 0: bad number` in some cases when counting conflict markers
4. **STABLE/CURRENT upgrade type** — `is_major_upgrade()` only compares
major version numbers; it does not distinguish STABLE or CURRENT targets
which may need different handling
5. **Terminal compatibility** — Unicode box-drawing characters in menus do not
render correctly in all FreeBSD terminal configurations; should fall back
to plain ASCII
---
## What I would love feedback on
This is where I genuinely need input from people who know FreeBSD internals
better than I do:
**1. Is using `$EDITOR` the right hook into freebsd-update?**
Is there a cleaner, more supported way to intercept freebsd-update's merge
phase? I am essentially abusing the EDITOR variable — it works, but it feels
fragile. Is there an official API or hook mechanism I am missing?
**2. The 3-way merge approach — is merge(1) the right tool?**
I am using `merge(1)` from the `rcs` package. Are there better alternatives
in the FreeBSD base system? `diff3(1)` is available but requires more
orchestration. `patch(1)` could work but loses context. Thoughts?
**3. Handling STABLE and CURRENT upgrades**
Upgrading to a -STABLE or -CURRENT branch is fundamentally different from
a RELEASE upgrade — freebsd-update behaves differently, and the base.txz
download path is different. Has anyone automated this reliably?
**4. The two-reboot major upgrade cycle**
I implemented this using a temporary `rc.d` service (`automerge_resume`)
that runs `freebsd-update install` after the first reboot and then removes
itself. It works, but it feels like a hack. Is there a cleaner pattern?
**5. pkg alignment after major upgrades**
After a major upgrade (e.g. 14.x → 15.x), all installed packages need to be
rebuilt or reinstalled. I currently just tell the user to run `pkg upgrade -f`
manually. Should this be integrated into the script? What is the safest
sequence?
---
## How to get it
The script is a single self-contained shell file with no external dependencies
beyond `merge(1)` (from `pkg install rcs`) and optionally `expect(1)` for
auto-confirm mode.
Usage:
```sh
# Show interactive target menu (queries mirror live)
sh freebsd-automerge.sh
# Filter menu to branch 14
sh freebsd-automerge.sh 14
# Direct upgrade, no menu
sh freebsd-automerge.sh 14.4-RELEASE
# Fully automated (requires: pkg install expect)
sh freebsd-automerge.sh --auto-confirm 14
```
---
## Credits
The script was conceived and directed by **ZioMario** (forums.freebsd.org user:
ZioMario), developed with the assistance of **Claude** (Anthropic AI).
The idea, architecture decisions, testing, and debugging were all driven by
ZioMario — Claude was used as an implementation tool.
All feedback, pull requests, and brutal criticism are welcome.