$ cat article/release-notes-pipeline.md

Building a Release Notes Pipeline with Hype

# @ Cory LaNou ~ 6 min
#tutorial #release-notes #ci-cd #pipeline #hype

Building a Release Notes Pipeline with Hype

Release notes are the first thing users read after updating. Bad release notes — outdated examples, broken migration commands, incorrect API signatures — erode trust and generate support tickets. Hype lets you build release notes where every code example compiles, every command runs, and every API reference reflects the actual release.

Why Release Notes Break

Traditional release notes are written in a Markdown file, usually by copying code snippets from the diff, manually running commands to verify output, and hoping someone catches mistakes in review. This breaks because:

  • Code examples are copy-pasted — they're not validated against the actual build
  • Migration commands are written from memory — they might work on the author's machine but not fresh installs
  • API signatures are typed by hand — typos and outdated signatures slip through
  • Version numbers are hardcoded — someone always forgets to update one

Project Structure

Here's how to structure release notes as a Hype project:

releases/
├── v1.5.0/
│   ├── hype.md                    # Release notes source
│   ├── src/
│   │   ├── new-feature/main.go   # New feature example
│   │   ├── migration/main.go      # Migration example
│   │   └── breaking/main.go       # Breaking change example
│   └── scripts/
│       ├── migrate.sh             # Migration script
│       └── verify.sh              # Verification script
├── v1.4.0/
│   └── ...
└── template/
    ├── hype.md                    # Release notes template
    └── shared/
        └── footer.md             # Standard footer

Each release gets its own directory with real, compilable code examples and runnable scripts.

Writing Release Notes with Hype

New Feature: Executable Examples

When documenting a new feature, include a working example that demonstrates it:

// src/new-feature/main.go
package main

import (
    "fmt"
    "yourpackage/v2/config"
)

