0008. Curated release notes are a committed file, announced on deploy
Context and problem statement
ADR-0007 made every deployment able to say which release it is. Nothing yet tells the people who use, operate, or follow HAI-Co² what a release means for them. The only release notes we have are the drafts Release Drafter assembles from PR titles andtype:* labels: written for contributors, full of #123 references, compare links and image refs, and unreadable by a community member who has never opened the repository. They are also the wrong artefact at the wrong time. The draft is not published when ci.yml builds and deploys (publishing is a manual, optional courtesy, and no workflow listens for it), the Releases API answers 404 for a draft, and the repository is private, so any GitHub link in an announcement is a dead end for a community member. The community lives on the “Co-Construction Studio” Discord server, whose #announcements channel is where a release should become visible, and the app has a “What’s new” panel showing the same text. The problem is where the curated text lives, who writes it, and which event publishes it, such that the app, Discord and the maintainer never disagree.
Decision drivers
- One text, several surfaces. Discord, the in-app panel, and the public
/whats-newpage must show the same words. - Available at deploy time. The announcement must not depend on the GitHub Release being published, on its body, or on any GitHub URL.
- Honest to each reader. Studio changes and research infrastructure both merit an announcement, but a research item must be visibly labelled and must not imply that it is available in the Studio. A release with nothing worth announcing must not manufacture a changelog out of dependency bumps.
- Exactly once, and recoverable. A re-run after a transient failure (VPN, Discord) must not post twice, and an outage must be recoverable by hand.
- No credential of consequence. No bot token with server-wide rights in CI, and no new
.envkeys for operators.
Considered options
- A curated committed file, announced on deploy success.
frontend/src/data/whats-new.json, hand-written and schema-tested, read by a CI job that runs after the prod deploy succeeded and posts through a Discord channel webhook. - Trigger on
release: published. Let the maintainer’s click on Publish post the draft’s body to Discord. - Bake notes from the Releases API at build time. Have the frontend build or the deploy job fetch the release notes and render them.
- Per-PR release-note blocks. Add a “user-facing notes” section to the PR template and aggregate those fragments at release time.
- A hosted changelog service. Publish notes through a third-party changelog/announcement product with its own widget and Discord integration.
- GitHub’s native Discord integration. Subscribe the Discord channel to the repository’s release events.
- A Discord bot token. Post (and crosspost) through a bot application whose token is stored in CI.
Decision outcome
Chosen option: a curated committed file, announced on deploy success, because it is the only option in which the text exists at deploy time, is reviewed like code, and is identical on every surface that shows it. The design:- The file is the source of truth.
frontend/src/data/whats-new.jsonholds entries newest first, at most one per production release, each with aversion, adate, a shorttitleand one to five items ofkindadded,improved,fixed,changedorremoved, a plain-prosetext, an optionalscopeofstudioorresearch, and an optional absolutehttps://href. Omittingscopemeans a Studio update. Aresearchitem gets a visible Research label in the app, public page, and Discord, and its text says that it is not a Studio workflow. It lives underfrontend/becauseci.ymlbuilds the frontend with that directory as its Docker context, so the in-app panel and/whats-newcan import it directly.whats-new.test.tsenforces the shape (semver, strictly descending versions, the kind and scope enums, item counts, length caps, no markdown, no issue refs, no mentions, no relative links), so a malformed entry fails CI before it can reach a deploy.node .github/scripts/whats-new.mjs X.Y.Zscaffolds an entry from the commits since the last prod tag; adding nothing is the correct entry for a release with nothing worth announcing. - Announce on deploy success, from the tag’s tree. A new
announce-releasejob inci.ymlneedsdeploy, checks out the tag, and runs.github/scripts/discord-release.sh, which builds one embed for a prod tag (the entry, or a neutral maintenance sentence when there is none) and a one-line ops note for a dev tag. Links point at the product site (haico.gr, later/whats-new), never at GitHub. Nothing is read from the GitHub Release. - A channel webhook, no bot. The webhook URLs are the repository secrets
DISCORD_RELEASE_WEBHOOK_URL(#announcements) andDISCORD_DEV_WEBHOOK_URL(a private dev channel). Repository-level on purpose: the job declares noenvironment:, and an Environment secret would resolve to empty there without an error. When a secret is unset the job writes a note to the run summary and exits 0. Every post carriesallowed_mentions: {"parse": []}, so no text can ping anyone, and uses?wait=true, so a dropped message is an error rather than a silent 204. A webhook cannot crosspost; publishing to following servers stays a manual click in Discord. - Idempotency is a git ref. After a
200the script createsrefs/discord-announced/<tag>(the job’s only reason forcontents: write); a later run for the same tag sees it and skips. Unlikerun_attempt == 1, the marker exists only once a post really went out, so a re-run after a VPN failure still announces and a re-run after a successful post does not.discord-resend.yml(workflow_dispatchwithtag,force,dry_rundefaulting to true, andnotes_reffor reading the notes from a branch other than the tag) is the manual escape hatch for outages, corrections and smoke tests.
Positive consequences
- People read short, hand-written Studio and research-infrastructure updates in the app and on Discord; the Research label makes clear when an item is not a Studio workflow, while contributors keep the generated technical notes untouched.
- The announcement needs neither a published release nor a GitHub URL, so it works for a private repository and for a maintainer who never clicks Publish.
- Entries are reviewed in the same PR flow as code and validated in CI before they reach any release-notes surface.
- Re-runs and re-sends are safe by construction, and a missing webhook degrades to a green job with a note, not a red pipeline.
Negative consequences
- Someone has to write the entry, and can forget: a prod release then announces the neutral maintenance sentence. Mitigated by the scaffolder, the ⚠️ summary note in that case, and the re-send workflow.
- A Studio user can see a research item they cannot invoke. The visible Research label and the item’s explicit boundary make that useful rather than misleading.
- The text is fixed at the tag; correcting a posted announcement means editing the Discord message in place or carrying the correction into the next release’s entry.
- Two kinds of release notes now exist (generated for contributors, curated for the community), which every release document must explain in one sentence.
- One more repository secret to rotate if the webhook ever leaks (a leaked webhook can post into that channel, nothing else).
Pros and cons of the options
Option 1: a curated committed file, announced on deploy success
- + The text exists at deploy time, is reviewed like code, and is shared by every surface.
- + Independent of the GitHub Release, of repository visibility, and of any bot.
- − A human must write it; a forgotten entry means a bland announcement.
Option 2: trigger on release: published
- + No new file; the maintainer’s click is the intent.
- − Publishing is manual and optional, so releases would silently go unannounced; a publish performed with
GITHUB_TOKENcreates no workflow run at all; the body is contributor-facing and full of links into a private repository.
Option 3: bake notes from the Releases API at build time
- + No duplicate writing.
- − The release is still a draft when the build runs (the API answers 404), so the build would fail or ship stale notes; same wrong audience and links as option 2.
Option 4: per-PR release-note blocks
- + Notes are written by the author while the change is fresh.
- − Dozens of fragments per release still need a human to merge them into three sentences; contributors and Coco cannot know how a change reads to a user; the aggregation step becomes the file this ADR adds anyway.
Option 5: a hosted changelog service
- + Polished widget, analytics, email digests.
- − A third party in the user’s browser and a subscription for a research project; the text still has to be written by hand, now outside review; the in-app panel would depend on an external script.
Option 6: GitHub’s native Discord integration
- + Zero code.
- − Fires on release events, which are manual and optional here; posts the technical body with links into a private repository; cannot be shaped, held until the deploy has succeeded, or made idempotent.
Option 7: a Discord bot token
- + Can crosspost to following servers and edit or pin messages.
- − A server-wide credential in CI for one message per release, plus an application, a member with the right permissions, and rotation discipline. A webhook is scoped to one channel and does everything the announcement needs.
Links
- Related ADRs: 0001-two-branch-model, 0007-build-identity-from-release-tag
- Docs: Maintainers handbook, Automations, Community guide, Deployment
- Code:
frontend/src/data/whats-new.json,frontend/src/data/whats-new.test.ts,.github/scripts/whats-new.mjs,.github/scripts/discord-release.sh,.github/workflows/ci.yml(announce-release),.github/workflows/discord-resend.yml