Versioning
One number names the application and its documentation together, because they ship
as one thing: the pages a reader opens at /docs are always the pages of the version
that is running, never a newer or older set.
One number, read from one file
VERSION, at the root of the repository, is the only place the number is written.
version.current() reads it once — a deploy restarts the process, so reading it
again would only ever find the same answer — and the sidebar, the CLI, the API and
the documentation header all print it from there, so there is no second copy to drift
out of step with the first.
version.build() is a different question: which commit is actually running, from
DPF_BUILD, else a BUILD file the deploy writes, else git rev-parse --short HEAD.
The version says what was released; the build says exactly what is deployed, and
between two releases those are not the same fact — a restart, a hotfix rebuilt from
the same tag, or a box that is a commit behind the one that was meant to go out all
show the same version and a different build. version.label() prints both together,
0.1.0 · b325fcd, for exactly that reason.
What MAJOR, MINOR and PATCH mean here
This application has no library that code calls, so "breaking" never means a function signature changed — it means something a user, an operator or an integration was depending on stopped being true.
- MAJOR — a setting, a CLI command or an API field is removed or changes meaning, the workflow file's format changes so an existing file no longer loads, or a database change needs a manual step. Something that already depended on the old behaviour now has to change.
- MINOR — something is added: a step, a setting, an endpoint, an expectation kind. Nothing that already worked stops working.
- PATCH — a fix, with no change to what anybody already relies on.
- Before
1.0.0, semantic versioning allows a breaking change to raise MINOR instead of MAJOR, and this factory uses that allowance rather than pretending to a stability it has not earned yet. A reader of the changelog should assume that rule is in force until the first1.0.0, not the convention that applies afterwards.
A data contract has a semantic version of its own, too: v0.1 when Source ends, 0.x while it is agreed, 1.0.0 when it first goes live, and after that computed from what changed, so a removed column is a major version of that product's promise. It is a different number for a different thing, and unrelated to the factory's.
The API keeps its own number
/api/v1 carries a version of its own (api_v1.VERSION, 1.0 today) that is
reported alongside the application's, not instead of it. It moves only when a system
reading the API would have to change something to keep working — which is rarer than
every change to the application — and the path itself, v1, is the part of that
promise a caller can see without reading a response body at all.
Cutting a release
python -m dpf release major|minor|patch — or an exact MAJOR.MINOR.PATCH — is the
one path to a new version, and it refuses to take it on trust. It reads
Unreleased in the changelog, and if there is nothing written
there it stops: "a version number has to be able to say what it means." Where there
is something, it moves those lines into a new section dated today, writes the number
into VERSION, keeps the comparison links at the foot of the file in step, and prints
the two git commands — commit, then tag v<number> — rather than running them,
because a release is the one commit somebody should read before it exists. See
Release a version.
Keeping the documentation honest about all of this
The documentation is reviewed in the same pull request as the code it describes, and five mechanisms hold it to that rather than trusting it:
- Generated sections. A block between
<!-- generated: NAME -->and<!-- /generated -->is rewritten from the code it describes — settings, the CLI, API endpoints, audit codes, permissions, the workflow and its drawing — whenever a page is served orpython -m dpf docs buildruns, so the copy on GitHub and the copy the application renders cannot disagree. - Review stamps. A page's front matter names what it
covers: the files that would make it wrong if they changed.fingerprintis a hash of those files' bytes at the moment somebody last read the page against them. Change a covered file and the fingerprint stops matching, which failspython -m dpf docs checkand names the page until it is read again and re-stamped withpython -m dpf docs stamp PAGE. Stamping without reading is the one way to defeat this, and nothing stops it mechanically — a check that can be lied to is still worth having, because most people are not looking for the lie. - Links and navigation. Every relative link has to resolve to a page listed in
nav.yaml, every#anchorto a heading that exists on the page it names, and every file underdocs/has to be listed exactly once. - Environment-variable drift. A variable the code reads that the settings registry has never heard of fails its own test, and so does the opposite — documented, and read by nothing. The CLI and the API cannot drift the same way, because their reference pages are generated rather than written by hand.
- Drawings. A drawing is a few lines of YAML in its page, drawn when the page is
served (
dpf/docs_diagrams.py). A box names the code it stands for, and the check fails the day that path is renamed or deleted; a drawing that cannot be laid out — two boxes in one place, a label longer than its line — fails it too. The layers drawing on Components is held to the real imports bytests/test_architecture.py.
python -m dpf docs check runs everything the test suite checks, faster, and says
what to do about each problem it finds.