Release Automation
How a Release Candidate flows from dev to production, covering versioning, release notes, the Linear and QA handover, and the tags, GitHub Releases, and dashboard release notes it produces.
Referenta release notes are generated from the GitHub release process, not from manually edited dashboard content.
Overview
The pipeline runs from dev to main. Feature branches merge into dev,
which is staging: anything on dev is a shippable Release Candidate. The
single pull request from dev into main is that Release Candidate, and
merging it ships production. Every version number, its release notes, and its
QA state are driven from that one PR.
End to end:
- Run the Release Candidate workflow (Actions → Release Candidate →
Run workflow, from
dev). With no RC open, it opens one as a draft PR (dev→main) titled with the next version, which it works out from the commits ondev(see Title And Description Rules). Its body becomes the release notes. - Referentautomat drafts the notes and hands them to QA. Opening the RC
triggers it: it audits
main...dev, writes the release notes into a marked region of the body, then applies theRelease Candidateandsync-to-linearlabels. The labels mirror the RC's issues into a Linear release at the Staging stage. See Referentautomat for the bot itself. - You review and refine the notes, then QA signs off in Linear, moving
each issue through the board. Every merge into
devwhile the RC is open revises the notes for what merged since the last draft, without anyone asking. - Mark the RC ready for review. That runs the e2e suite, which skips while the RC is a draft.
- Merge into
main: the Release workflow cuts the git tag and GitHub Release, and the Linear release is completed. - The dashboard reflects it: the in-app release-notes page reads the new GitHub Release.
The rest of this page details each step.
Trigger
The tag and the GitHub Release come from .github/workflows/release.yml,
which only runs when all of the following are true:
- the event is a pull request into
main, - the pull request is closed, and
- the pull request was merged.
This means the release flow is tied to the promotion step into production rather than to everyday feature branches.
Title And Description Rules
For the release job to create a version tag, the merged PR title must match one of these formats:
Release Candidate: vX.Y.Z
RC: vX.Y.Z
Release: vX.Y.ZThe version must include the leading v (e.g. v3.3.4, not 3.3.4). A title that looks like a release (starts with Release Candidate:, RC:, or Release:) but does not match <Prefix>: vX.Y.Z fails the check loudly rather than silently producing no tag or release — so a near-miss like a missing v or missing space is caught instead of shipping unnoticed. The same guard gates the Linear sync. A title that is not release-shaped at all is ignored.
Version numbers follow SemVer (MAJOR.MINOR.PATCH): bump the minor for the
features accumulated on dev, the patch for a fix-only release. The Release
Candidate workflow computes the version from the latest vX.Y.Z tag and the
bump. With bump left on auto, it reads the commit subjects in main...dev:
any feat commit bumps the minor version, otherwise the patch.
Features in tooling scopes (feat(ci), feat(docs), feat(e2e), feat(deps),
feat(build), feat(test) and similar) do not count, since users never see
them. A major version is never inferred: pick major or type the version. An
open RC keeps its version; when a feature lands on an RC opened as a patch,
the run warns and names the version to retitle it to. Its version input sets
an exact version instead, which must be above the latest release. On an open RC it retitles the PR; see
Linear Release Sync for the stray release a retitle
leaves behind.
The workflow extracts the version directly from the title. The pull request body becomes the GitHub Release description.
Operationally, that makes the PR itself the release handoff artifact:
- the title defines the version number,
- the description defines the public release content, and
- the merge into
maintriggers the publication step.
What The Workflow Creates
When the title matches and the PR is merged into main, the workflow:
- checks out the repo with tag push permissions,
- extracts the version from the PR title,
- creates an annotated git tag on the merge commit if the tag does not already exist, and
- creates a GitHub Release whose title is the version and whose notes come from the PR body.
This keeps releases traceable to the exact merge commit and avoids manually creating production tags outside the CI flow.
Referentautomat On The RC
Referentautomat is the org-owned GitHub App that runs the
handover, in .github/workflows/referenta-bot-release.yml (the Release
Candidate workflow). On the Release Candidate PR it does four things, in this
order:
- Validates the title. Without an exact
<Prefix>: vX.Y.Ztitle, the tag, the GitHub Release, and the Linear sync all skip silently at merge, so a near-miss is reported while the PR is fresh. - Audits
main...devand posts one sticky comment listing merged PRs with no Linear attachment, PRs missing the area labels their diff would earn, migrations with no matchingsupabase/schemas/change, and commits that reacheddevwith no PR. A clean range says so explicitly. This re-runs on every push into the RC. - Drafts the release notes into a region of the PR body delimited by
<!-- referentautomat:notes -->markers, following.github/referenta-bot/skills/release-notes/SKILL.md: bilingual with German first, internal work collapsed into one catch-all line, flag-gated work omitted. It also strips the Codesmith footer, since this body ships as the public release notes. - Applies
Release Candidateandsync-to-linear, last and only once the notes exist, so the first Linear sync mirrors a body with content instead of creating a release with empty notes.
Steps 3 and 4 run automatically when the RC PR is opened, and step 3 runs
again on every push into the RC that moves past the commit the notes were
drafted for (the notes head recorded inside the region), so a feature merged
into dev while the RC is open shows up in the notes without anyone asking.
Each revision sends the writer only the PRs merged since the notes head. The
audit reports how far the notes cover dev.
The Release Candidate workflow (Actions → Run workflow) only opens the RC; it drafts nothing itself. Its inputs:
| Input | Effect |
|---|---|
bump | auto (default), patch, minor, or major: the version for a new RC. Ignored when one is open. |
version | An exact vX.Y.Z instead of the bump. Retitles an open RC. |
Commands are still available for an immediate refresh; they must start a line and come from an owner, member, or collaborator:
@referentautomat notes: refresh the notes region. It revises in place: prose already in the region is treated as authoritative and is not reworded; only missing changes are added and inaccurate lines corrected. Polish the German and English freely, then refresh without losing your edits. A hand-written body with no markers is adopted rather than overwritten.@referentautomat audit: re-run the readiness audit.
Everything the bot writes is gated on the REFERENTA_BOT_DRY_RUN repository
variable: while it is true, the bot narrates its intended writes into logs and
comments instead of performing them. That is also how you pause it mid-release.
Referentautomat handles day-to-day PRs too (area labels, Linear attachment, wrong-base warnings) and is what the Release workflow mints a token from so the tag and GitHub Release carry the bot's identity. Both are covered on its own page: Referentautomat.
Linear Release Sync
Alongside the tag automation, .github/workflows/linear-release.yml mirrors the
Release Candidate into Linear's Releases product (pipeline Referenta App,
stages Staging → Released) so QA can work from the release object.
- The Staging sync is gated on the
sync-to-linearlabel. Referentautomat applies it (withRelease Candidate) once the RC title validates and the notes region exists, so in normal operation you do not add it by hand; adding it manually still works and is the fallback if the bot is disabled. The sync then attaches every Linear issue found in the new commit range to the release for that version at the Staging stage, and re-runs on each subsequent title/body edit or push todevwhile the label is present. Without the label, nothing syncs during Staging. Issues are discovered from commit messages, branch names, and squash-merge PR references ((#123)) resolved through the Linear GitHub integration — no special commit format is required. - When the RC PR merges, the workflow runs a final sync and completes the release, landing it on the Released stage — independent of the label, so a release is always recorded on merge (the final sync creates it if the RC was never labeled during Staging). Linear release automations then move the contained issues to their final status.
If a sync or promotion run fails partway (for example between the final sync and completion), re-run the failed job from the Actions tab — steps address the release by version, so a re-run resumes where it failed. If an RC PR is retitled to a different version after syncs ran, archive the stray old release manually in Linear.
Feature-branch PRs into dev need no special handling: their squash commit's
PR reference is resolved to linked issues at sync time. Direct pushes to dev
are only attached to issues when the commit message or branch names a Linear
identifier.
The workflow can be exercised manually via workflow_dispatch (inputs:
version, dry_run, base_ref) from the Actions tab; dry_run defaults
to true for a mutation-free preview — uncheck it to perform a real sync.
The optional base_ref overrides where the commit scan starts (normally
the last release's commit) — useful to backfill a release's issue list from
a known point. It must be a commit on the dev history: a version tag will
not work, because release tags sit on main's merge commits, which are
not ancestors of dev. To backfill from a shipped version, find its
dev-side anchor locally with git merge-base origin/dev vX.Y.Z and pass the
resulting commit SHA as base_ref.
Release notes in Linear
The RC PR body is the hand-written, customer-facing release notes (see the
referenta-release-notes skill) and the single source of truth. Both jobs
mirror that body into the Linear release's notes (the action's release_notes
input, with the Codesmith footer stripped): the sync job mirrors it while
the release is at Staging — so QA sees the current description as it is
written and edited on the open RC PR — and the promote job mirrors the final
body on completion. The result is that the Linear changelog, the GitHub Release,
and the dashboard all show the same text. Mirroring only runs when the body has
content, so an empty body never clears notes already mirrored.
Because the notes are supplied explicitly, auto-generate-on-completion should be turned off in the pipeline settings (Settings → Releases → Referenta App) so Linear does not overwrite the mirrored notes with agent-generated ones.
If you ever want Linear to draft notes instead — e.g. a release with no hand-written body — generate them manually from the Changelog tab. The house-style template below is kept version-controlled for that case; paste it into the pipeline's Template field:
You are writing user-facing release notes for Referenta, a SaaS platform for political and public-sector work. The audience is end users, not developers — describe what changed for them, never how it was built. Analyze the issues included in this release and produce notes in the exact format below.
LANGUAGE: Bilingual. Write German first, then a line with only `---`, then English. The English section mirrors the German one section-for-section. Use the formal German register ("Sie").
STRUCTURE (apply within each language):
1. Lead with the release's main feature as a heading: `### <Headline feature> — <short tagline>`, followed by one framing sentence using this release's version number:
- DE: "Das Highlight von <version>: <one sentence>."
- EN: "The headline of <version>: <one sentence>."
Then bold-lead sub-bullets: "- **<capability>** — <what it does for the user>."
2. Add further `### <themed section>` headings for other notable features, grouped by product area (e.g. Assistent, Recherche, Knowledgebase, Press Monitor, Organisationsverwaltung). Use bullets, with bold lead-ins where they help.
3. Close with `### Feinschliff & Fehlerbehebungen` (DE) / `### Polish & fixes` (EN): list only user-visible fixes, and put the internal catch-all line as the LAST bullet.
RULES:
- Classify each issue. User-facing → describe concretely. Internal (refactors, security, tooling/CI, dependency bumps, tests, infra, performance with no visible effect) → do NOT itemize; fold all of it into a single final bullet, verbatim:
- DE: "Interne Architektur- und Developer-Experience-Verbesserungen."
- EN: "Internal architecture and developer-experience improvements."
- Never include ticket IDs (REF-N), PR or commit references, branch names, or internal jargon.
- The readers are policy and public-sector staff with no technical background. Describe what they notice or can now do, not how it works. Use everyday words instead of technical terms (link, not URL; search words, not query; personal data, not PII). Leave out layout details and numbers the reader does not act on.
- Keep it short: at most four one-sentence bullets per section; small improvements go into the polish section.
- Do not restate the version in section headings (only in the framing line).
- Keep it concise, professional, and benefit-first.QA Handover And Sign-Off
Once the Staging sync has run, QA works the Release Candidate entirely in Linear. The release object lists every issue in the RC, and each issue carries a QA status.
Alongside the per-issue status, Referentautomat maintains a QA checklist
issue for the release, QA: vX.Y.Z: a fixed smoke test plus concrete test
steps for every merged PR, appended as PRs land so QA's ticks survive a
refresh. The checklist joins the Linear release for the version when one
exists; otherwise it is created without one. It is the entry point for a QA
pass; the per-issue QA Approved transitions remain the sign-off. The RC PR
body links to it at the top; that link is internal and is stripped from the
GitHub Release, the Linear release notes, and the dashboard. See
Referentautomat.
The board flow is:
In Review → On Staging → QA Approved → Merged on Prod
Each transition is driven by a different mechanism:
On Stagingis set automatically when a feature PR merges intodev. A Linear Git automation (a branch rule where merging todevsets the status toOn Staging) marks the issue as landed on staging and ready to test. Merging todevalso advances the RC PR, so the Staging sync re-runs.QA Approvedis set manually by QA after testing the issue on the staging deploy. This is the only human step in the flow. If an issue fails, QA moves it back toIn Progress; the developer fixes it, re-merges todev, and the next sync returns it toOn Stagingfor re-test.Merged on Prodis set automatically when the release is completed (on merge tomain). A Linear release automation moves every issue in the completed release to its final status.
QA status lives natively in Linear, which every developer can already see, so it is deliberately not mirrored onto the GitHub PR: GitHub carries the code and the release, Linear carries the issues and their QA state.
How The Dashboard Uses Release Data
The Referenta dashboard reads release information directly from GitHub:
getGithubReleasePayloadfetches release and tag data from the repository,/release-notesrenders that data throughReleaseNotesView,- the page body is taken from the GitHub Release description, and
- the sidebar version badge uses
/api/v1/github/latest-tagtogether withuseGithubTag.
In product terms, GitHub Releases are the content source for the dashboard's release communication. Updating a GitHub Release updates what the dashboard can display.
Freshness Model
The dashboard behavior is automatic, but it is not true push-based realtime.
The current implementation uses cached fetches and revalidation:
- the release API route is revalidated on a timed cache window,
- the latest-tag API response is cached server-side, and
- the client hook stores the latest tag in session storage for the current browser session.
So release updates propagate automatically on subsequent fetches and refreshes, but not through a live websocket-style broadcast channel.
Practical Release Flow
The expected release workflow is:
- validate changes on
dev, - run the Release Candidate workflow, which opens a draft PR from
devtomaintitledRelease Candidate: vX.Y.Z; the bot then drafts the notes, - refine the notes in the PR body; merges into
devkeep them current, - mark the PR ready for review so the e2e suite runs,
- merge the PR, and
- let GitHub Actions create the tag and GitHub Release that the dashboard consumes.
Opening the PR by hand still works: title it Release Candidate: vX.Y.Z (or
RC: vX.Y.Z / Release: vX.Y.Z), and the bot drafts the notes the same way.
That gives Referenta one release source of truth and keeps versioning, release notes, and dashboard display aligned.