@releasenote/output-markdown
v0.1.17
Published
Markdown/Handlebars output plugin for releasenote
Downloads
144
Readme
releasenote
A modular release note generator that aggregates changes from GitHub and Azure DevOps, links them together, enriches with LLM-generated summaries, and renders via customizable Handlebars templates.
Quick Start
# Generate a starter config interactively
npx @releasenote/cli init
# Generate release notes
npx @releasenote/cli generateThat's it. The init wizard detects your git remote, walks you through source and LLM setup, and writes a ready-to-use .releasenote.yaml.
Minimal Example — GitHub Only
If you just want release notes from a GitHub milestone:
export GITHUB_TOKEN=ghp_...
npx @releasenote/cli generate \
--set release.name=v2.0.0 \
--set sources.0.milestone=v2.0.0Or create a .releasenote.yaml:
version: 1
release:
name: "v2.0.0"
sources:
- type: github
owner: your-org
repo: your-repo
milestone: "v2.0.0"
primary: true
token_env: GITHUB_TOKEN
output:
type: markdown
template: preset:compact
file: RELEASE_NOTES.mdexport GITHUB_TOKEN=ghp_...
npx @releasenote/cli generateInstall
npm install -g @releasenote/cliOr use directly with npx (no install needed):
npx @releasenote/cli generateHow It Works
- Fetch items from configured sources (GitHub PRs, Azure DevOps work items)
- Link items across sources using pattern matching (e.g.,
AB#1234in a PR links to ADO work item 1234) - Facet each change deterministically from source fields (e.g. a
platform), with no LLM — see Facets - Enrich with LLM-generated summaries and categories
- Render via Handlebars templates with full control over output
Configuration
Create a .releasenote.yaml in your project root:
version: 1
release:
name: "v2.0.0"
date: "2026-03-10"
sources:
- type: github
owner: your-org
repo: your-repo
milestone: "v2.0.0"
primary: true
token_env: GITHUB_TOKEN
output:
type: markdown
file: RELEASE_NOTES.mdOr run npx @releasenote/cli init to generate one interactively.
Environment Variables
Use ${VAR} or ${VAR:-default} anywhere in the config:
release:
name: "${RELEASE_NAME:-Unnamed Release}"
sources:
- type: github
owner: ${GITHUB_ORG}
repo: ${GITHUB_REPO}
compare:
base: ${COMPARE_BASE}
head: ${COMPARE_HEAD:-main}Missing variables without defaults will throw a clear error at load time.
Sources
GitHub
Fetch PRs by milestone:
sources:
- type: github
owner: your-org
repo: your-repo
milestone: "v2.0.0"
token_env: GITHUB_TOKENUse milestone: latest to automatically pick the most recent open milestone — no need to pass a name:
sources:
- type: github
owner: your-org
repo: your-repo
milestone: latest
primary: true
token_env: GITHUB_TOKENThis is ideal for CI where you always want the current milestone without parameterising.
Or by commit range (compare two refs):
sources:
- type: github
owner: your-org
repo: your-repo
compare:
base: v1.0.0 # tag, SHA, or branch
head: main # defaults to HEAD
token_env: GITHUB_TOKENThe compare source also fetches the code diff for each PR, which is included in the LLM context. Control this with:
compare:
base: v1.0.0
head: main
include_diff: false # disable diff fetching (default: true)
max_diff_lines: 500 # cap diff lines per PR (default: 500)Azure DevOps
Fetch work items by WIQL query:
sources:
- type: azure-devops
org: your-org
project: your-project
query: "SELECT [System.Id] FROM WorkItems WHERE ..."
token_env: AZURE_DEVOPS_TOKENOr fetch only items linked from other sources:
sources:
- type: azure-devops
org: your-org
project: your-project
linked: true
token_env: AZURE_DEVOPS_TOKENWhen linked: true, the pipeline extracts IDs from other sources using linking rules and fetches only those specific work items.
Linking
Link items across sources using regex patterns:
linking:
rules:
- pattern: "AB#(\\d+)" # regex with capture group
search_in: [title, body] # fields to scan (default)
links_to: externalId # field to match on target items
from: github # only scan items from this source
to: azure-devops # only match items from this sourceThis scans GitHub PR titles and bodies for AB#1234, extracts 1234, and links to the Azure DevOps work item with that external ID.
You can add multiple rules to link different sources:
linking:
rules:
- pattern: "AB#(\\d+)"
from: github
to: azure-devops
links_to: externalId
- pattern: "JIRA-(\\d+)"
from: github
to: jira
links_to: externalIdFacets
Facets deterministically derive a field (e.g. platform) for each group from its source items — no LLM involved — so you can route sections in your template without relying on the model to classify prose.
facets:
platform:
rules:
# Rules are evaluated in order; the first one matching ANY item in a
# group wins. Patterns are matched case-insensitively.
- when: { field: title, pattern: "\\bios\\b|android|mobile" }
value: "Mobile"
- when: { field: meta.areaPath, pattern: "Mobile" }
value: "Mobile"
default: "Web" # used when no rule matches (optional)Each rule's when takes a regex pattern and an optional field:
| field | Matches against |
| --- | --- |
| omitted | the item's title + body + labels joined together |
| title, body, labels, author | that top-level field |
| meta.<key> | a source-specific metadata field (e.g. meta.areaPath) |
Because rules are checked against every item in a group (not just the primary), a signal can come from a linked work item — e.g. an Azure DevOps area path — even when the PR title is silent.
The result is available in templates as {{this.facets.<name>}}, which composes with the {{eq}} helper for deterministic sectioning:
{{#each groups}}
{{#if (eq this.facets.platform "Mobile")}}
- {{this.summary}}
{{/if}}
{{/each}}Primary Source
Mark a source as primary: true to control what appears in the output. Only groups containing at least one item from a primary source are included. Other sources provide supporting context.
sources:
# Defines what's in the release
- type: github
milestone: "v2.0.0"
primary: true
# Adds PR writeups and diffs as context
- type: github
compare:
base: v1.0.0
head: main
# Linked work items as context
- type: azure-devops
linked: trueBuild Metadata
Resolve metadata about a CI build and expose it in templates as {{build.*}} — useful for stamping the exact build a release came from.
build:
type: azure-devops
org: your-org
project: your-project
definition: Your-Pipeline # optional — narrow to one pipeline by name
tag: production # required — match builds carrying this tag
token_env: AZURE_DEVOPS_TOKEN # optional (this is the default)It picks the most recent completed build carrying the given tag (optionally narrowed to a definition/pipeline by name) — so tag the build you want to ship in Azure DevOps and this finds it. Set the PAT in AZURE_DEVOPS_TOKEN (or whatever token_env names).
Available in templates:
| Variable | Description |
|---|---|
| {{build.number}} | Build number (e.g. 2.110.0+159722) |
| {{build.id}} | Numeric build ID |
| {{build.url}} | Link to the build in Azure DevOps |
| {{build.result}} | succeeded, failed, etc. |
| {{build.status}} | Build status (e.g. completed) |
| {{build.sourceBranch}} | Branch the build ran from |
| {{build.finishTime}} | When the build finished (ISO timestamp) |
LLM Enrichment
Add an llm section to enable AI-generated summaries and categorization:
OpenAI
llm:
provider: openai
model: gpt-4.1-mini
categories:
- Features
- Bug Fixes
- Breaking Changes
- MaintenanceSet OPENAI_API_KEY in your environment.
Azure OpenAI
llm:
provider: azure-openai
model: gpt-4o
endpoint: https://your-resource.openai.azure.com
deployment: your-deployment-name # defaults to model name
api_version: "2025-01-01-preview" # optionalSet AZURE_OPENAI_API_KEY in your environment (or use AZURE_OPENAI_ENDPOINT instead of endpoint in config).
Anthropic (Claude)
llm:
provider: anthropic
model: claude-sonnet-4-20250514Set ANTHROPIC_API_KEY in your environment.
Temperature
Control how deterministic the generated prose is with temperature (0–2, default 0.3):
llm:
provider: openai
model: gpt-4.1-mini
temperature: 0.1 # lower = more consistent run-to-runThis applies to the creative passes — summarization and {{#llm}} template blocks. Categorization always runs at temperature 0 regardless, so classification stays stable.
Audience
Control the tone and detail level by setting a target audience:
llm:
provider: openai
model: gpt-4.1-mini
audience: "end users" # or "developers", "stakeholders"Built-in audiences:
- end users — plain language, no technical terms, focuses on what users can do
- developers — includes API changes, new methods, configuration details
- stakeholders — business value, UX improvements, risk reduction
You can also use a custom string (e.g. "mobile app testers") and the LLM will adapt.
Custom Prompts
Override the default LLM instructions for full control:
llm:
provider: openai
model: gpt-4.1-mini
prompts:
summarize: "Write a one-line changelog entry. Use past tense. Be specific."
categorize: "Classify into: {categories}. Return only the name."
refine: "Review these summaries for consistency. Return a JSON array."Use {categories} in the categorize prompt — it's replaced with the configured category list. Use {audience_instruction} in any prompt to inject the audience-specific guidance.
Refinement Pass
After individual items are summarized and categorized, a refinement pass reviews all summaries together to:
- Ensure consistent verb tense and level of detail
- Remove any remaining jargon or implementation details
- Deduplicate entries that describe the same change differently
This happens automatically when an LLM provider is configured.
The LLM receives structured context from all linked items — PR descriptions, work item details, and code diffs — organized with clear section headers so it can distinguish between what changed, why it changed, and how it changed.
Templates
Control the output with a Handlebars template:
output:
type: markdown
template: .releasenote.template.md
file: RELEASE_NOTES.mdBuilt-in Presets
Use a preset instead of writing your own template:
output:
type: markdown
template: preset:compact
file: RELEASE_NOTES.md| Preset | Description |
|---|---|
| preset:compact | Bullet list grouped by category — clean and minimal |
| preset:detailed | Full descriptions with LLM executive summary and contributor credits |
| preset:executive | Stakeholder-focused with summary, what's new, fixes, and breaking changes |
| preset:changelog | Keep-a-changelog style — great for appending to CHANGELOG.md |
The detailed and executive presets use {{#llm}} blocks to generate executive summaries, so they work best with an LLM provider configured.
Custom Templates
Write your own Handlebars template and reference the file path:
output:
type: markdown
template: .releasenote.template.md
file: RELEASE_NOTES.md# {{release.name}}
> {{release.date}}
{{#llm}}
Write a 2-3 sentence executive summary of this release:
{{#each groups}}
- {{this.summary}}
{{/each}}
{{/llm}}
{{#each categories}}
## {{this.name}}
{{#each this.groups}}
- {{this.summary}} ({{refs}})
{{/each}}
{{/each}}Run releasenote init to generate a starter template from any preset.
Template Variables
| Variable | Description |
|---|---|
| {{release.name}} | Release name from config |
| {{release.date}} | Release date from config |
| {{#each categories}} | Iterate categorized groups |
| {{this.name}} | Category name |
| {{this.groups}} | Groups in this category |
| {{#each groups}} | Iterate all groups (flat) |
Group Fields
Inside {{#each groups}} or {{#each this.groups}}:
| Field | Description |
|---|---|
| {{this.summary}} | LLM-generated or PR title |
| {{this.title}} | Primary item title |
| {{this.body}} | Primary item description |
| {{this.category}} | Assigned category |
| {{this.facets.<name>}} | Deterministically-derived facet (see Facets) |
| {{this.context}} | Full assembled context |
| {{this.author}} | Primary item author |
| {{this.authors}} | All authors (deduplicated) |
| {{this.labels}} | Primary item labels |
| {{this.items}} | All linked items |
| {{refs}} | Formatted source links |
{{#llm}} Block
Place anywhere in the template. The content inside is rendered as a prompt, sent to the LLM, and replaced with the response:
{{#llm}}
Write a paragraph summarizing these bug fixes for end users:
{{#each categories}}
{{#if (eq this.name "Bug Fixes")}}
{{#each this.groups}}
- {{this.summary}}
{{/each}}
{{/if}}
{{/each}}
{{/llm}}CLI Reference
releasenote init
Interactive setup wizard — walks you through configuring sources, LLM, linking, and template selection. Generates .releasenote.yaml and optionally a starter template file.
releasenote generate
Options:
-c, --config <path> Config file path (default: ".releasenote.yaml")
-o, --output <path> Override output file path
--dry-run Print to stdout instead of writing
--set <key=value...> Override config values with dot notation
--artifact [path] Copy output to artifact directory
-v, --verbose Show all pipeline messages for debugging--set examples
# Override release name
releasenote generate --set release.name=v2.0.0
# Override compare base ref
releasenote generate --set sources.0.compare.base=abc123
# Multiple overrides
releasenote generate \
--set release.name=v2.0.0 \
--set release.date=2026-03-10 \
--set sources.0.primary=trueValues are auto-coerced: true/false become booleans, numeric strings become numbers.
--artifact
Copy the output to an artifact directory for CI/CD:
# Explicit path
releasenote generate --artifact ./artifacts
# Auto-detect from Azure DevOps pipeline variable
releasenote generate --artifact
# Uses BUILD_ARTIFACTSTAGINGDIRECTORYWhen running in Azure DevOps (TF_BUILD is set), --artifact automatically attaches the release notes to the build summary tab via ##vso[task.uploadsummary].
--verbose
Shows all pipeline messages including fetched items, formed groups, LLM results, and resolved config. Useful for debugging why items are missing or miscategorized.
releasenote generate --verbose
# or
releasenote generate -vAlso activatable via DEBUG=1 environment variable (useful in CI).
CI/CD Integration
GitHub Actions
Add to your workflow (e.g. .github/workflows/release-notes.yml):
name: Release Notes
on:
milestone:
types: [closed]
jobs:
generate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- name: Generate release notes
run: >
npx @releasenote/cli generate
--set "release.name=${{ github.event.milestone.title }}"
--set "release.date=$(date +%Y-%m-%d)"
--dry-run
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
# Or commit to the repo:
# - name: Generate and commit
# run: |
# npx @releasenote/cli generate \
# --set "release.name=${{ github.event.milestone.title }}"
# git config user.name "github-actions"
# git config user.email "[email protected]"
# git add RELEASE_NOTES.md
# git commit -m "docs: release notes for ${{ github.event.milestone.title }}"
# git pushTrigger on release tag
on:
push:
tags:
- "v*"
jobs:
generate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- name: Generate release notes
run: >
npx @releasenote/cli generate
--set "release.name=${GITHUB_REF_NAME}"
--set "release.date=$(date +%Y-%m-%d)"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
- name: Create GitHub Release
run: gh release create "$GITHUB_REF_NAME" --notes-file RELEASE_NOTES.md
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}Azure DevOps Pipeline
Follow these steps to add release note generation to an existing Azure DevOps build pipeline.
Step 1: Add config files to your repo
Create .releasenote.yaml in your repo root. Use environment variable references for anything that changes per build:
version: 1
release:
name: "${RELEASE_NAME:-dev}"
date: "${RELEASE_DATE}"
sources:
- type: github
owner: ${GITHUB_ORG}
repo: ${GITHUB_REPO}
milestone: "${MILESTONE_NAME}"
primary: true
token_env: GITHUB_TOKEN
output:
type: markdown
file: RELEASE_NOTES.mdOptionally add a .releasenote.template.md for custom output formatting (see Templates).
Run
npx @releasenote/cli initto generate a starter config interactively.
Step 2: Set up pipeline variables
In Azure DevOps, go to Pipelines > Your Pipeline > Edit > Variables (or use a Variable Group under Pipelines > Library).
Add the following variables:
| Variable | Value | Secret? |
|---|---|---|
| GITHUB_TOKEN | GitHub PAT with repo read access | Yes |
| OPENAI_API_KEY | OpenAI API key (only if using LLM enrichment) | Yes |
| AZURE_DEVOPS_TOKEN | ADO PAT (only if using ADO source — or use $(System.AccessToken) for same-org access) | Yes |
Any variables referenced in your .releasenote.yaml via ${VAR} (e.g. MILESTONE_NAME, GITHUB_ORG) should also be defined here or passed via --set.
Step 3: Add the pipeline steps
Add these steps to your azure-pipelines.yml after your existing build steps but before any PublishBuildArtifacts task:
# Install Node.js (skip if your pipeline already has it)
- task: NodeTool@0
displayName: "Install Node.js"
inputs:
versionSpec: "20.x"
# Generate release notes
- script: >
npx @releasenote/cli generate
--config .releasenote.yaml
--set "release.name=$(Build.BuildNumber)"
--artifact "$(Build.ArtifactStagingDirectory)"
displayName: "Generate Release Notes"
env:
GITHUB_TOKEN: $(GITHUB_TOKEN)
AZURE_DEVOPS_TOKEN: $(System.AccessToken)
OPENAI_API_KEY: $(OPENAI_API_KEY)This will:
- Generate the release notes markdown
- Write it to the artifact staging directory
- Automatically attach it to the Build Summary tab (the
##vso[task.uploadsummary]command is emitted automatically in Azure DevOps)
If you already have a PublishBuildArtifacts step that publishes from $(Build.ArtifactStagingDirectory), the release notes file will be included alongside your other artifacts. Otherwise, add one:
- task: PublishBuildArtifacts@1
displayName: "Publish Release Notes"
inputs:
pathToPublish: "$(Build.ArtifactStagingDirectory)"
artifactName: "release-notes"Step 4: Pass dynamic values
Use --set to inject build-time values without modifying the config file:
- script: >
npx @releasenote/cli generate
--config .releasenote.yaml
--set "release.name=$(Build.BuildNumber)"
--set "release.date=$(Build.SourceVersion)"
--set "sources.0.milestone=$(MilestoneName)"
--set "sources.0.compare.base=$(PreviousTag)"
--artifact "$(Build.ArtifactStagingDirectory)"
displayName: "Generate Release Notes"
env:
GITHUB_TOKEN: $(GITHUB_TOKEN)Optional: Error handling
If release note generation should not fail the build, add continueOnError:
- script: >
npx @releasenote/cli generate ...
displayName: "Generate Release Notes"
continueOnError: trueTo only generate release notes on successful builds:
- script: >
npx @releasenote/cli generate ...
displayName: "Generate Release Notes"
condition: succeeded()Full pipeline example
trigger:
- main
pool:
vmImage: "ubuntu-latest"
variables:
- name: nodeVersion
value: "20.x"
steps:
# ... your existing build steps ...
- task: NodeTool@0
displayName: "Install Node.js"
inputs:
versionSpec: "$(nodeVersion)"
- script: >
npx @releasenote/cli generate
--config .releasenote.yaml
--set "release.name=$(Build.BuildNumber)"
--artifact "$(Build.ArtifactStagingDirectory)"
displayName: "Generate Release Notes"
condition: succeeded()
continueOnError: true
env:
GITHUB_TOKEN: $(GITHUB_TOKEN)
AZURE_DEVOPS_TOKEN: $(System.AccessToken)
OPENAI_API_KEY: $(OPENAI_API_KEY)
- task: PublishBuildArtifacts@1
displayName: "Publish Artifacts"
inputs:
pathToPublish: "$(Build.ArtifactStagingDirectory)"
artifactName: "release-notes"Troubleshooting
Config file not found
✖ ERROR ─ Config file not found: .releasenote.yamlRun releasenote init to create one, or pass a path with -c:
releasenote generate -c path/to/.releasenote.yamlMissing environment variable
✖ ERROR ─ Environment variable ${GITHUB_TOKEN} is referenced in config but not setExport the variable before running, or add a default:
# In config — use a default so it doesn't fail:
token_env: ${GITHUB_TOKEN:-}Milestone not found
✖ ERROR ─ Milestone "v2.0.0" not found in your-org/your-repoCheck the milestone name matches exactly (case-sensitive). In GitHub, go to Issues > Milestones to see the exact title.
TLS warnings in corporate environments
If you see NODE_TLS_REJECT_UNAUTHORIZED warnings, the CLI suppresses them automatically. If they still appear, set NODE_OPTIONS=--no-warnings:
NODE_OPTIONS=--no-warnings releasenote generateAzure OpenAI API version errors
If you get a 400 API version not supported error, set api_version explicitly:
llm:
provider: azure-openai
api_version: "2025-01-01-preview"LLM timeout or rate limit
API calls retry automatically (up to 3 times with exponential backoff). If you're hitting rate limits consistently, reduce concurrency by splitting into smaller milestones or using --set to target fewer items.
Debugging
Use --verbose to see every pipeline step:
releasenote generate --verboseThis shows the resolved config, every fetched item, formed groups, and LLM results.
Architecture
packages/
cli/ CLI entry point and commands
core/ Pipeline, linker, config, types
source-github/ GitHub source (milestone + compare)
source-azure-devops/ Azure DevOps source (query + linked)
output-markdown/ Markdown output with Handlebars templates
llm-openai/ OpenAI and Azure OpenAI LLM provider
llm-anthropic/ Anthropic Claude LLM providerThe pipeline is pluggable — each source, output, and LLM provider is a separate package implementing a simple interface.
Environment Variables
| Variable | Used By | Description |
|---|---|---|
| GITHUB_TOKEN | source-github | GitHub personal access token |
| AZURE_DEVOPS_TOKEN | source-azure-devops | Azure DevOps PAT |
| OPENAI_API_KEY | llm (openai) | OpenAI API key |
| AZURE_OPENAI_API_KEY | llm (azure-openai) | Azure OpenAI API key |
| AZURE_OPENAI_ENDPOINT | llm (azure-openai) | Azure OpenAI resource endpoint |
| ANTHROPIC_API_KEY | llm (anthropic) | Anthropic API key |
License
MIT
