v2.38.0 (things that were true somewhere else)
Four changes, and three of them are the same shape: the product knew the right
answer and wrote it down somewhere that was not the place doing the work.
A webhook's secrets were matched back to it by name, while an id would have
been the honest key. The activity feed's two methods documented "newest first"
and returned the oldest first. The number of DNS providers was written three
different ways in one file, and Google's AI summary picked the worst one. In
each case nothing was missing — a true statement existed, and something else
was doing the opposite.
The fourth is the same defect at a distance: custom-script is how you use
CertMate with a DNS provider that has no plugin, and that answer lived in a
GitHub issue comment rather than in the documentation.
The API contract moves to 2.22, from 2.20. Read it from
X-CertMate-API-Version on any response, or api_contract_version in
/api/health. Two MINOR steps, each written down beside the constant in
modules/core/constants.py; nothing was removed or retyped.
| step | what grew |
|---|---|
| 2.21 | an id on each webhook in GET /api/notifications/config, and the same field accepted on POST
|
| 2.22 | GET /api/activity states its ordering for the first time
|
Read this before upgrading
The activity feed opens on the newest entry
/activity opened on the oldest thing that ever happened. On a seventeen-day
log the first six rows were from seventeen days ago, with today's events below
the fold. Both methods behind it already documented "newest first" and both
returned the oldest first.
GET /api/activity and GET /api/web/audit-logs now answer newest first.
limit still selects the same window — the most recent N — and only the
direction within it has changed.
This is counted as a MINOR rather than a MAJOR, and the reasoning is recorded
beside the constant so you can disagree having seen it: nothing published
promised the other direction. The API reference described the window ("the
most recent entries") and never the direction within it, so a client relying on
ascending was relying on behaviour that contradicted the documentation it
shipped with. If you have a script that reads /api/activity and assumes the
oldest row is first, it needs reversing.
Webhooks now carry an id
Each webhook in GET /api/notifications/config has an id, and it is what a
masked secret is matched back to when you save. Echo it back unchanged. A
submission carrying an id that names no stored webhook is treated as a new
one and inherits nothing; one sent without an id still matches on
(type, name), exactly as before.
Existing configurations are given ids the first time CertMate loads them, so
there is nothing to do on upgrade. That first load rewrites settings.json,
which is the only visible effect.
Fixed
Renaming a notification webhook deleted its URL, and the save answered 200.
Masked secrets were matched back to the stored entry by (type, name) — the
two fields an operator edits. So renaming a webhook, or changing its type, meant
the save could not find the entry the secrets belonged to and dropped them: the
webhook stayed enabled, lost its URL, its bearer token and every custom
header, and nothing on screen said so, because the URL is never displayed. The
next event logged Webhook 'ops-renamed' not retried: Webhook URL not configured and delivered nothing.
The fix is an id the operator cannot edit, assigned when settings are loaded
rather than when they are saved — assigning at save would have left exactly one
unprotected save, the first after upgrading, which is the save someone makes to
fix a name.
The activity feed showed the oldest entry first. See above.
The README stated the provider count three different ways. "29", "two
dozen+" and "25+", in one file, and Google's AI Overview — the first thing on
the page when you search for CertMate — repeated one of the vague ones as
"supports over 22 DNS providers". Counting across the translated trees, the same
product described itself with a quantity in fifteen places and five languages.
The count is written once now, and every other place points at the list. A
test derives it from the provider registry and fails if a second count appears
or the one that remains disagrees with the code. The README also lost a section
headed "What's New in v2.0.0", in a product at 2.37.0.
New
A guide for DNS providers with no plugin
custom-script is how you use CertMate with a provider that has neither a
certbot plugin nor dns-lexicon support — Total Uptime and Netriplex in the
report that prompted this, and anything else where you can create a TXT record
programmatically but nobody has written a plugin.
It has worked for several releases. What was missing is that the DNS page gave
it two lines of JSON, so the answer lived in a GitHub issue comment.
Custom DNS script is the guide: the six path rules
and why they are strict, the exact environment your script is called with, why
waiting is your script's job, the wildcard-plus-apex case, what renewal
remembers, and a worked example.
Two things worth knowing whether or not you read the page:
CERTBOT_TOKENis not set for DNS-01. certbot removes it in the
non-HTTP-01 branch. A hook written to expect it gets an empty string.- The renewal configuration holds the path your script had at issuance.
Move the script and renewal still points at the old location; reissue after
relocating hooks.
Inside
Not operator-visible, but they are why the list above exists.
A test was guarding the defect it claimed to prevent. The activity
ordering had this:
def test_the_newest_match_is_first_from_the_callers_point_of_view(audit):
"""... a search that came back oldest-first would put the least
interesting row at the top."""
assert found['entries'][-1]['resource_id'] == 'newest.example.com'The name carries the intent, the docstring states the hazard, and the index
asserts the hazard happening. Someone saw it red and changed the assertion
rather than the code.
Three issues were describing a state that no longer existed. #671 alarmed
about "7 silent pass, 1 bare except" — both zero, and the lint gate enforces
it, so it is closed. #666 claimed create_certificate is F(115); it is 21. But
its other half is not done and the numbers hid that: create and renew were each
decomposed and share exactly two of the twenty-six helpers they call, so they
are still two implementations. #674 said RELEASE_NOTES.md is 3,719 lines; it
is 401, split per release with a generated index. Both were retitled to what
remains.
The documentation surface is wired together by gates now. Adding one page
in this release touched eleven files, because tests/test_docs_navigation.py
required the index and all four translated trees — each pointing at the English
original with the "in English" marker the convention already uses — plus the
README's documentation table, the file listing, and the DNS page in five
languages. That is the point: a page nobody links
to is a page nobody finds, which was the defect rather than a detail of it.