mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-19 10:33:29 +00:00
fix(release): keep changelog synthesis model-led
This commit is contained in:
parent
9622f89fe6
commit
9e17122713
6 changed files with 212 additions and 83 deletions
|
|
@ -4,8 +4,9 @@
|
|||
#
|
||||
# Engine: headless Claude Code (`claude -p`, uses your Claude subscription —
|
||||
# no API key), with Codex CLI (`codex exec`, OpenAI subscription) as fallback.
|
||||
# The agent runs read-only git/gh commands itself instead of being fed
|
||||
# pre-chewed diff fragments, so nothing user-visible is missed by grep luck.
|
||||
# Independent research, drafting, and review passes use read-only repository
|
||||
# tools. The models decide what matters from the evidence; the shell only owns
|
||||
# the comparison range and the public release-note shape.
|
||||
#
|
||||
# Usage: ./scripts/generate-release-notes.sh <version> [comparison-tag]
|
||||
# ./scripts/generate-release-notes.sh --resolve-base <version>
|
||||
|
|
@ -16,7 +17,8 @@
|
|||
#
|
||||
# Env overrides:
|
||||
# RELEASE_NOTES_ENGINE=claude|codex force an engine (default: claude, codex fallback)
|
||||
# RELEASE_NOTES_MODEL=<model> model for the claude engine (default: sonnet)
|
||||
# RELEASE_NOTES_MODEL=<model> model for the claude engine (default: opus)
|
||||
# RELEASE_NOTES_TRACE_DIR=<directory> retain each pass for inspection
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
|
|
@ -100,77 +102,44 @@ if [ "$MODE" = "resolve-base" ]; then
|
|||
fi
|
||||
|
||||
if [[ "$VERSION" == *-rc.* ]]; then
|
||||
RELEASE_RANGE_GUIDANCE="This is an RC release. Cover only the user-visible changes since the immediately preceding RC, ${PREVIOUS_TAG}. Do not repeat changes already announced in an earlier RC."
|
||||
RELEASE_RANGE_GUIDANCE="This RC covers ${PREVIOUS_TAG}..HEAD."
|
||||
else
|
||||
RELEASE_RANGE_GUIDANCE="This is a stable GA release. Cover the complete release train since the previous stable release, ${PREVIOUS_TAG}. Synthesize that potentially large commit range into a few user-relevant themes; do not concatenate RC notes or enumerate every commit."
|
||||
RELEASE_RANGE_GUIDANCE="This stable release covers ${PREVIOUS_TAG}..HEAD. The same-version RC packets under docs/releases/ are available evidence."
|
||||
fi
|
||||
|
||||
echo "Generating release notes for v${VERSION} (changes since ${PREVIOUS_TAG})..." >&2
|
||||
|
||||
read -r -d '' PROMPT <<EOF || true
|
||||
You are generating the release notes for Pulse v${VERSION}.
|
||||
read -r -d '' RESEARCH_PROMPT <<EOF || true
|
||||
Create a comprehensive factual account of the user-observable differences in
|
||||
Pulse between ${PREVIOUS_TAG} and the current HEAD.
|
||||
|
||||
Pulse is a self-hosted monitoring dashboard for Proxmox VE, PBS, Docker, and
|
||||
Kubernetes, used mostly by homelab and small-ops users.
|
||||
|
||||
The repo is checked out at the commit that will become v${VERSION}. The
|
||||
previous release tag is ${PREVIOUS_TAG}. Investigate the changes yourself —
|
||||
start with \`git log ${PREVIOUS_TAG}..HEAD --oneline\` and
|
||||
\`git diff ${PREVIOUS_TAG}..HEAD --stat\`, then use targeted \`git diff\` /
|
||||
\`git show\` and read source files where the diff alone is unclear. For
|
||||
commits that reference GitHub issues (#1234), you may use \`gh issue view\` to
|
||||
understand the user-facing symptom. Only describe changes that exist in the
|
||||
final code state; verify anything you are unsure about before writing it.
|
||||
|
||||
${RELEASE_RANGE_GUIDANCE}
|
||||
|
||||
Focus on USER-VISIBLE changes only: features, fixes, and behavior users will
|
||||
notice. Ignore internal refactors, test changes, CI/tooling, and docs. Group
|
||||
related commits into the outcome a user would recognize, such as a rewritten
|
||||
alert system, rather than listing the implementation steps separately.
|
||||
|
||||
Write the release notes in exactly this format:
|
||||
Use the available read-only repository and GitHub tools as you judge useful.
|
||||
Take the time needed to understand the range and final source state. Return the
|
||||
account with supporting evidence in Markdown. It has no public length or item
|
||||
limit.
|
||||
EOF
|
||||
|
||||
read -r -d '' NOTE_FORMAT <<EOF || true
|
||||
# Pulse v${VERSION} Release Notes
|
||||
|
||||
[One short paragraph explaining the customer outcome of this release. Lead
|
||||
with what feels better or works now, not how it was implemented.]
|
||||
[One short paragraph explaining the customer outcome of this release.]
|
||||
|
||||
## What's improved
|
||||
|
||||
- **[Short outcome]** — [Where users notice it and why it matters.]
|
||||
|
||||
[Use 4-6 meaningful bullets for a normal RC or minor release. A narrow patch
|
||||
may use fewer. Keep every bullet concrete and independently useful.]
|
||||
[Use a concise set of meaningful bullets. Each full bullet, including Markdown
|
||||
links, must be 260 characters or fewer.]
|
||||
|
||||
## Before you upgrade
|
||||
|
||||
[Only user-relevant compatibility, migration, signing, companion-app, or known
|
||||
risk information. Omit this section when there is nothing users need to do or
|
||||
understand before upgrading.]
|
||||
|
||||
Guidelines:
|
||||
- Plain, factual, understated. No marketing language, no emojis.
|
||||
- Omit any section that has no items.
|
||||
- Every bullet must stand on its own. A reader should understand where they
|
||||
would notice the change and what is different without knowing Pulse's
|
||||
implementation.
|
||||
- Put features and fixes together under What's improved. Describe each
|
||||
user-visible outcome exactly once; do not repeat it under another heading or
|
||||
split one outcome into separate feature and fix bullets.
|
||||
- Avoid internal release and architecture vocabulary such as canonical,
|
||||
governed, schema, provider transport, preflight, convergence, or runtime
|
||||
boundary unless that exact term is visible to the user in the product.
|
||||
- Do not use vague entries such as "improved agent handling" or "various UI
|
||||
fixes". Split unrelated changes and name the behavior that changed.
|
||||
- Do NOT write Install, Roll back, Promotion Metadata, Release Qualification,
|
||||
validation, gate, workflow, or governance sections. The release pipeline
|
||||
appends concise install and rollback instructions, while machine promotion
|
||||
records stay outside the customer changelog.
|
||||
|
||||
Your reply must be ONLY the release-notes markdown, starting with
|
||||
"# Pulse v${VERSION} Release Notes" — no preamble, no code fences, no
|
||||
commentary.
|
||||
[Only information users must act on or understand before upgrading. Omit this
|
||||
section when there is none.]
|
||||
EOF
|
||||
|
||||
# Strip accidental markdown fences and anything before the release title.
|
||||
|
|
@ -188,25 +157,30 @@ scrub_env() {
|
|||
}
|
||||
|
||||
generate_with_claude() {
|
||||
local prompt=$1
|
||||
command -v claude >/dev/null || return 1
|
||||
echo "Engine: claude (model: ${RELEASE_NOTES_MODEL:-sonnet})" >&2
|
||||
scrub_env claude -p "$PROMPT" \
|
||||
--model "${RELEASE_NOTES_MODEL:-sonnet}" \
|
||||
echo "Engine: claude (model: ${RELEASE_NOTES_MODEL:-opus})" >&2
|
||||
scrub_env claude -p "$prompt" \
|
||||
--model "${RELEASE_NOTES_MODEL:-opus}" \
|
||||
--allowedTools \
|
||||
"Bash(git log:*)" "Bash(git diff:*)" "Bash(git show:*)" \
|
||||
"Bash(git describe:*)" "Bash(git tag:*)" "Bash(git rev-parse:*)" \
|
||||
"Bash(git rev-list:*)" "Bash(git merge-base:*)" \
|
||||
"Bash(git grep:*)" "Bash(git ls-tree:*)" "Bash(git blame:*)" \
|
||||
"Bash(git status:*)" \
|
||||
"Bash(gh issue view:*)" "Bash(gh pr view:*)" \
|
||||
"Read" "Grep" "Glob" \
|
||||
</dev/null
|
||||
}
|
||||
|
||||
generate_with_codex() {
|
||||
local prompt=$1
|
||||
command -v codex >/dev/null || return 1
|
||||
echo "Engine: codex" >&2
|
||||
local out
|
||||
out=$(mktemp)
|
||||
# Session log goes to stderr; only the agent's final message is kept.
|
||||
if ! scrub_env codex exec --sandbox read-only -o "$out" "$PROMPT" >&2 </dev/null; then
|
||||
if ! scrub_env codex exec --sandbox read-only -o "$out" "$prompt" >&2 </dev/null; then
|
||||
rm -f "$out"
|
||||
return 1
|
||||
fi
|
||||
|
|
@ -214,24 +188,62 @@ generate_with_codex() {
|
|||
rm -f "$out"
|
||||
}
|
||||
|
||||
RELEASE_NOTES=""
|
||||
case "${RELEASE_NOTES_ENGINE:-claude}" in
|
||||
codex)
|
||||
RELEASE_NOTES=$(generate_with_codex) || {
|
||||
echo "Codex generation failed" >&2
|
||||
exit 1
|
||||
}
|
||||
;;
|
||||
*)
|
||||
if ! RELEASE_NOTES=$(generate_with_claude); then
|
||||
echo "Claude generation failed, trying Codex fallback..." >&2
|
||||
RELEASE_NOTES=$(generate_with_codex) || {
|
||||
echo "Both engines failed (need 'claude' or 'codex' CLI, logged in)" >&2
|
||||
exit 1
|
||||
generate_notes() {
|
||||
local prompt=$1
|
||||
case "${RELEASE_NOTES_ENGINE:-claude}" in
|
||||
codex)
|
||||
generate_with_codex "$prompt" || {
|
||||
echo "Codex generation failed" >&2
|
||||
return 1
|
||||
}
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
*)
|
||||
if ! generate_with_claude "$prompt"; then
|
||||
echo "Claude generation failed, trying Codex fallback..." >&2
|
||||
generate_with_codex "$prompt" || {
|
||||
echo "Both engines failed (need 'claude' or 'codex' CLI, logged in)" >&2
|
||||
return 1
|
||||
}
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
save_trace() {
|
||||
local name=$1
|
||||
local content=$2
|
||||
[ -n "${RELEASE_NOTES_TRACE_DIR:-}" ] || return 0
|
||||
mkdir -p "$RELEASE_NOTES_TRACE_DIR"
|
||||
printf '%s\n' "$content" > "$RELEASE_NOTES_TRACE_DIR/$name"
|
||||
}
|
||||
|
||||
echo "Researching the complete release range..." >&2
|
||||
RESEARCH_BRIEF=$(generate_notes "$RESEARCH_PROMPT") || exit 1
|
||||
|
||||
if [ -z "$RESEARCH_BRIEF" ]; then
|
||||
echo "Error: release research returned an empty brief" >&2
|
||||
exit 1
|
||||
fi
|
||||
save_trace research.md "$RESEARCH_BRIEF"
|
||||
|
||||
read -r -d '' DRAFT_PROMPT <<EOF || true
|
||||
Write the most useful customer release notes for Pulse v${VERSION}.
|
||||
|
||||
${RELEASE_RANGE_GUIDANCE}
|
||||
|
||||
Use your judgment to select and explain what matters most to users. The
|
||||
read-only repository and GitHub tools are available as you judge useful.
|
||||
|
||||
Use exactly this public shape:
|
||||
|
||||
${NOTE_FORMAT}
|
||||
|
||||
Reply only with the release-notes Markdown beginning exactly
|
||||
"# Pulse v${VERSION} Release Notes".
|
||||
EOF
|
||||
|
||||
echo "Drafting an independent customer release story..." >&2
|
||||
RELEASE_NOTES=$(generate_notes "$DRAFT_PROMPT") || exit 1
|
||||
|
||||
RELEASE_NOTES=$(printf '%s\n' "$RELEASE_NOTES" | clean_notes)
|
||||
|
||||
|
|
@ -239,6 +251,67 @@ if [ -z "$RELEASE_NOTES" ]; then
|
|||
echo "Error: release notes generation returned no canonical release title" >&2
|
||||
exit 1
|
||||
fi
|
||||
save_trace independent-draft.md "$RELEASE_NOTES"
|
||||
|
||||
echo "Running an independent improvement pass..." >&2
|
||||
read -r -d '' REVIEW_PROMPT <<EOF || true
|
||||
Independently improve the Pulse v${VERSION} customer release notes below.
|
||||
|
||||
The comparison range is ${PREVIOUS_TAG}..HEAD. Use the available read-only
|
||||
repository and GitHub tools as you judge useful, along with the internal brief.
|
||||
Decide whether the draft selected, weighted, and explained the changes in the
|
||||
way most useful to users, and make any changes you judge warranted.
|
||||
|
||||
Use this public shape:
|
||||
|
||||
${NOTE_FORMAT}
|
||||
|
||||
Reply only with the complete corrected Markdown beginning exactly
|
||||
"# Pulse v${VERSION} Release Notes".
|
||||
|
||||
Factual account from a separate investigation:
|
||||
|
||||
${RESEARCH_BRIEF}
|
||||
|
||||
Candidate release notes:
|
||||
|
||||
${RELEASE_NOTES}
|
||||
EOF
|
||||
RELEASE_NOTES=$(generate_notes "$REVIEW_PROMPT") || exit 1
|
||||
RELEASE_NOTES=$(printf '%s\n' "$RELEASE_NOTES" | clean_notes)
|
||||
save_trace reviewed-notes.md "$RELEASE_NOTES"
|
||||
|
||||
validate_notes() {
|
||||
printf '%s\n' "$1" | python3 scripts/release_control/render_release_body.py \
|
||||
--version "$VERSION" \
|
||||
--validate-notes-file /dev/stdin
|
||||
}
|
||||
|
||||
if ! VALIDATION_ERROR=$(validate_notes "$RELEASE_NOTES" 2>&1); then
|
||||
echo "Generated notes failed release-body validation; requesting one constrained revision..." >&2
|
||||
echo "$VALIDATION_ERROR" >&2
|
||||
read -r -d '' REPAIR_PROMPT <<EOF || true
|
||||
Correct the Pulse v${VERSION} release notes below so they pass this exact
|
||||
validation error while preserving their meaning:
|
||||
|
||||
${VALIDATION_ERROR}
|
||||
|
||||
Requirements:
|
||||
- Output only Markdown beginning with # Pulse v${VERSION} Release Notes.
|
||||
- Preserve the factual meaning selected by the independent review.
|
||||
- Satisfy the reported format constraint.
|
||||
|
||||
Release notes to repair:
|
||||
|
||||
${RELEASE_NOTES}
|
||||
EOF
|
||||
RELEASE_NOTES=$(generate_notes "$REPAIR_PROMPT") || exit 1
|
||||
RELEASE_NOTES=$(printf '%s\n' "$RELEASE_NOTES" | clean_notes)
|
||||
if ! validate_notes "$RELEASE_NOTES"; then
|
||||
echo "Error: revised release notes still fail release-body validation" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Notes to stdout only — callers capture this.
|
||||
printf '%s\n' "$RELEASE_NOTES"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue