Releasing
This page is for maintainers. It covers how a release is cut and how the public Canny board (ideas and roadmap) and Canny changelog fit around it, with an explicit split between what runs by itself and what you still do by hand.
What is automated
| Step | Done by | Trigger |
|---|---|---|
| Bump the version, draft and tag the release post | You, with scripts/release.mjs | Manual |
| Validate metadata, test, build and publish images, create the GitHub release | release.yml | Automatic on a v*.*.* tag push |
| Publish the Canny changelog entry | release.yml, job canny-changelog | Automatic, after the images and GitHub release succeed |
| Link shipped ideas to that entry | Same job | Automatic, using each idea's tag and status in Canny at that moment |
| Mark an idea complete | You | Manual: in Canny, or via the roadmap sync workflow |
| Create ideas, add version tags | Roadmap sync workflow | Manual dispatch |
| Email voters that their idea shipped | Canny | When the changelog entry is published with notifications on |
The two things people tend to assume are automatic but are not: ideas are never marked complete for you, and nothing runs the roadmap sync on release. Because the link step only picks up ideas that are already complete, marking them complete has to happen before you push the tag.
How ideas are organised on the board
Canny has no release or version field on ideas, so a target release is expressed three ways:
- A version tag such as
v0.7.0on the idea. Tags are only visible to Canny admins, so this is for your own filtering and for the changelog linking below. - A "Target release: v0.7.0" line at the top of the description (or "Released in: v0.6.0" once that version has actually been released). This is what a public visitor actually sees.
- The status:
planned,in progress,complete, orunder reviewfor ideas with no version.
No ETAs are set, since they would be public promises. Public roadmap columns follow status and are configured in Canny's own settings; nothing here touches them.
The source of truth for ideas we create or track is website/scripts/canny-roadmap.json. Each entry
has a title, a version (or null), a status, and details (a list of paragraphs).
Marking an idea complete
There are two ways, and either works for the changelog linking.
In Canny. Change the idea's status to Complete in the Canny UI. This is the quickest option.
Through the roadmap sync. Set "status": "complete" on the idea in canny-roadmap.json, push
it, then run the Canny Roadmap Sync workflow from the Actions tab. Run it with dry_run ticked
first; the log lists what it would change.
| Input | Meaning |
|---|---|
dry_run | Default on. Reports what would change and writes nothing. |
update_details | For ideas that already exist: none leaves the description alone (apart from the release line change described below), prepend adds the release line above it, replace swaps in the description from the JSON and keeps the original text underneath as "Original request:". |
notify_voters | Default off. Whether moving an idea to complete emails its voters. Leave it off if the changelog entry is going to notify them anyway. |
author_id | Only needed if the board has more than one Canny admin, so the script cannot pick the author itself. |
What the sync does, and deliberately does not do:
- It matches ideas to existing posts by title (trimmed, case-insensitive). A match is never duplicated.
- For an existing idea it only ever brings the version tag in line with the JSON, optionally
rewrites the description (per
update_details), and moves the status to complete when the JSON says so and the idea's version has been released (it has a GitHub release). It never changes any other status, since statuses are otherwise yours to manage on the board. - Moving an idea to another release is a one-line change: edit its
versionin the JSON and run the sync. The new version tag is added, any other tag shaped like a version (v0.7.0) is removed so the idea isn't listed under two releases, and a "Target release: vX" line in its description is updated to match. Other tags, such as "In Dev", are never touched. - "Released in" is only used once the version is released. When the JSON says an idea is complete
and vX has a GitHub release, a first line of "Target release: vX" is changed to "Released in: vX"
regardless of
update_details. While vX has no release, a complete idea keeps "Target release: vX", and a "Released in: vX" written earlier is put back to "Target release: vX". If GitHub can't be reached the script assumes "not released", and never claims a release it couldn't confirm. Only the first line is touched; the rest of the description is left as it is. - It creates any idea that has no matching post, along with its version tag, and sets its status. A
complete idea whose version isn't released yet is created as
in progress, notcomplete. - It is safe to re-run: a second run with no JSON changes makes no writes.
Changelog publishing and idea linking
The canny-changelog job in release.yml runs once the images and GitHub release have succeeded. It
only runs for a real tag push, never for a manual re-run of the workflow. It first runs the roadmap
sync (the release now exists, so ideas finished in this version are marked complete and their line
becomes "Released in"), then website/scripts/publish-canny-changelog.mjs. A failure in the sync step
is reported but does not stop the changelog entry. For the tagged version the changelog script does
the following:
- Finds the release post in
website/blog/by itsslug(v0.7.0becomesv0-7-0), so the post must exist at the tagged commit.release.mjs tagguarantees that. - Converts MDX to Markdown: drops
importlines and the{/* truncate */}marker, turns<ThemedImage>into a plain Markdown image (light variant), rewrites relative/docs/and/releases-updates/links (and the older/blog/form) to absolutehttps://dotmarc.app/...links, and tolerates CRLF line endings. - Skips if it already exists. Canny's create call has no idempotency key, so the script lists existing entries and stops if one has the same title. If it cannot list them, it posts nothing.
- Finds ideas to link: every idea on the board that is tagged
v0.7.0and has statuscomplete. Ideas tagged with the version but not complete are skipped and logged with their status, because a tag is only a target and an idea that slipped must not be announced as shipped. - Creates the entry as published, dated with the release post's filename date, with the
matching ideas passed as
postIDs. Notifications default on (NOTIFY=falseturns them off).
Linking is a bonus, not a gate. If the idea lookup fails, the entry is published without links and a warning is logged. The job only fails outright if the post cannot be found or parsed, the existing entries cannot be listed, or Canny rejects the entry.
Cutting a release, step by step
- Settle the ideas. For everything shipping in this release, make sure it carries the version
tag and is
completeincanny-roadmap.jsonbefore you tag. Canny itself stays "Target release" and not complete until the release exists, and the release job then marks them complete and links them. Anything that slipped should be moved to a later version (change itsversionin the JSON and run the sync), or it will simply be skipped and logged. - Prepare.
node scripts/release.mjs prepare 0.7.0bumpsDirectory.Build.propsand creates the release post. The post gets adatewith the time as well as the day, because the Releases and Updates page sorts by date alone: two releases on the same day would otherwise list in the wrong order. - Write the post, then
node scripts/release.mjs tag 0.7.0, which refuses to run with unrelated working tree changes, a missing or placeholder post, or an existing tag. - Push.
git push origin main v0.7.0. Everything from here is automatic. - Check the run. In the
canny-changelogjob log, look for oneLinking "..."line per shipped idea and a finalPublished entry ... linkedIdeas=Nline. ANot linking "..."line means an idea was tagged for this version but was not complete at the time.
The website follows the latest release
The homepage's "Latest release" badge, and anywhere a doc says <LatestVersion /> (for example an image tag),
show the newest published GitHub release, worked out when the site is built by website/lib/latest-version.cjs.
If GitHub can't be reached it falls back to the version in Directory.Build.props, and if neither is available
the docs say latest. Nothing needs editing at a release: the refresh-website job in release.yml starts a
website publish on main once the GitHub release exists. Use <LatestVersion /> in a doc when it should track
the release, and write a fixed version only where it is an example. The rules are unit tested:
node --test website/lib/latest-version.test.cjs.
Backfilling or republishing
The Canny Changelog Backfill workflow runs the same script for a list of versions you give it. It defaults to notifications off, so publishing old releases does not email subscribers. Because of the duplicate check it is harmless to list a version that is already in Canny. It also links complete ideas tagged with that version.
Setup
- The
CANNY_API_KEYrepository secret (Canny settings, API) is used by all three workflows. - Both scripts identify the board by its widget token, which must match
BOARD_TOKENinwebsite/src/plugins/featureRequests/FeatureRequestsPage.tsx. - To try the sync locally:
CANNY_API_KEY=... DRY_RUN=true node website/scripts/sync-canny-roadmap.mjs. The release-line and status rules are unit tested without a Canny key:node --test website/scripts/canny-release-labels.test.mjs. The changelog script has no dry-run mode and posts for real, so test changes to it against a mockedfetchrather than the live API.
Known limitations
- Renaming an idea in Canny breaks the match. The sync finds ideas by title, so if you rename
one on the board, update its
titleincanny-roadmap.jsontoo, or the next sync creates a duplicate. - Re-running does not add links after the fact. The duplicate check means a second run skips an entry that already exists. If you forgot to mark an idea complete before tagging, fix the entry in Canny's changelog editor.
- Marking an idea complete directly in Canny leaves its release line as "Target release". The
line is only changed to "Released in" by the sync, and only once the version has a GitHub release,
so set the idea to
completein the JSON and let the release job (or a manual sync after the release) do it, or edit the line by hand. - A manual sync before the release won't mark anything complete. That is deliberate: the sync checks GitHub for the release, so an idea can't read "Released in" or "Complete" ahead of it.
- Roadmap columns and public visibility of tags are Canny settings, not something these scripts can change.