Until this week, Stylus SFTP Server's release notes existed in two hand-written copies: the release-notes page on this site, and a plain-text FIXES.txt that ships inside every installer. Two documents, same facts, both maintained by hand.
You already know how that story goes, because it goes the same way everywhere. We found our shipped FIXES.txt frozen several releases behind the website. Another time, a finished document sat committed in the repository for a full day while the website served the old version — written, reviewed, and never uploaded. Nothing was wrong with the discipline; checklists existed and were mostly followed. Mostly is the operating word. Every hand-maintained copy of a fact is a promise to update it forever, and promises don't scale.
Then we needed a third copy. We were building an update notifier — the server should tell its administrator when a newer version is published, and show what changed. That needs release notes in machine-readable form: a feed. Three copies of the same document, one of them consumed by software, all drifting independently? That was the moment to stop.
One artifact
There is now exactly one release-notes document in the entire product: an XML file, validated by a schema.
<release-notes xmlns="http://www.xmlpipelineserver.com/sftp/">
<release build="707" version="1.0.0.707" date="2026-08-05">
<downloads>
<setup>https://stylussftpserver.com/downloads/current/StylusSFTPServer-Setup.exe</setup>
...
</downloads>
<group type="new">
<item><strong>Realtime everywhere — polling is gone.</strong>
A WebSocket notification hub now runs inside the server...</item>
</group>
<group type="fixed">
<item>...</item>
</group>
</release>
</release-notes>
Everything else is generated from it by XSLT — the transformation language that has been quietly excellent at exactly this job since 1999, and which ships inside every JDK, so the pipeline added zero dependencies:
- One stylesheet renders the release-notes page you can read on this site — the full page, design and all.
- A second stylesheet, with text output, renders the
FIXES.txtthat ships in the installer. (The years of hand-written build-by-build history weren't thrown away — they're preserved verbatim in an archive file the generator appends.) - And the third surface needs no stylesheet at all, which is the point of this post: the XML file itself is published as-is, and it is the update feed.
Writing release notes is now: edit one XML file, run the generator, done. The page, the installer text, and the feed cannot disagree, because two of them are projections of the third.
Making drift impossible, not discouraged
A generator you can forget to run is just a faster way to drift. So the generator runs in three places, each covering the others' blind spot:
- The test suite regenerates both outputs and byte-compares them against the committed files. Hand-edit a generated file — or edit the XML and forget to regenerate — and the build goes red with a message telling you which file to fix and how.
- The installer build regenerates before packaging, so even a build that skips the tests cannot put a stale
FIXES.txtinside an installer. - The publish script regenerates before uploading, and aborts the upload if the XML doesn't validate against its schema.
The difference between a checklist and a constraint is that a constraint doesn't have good days and bad days. Our release checklist still exists — but the failure mode it used to guard is now a compile error.
What a structured changelog buys you
Here's what you can't do with an HTML changelog: compute a difference.
When a Stylus SFTP Server installation learns that a newer version exists, its admin console doesn't just say "an update is available." It shows the changes between the build you are running and the newest one — if you're three releases behind, you see all three releases' items, rolled up, labeled New / Security / Fixed. That query is trivial against the XML (give me every release whose build number exceeds mine) and impossible against a web page. The what's-new panel in the admin console and the release-notes page on this site aren't two features that happen to agree; they are two renderings of one document.
The notice itself is deliberately modest: a dismissible card in the Web Admin Dashboard and the desktop console (plus admin update-status on the command line), a download link, nothing else. Display-only — the server never downloads or installs anything by itself. Dismissing it remembers the specific version you dismissed; a newer release brings it back.
Our first phone-home, disclosed in full
Here is the part we want on the record, because it's the part vendors usually bury in a EULA.
Until this release, Stylus SFTP Server made zero outbound connections. Not one. License activation is verified locally; nothing ever called anywhere. The update check is the first exception ever, and we designed it so that full disclosure fits in a paragraph:
Every six hours, the server performs one HTTPS GET ofhttps://stylussftpserver.com/release-notes/release-notes.xml. The request carries no license key, no build number, no installation identifier — nothing but the request itself. The User-Agent isStylusSFTPServer-UpdateCheck, without a version.
Why can it send nothing? Because of the design above. The feed is a static file — the release notes themselves — on a static host. The server fetches the whole document and computes the "what's new since my version" delta locally. There's no per-installation endpoint, no query parameters to be tempted by, no server-side logic that could quietly grow an appetite for telemetry. From our web server's perspective, an update check is indistinguishable from a person reading the release notes in a browser. We couldn't count installations from it if we wanted to.
The operational details, equally plainly:
| Question | Answer |
|---|---|
| Default | On. One GET every six hours, plus one shortly after startup. |
| Turning it off | <update-check><enabled>false</enabled></update-check> in sftp-server.xml — hot-applied, no restart — or a JVM property kill switch. |
| Air-gapped hosts | One line in the log, then silence. No error spam, no red banners; the notice simply never appears. |
| Audit trail | Each newly discovered version is recorded once in the audit log; a manual "check now" is recorded with the administrator who ran it. |
| What it will never do | Download or install anything. It is a notice, not an updater. |
If your compliance review needs a sentence for the record: the product's only outbound connection is an unauthenticated, unparameterized GET of a public static file, and it can be disabled in configuration. We'd rather hand you that sentence than make you find it with a packet capture.
The shape worth stealing
The general lesson isn't about release notes. It's that when the same facts must appear on several surfaces, the fix is never more diligence — it's one authored source, generators for everything else, and enforcement that makes drift a build failure instead of a discovery. Do that, and secondary benefits start falling out on their own: our update feed cost nothing to build, and its privacy story wasn't a design achievement — it was a consequence.
Your changelog is probably three copies of one document too. One of them is lying to someone right now.