Skip to content
TechDug

dig-log

Writing a migration guide readers can trust

Writing a migration guide readers can trust

A reader opens a migration guide at a bad moment. An upgrade is waiting, a nightly job or real data depends on it, and they cannot tell by looking whether your steps are right. What they can judge is whether the guide behaves as if its author has done this: does it say who it is for, how to check each step, and how to get back?

In our changelog post we said that a migration longer than a few lines deserves its own guide. This is how we would write one. The example continues with tidepool, the fictional command-line tool from that post, moving from 2.4.1 to 3.0.0. Tidepool does not exist, so its behaviour is invented for the example. The shell commands below are real: we ran them against sample files on macOS 26 (bash 3.2) and in a Debian 13 container (bash 5.2), and the outputs shown are from Debian.

Open with the exact journey

Before step one, readers need to know whether this page describes their situation. Put that at the very top, as ordinary text:

code
Moving from:  tidepool 2.4.x. On anything older, upgrade to 2.4.1 first.
Moving to:    tidepool 3.0.0
You need:     Python 3.10 or newer (check with: python3 --version)
Pause first:  any scheduled job that runs tidepool
Tested from:  a fresh 2.4.1 install on Debian 13, 3 November 2026

The first line carries the most weight. A guide tested from 2.4.1 says nothing about someone jumping from 2.1, and “should work” is not a tested path. If you have not tried a jump, say so and send those readers through a version you have tried.

Follow the header with the changes in a sentence or two each, and why they happened. In our case: the config file moves, --force gives way to --overwrite, and the data folder gets a new format. A reader who knows why can adapt the steps when their setup differs from yours.

Let readers check whether they are affected

A list of breaking changes makes people read and guess. A command gives them an answer. Tell them to replace ~/scripts with wherever they keep their scripts:

code
test -f ~/.tidepoolrc && echo "old config found"
grep -rn 'tidepool sync.*--force' ~/scripts
crontab -l 2>/dev/null | grep -n 'tidepool sync.*--force'
code
old config found
/home/you/scripts/nightly.sh:3:tidepool sync --force ./data

Then say what the result means, including the empty case: no output from any of the three means nothing here needs changing.

Keep the pattern narrow. Searching for --force alone also matches rsync --force in an unrelated script, and the reader starts editing lines that were never broken. Also be open about what the check misses. This one cannot see a flag built up in a shell variable, or a job defined in a CI config outside that folder, and a sentence saying so is worth more than a check that implies certainty.

Back up, then prove the backup works

Name exactly what to save. Tidepool 2.x keeps its config in one file and its data in one folder:

code
tar -czf ~/tidepool-before-3.0.tar.gz -C ~ .tidepoolrc .local/share/tidepool

Many guides stop there, and a backup nobody has restored is only a hope. Have the reader unpack it somewhere else and compare, while the originals still exist:

code
restore=$(mktemp -d)
tar -xzf ~/tidepool-before-3.0.tar.gz -C "$restore"
diff -r "$restore/.local/share/tidepool" ~/.local/share/tidepool && echo "data matches"
diff "$restore/.tidepoolrc" ~/.tidepoolrc && echo "config matches"
code
data matches
config matches

If your tool has a running service, tell readers to stop it first, since files being written during the backup can be captured half-finished. If it has its own export or dump command, use that instead and explain why.

One change, one check, in an order you can defend

Put reversible steps first and leave deletions for last. This is why the config is copied, not moved. Its format has not changed, 2.4.1 still needs the old file, and the reader may yet need 2.4.1.

code
mkdir -p ~/.config/tidepool
cp ~/.tidepoolrc ~/.config/tidepool/config.toml
cmp ~/.tidepoolrc ~/.config/tidepool/config.toml && echo "config copied"
code
config copied

Every step should end like this, with something the reader can compare. Next, the script:

code
sed -i.bak 's/tidepool sync --force/tidepool sync --overwrite --yes/' ~/scripts/nightly.sh
diff ~/scripts/nightly.sh.bak ~/scripts/nightly.sh
code
3c3
< tidepool sync --force ./data
---
> tidepool sync --overwrite --yes ./data

Three details deserve a sentence each in the guide. The --yes matters because --overwrite asks for confirmation, and nobody is awake to answer a nightly job. The form -i.bak works in both GNU and BSD sed, where a bare -i does not, and it leaves an undo file behind. And the rewrite only matches that exact spelling, so tidepool sync --verbose --force survives it. Repeat the search to catch those, skipping the backup copy:

code
grep -rn --exclude='*.bak' 'tidepool sync.*--force' ~/scripts

No output means the scripts are done.

Describe the state between steps as well. The edited script will not run on 2.4.1, which is why the header says to pause scheduled jobs. Then mark the step that cannot be undone, in words that stop someone skimming:

Point of no return. The first run of 3.0.0 converts your data folder, and 2.4.1 cannot read the result. Going back after this means restoring the backup and losing anything synced since.

Our publishing guidelines ask authors to flag anything that deletes data or alters system settings. A migration guide is where that flag does the most work.

Write the way back before anyone needs it

Rollback belongs in the guide as a tested procedure. Say first what it costs: these commands delete the new config and the converted data, so anything written since the upgrade is lost.

code
rm -rf ~/.config/tidepool ~/.local/share/tidepool
tar -xzf ~/tidepool-before-3.0.tar.gz -C ~
mv ~/scripts/nightly.sh.bak ~/scripts/nightly.sh
tail -n 1 ~/scripts/nightly.sh
code
tidepool sync --force ./data

After that comes the pinned install of the old version. Also state when rollback stops being safe, for example once other machines have synced with the new format. If there is truly no way back, say that plainly near the top, because it changes whether the reader starts today.

Cleanup goes at the very end and is optional: remove ~/.tidepoolrc, the .bak file and the archive only after the new version has run long enough for the reader to trust it. Removing them earlier takes the rollback away.

Test it from the old version, then keep testing

The classic failure is a guide checked on a machine that was already upgraded, or on a fresh install of the new version. Neither has anything to migrate. Start from a clean machine, install the old version, and use it the way a user would: write a config, create some data, schedule a script with the old flag. Then follow your draft, copying each command from the page.

While you are there, collect three more things:

  • The errors people hit when they skip a step. Quote them exactly in the guide, with the fix beside them, because a stuck reader searches for that text.
  • A rollback run. Afterwards, the check from the second section should report the reader as affected again. If it does not, the rollback is incomplete.
  • What you did not test. Other operating systems, older versions and large data sets get one honest line each.

Publish the guide with the release and link to it from the changelog entry, so people find it before they upgrade. On TechDug, top-level sections are numbered and long posts get a table of contents, as the features page describes, so a guide built from one section per stage already reads as a procedure. When a reader reports a step that failed, fix the guide and update its “tested from” line, since that line is the reason anyone believed it.

Written by

TechDug Editors

The house journal of TechDug. Notes from the editors on writing technical posts that work: reproducible steps, copyable commands, clear error write-ups and honest updates.

Comments 0

Moderated before publication. Sorted oldest first.

No comments yet. Spotted a typo, a better flag, or a step that failed for you? Say so below.

Reply to this post

Required: a name. Not required: an account or an email address. A moderator checks each reply before it goes live.

Plain text only, 2,000-character limit.