func main() {
    // snippet: basic-usage
    cfg, err := config.Load("app.yaml",
        config.WithDefaults(),
        config.WithEnvOverrides("APP_"),
    )
    if err != nil {
        fmt.Printf("Error: %v  
", err)
        return
    }
    fmt.Printf("Loaded %d settings  
", cfg.Len())
    // snippet: basic-usage
}

Reference it in your release notes:

## New: Configuration Loading API

The new `config.Load` function supports option chaining
for cleaner configuration setup:

<code src="src/new-feature/main.go" snippet="basic-usage"></code>

Running this with a sample config file:

<go src="src/new-feature" run="."></go>

When you build these release notes, Hype compiles the example against your actual package and runs it. If the API changed since you wrote the notes, the build fails.

Migration Guides: Validated Step by Step

Migration instructions are the most critical part of release notes. A wrong command during migration can cause data loss. Validate every step:

#!/bin/bash
# src/scripts/migrate.sh

echo "=== Migration from v1.4 to v1.5 ==="

# Step 1: Update the dependency
echo "Step 1: Updating dependency..."
go get yourpackage/v2@v1.5.0

# Step 2: Run the migration tool
echo "Step 2: Running schema migration..."
go run ./cmd/migrate --from=v1.4 --to=v1.5 --dry-run

# Step 3: Verify
echo "Step 3: Verifying..."
go run ./cmd/verify
## Migration Guide

Follow these steps to migrate from v1.4 to v1.5:

<code src="scripts/migrate.sh"></code>

Expected output on a successful migration:

<cmd exec="bash scripts/migrate.sh"></cmd>

If you see errors at Step 2, check the
[troubleshooting guide](/docs/troubleshooting/).

The migration script runs in CI. If the commands don't work against the current release, you find out before users do.

Breaking Changes: Show the Compiler Error

When documenting breaking changes, show users exactly what they'll see and how to fix it:

// src/breaking/old/main.go
package main

import "yourpackage/v2/auth"

// snippet: old-way
func authenticate(token string) bool {
    // This no longer compiles in v1.5
    return auth.Verify(token)
}
// snippet: old-way
// src/breaking/new/main.go
package main

import (
    "context"
    "yourpackage/v2/auth"
)

// snippet: new-way
func authenticate(ctx context.Context, token string) (*auth.Claims, error) {
    return auth.Verify(ctx, token)
}
// snippet: new-way
## Breaking Changes

### `auth.Verify` Now Requires Context

**Before (v1.4):**

<code src="src/breaking/old/main.go" snippet="old-way"></code>

Compiling old code produces:

<go src="src/breaking/old" run="." exit="1"></go>

**After (v1.5):**

<code src="src/breaking/new/main.go" snippet="new-way"></code>

The exit="1" attribute tells Hype the old code is expected to fail. Users see the exact compiler error they'll encounter, making it easy to find and fix their code.

Version Information: Dynamic, Not Hardcoded

Use command execution to include version information dynamically:

## Version Information

<cmd exec="go run ./cmd/tool version"
     replace-1="v\d+\.\d+\.\d+"
     replace-1-with="v1.5.0"></cmd>

Built with:

<cmd exec="go version"
     replace-1="go\d+\.\d+\.\d+"
     replace-1-with="go1.22.0"></cmd>

The replace attributes stabilize dynamic output. The actual command runs (validating it works), but version strings are normalized so the output doesn't change with every Go patch release.

Automating the Pipeline

CI Workflow for Release Notes

Validate release notes as part of your release process:

name: Validate Release Notes
on:
  push:
    paths:
      - 'releases/**'
  pull_request:
    paths:
      - 'releases/**'

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version-file: go.mod
      - run: go install github.com/gopherguides/hype/cmd/hype@main

      - name: Validate all release notes
        run: |
          for release in releases/*/hype.md; do
            dir=$(dirname "$release")
            echo "Validating ${dir}..."
            cd "$dir"
            hype export -format markdown -f hype.md > /dev/null
            cd -
          done

      - name: Build latest release notes
        run: |
          latest=$(ls -d releases/v* | sort -V | tail -1)
          cd "$latest"
          hype export -format html -f hype.md > release-notes.html

      - uses: actions/upload-artifact@v4
        with:
          name: release-notes
          path: releases/*/release-notes.html

Generate Multiple Formats

Produce release notes in every format your users need:

#!/bin/bash
# scripts/build-release-notes.sh

VERSION="${1:?Usage: build-release-notes.sh <version>}"
DIR="releases/${VERSION}"

if [ ! -f "${DIR}/hype.md" ]; then
    echo "No release notes found for ${VERSION}"
    exit 1
fi

cd "$DIR"

# HTML for the website
hype export -format html -f hype.md > release-notes.html

# Markdown for GitHub release
hype export -format markdown -f hype.md > RELEASE.md

echo "Built release notes for ${VERSION}"

Integrate with GitHub Releases

Automate GitHub release creation with validated release notes:

name: Create Release
on:
  push:
    tags:
      - 'v*'

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version-file: go.mod
      - run: go install github.com/gopherguides/hype/cmd/hype@main

      - name: Build release notes
        run: |
          VERSION=${GITHUB_REF_NAME}
          cd "releases/${VERSION}"
          hype export -format markdown -f hype.md > RELEASE.md

      - name: Create GitHub Release
        uses: softprops/action-gh-release@v1
        with:
          body_path: releases/${{ github.ref_name }}/RELEASE.md

Push a tag, the workflow builds the release notes (validating all examples), and creates the GitHub release with the generated Markdown.

Release Notes Template

Start each release with a template that ensures consistency:


# Release vX.Y.Z

Released: [DATE]

## Highlights

[2-3 sentence summary of the most impactful changes]

## New Features

### [Feature Name]

[Description]

<code src="src/feature/main.go" snippet="example"></code>

## Improvements

- [Improvement 1]
- [Improvement 2]

## Breaking Changes

### [Change Description]

**Before:**

<code src="src/breaking/old.go" snippet="old"></code>

**After:**

<code src="src/breaking/new.go" snippet="new"></code>

## Migration Guide

<code src="scripts/migrate.sh"></code>

## Bug Fixes

- [Fix 1]
- [Fix 2]

<include src="../shared/footer.md"></include>

Key Takeaways

  • Every example compiles — code in release notes is validated against the actual release
  • Migration commands run — scripts are executed in CI, not just documented
  • Breaking changes show real errors — users see exact compiler messages they'll encounter
  • Dynamic version info — command output is captured live, not typed by hand
  • Automated pipeline — tag a release and get validated, multi-format release notes automatically
  • Templates ensure consistency — every release follows the same structure

Get Started

brew install gopherguides/tap/hype

mkdir -p releases/v1.0.0/src/feature

Start with your next release. Write one feature example as real code, include it in the notes, and build. Once you see a broken example caught at build time, you'll never go back to hand-written release notes.