# Announcing a release

2026-08-10. Ernest: *"Add a way to announce new versions on the forum with a
changelog of what changed."*

---

## Half of this already existed

The storefront has announced downloads since 2026-08-09: a topic per product, a
reply per version, the changelog in the reply, idempotent on the version. That
shape was argued out then and it is the right one, so this is the same shape
applied to Convoro's own releases — which had `releases.notes` to say it with
and nowhere to say it.

**One topic per major line, a reply per release.** Six patch releases is one
conversation with six updates in it. Six topics would bury the forum, and
somebody who asked a question against 1.0.1 would never see it answered against
1.0.4. A *major* release opens a new topic, because a major release is a
different conversation — that is what makes it major.

## What is different from the storefront's version

- **The forum is a site setting, not a category column.** There is no category
  to hang it on; there is one Convoro. 🚨 It has **no default and null means do
  not announce** — every install has the `releases` table and only convoro.co
  ever has rows in it, so a default would mean a site posting into one of its
  own forums the first time somebody recorded a release.
- **The author is the oldest administrator**, not the person who ran the
  command. `release:publish` runs from a shell with no session, so there is
  nobody to attribute it to, and the oldest admin is stable across runs —
  which matters because every reply in a series should come from one name.
- **A release must be published before it is announced.** An unpublished
  release is refused by the download route, so announcing one would point
  people at a 404.

## The changelog is a list, and that is not cosmetic

`Prose::blocks()` turns release notes into a bullet list when they look like
one and paragraphs when they do not. Pasted in as a single paragraph, eight
bullet points become one run-on sentence — and the changelog is the whole
reason anybody opens a release post. The newlines *are* the structure.

## Prose exists because four places were building documents by hand

A post's content is a TipTap document, and the storefront announcer, the legacy
importer and two API controllers each built the JSON themselves. That is the
shape most likely to drift: a fifth caller copies whichever one it found.
`Prose` is now the one place that knows what a document looks like, and text
normalisation and length capping happen there rather than at each call site.

## 🚨 What a test caught, and it is the kind that only a test catches

The first version collapsed "never announced" and "the announcement topic was
deleted" into the same answer — both returned null, and null meant *open a new
topic*. So deleting an announcement thread brought it straight back on the next
release, which undoes a moderator's decision.

They are different answers and must stay different. The storefront announcer
had already made this call and said so in a comment; the comment was copied
across and the behaviour was not.
