# How we generate release notes from merged PRs

The release-notes agent we run on Kortix — connected to GitHub. On each release it reads the merged PRs since the last one, groups them, writes the notes, and opens a changelog PR.

Canonical page: https://kortix.com/use-cases/release-notes

Release notes are the first thing that gets skipped under a deadline. The
information is all there in the merged PRs, but turning it into something a reader
can follow — grouped by area, in plain language, with the noise dropped — is
manual work that lands after the release is already out.

We run a release-notes agent on Kortix that does it from the PR history. On each
release, the agent reads every PR merged since the last release, groups them by
area, writes human-readable notes, and opens a PR to the changelog for review.
This write-up covers how the setup works: the trigger, the session model, and the
guardrails.

- **Team:** Kortix
- **Runs on:** Every tag or release
- **Connected systems:** GitHub
- **Mode:** Trigger-driven · changelog PR opened for review

## The problem

Writing release notes means reading back through everything that merged since the
last release, deciding what a reader cares about, and phrasing it for someone who
wasn't in the PRs. It's the kind of task that's easy to defer, so the changelog
falls behind or gets a one-line summary that helps no one.

The common fixes are incomplete. An auto-generated list of PR titles is accurate
but unreadable — internal wording, no grouping, every dependency bump included.
Writing them by hand is better but slow, and it competes with actually shipping.
Either way the notes arrive after the release, if they arrive at all.

## What we built

On Kortix, each release triggers an agent. When a tag is pushed, the release
spawns an isolated session (a cloud sandbox) with scoped access to the repository.
The agent finds the previous release, reads every PR merged in between, groups
them by area, writes notes in plain language, and opens a PR to the changelog. A
human reviews and merges.

## How it works

### Connect the release as the trigger

A signed GitHub webhook points at the project. A pushed tag or published release
fires it, and each firing spawns a fresh **session** in its own sandbox, seeded
with the new tag. One release maps to one session on one disposable machine, so
nothing carries over between releases.

### Give the agent the notes playbook

How we write release notes lives as **skills** and **memory** that travel with the
agent: how to group changes by area, which labels mark internal-only work to drop,
the voice the changelog is written in, and the format the file expects. When we
adjust how a section reads, we write it down and the agent follows it on the next
release.

### Connect the systems the notes need

Through scoped **connectors**, brokered server-side so no raw token reaches the
model, the agent can:

- **Find the range** — locate the previous release tag and the commit range up to
the new one.
- **Read the merged PRs** — titles, descriptions, labels, and authors for every PR
merged in that range, to understand what actually changed.
- **Group and write** — cluster the PRs by area and write plain-language notes,
dropping internal churn and dependency noise.
- **Open a changelog PR on GitHub** — the drafted notes land as a pull request
against the changelog file.

### Set the guardrails

The agent's output is a **PR against the changelog**, nothing more — it does not
publish, tag, or announce. A human reviews the wording and merges. Credentials are
encrypted in the secrets manager and injected at runtime, scoped to the agents you grant them to
or written to logs.

### Let each release draft its own notes

With that in place, a pushed tag reads its own history: the agent finds the range,
reads the merged PRs, groups them by area, writes the notes, and opens a changelog
PR — grouped, readable, with internal churn dropped. The team reviews the wording
instead of assembling the notes from scratch.

> **The pattern**
> A **trigger** on every release spawns a session with a scoped **connector** into
> the repo. The notes playbook is encoded as **skills** and **memory**. The agent
> drafts a changelog PR and a human owns the wording and the merge.

## Guardrails

The agent reads the repository and writes a draft, so the access is scoped and
contained:

- **Isolation.** Every release runs in its own isolated sandbox. The session reads
  the PR history and drafts the notes; only the changelog branch is written back out.
- **Scoped secrets.** The GitHub credentials are encrypted in the secrets manager
  and injected into the sandbox at runtime, scoped to the agents you grant them to.
- **PR-gated.** The agent opens a pull request against the changelog and stops. It
  never publishes or announces the release; a human reviews the wording and merges.
- **Everything is code.** The agent's configuration, skills, and permissions are
  files in the repo, versioned and changed through a reviewed **change request**
  rather than a dashboard setting.

## The outcome

- **Every release:** Notes drafted from the PR history automatically
- **Grouped:** Changes clustered by area with internal churn dropped
- **Human review:** The agent drafts; the team owns the wording

The changelog stops falling behind, and the notes that reach review are already
grouped and readable instead of a raw list of PR titles. Reviewers edit wording
rather than reconstructing what shipped from the git history.
