Skip to main content

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​

StepDone byTrigger
Bump the version, draft and tag the release postYou, with scripts/release.mjsManual
Validate metadata, test, build and publish images, create the GitHub releaserelease.ymlAutomatic on a v*.*.* tag push
Publish the Canny changelog entryrelease.yml, job canny-changelogAutomatic, after the images and GitHub release succeed
Link shipped ideas to that entrySame jobAutomatic, using each idea's tag and status in Canny at that moment
Mark an idea completeYouManual: in Canny, or via the roadmap sync workflow
Create ideas, add version tagsRoadmap sync workflowManual dispatch
Email voters that their idea shippedCannyWhen 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.0 on 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, or under review for 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.

InputMeaning
dry_runDefault on. Reports what would change and writes nothing.
update_detailsFor 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_votersDefault off. Whether moving an idea to complete emails its voters. Leave it off if the changelog entry is going to notify them anyway.
author_idOnly 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 version in 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, not complete.
  • 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:

  1. Finds the release post in website/blog/ by its slug (v0.7.0 becomes v0-7-0), so the post must exist at the tagged commit. release.mjs tag guarantees that.
  2. Converts MDX to Markdown: drops import lines 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 absolute https://dotmarc.app/... links, and tolerates CRLF line endings.
  3. 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.
  4. Finds ideas to link: every idea on the board that is tagged v0.7.0 and has status complete. 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.
  5. Creates the entry as published, dated with the release post's filename date, with the matching ideas passed as postIDs. Notifications default on (NOTIFY=false turns 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​

  1. Settle the ideas. For everything shipping in this release, make sure it carries the version tag and is complete in canny-roadmap.json before 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 its version in the JSON and run the sync), or it will simply be skipped and logged.
  2. Prepare. node scripts/release.mjs prepare 0.7.0 bumps Directory.Build.props and creates the release post. The post gets a date with 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.
  3. 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.
  4. Push. git push origin main v0.7.0. Everything from here is automatic.
  5. Check the run. In the canny-changelog job log, look for one Linking "..." line per shipped idea and a final Published entry ... linkedIdeas=N line. A Not 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_KEY repository secret (Canny settings, API) is used by all three workflows.
  • Both scripts identify the board by its widget token, which must match BOARD_TOKEN in website/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 mocked fetch rather 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 title in canny-roadmap.json too, 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 complete in 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.