The Pheidi changelog, and the version that forgot things
I use Merge & Tell every day to promote Merge & Tell and my other projects. One of them is Pheidi, a running training app I also build alone. Its changelog at pheidi.training/updates is written by Merge & Tell from merged pull requests, and every entry on it now has its own page that never moves. This is how it got there, including the version before it that quietly hid 14 of 19 entries.
The reason
A changelog entry is a link you send someone
In August a runner told me Pheidi was showing tomorrow’s workout as today’s. It happened in the evening, US time, and the cause was dumb. Part of the app read “today” off the server’s clock instead of the runner’s. The fix shipped on September 14, and Merge & Tell turned that pull request into an entry called “Your ‘today’ workout is now actually today”.
The best reply to a bug report is a link to the entry that says it’s fixed. It tells the person their report went somewhere and that somebody read it. That only works if the link still works when they come back to check, which is the whole problem with a changelog that rotates entries off the end.
For a one person app there’s a second reason. A changelog that keeps moving is the proof somebody is still home. Nobody trusts a training plan from an app that looks abandoned.
The wrong turn
The version that forgot things
The Merge & Tell JSON Feed holds the newest 50 entries and has no paging. I designed it that way, and it’s fine for a widget. It’s less fine as somebody’s only copy of their history, because an entry that falls off the end is gone from the feed for good.
The first Pheidi version, in August, handled that. A scheduled job saved the entries into a file in the repo, and the site built from the file. Three days later I replaced it with something simpler. The site fetched the feed while it built and showed the newest five entries. No job, no saved file.
It also kept nothing. For seven weeks the page showed five entries and dropped the rest, and by the time I fixed it there were 19, so 14 were invisible. No entry had its own page, so there was nothing to link to. And because the Pheidi site only rebuilds when its own files change, a new entry didn’t even appear until I happened to deploy something else. Simpler for me, worse for the runner, which is the usual trade when I call something simpler.
The design
How it works now
On October 6 I went back to the first design and finished it. Every six hours a GitHub Actions job reads the feed, with the URL kept in an Actions secret. It merges new entries into a JSON file in the repo, matching on each entry’s id, opens a pull request with that one file and merges it. The merge triggers the normal deploy. The Pheidi site never talks to Merge & Tell at all.
Each entry gets its URL the first time the job sees it, for example /updates/2026-09-14-your-today-workout-is-now-actually-today/, and nothing changes it after that. If I reword the title later, the page text changes and the link I already sent stays put.
Because the site builds from a file in git, a feed outage changes nothing on the live page. That includes a Merge & Tell outage, which is a strange thing to design against when you run both ends, but I’d rather my changelog not depend on me having a good day.
The full recipe, with the workflow, the merge code and the escaping, is in a changelog on your own site.
The numbers
The receipts
These come from the first sync run on October 6, which I started by hand, and from the live site the same day.
| Build time fetch (before) | Synced archive (now) | |
|---|---|---|
| Entries on pheidi.training/updates | 5 of 19 | 19 of 19 |
| Pages for single entries | 0 | 19 |
| Entry URLs in the sitemap | 0 | 19 |
| What a new entry needed to show up | An unrelated deploy | The next sync, within 6 hours |
| What a feed outage did | Failed the build, old deploy stayed up | Nothing visible |
The job is small. That run took 24 seconds start to finish, and reading plus merging the feed took about one of them. Most of the rest was opening and merging the pull request. It found 19 entries. Three were already saved from the August version, so it added 16. The feed URL appears zero times in the run’s log.
The bill
What it costs
It isn’t instant. An entry can take up to six hours to reach the page, plus however long GitHub takes to find a machine. The first run waited about six minutes for one. Nobody sits on a changelog hitting refresh, so I can live with it.
It costs Actions minutes whether or not anything changed. Four runs a day, each billed as at least a minute on a private repo, is about 120 minutes a month, and most runs will find nothing new. I spent part of September cutting CI minutes on that repo, so this one stung a little.
And the job vouches for itself. Pheidi’s main branch needs two test suites to pass before anything merges, and a pull request opened by a GitHub Actions job doesn’t start other workflows. So the job marks both checks as passed itself, after confirming the pull request touches exactly one file, the changelog data. That’s the answer the test workflow gives any change that only touches the marketing site. It’s still a job grading its own homework, and I’d rather say so.
Restraint
What it doesn’t do
The archive hasn’t saved a single entry yet. Pheidi has 19 entries and the feed holds 50, so today everything in the archive is also in the feed. The day it earns its keep is the day an old entry rotates out, and at my pace that’s months away. Right now it’s insurance.
It also isn’t the only way to publish entries. Merge & Tell already serves your changelog as an HTML page with a page per entry, same as my own updates page. I built the Pheidi version because I wanted the entries on pheidi.training, under URLs I control. If linking out is fine for you, none of this is needed.
The rule I took from it, for any page built from a feed that only remembers recent things: build the page from what you’ve committed, not from what you can fetch. The runner who reported the today bug can check it now. The entry’s still there.
Pheidi's entries start as merged pull requests.
You were going to merge the PR anyway.