CADENCE — Feed Authoring Guide
The feed is the product. This is the operating manual for the person (Stoffel) who writes the advisories — the 3—8h/week curation discipline that is the business (see
ttm.md). The software just delivers and applies what you write here. Get the judgment right; the engine handles the rest.
The mental model
You are a consultant answering, per product version: "should I install this, and when?" — and encoding that answer so a customer’s appliance can apply it to their specific inventory. The value is not "an update exists" (every package manager says that). It’s the conditional judgment: hold this point release, wait for the patch, this is blocked until a dependency moves first.
A feed is a directory of hand-written YAML, one subdirectory per module (product family). The Feed Builder hashes + ed25519-signs it; the client pulls, verifies, and evaluates it against local inventory. You never see customer data — you publish judgment, they apply it locally.
Layout
feed/
proxmox-ve/
advisories.yaml
service_intervals.yaml
nextcloud/
advisories.yaml
postgresql/
advisories.yaml
service_intervals.yaml
Each module dir may have advisories.yaml (the verdicts) and/or service_intervals.yaml (recurring reminders). The dir name is the module id; an advisory’s product field is what’s matched against inventory (it need not equal the dir name — e.g. the proxmox-ve/ module also carries ceph advisories).
An advisory, field by field
- product: nextcloud # matched against each inventory asset's product
version: "29.0.7" # the version this advisory is ABOUT (quote it — YAML eats 1.10)
match: installed # installed (default) | upgrade-to
verdict: hold # the base call (see verdicts below)
severity: important # info | recommended | important | critical
effective: 2026-05-10 # optional: advice is inert before this date
expires: 2026-08-01 # optional: inert on/after this date (exclusive)
summary: "Several popular apps fail on 29.0.7; hold for 29.0.8."
conditions: # optional gates — escalate the verdict when local facts hold
- when: { app_installed: ["calendar", "groupware"] }
then: blocked
note: "Calendar + Groupware break on this point release."
cves: ["CVE-2026-12345"] # optional: surfaced as CVE highlights on the verdict
references: ["https://..."] # CITE YOUR SOURCE — the linter warns on an uncited verdict
match — who does this apply to?
-
installed(default): "you are running exactly this version." Use for "you’re on 29.0.7 → hold." Matched when the asset’s version equalsversion. -
upgrade-to: forward-looking guidance about moving up to this version. Matched when the asset’s version is belowversion. Use for "before you go to PVE 9, …".
affected_max — cover a whole version range with one advisory
For an installed-match advisory, an optional affected_max turns the exact match into an inclusive range [version, affected_max]. So instead of writing one advisory per patch release to "hold all of 26.2.x", write one:
- product: nextcloud
version: "26.2.0"
affected_max: "26.2.4" # applies to every 26.2.0 … 26.2.4 (inclusive)
verdict: hold
severity: important
summary: "All 26.2.x point releases break several apps; hold for 26.3."
Omit affected_max for an exact-version advisory (the default). It’s ignored for upgrade-to. The linter errors on an inverted range (affected_max below version — it would match nothing).
verdict — the call (lowest → highest precedence)
| verdict | meaning | |------|------| | install | safe to install at the customer’s convenience | | install-urgent | install promptly — carries an important fix | | hold | do not move to this version yet; a known issue makes it unwise | | wait-for-patch | hold specifically until the next patch release lands | | eol | this version/line is end-of-life — plan to move off it | | blocked | hard stop: a precondition is unmet (e.g. a dependency ordering) |
Precedence matters. When several signals apply, the engine keeps the highest-precedence one. A condition can only escalate the base verdict (raise it), never lower it — so set the base to the call that holds generally, and use conditions for the stricter cases. (The linter flags a condition whose then can’t outrank the base — it would be dead.)
severity — how loud?
info < recommended < important < critical. This drives notification thresholds: the client delivers important+ verdicts as alerts. Reserve critical for security-driven install-urgent or a blocked that will cause an outage.
conditions — the conditional judgment (the whole point)
Each condition is when (a predicate over local facts) → then (a verdict to escalate to) + an optional note. All present sub-predicates in a when must hold (AND). Predicates:
-
app_installed: [..]— every named app/plugin is installed on the asset. (Nextcloud apps, PostgreSQL extensions, etc.) -
product_below: { product, version }— another product’s installed version is strictly belowversion. This is cross-product ordering — the flagship capability. Example: PVE 9 isblockedwhilecephis below19(Squid). -
product_at_least: { product, version }— another product’s installed version is at leastversion. Example: a Ceph maintenance release is routine generally butinstall-urgentspecifically whenproxmox-veis>= 9.
A condition that references a product absent from the inventory does not fire (we act only on positive evidence — a host with no Ceph isn’t blocked by a Ceph-ordering gate).
Service intervals (recurring reminders)
# proxmox-ve/service_intervals.yaml
- product: proxmox-ve
interval: cluster-upgrade-review
every: 180d # d / w / m(~30d) / y(~365d), or a bare number (days)
note: "Review cluster upgrade posture; check EOL runway."
A reminder is "due" when every has elapsed since the last recorded action (or immediately if never actioned), for products actually present in inventory. The daemon notifies once when each becomes due. Pick a parseable every — the linter errors on an unparseable cadence (it would silently never fire).
The publish workflow
# 1. write/edit YAML under feed/<module>/
# 2. LINT — catch authoring mistakes before signing (dead windows, inert conditions, bad cadences/CVEs, uncited verdicts)
feed-builder lint --feed feed # exits non-zero on errors
# 2b. DIFF (optional) — review what advice changed vs the last published feed before you sign
feed-builder diff --old ../feed-2026.06.15 --new feed # added / removed / verdict+content changes
# 3. BUILD — validate + hash every file → manifest.yaml
feed-builder build --feed feed --feed-version 2026.06.22
# 4. VERIFY — operator self-check against the public key (dry run)
feed-builder verify --feed feed --public-key cadence-feed.pub
# 5. SIGN — detached ed25519 over the manifest → manifest.yaml.sig (needs the secret key)
feed-builder sign --feed feed --secret-key signing.key
# 6. PUBLISH — copy the dir (manifest.yaml + .sig + module files) to the feed host / CDN
# Customers pull + verify on their schedule; air-gap mirrors rsync the directory.
Feed version is calendar-style YYYY.MM.DD. Publish on a predictable beat (e.g. weekly) plus out-of-band for urgent verdicts. The secret signing key never leaves your machine and is never committed.
Authoring checklist (run before every publish)
-
Every advisory has a non-empty
summaryan operator can act on (no jargon-only notes). -
Base
verdictis the call that holds generally; conditions only escalate it. -
Cross-product gates name the right
product(matches what an adapter reports) and version. -
CVEs are well-formed
CVE-YYYY-N…. -
Every advisory cites its source (
references): release notes, the CVE record, the EOL schedule — whatever backs the verdict. CADENCE sells auditable, sourced judgment, so an uncited verdict is half-built. The linter warns on a missing reference and on any reference that isn’t a realhttp(s)://…URL; the committed sample is held to zero such warnings. -
Dates:
effectivebeforeexpires; nothing already-expired left lying around. -
feed-builder lintis clean (zero errors), thenbuild→verify→sign. -
Spot-check with
feed-builder analyze --feed … --inventory examples/inventory.yaml— do the verdicts read like advice you’d give a client out loud?
Worked examples (in feed-sample/)
-
App-compat gate —
nextcloud: hold 29.0.7, blocked ifcalendar+groupwareare installed. -
Cross-product ordering —
proxmox-ve: upgrading to 9 is blocked whileceph < 19(Squid). -
product_at_leastescalation —ceph19.2.2 is routine, but install-urgent on PVE 9 clusters. -
CVE-driven urgency —
postgresql16.3 isinstall-urgent/criticalwith CVEs surfaced. -
Extension gate —
postgresqlmajor upgrade blocked whiletimescaledbis installed. -
Flagship → long-tail —
nextcloud31 blocked whenpostgresqlis below its required minimum.
Read those YAML files alongside this guide — they are the canonical, lint-clean reference.