Files
boshu2__agentops/scripts/scaffold-release-notes.sh
T
Bo ff6f8569c7 feat(release): tiered release-notes + coverage gate — end the release churn (ag-nc9) (#544)
Makes release-notes scope mechanical per tier so a release can never
again under-scope its notes and burn the re-mining churn (root cause of
the v3.0.x rewrites).

- **Scope rule + tiers** in the standard (+ codex twin): notes = git
range since the previous release (a major = the whole major), with a
major/minor/hotfix table.
- **Coverage gate** in validate-release-notes.sh: maps the diff to
product areas, FAILS if a heavily-touched area (>=3 files) is
undocumented. Wired into release.yml via --since. It immediately caught
a real gap (v3.0.1 missing the Codex area — fixed here).
- **scaffold-release-notes.sh**: mines the range into a tier-correct
draft so notes start comprehensive.
- 6 new bats (14 total), shellcheck clean, codex parity green. v3.0.1
notes verified to PASS coverage over the real v2.41.1..v3.0.1 diff.

Closes-scenario: ag-nc9#tiers-coverage-gate
Bounded-context: BC5-Runtime
Evidence: scripts/validate-release-notes.sh
2026-05-25 11:23:43 -04:00

139 lines
5.7 KiB
Bash
Executable File

#!/usr/bin/env bash
# Scaffold a curated release-notes draft from the git range, in the correct
# tier shape, so notes start comprehensive instead of hand-mined from scratch.
#
# Usage: scripts/scaffold-release-notes.sh <version> [--since <prev-tag>] [--force]
# <version> release version (with or without leading "v")
# --since <prev-tag> the previous release tag; defaults to the latest tag that
# is not <version> (i.e. the prior release)
# --force overwrite an existing notes file
#
# Output: docs/releases/<today>-v<version>-notes.md, pre-populated with:
# - the tier-correct section skeleton (major adds "## Breaking Changes")
# - one "### <product area>" per area the range touched (>= threshold files),
# each seeded with the conventional-commit subjects for that area as
# "- Changed: <subject>" stubs to curate.
# The result is a DRAFT — curate the prose, fix action labels, then run
# scripts/validate-release-notes.sh to confirm it conforms.
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$REPO_ROOT"
COVERAGE_THRESHOLD="${RELEASE_NOTES_COVERAGE_THRESHOLD:-3}"
VERSION=""
SINCE=""
FORCE=0
while [[ $# -gt 0 ]]; do
case "$1" in
--since) SINCE="${2:-}"; shift 2 ;;
--force) FORCE=1; shift ;;
-*) echo "Unknown flag: $1" >&2; exit 2 ;;
*) VERSION="$1"; shift ;;
esac
done
[[ -n "$VERSION" ]] || { echo "Usage: scaffold-release-notes.sh <version> [--since <prev-tag>] [--force]" >&2; exit 2; }
VERSION="${VERSION#v}"
if [[ "$VERSION" =~ ^[0-9]+\.0\.0$ ]]; then TIER="major"
elif [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.0$ ]]; then TIER="minor"
elif [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then TIER="hotfix"
else echo "ERROR: version '${VERSION}' is not X.Y.Z" >&2; exit 1; fi
if [[ -z "$SINCE" ]]; then
SINCE="$(git tag --sort=-version:refname | grep -v "^v${VERSION}$" | head -1)"
fi
[[ -n "$SINCE" ]] || { echo "ERROR: could not determine --since tag" >&2; exit 1; }
endpoint="HEAD"
git rev-parse "v${VERSION}^{commit}" >/dev/null 2>&1 && endpoint="v${VERSION}"
OUT="docs/releases/$(date -u +%Y-%m-%d)-v${VERSION}-notes.md"
if [[ -e "$OUT" && "$FORCE" -ne 1 ]]; then
echo "ERROR: $OUT exists (use --force to overwrite)" >&2
exit 1
fi
map_path_to_area() {
case "$1" in
.codex-plugin/*|scripts/install-codex*|scripts/validate-codex*) echo "Codex and Runtime Integrations" ;;
scripts/install*|.goreleaser*|.github/workflows/release.yml|packs/*install*) echo "Install, Upgrade, and Distribution" ;;
cli/cmd/ao/*|cli/docs/COMMANDS.md) echo "CLI and Operator Commands" ;;
cli/internal/daemon/*|cli/internal/schedule/*|cli/internal/agentworker/*|cli/internal/gascity/*) echo "Daemon, Scheduling, and Factory" ;;
skills/*|skills-codex*) echo "Skills and Workflows" ;;
hooks/*|cli/embedded/hooks/*) echo "Hooks and Lifecycle" ;;
cli/internal/knowledge/*|cli/internal/harvest/*|cli/internal/pool/*|cli/internal/lifecycle/*|cli/internal/search/*) echo "Knowledge Flywheel, Search, and Memory" ;;
cli/internal/eval/*|evals/*|tests/*|.github/workflows/validate.yml) echo "Eval, Validation, and Release Gates" ;;
scripts/security*|scripts/toolchain-validate*|*sbom*) echo "Security, Privacy, and Supply Chain" ;;
README.md|docs/*|PRODUCT.md) echo "Docs and Onboarding" ;;
*) echo "" ;;
esac
}
# Tally touched files per area.
declare -A area_hits=()
while IFS= read -r f; do
[[ -n "$f" ]] || continue
a="$(map_path_to_area "$f")"
[[ -n "$a" ]] && area_hits["$a"]=$(( ${area_hits["$a"]:-0} + 1 ))
done < <(git diff --name-only "${SINCE}..${endpoint}" 2>/dev/null || true)
ncommits="$(git rev-list --count --no-merges "${SINCE}..${endpoint}" 2>/dev/null || echo "?")"
{
echo "## Highlights"
echo ""
echo "<!-- ${TIER} release. ${ncommits} commits since ${SINCE}. 2-4 sentences: theme + biggest outcomes. -->"
echo ""
echo "## Upgrade Notes"
echo ""
echo "- <required action, or \"No manual action required\">"
[[ "$TIER" == "major" ]] && echo "- See \`docs/MIGRATION-${VERSION%%.*}.0.md\` for the full migration."
echo ""
if [[ "$TIER" == "major" ]]; then
echo "## Breaking Changes"
echo ""
echo "- <each breaking change: what changed, what to use instead, migration pointer>"
echo ""
fi
echo "## At a Glance"
echo ""
echo "| Product Area | Added | Changed | Refactored | Fixed | Deprecated/Removed |"
echo "|---|---:|---:|---:|---:|---:|"
for a in "${!area_hits[@]}"; do
[[ "${area_hits[$a]}" -ge "$COVERAGE_THRESHOLD" ]] && echo "| $a | 0 | 0 | 0 | 0 | 0 |"
done
echo ""
echo "## Product Areas"
echo ""
# One section per touched area, seeded with that area's commit subjects.
for a in "${!area_hits[@]}"; do
[[ "${area_hits[$a]}" -ge "$COVERAGE_THRESHOLD" ]] || continue
echo "### $a"
echo ""
# Seed bullets from commit subjects whose changed files map to this area.
git log --no-merges --pretty='%H %s' "${SINCE}..${endpoint}" 2>/dev/null | while IFS= read -r line; do
sha="${line%% *}"; subj="${line#* }"
files="$(git diff --name-only "${sha}~1..${sha}" 2>/dev/null || true)"
while IFS= read -r f; do
[[ -n "$f" ]] || continue
if [[ "$(map_path_to_area "$f")" == "$a" ]]; then
clean="$(printf '%s' "$subj" | sed -E 's/ \(#[0-9]+\)$//; s/^[a-z]+\([a-z0-9_-]+\)!?: //')"
echo "- Changed: ${clean}"
break
fi
done <<< "$files"
done | sort -u | head -12
echo ""
done
echo "## Known Issues"
echo ""
echo "- <known risk/limitation, or \"No release-blocking known issues.\">"
echo ""
echo "[Full changelog](../CHANGELOG.md)"
} > "$OUT"
echo "Scaffolded $OUT (tier $TIER, ${ncommits} commits since ${SINCE})"
echo "Next: curate prose + action labels, then: scripts/validate-release-notes.sh v${VERSION} --since ${SINCE}"