Skip to content

Draft release notes

Draft release notes #3

# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
# Generates release notes for an OSMO version using Claude Code, then opens
# (or updates) a PR adding releases/X.Y.Z.md on main.
#
# Inputs come from git history between the previous release tag and the
# current X.Y.Z tag (or main HEAD if the tag does not yet exist). The system
# prompt is .github/release-notes-template.md.
name: Draft release notes
on:
workflow_dispatch:
inputs:
version:
description: 'Release version (e.g. 6.3.0)'
required: true
model:
description: 'LLM model name on NVIDIA gateway'
default: 'aws/anthropic/claude-opus-4-7'
permissions:
contents: write
pull-requests: write
concurrency:
# Serialize per-version so re-running for the same version replaces the
# in-flight PR rather than racing.
group: draft-release-notes-${{ inputs.version }}
cancel-in-progress: true
jobs:
draft:
environment: testbot-generate
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
fetch-depth: 0
token: ${{ secrets.SVC_OSMO_CI_TOKEN }}
- name: Configure git
run: |
git config user.name "svc-osmo-ci"
git config user.email "svc-osmo-ci@users.noreply.github.com"
- name: Resolve tag range
id: tags
env:
VERSION: ${{ inputs.version }}
run: |
set -euo pipefail
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::version must be MAJOR.MINOR.PATCH (got: $VERSION)"
exit 1
fi
# Largest semver tag strictly less than VERSION wins. sort -V handles
# the ordering; awk filters out the requested version and any non-
# semver tags.
prev_tag=$(git tag --list '[0-9]*.[0-9]*.[0-9]*' \
| grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' \
| awk -v v="$VERSION" '$0 != v' \
| sort -V \
| awk -v v="$VERSION" '
{
split($0, a, ".");
split(v, b, ".");
if (a[1] < b[1] ||
(a[1] == b[1] && a[2] < b[2]) ||
(a[1] == b[1] && a[2] == b[2] && a[3] < b[3])) {
print $0;
}
}
' \
| tail -n1)
if [[ -z "$prev_tag" ]]; then
echo "::error::no previous semver tag less than $VERSION"
exit 1
fi
echo "prev_tag=$prev_tag"
# Upper bound: prefer the X.Y.Z tag if it exists (set by the GitLab
# nominate pipeline). Otherwise use main HEAD, but only if main's
# version.yaml matches the requested version.
if git rev-parse --verify --quiet "refs/tags/$VERSION" >/dev/null; then
current_ref="refs/tags/$VERSION"
echo "Using tag $VERSION as upper bound"
else
file_version=$(yq -r '"\(.major).\(.minor).\(.revision)"' \
src/lib/utils/version.yaml 2>/dev/null || echo "")
if [[ "$file_version" != "$VERSION" ]]; then
echo "::error::tag $VERSION does not exist and version.yaml on main is '$file_version' (need '$VERSION'). Bump version on main first or wait for nominate."
exit 1
fi
current_ref="HEAD"
echo "Tag $VERSION not found; using main HEAD (version.yaml matches)"
fi
{
echo "prev_tag=$prev_tag"
echo "current_ref=$current_ref"
} >> "$GITHUB_OUTPUT"
- name: Build input bundle
id: bundle
env:
PREV_TAG: ${{ steps.tags.outputs.prev_tag }}
CURRENT_REF: ${{ steps.tags.outputs.current_ref }}
run: |
set -euo pipefail
bundle="$RUNNER_TEMP/release-notes-input.md"
{
echo "## Range"
echo
echo "From: \`$PREV_TAG\` → To: \`$CURRENT_REF\`"
echo
echo "## git log --no-merges (with bodies)"
echo
echo '```'
git log --no-merges --pretty=format:'%h %s%n%b' "$PREV_TAG..$CURRENT_REF"
echo
echo '```'
echo
echo "## git diff --stat"
echo
echo '```'
git diff --stat "$PREV_TAG..$CURRENT_REF"
echo '```'
echo
echo "## Per-file diffs (user-facing surfaces)"
for path in \
src/cli \
src/service/core/auth \
src/service/core/workflow \
src/service/core/data \
src/service/core/app \
src/service/core/config \
src/service/core/profile \
src/utils/roles \
src/lib/data \
src/ui ; do
if git diff --quiet "$PREV_TAG..$CURRENT_REF" -- "$path" 2>/dev/null; then
continue
fi
echo
echo "### $path"
echo
echo '```diff'
git diff "$PREV_TAG..$CURRENT_REF" -- "$path" || true
echo '```'
done
} > "$bundle"
echo "bundle_path=$bundle" >> "$GITHUB_OUTPUT"
echo "::group::Input bundle (first 200 lines)"
head -200 "$bundle"
echo "::endgroup::"
- name: Generate release notes
id: generate
env:
ANTHROPIC_API_KEY: ${{ secrets.NVIDIA_NIM_KEY }}
ANTHROPIC_BASE_URL: https://inference-api.nvidia.com
ANTHROPIC_MODEL: ${{ inputs.model }}
DISABLE_PROMPT_CACHING: "1"
CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS: "1"
VERSION: ${{ inputs.version }}
BUNDLE: ${{ steps.bundle.outputs.bundle_path }}
run: |
set -euo pipefail
template=".github/release-notes-template.md"
if [[ ! -f "$template" ]]; then
echo "::error::missing $template"
exit 1
fi
out="$RUNNER_TEMP/release-notes.md"
prompt=$(cat <<EOF
$(cat "$template")
----
Generate release notes for OSMO $VERSION following the template above.
Output ONLY the markdown body of the release notes file — no
surrounding commentary, no code fences. Start with the RC notice
block (this is a pre-release variant) and end with the Getting OSMO
section verbatim from the template.
Inputs from git history follow:
$(cat "$BUNDLE")
EOF
)
npx @anthropic-ai/claude-code@2.1.91 --print \
--model "$ANTHROPIC_MODEL" \
--allowedTools "Read,Glob,Grep" \
--max-turns 20 \
"$prompt" > "$out"
if [[ ! -s "$out" ]]; then
echo "::error::Claude returned empty output"
exit 1
fi
mkdir -p releases
cp "$out" "releases/${VERSION}.md"
echo "wrote releases/${VERSION}.md ($(wc -c < "releases/${VERSION}.md") bytes)"
echo "::group::Generated release notes"
cat "releases/${VERSION}.md"
echo "::endgroup::"
- name: Open or update PR
env:
GH_TOKEN: ${{ secrets.SVC_OSMO_CI_TOKEN }}
VERSION: ${{ inputs.version }}
run: |
set -euo pipefail
branch="release-notes/${VERSION}"
title="release notes: ${VERSION}"
git checkout -B "$branch"
git add "releases/${VERSION}.md"
if git diff --cached --quiet; then
echo "no changes to commit; release notes already up to date"
exit 0
fi
git commit -m "$title"
git push --force-with-lease origin "$branch"
if existing=$(gh pr list --head "$branch" --state open --json number --jq '.[0].number' 2>/dev/null) && [[ -n "$existing" ]]; then
echo "Updated existing PR #$existing on branch $branch"
gh pr comment "$existing" --body "Refreshed by workflow run ${{ github.run_id }}."
else
gh pr create \
--title "$title" \
--body "Release notes for OSMO ${VERSION}, generated by the draft-release-notes workflow. Review the content for accuracy before merging." \
--base main \
--head "$branch"
fi