From 5887c5e9e8f8786b0b776861705fdf36d4037afb Mon Sep 17 00:00:00 2001 From: Jennifer Hasegawa <5481259+jhaaaa@users.noreply.github.com> Date: Tue, 16 Dec 2025 18:52:09 -0800 Subject: [PATCH 1/8] first pass gen rn --- gen-rn/README.md | 107 +++++++++++ gen-rn/gen-rn.sh | 452 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 559 insertions(+) create mode 100644 gen-rn/README.md create mode 100755 gen-rn/gen-rn.sh diff --git a/gen-rn/README.md b/gen-rn/README.md new file mode 100644 index 000000000..6b427d306 --- /dev/null +++ b/gen-rn/README.md @@ -0,0 +1,107 @@ +# gen-rn: XMTP SDK release notes generator + +AI-powered release notes generation for XMTP SDK releases using git history analysis and Claude AI. + +## Purpose + +1. **Engineering sanity check**: Help engineers understand exactly what's included in a release +2. **Technical writing support**: Help technical writers with structured, well-organized release content +3. **Cross-SDK consistency**: Run in each XMTP SDK repo and compare structured outputs to gauge consistency of feature and fix implementations across SDKs + +## Prerequisites + +- Node.js installed +- Anthropic API key: Get a key at: [https://console.anthropic.com/](https://console.anthropic.com/) + +## Setup + +```bash +# Set your API key +export ANTHROPIC_API_KEY="sk-ant-your-key-here" +echo 'export ANTHROPIC_API_KEY="your-key"' >> ~/.zshrc +source ~/.zshrc + +# Fetch latest tags +git fetch --tags + +# Make script executable +chmod +x gen-rn.sh + +# Generate release notes +./gen-rn.sh 4.6.4 +``` + +## Usage + +### Interactive mode + +```bash +./gen-rn.sh +# Displays available tags and enables you to enter a tag. +# Compares against auto-detected previous tag. +``` + +### Specific release + +```bash +# Generate release notes for 4.6.4 (compares against auto-detected previous tag, likely 4.6.3) +./gen-rn.sh 4.6.4 + +# Specify both tags for comparison (show what changed from 4.6.3 to 4.6.4) +./gen-rn.sh 4.6.4 4.6.3 + +# Use latest tag (compares latest against its predecessor) +./gen-rn.sh latest +``` + +### Supported release types + +The script automatically detects release types based on tag naming: + +```bash +# Stable release (production-ready) +./gen-rn.sh 4.6.7 # → Release type: stable + +# Release candidate (pre-release testing) +./gen-rn.sh 4.6.7-rc.1 4.6.6 # → Release type: release-candidate + +# Development release (experimental) +./gen-rn.sh 4.6.7-dev.abc123 4.6.7 # → Release type: development +``` + +### Help + +```bash +./gen-rn.sh --help +``` + +## Output files + +After running, check the `output/` directory: + +```text +output/ +├── release-notes.md # Main AI-generated release notes +├── release-data.txt # Raw git analysis data (for debugging) +├── pr-refs.txt # PR numbers found in commits +└── ai-script.js # Temporary AI generation script +``` + +> [!IMPORTANT] +> Generated release notes must be reviewed by humans before publication. The AI provides a starting point, not a final product. + +## Troubleshooting + +### "Not in a git repository" + +- Run from the root of your XMTP SDK repository + +### "ANTHROPIC_API_KEY not set" + +- Set your API key: `export ANTHROPIC_API_KEY="your-key"` +- Get a key at: https://console.anthropic.com/ + +### "Could not compare tags" + +- Verify both tags exist: `git tag | grep 4.6.4` +- Use explicit tag names: `./gen-rn.sh 4.6.4 4.6.3` diff --git a/gen-rn/gen-rn.sh b/gen-rn/gen-rn.sh new file mode 100755 index 000000000..d1ab9d247 --- /dev/null +++ b/gen-rn/gen-rn.sh @@ -0,0 +1,452 @@ +#!/bin/bash + +# XMTP SDK Release Notes Generator +# Generate AI-powered release notes for XMTP SDK releases using Claude + +set -e + +# Configuration +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +OUTPUT_DIR="./output" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +print_header() { + echo -e "${BLUE}========================================${NC}" + echo -e "${BLUE}$1${NC}" + echo -e "${BLUE}========================================${NC}" +} + +print_success() { + echo -e "${GREEN}✓ $1${NC}" +} + +print_warning() { + echo -e "${YELLOW}⚠ $1${NC}" +} + +print_error() { + echo -e "${RED}✗ $1${NC}" +} + +# Function to check prerequisites +check_prerequisites() { + print_header "Checking Prerequisites" + + # Check if we're in a git repository + if ! git rev-parse --git-dir > /dev/null 2>&1; then + print_error "Not in a git repository. Please run this from an XMTP SDK repository." + exit 1 + fi + + # Check for Node.js (for the AI script) + if ! command -v node &> /dev/null; then + print_error "Node.js is required but not installed." + echo "Please install Node.js from https://nodejs.org/" + exit 1 + fi + + # Check for API key + if [ -z "$ANTHROPIC_API_KEY" ]; then + print_error "ANTHROPIC_API_KEY environment variable is not set." + echo "Please set your API key: export ANTHROPIC_API_KEY=\"your-key-here\"" + exit 1 + fi + + # Check for jq (optional but recommended) + if ! command -v jq &> /dev/null; then + print_warning "jq is recommended for better output formatting." + echo "Install with: brew install jq (macOS) or sudo apt install jq (Ubuntu)" + fi + + print_success "Prerequisites check completed" +} + +# Function to get tags and setup environment +setup_environment() { + print_header "Setting Up Environment" + + # Create output directory + mkdir -p "$OUTPUT_DIR" + + # Get available tags + echo "Available git tags:" + echo "Recent tags:" + git tag --sort=-version:refname | grep -E "^[0-9]" | head -10 + echo "" + echo "Legacy tags:" + git tag --sort=-version:refname | grep "^v" | head -5 + echo "" + + # Get current tag or prompt user + if [ "$1" != "" ]; then + TAG_NAME="$1" + else + echo "Enter the tag name to generate release notes for:" + read -r TAG_NAME + + if [ "$TAG_NAME" = "latest" ]; then + TAG_NAME=$(git tag --sort=-version:refname | head -n 1) + if [ -z "$TAG_NAME" ]; then + print_error "No tags found in repository" + exit 1 + fi + fi + fi + + # Get previous tag with smart detection (from working local-test-runner.sh) + if [ "$2" != "" ]; then + PREVIOUS_TAG="$2" + else + # Try to find the previous tag more intelligently + + # First, determine if current tag has 'v' prefix + if [[ "$TAG_NAME" == v* ]]; then + # Look for other v-prefixed tags + PREVIOUS_TAG=$(git tag --sort=-version:refname | grep "^v" | grep -v "$TAG_NAME" | head -n 1) + else + # Look for non-v-prefixed tags in the same version series + VERSION_SERIES=$(echo "$TAG_NAME" | cut -d. -f1-2) # e.g., "4.6" from "4.6.4" + + # Try to find previous tag in same series first + PREVIOUS_TAG=$(git tag | grep "^${VERSION_SERIES}\." | sort -V | grep -B1 "^${TAG_NAME}$" | head -n1) + + # If no same-series tag found, try broader search + if [ -z "$PREVIOUS_TAG" ]; then + MAJOR_VERSION=$(echo "$TAG_NAME" | cut -d. -f1) # e.g., "4" from "4.6.4" + PREVIOUS_TAG=$(git tag | grep "^${MAJOR_VERSION}\." | sort -V | grep -B1 "^${TAG_NAME}$" | head -n1) + fi + + # If still no tag, fall back to any non-v tag + if [ -z "$PREVIOUS_TAG" ]; then + PREVIOUS_TAG=$(git tag --sort=-version:refname | grep -v "^v" | grep -v "$TAG_NAME" | head -n 1) + fi + fi + + if [ -z "$PREVIOUS_TAG" ]; then + PREVIOUS_TAG=$(git rev-list --max-parents=0 HEAD) + print_warning "No previous tag found, comparing against first commit" + else + echo "Using previous tag: $PREVIOUS_TAG" + fi + fi + + # Determine release type + if [[ "$TAG_NAME" == *"rc"* ]]; then + RELEASE_TYPE="release-candidate" + elif [[ "$TAG_NAME" == *"dev"* ]]; then + RELEASE_TYPE="development" + else + RELEASE_TYPE="stable" + fi + + echo "" + print_success "Environment configured:" + echo " Repository: $(basename $(git rev-parse --show-toplevel))" + echo " Current tag: $TAG_NAME" + echo " Previous tag: $PREVIOUS_TAG" + echo " Release type: $RELEASE_TYPE" + echo " Output directory: $OUTPUT_DIR" +} + +# Function to gather release information +gather_release_info() { + print_header "Gathering Release Information" + + local info_file="$OUTPUT_DIR/release-data.txt" + + echo "=== XMTP SDK Release Analysis ===" > "$info_file" + echo "Repository: $(basename $(git rev-parse --show-toplevel))" >> "$info_file" + echo "Current tag: $TAG_NAME" >> "$info_file" + echo "Previous tag: $PREVIOUS_TAG" >> "$info_file" + echo "Release type: $RELEASE_TYPE" >> "$info_file" + echo "Generated: $(date)" >> "$info_file" + echo "" >> "$info_file" + + # Get commit messages between tags + echo "=== COMMIT MESSAGES ===" >> "$info_file" + if git rev-list "$PREVIOUS_TAG..$TAG_NAME" &>/dev/null; then + git log "$PREVIOUS_TAG..$TAG_NAME" --pretty=format:"- %s (%an, %ad)" --date=short >> "$info_file" || true + else + print_warning "Could not compare tags, using last 10 commits" + git log -10 --pretty=format:"- %s (%an, %ad)" --date=short >> "$info_file" + fi + echo -e "\n" >> "$info_file" + + # Get changed files with stats + echo "=== CHANGED FILES ===" >> "$info_file" + if git rev-list "$PREVIOUS_TAG..$TAG_NAME" &>/dev/null; then + git diff --name-status "$PREVIOUS_TAG..$TAG_NAME" >> "$info_file" 2>/dev/null || true + else + git diff --name-status HEAD~10..HEAD >> "$info_file" 2>/dev/null || true + fi + echo -e "\n" >> "$info_file" + + # Get detailed diff for key files + echo "=== KEY FILE CHANGES ===" >> "$info_file" + for file in package.json package-lock.json Cargo.toml Cargo.lock setup.py requirements.txt build.gradle app/build.gradle README.md CHANGELOG.md; do + if [ -f "$file" ]; then + if git rev-list "$PREVIOUS_TAG..$TAG_NAME" &>/dev/null; then + if git diff --name-only "$PREVIOUS_TAG..$TAG_NAME" | grep -q "^$file$"; then + echo "Changes in $file:" >> "$info_file" + git diff "$PREVIOUS_TAG..$TAG_NAME" -- "$file" >> "$info_file" 2>/dev/null || true + echo -e "\n" >> "$info_file" + fi + fi + fi + done + + # Get PR information if available + echo "=== PULL REQUEST REFERENCES ===" >> "$info_file" + if git rev-list "$PREVIOUS_TAG..$TAG_NAME" &>/dev/null; then + git log "$PREVIOUS_TAG..$TAG_NAME" --pretty=format:"%s" | grep -oE "#[0-9]+" | sort -u > "$OUTPUT_DIR/pr-refs.txt" 2>/dev/null || touch "$OUTPUT_DIR/pr-refs.txt" + else + git log -10 --pretty=format:"%s" | grep -oE "#[0-9]+" | sort -u > "$OUTPUT_DIR/pr-refs.txt" 2>/dev/null || touch "$OUTPUT_DIR/pr-refs.txt" + fi + + if [ -s "$OUTPUT_DIR/pr-refs.txt" ]; then + echo "PR numbers found in commits:" >> "$info_file" + cat "$OUTPUT_DIR/pr-refs.txt" >> "$info_file" + else + echo "No PR references found in commit messages" >> "$info_file" + fi + + local filesize=$(wc -c < "$info_file") + print_success "Release information gathered ($filesize bytes)" + echo " Data file: $info_file" + echo " PR refs: $OUTPUT_DIR/pr-refs.txt" +} + +# Function to generate AI release notes +generate_ai_release_notes() { + print_header "Generating AI Release Notes" + + local info_file="$OUTPUT_DIR/release-data.txt" + local ai_script="$OUTPUT_DIR/ai-script.js" + + # Create the AI generation script + cat > "$ai_script" << 'EOF' +const fs = require('fs'); + +async function generateReleaseNotes() { + const releaseInfo = fs.readFileSync('./output/release-data.txt', 'utf8'); + + // Truncate if too long (API limits) + const maxLength = 50000; + const truncatedInfo = releaseInfo.length > maxLength + ? releaseInfo.substring(0, maxLength) + "\n\n[Content truncated due to length...]" + : releaseInfo; + + const prompt = `Analyze the following XMTP SDK release information and generate comprehensive release notes. + +CONTEXT: +- XMTP is a messaging protocol for web3 applications +- This is for SDK releases used by developers building messaging applications +- The audience includes: engineers making releases, technical writers, and cross-SDK comparison +- Focus on developer-facing changes, new features, bug fixes, and breaking changes + +RELEASE INFORMATION: +${truncatedInfo} + +Please generate release notes that include: + +1. **Release Summary** (2-3 sentences about what this release contains) + +2. **New Features** (if any) +- List new capabilities added +- Explain developer benefits and use cases + +3. **Improvements & Enhancements** (if any) +- Performance improvements +- Developer experience enhancements +- API improvements + +4. **Bug Fixes** (if any) +- Issues resolved +- Stability improvements + +5. **Breaking Changes** (if any) +- API changes that require developer action +- Clear migration guidance + +6. **Dependencies & Infrastructure** (if any) +- Dependency updates +- Build or tooling changes +- Library version updates + +7. **Developer Notes** (if any) +- Important implementation details +- Testing recommendations +- Known limitations + +FORMAT REQUIREMENTS: +- Use clear markdown formatting +- Be specific about technical changes but explain them in plain English +- Focus on changes that matter to developers integrating this SDK +- If certain sections don't apply, omit those sections +- Include code examples for breaking changes where helpful +- Avoid overly technical git details unless relevant to SDK users`; + + try { + console.log("🤖 Generating release notes with AI..."); + console.log(`📄 Prompt length: ${prompt.length} characters`); + + const response = await fetch("https://api.anthropic.com/v1/messages", { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-api-key": process.env.ANTHROPIC_API_KEY, + "anthropic-version": "2023-06-01", + }, + body: JSON.stringify({ + model: "claude-sonnet-4-20250514", + max_tokens: 1000, + messages: [ + { role: "user", content: prompt } + ], + }) + }); + + console.log(`📡 API Response status: ${response.status}`); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`HTTP error! status: ${response.status}, body: ${errorText}`); + } + + const data = await response.json(); + const releaseNotes = data.content + .map(item => (item.type === "text" ? item.text : "")) + .filter(Boolean) + .join("\n"); + + // Save release notes + fs.writeFileSync('./output/release-notes.md', releaseNotes); + console.log("✅ Release notes generated successfully"); + + } catch (error) { + console.error('❌ Error generating release notes:', error.message); + + const fallbackNotes = `# Release Notes for ${process.env.TAG_NAME || 'Unknown Tag'} + +**⚠️ AI generation failed - please create release notes manually** + +Error: ${error.message} + +## Changes in this release +See commit history between ${process.env.PREVIOUS_TAG || 'unknown'} and ${process.env.TAG_NAME || 'unknown'} + +## Raw release information: +\`\`\` +${truncatedInfo.substring(0, 2000)} +\`\`\` + +--- +*Note: This is fallback content. Please review git history and create proper release notes.* +`; + fs.writeFileSync('./output/release-notes.md', fallbackNotes); + console.log("📁 Fallback release notes created"); + process.exit(1); + } +} + +// Set environment variables for the script +process.env.TAG_NAME = process.argv[2] || process.env.TAG_NAME; +process.env.PREVIOUS_TAG = process.argv[3] || process.env.PREVIOUS_TAG; +process.env.RELEASE_TYPE = process.argv[4] || process.env.RELEASE_TYPE; + +generateReleaseNotes(); +EOF + + # Set environment variables for the Node.js script + export TAG_NAME="$TAG_NAME" + export PREVIOUS_TAG="$PREVIOUS_TAG" + export RELEASE_TYPE="$RELEASE_TYPE" + + # Run the AI generation + if node "$ai_script" "$TAG_NAME" "$PREVIOUS_TAG" "$RELEASE_TYPE"; then + print_success "AI release notes generated successfully" + else + print_warning "AI generation failed, but fallback content was created" + fi +} + +# Function to display results +display_results() { + print_header "Release Notes Generated" + + echo "📁 Generated files in $OUTPUT_DIR/:" + ls -la "$OUTPUT_DIR/" | grep -v "^d" | awk '{print " " $9 " (" $5 " bytes)"}' + echo "" + + if [ -f "$OUTPUT_DIR/release-notes.md" ]; then + echo "📝 Release Notes Preview:" + echo "========================" + head -30 "$OUTPUT_DIR/release-notes.md" + echo "" + echo "[... view complete release notes in $OUTPUT_DIR/release-notes.md ...]" + echo "" + fi + + print_success "Release notes generation completed!" + echo "" + echo "📋 Next steps:" + echo "1. Review the release notes: $OUTPUT_DIR/release-notes.md" + echo "2. Edit/customize as needed for your release" + echo "3. Use the notes in your GitHub release or documentation" + echo "" + echo "📊 For debugging or customization:" + echo "- Raw data analyzed: $OUTPUT_DIR/release-data.txt" + echo "- PR references found: $OUTPUT_DIR/pr-refs.txt" +} + +# Main execution +main() { + echo "🚀 XMTP SDK Release Notes Generator" + echo "===================================" + echo "" + + check_prerequisites + setup_environment "$1" "$2" + gather_release_info + generate_ai_release_notes + display_results +} + +# Script usage help +if [ "$1" = "--help" ] || [ "$1" = "-h" ]; then + echo "XMTP SDK Release Notes Generator" + echo "" + echo "Generates AI-powered release notes for XMTP SDK releases using git history analysis." + echo "" + echo "Usage:" + echo " $0 [tag_name] [previous_tag]" + echo " $0 --help|-h" + echo "" + echo "Examples:" + echo " $0 # Interactive mode - will prompt for tag" + echo " $0 4.6.4 # Generate notes for v4.6.4 (auto-detect previous)" + echo " $0 4.6.4 4.6.3 # Generate notes comparing 4.6.4 to 4.6.3" + echo " $0 latest # Generate notes for most recent tag" + echo "" + echo "Requirements:" + echo " - Git repository with release tags" + echo " - Node.js installed" + echo " - ANTHROPIC_API_KEY environment variable set" + echo " - Internet connection (for AI API)" + echo "" + echo "Output:" + echo " - output/release-notes.md # Main AI-generated release notes" + echo " - output/release-data.txt # Raw git analysis data" + echo " - output/pr-refs.txt # PR references found" + echo "" + exit 0 +fi + +# Run the main function +main "$@" \ No newline at end of file From 11a41e0f19ced03e3a57cc0af77711e67bd62717 Mon Sep 17 00:00:00 2001 From: Jennifer Hasegawa <5481259+jhaaaa@users.noreply.github.com> Date: Tue, 16 Dec 2025 19:07:49 -0800 Subject: [PATCH 2/8] claude fixes claude feedback --- gen-rn/README.md | 30 ++++++++++++++++++ gen-rn/gen-rn.sh | 80 ++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 93 insertions(+), 17 deletions(-) diff --git a/gen-rn/README.md b/gen-rn/README.md index 6b427d306..cc384f508 100644 --- a/gen-rn/README.md +++ b/gen-rn/README.md @@ -13,6 +13,19 @@ AI-powered release notes generation for XMTP SDK releases using git history anal - Node.js installed - Anthropic API key: Get a key at: [https://console.anthropic.com/](https://console.anthropic.com/) +## Configuration + +The script supports the following environment variables: + +- `ANTHROPIC_API_KEY` (required): Your Anthropic API key for Claude AI +- `MAX_TOKENS` (optional): Maximum tokens for AI response (default: 4000, range: 1000-8000) + +Example with custom token limit: +```bash +export MAX_TOKENS=6000 +./gen-rn.sh 4.6.4 +``` + ## Setup ```bash @@ -75,6 +88,23 @@ The script automatically detects release types based on tag naming: ./gen-rn.sh --help ``` +## Security + +### API Key Handling +- Never commit your `ANTHROPIC_API_KEY` to version control +- Store your API key in your shell profile (e.g., `~/.zshrc`, `~/.bashrc`) +- Consider using a secrets manager for production environments +- Rotate your API key if it's accidentally exposed + +### Input Validation +The script validates all tag names to prevent command injection attacks. Tag names must contain only: +- Letters (a-z, A-Z) +- Numbers (0-9) +- Dots (.) +- Hyphens (-) +- Underscores (_) +- Forward slashes (/) + ## Output files After running, check the `output/` directory: diff --git a/gen-rn/gen-rn.sh b/gen-rn/gen-rn.sh index d1ab9d247..3330d0451 100755 --- a/gen-rn/gen-rn.sh +++ b/gen-rn/gen-rn.sh @@ -3,11 +3,12 @@ # XMTP SDK Release Notes Generator # Generate AI-powered release notes for XMTP SDK releases using Claude -set -e +set -euo pipefail # Configuration SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" OUTPUT_DIR="./output" +MAX_TOKENS="${MAX_TOKENS:-4000}" # Configurable via environment variable, default 4000 # Colors for output RED='\033[0;31m' @@ -34,6 +35,25 @@ print_error() { echo -e "${RED}✗ $1${NC}" } +# Function to validate tag name to prevent command injection +validate_tag_name() { + local tag="$1" + + # Allow alphanumeric, dots, hyphens, underscores, and forward slashes (for v-prefixed tags) + if [[ ! "$tag" =~ ^[a-zA-Z0-9._/-]+$ ]]; then + print_error "Invalid tag name: $tag" + echo "Tag names must contain only letters, numbers, dots, hyphens, underscores, and slashes" + exit 1 + fi + + # Check if tag exists in repository + if ! git rev-parse "$tag" >/dev/null 2>&1; then + print_error "Tag does not exist: $tag" + echo "Use 'git tag' to see available tags" + exit 1 + fi +} + # Function to check prerequisites check_prerequisites() { print_header "Checking Prerequisites" @@ -84,12 +104,12 @@ setup_environment() { echo "" # Get current tag or prompt user - if [ "$1" != "" ]; then + if [ "${1:-}" != "" ]; then TAG_NAME="$1" else echo "Enter the tag name to generate release notes for:" read -r TAG_NAME - + if [ "$TAG_NAME" = "latest" ]; then TAG_NAME=$(git tag --sort=-version:refname | head -n 1) if [ -z "$TAG_NAME" ]; then @@ -98,10 +118,15 @@ setup_environment() { fi fi fi + + # Validate tag name + validate_tag_name "$TAG_NAME" # Get previous tag with smart detection (from working local-test-runner.sh) - if [ "$2" != "" ]; then + if [ "${2:-}" != "" ]; then PREVIOUS_TAG="$2" + # Validate previous tag + validate_tag_name "$PREVIOUS_TAG" else # Try to find the previous tag more intelligently @@ -171,30 +196,45 @@ gather_release_info() { # Get commit messages between tags echo "=== COMMIT MESSAGES ===" >> "$info_file" if git rev-list "$PREVIOUS_TAG..$TAG_NAME" &>/dev/null; then - git log "$PREVIOUS_TAG..$TAG_NAME" --pretty=format:"- %s (%an, %ad)" --date=short >> "$info_file" || true + if ! git log "$PREVIOUS_TAG..$TAG_NAME" --pretty=format:"- %s (%an, %ad)" --date=short >> "$info_file" 2>&1; then + print_warning "Failed to get commit messages between tags" + echo "Error retrieving commit messages" >> "$info_file" + fi else print_warning "Could not compare tags, using last 10 commits" - git log -10 --pretty=format:"- %s (%an, %ad)" --date=short >> "$info_file" + if ! git log -10 --pretty=format:"- %s (%an, %ad)" --date=short >> "$info_file" 2>&1; then + print_error "Failed to get commit messages" + echo "Error retrieving commit messages" >> "$info_file" + fi fi echo -e "\n" >> "$info_file" # Get changed files with stats echo "=== CHANGED FILES ===" >> "$info_file" if git rev-list "$PREVIOUS_TAG..$TAG_NAME" &>/dev/null; then - git diff --name-status "$PREVIOUS_TAG..$TAG_NAME" >> "$info_file" 2>/dev/null || true + if ! git diff --name-status "$PREVIOUS_TAG..$TAG_NAME" >> "$info_file" 2>&1; then + print_warning "Failed to get changed files between tags" + echo "Error retrieving changed files" >> "$info_file" + fi else - git diff --name-status HEAD~10..HEAD >> "$info_file" 2>/dev/null || true + if ! git diff --name-status HEAD~10..HEAD >> "$info_file" 2>&1; then + print_warning "Failed to get changed files" + echo "Error retrieving changed files" >> "$info_file" + fi fi echo -e "\n" >> "$info_file" - # Get detailed diff for key files + # Get detailed diff for key files (Android-specific) echo "=== KEY FILE CHANGES ===" >> "$info_file" - for file in package.json package-lock.json Cargo.toml Cargo.lock setup.py requirements.txt build.gradle app/build.gradle README.md CHANGELOG.md; do + for file in build.gradle settings.gradle gradle.properties library/build.gradle example/build.gradle README.md CHANGELOG.md; do if [ -f "$file" ]; then if git rev-list "$PREVIOUS_TAG..$TAG_NAME" &>/dev/null; then if git diff --name-only "$PREVIOUS_TAG..$TAG_NAME" | grep -q "^$file$"; then echo "Changes in $file:" >> "$info_file" - git diff "$PREVIOUS_TAG..$TAG_NAME" -- "$file" >> "$info_file" 2>/dev/null || true + if ! git diff "$PREVIOUS_TAG..$TAG_NAME" -- "$file" >> "$info_file" 2>&1; then + print_warning "Failed to get diff for $file" + echo "Error retrieving diff for $file" >> "$info_file" + fi echo -e "\n" >> "$info_file" fi fi @@ -232,9 +272,11 @@ generate_ai_release_notes() { # Create the AI generation script cat > "$ai_script" << 'EOF' const fs = require('fs'); +const path = require('path'); async function generateReleaseNotes() { - const releaseInfo = fs.readFileSync('./output/release-data.txt', 'utf8'); + const outputDir = process.env.OUTPUT_DIR || './output'; + const releaseInfo = fs.readFileSync(path.join(outputDir, 'release-data.txt'), 'utf8'); // Truncate if too long (API limits) const maxLength = 50000; @@ -305,7 +347,7 @@ FORMAT REQUIREMENTS: }, body: JSON.stringify({ model: "claude-sonnet-4-20250514", - max_tokens: 1000, + max_tokens: parseInt(process.env.MAX_TOKENS) || 4000, messages: [ { role: "user", content: prompt } ], @@ -326,7 +368,8 @@ FORMAT REQUIREMENTS: .join("\n"); // Save release notes - fs.writeFileSync('./output/release-notes.md', releaseNotes); + const outputPath = path.join(outputDir, 'release-notes.md'); + fs.writeFileSync(outputPath, releaseNotes); console.log("✅ Release notes generated successfully"); } catch (error) { @@ -349,7 +392,8 @@ ${truncatedInfo.substring(0, 2000)} --- *Note: This is fallback content. Please review git history and create proper release notes.* `; - fs.writeFileSync('./output/release-notes.md', fallbackNotes); + const fallbackPath = path.join(outputDir, 'release-notes.md'); + fs.writeFileSync(fallbackPath, fallbackNotes); console.log("📁 Fallback release notes created"); process.exit(1); } @@ -367,6 +411,8 @@ EOF export TAG_NAME="$TAG_NAME" export PREVIOUS_TAG="$PREVIOUS_TAG" export RELEASE_TYPE="$RELEASE_TYPE" + export OUTPUT_DIR="$OUTPUT_DIR" + export MAX_TOKENS="$MAX_TOKENS" # Run the AI generation if node "$ai_script" "$TAG_NAME" "$PREVIOUS_TAG" "$RELEASE_TYPE"; then @@ -379,9 +425,9 @@ EOF # Function to display results display_results() { print_header "Release Notes Generated" - + echo "📁 Generated files in $OUTPUT_DIR/:" - ls -la "$OUTPUT_DIR/" | grep -v "^d" | awk '{print " " $9 " (" $5 " bytes)"}' + ls -la "$OUTPUT_DIR/" | grep -v "^d" | grep -v "^total" | awk 'NF > 0 && $9 != "." && $9 != ".." {print " " $9 " (" $5 " bytes)"}' echo "" if [ -f "$OUTPUT_DIR/release-notes.md" ]; then From 1b1dd347c12fa66df4b4d46bfec29ddccf630dfd Mon Sep 17 00:00:00 2001 From: Jennifer Hasegawa <5481259+jhaaaa@users.noreply.github.com> Date: Tue, 16 Dec 2025 19:19:11 -0800 Subject: [PATCH 3/8] more claude fixes to claude feedback --- gen-rn/gen-rn.sh | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/gen-rn/gen-rn.sh b/gen-rn/gen-rn.sh index 3330d0451..6ab052938 100755 --- a/gen-rn/gen-rn.sh +++ b/gen-rn/gen-rn.sh @@ -72,7 +72,7 @@ check_prerequisites() { fi # Check for API key - if [ -z "$ANTHROPIC_API_KEY" ]; then + if [ -z "${ANTHROPIC_API_KEY:-}" ]; then print_error "ANTHROPIC_API_KEY environment variable is not set." echo "Please set your API key: export ANTHROPIC_API_KEY=\"your-key-here\"" exit 1 @@ -123,13 +123,14 @@ setup_environment() { validate_tag_name "$TAG_NAME" # Get previous tag with smart detection (from working local-test-runner.sh) + PREVIOUS_TAG="" # Initialize to empty string for set -u compatibility if [ "${2:-}" != "" ]; then PREVIOUS_TAG="$2" # Validate previous tag validate_tag_name "$PREVIOUS_TAG" else # Try to find the previous tag more intelligently - + # First, determine if current tag has 'v' prefix if [[ "$TAG_NAME" == v* ]]; then # Look for other v-prefixed tags @@ -160,6 +161,13 @@ setup_environment() { echo "Using previous tag: $PREVIOUS_TAG" fi fi + + # Prevent self-comparison + if [ "$TAG_NAME" = "$PREVIOUS_TAG" ]; then + print_error "Cannot compare tag to itself: $TAG_NAME" + echo "Please specify a different previous tag" + exit 1 + fi # Determine release type if [[ "$TAG_NAME" == *"rc"* ]]; then @@ -172,7 +180,7 @@ setup_environment() { echo "" print_success "Environment configured:" - echo " Repository: $(basename $(git rev-parse --show-toplevel))" + echo " Repository: $(basename "$(git rev-parse --show-toplevel)")" echo " Current tag: $TAG_NAME" echo " Previous tag: $PREVIOUS_TAG" echo " Release type: $RELEASE_TYPE" @@ -184,9 +192,9 @@ gather_release_info() { print_header "Gathering Release Information" local info_file="$OUTPUT_DIR/release-data.txt" - + echo "=== XMTP SDK Release Analysis ===" > "$info_file" - echo "Repository: $(basename $(git rev-parse --show-toplevel))" >> "$info_file" + echo "Repository: $(basename "$(git rev-parse --show-toplevel)")" >> "$info_file" echo "Current tag: $TAG_NAME" >> "$info_file" echo "Previous tag: $PREVIOUS_TAG" >> "$info_file" echo "Release type: $RELEASE_TYPE" >> "$info_file" @@ -427,7 +435,7 @@ display_results() { print_header "Release Notes Generated" echo "📁 Generated files in $OUTPUT_DIR/:" - ls -la "$OUTPUT_DIR/" | grep -v "^d" | grep -v "^total" | awk 'NF > 0 && $9 != "." && $9 != ".." {print " " $9 " (" $5 " bytes)"}' + ls -la "$OUTPUT_DIR/" | awk '/^-/ && $9 != "" {print " " $9 " (" $5 " bytes)"}' echo "" if [ -f "$OUTPUT_DIR/release-notes.md" ]; then @@ -486,6 +494,10 @@ if [ "$1" = "--help" ] || [ "$1" = "-h" ]; then echo " - ANTHROPIC_API_KEY environment variable set" echo " - Internet connection (for AI API)" echo "" + echo "Environment Variables:" + echo " ANTHROPIC_API_KEY Required: Your Anthropic API key" + echo " MAX_TOKENS Optional: Max tokens for AI response (default: 4000, range: 1000-8000)" + echo "" echo "Output:" echo " - output/release-notes.md # Main AI-generated release notes" echo " - output/release-data.txt # Raw git analysis data" From 2a5108b25a3c5c1e1d0a6ea0a5c172cc74038ee4 Mon Sep 17 00:00:00 2001 From: Jennifer Hasegawa <5481259+jhaaaa@users.noreply.github.com> Date: Tue, 16 Dec 2025 21:35:29 -0800 Subject: [PATCH 4/8] more fixes --- gen-rn/README.md | 2 +- gen-rn/gen-rn.sh | 42 ++++++++++++++++++++++++++++++++---------- 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/gen-rn/README.md b/gen-rn/README.md index cc384f508..45c8ea249 100644 --- a/gen-rn/README.md +++ b/gen-rn/README.md @@ -10,7 +10,7 @@ AI-powered release notes generation for XMTP SDK releases using git history anal ## Prerequisites -- Node.js installed +- Node.js 18 or later (required for native fetch API support) - Anthropic API key: Get a key at: [https://console.anthropic.com/](https://console.anthropic.com/) ## Configuration diff --git a/gen-rn/gen-rn.sh b/gen-rn/gen-rn.sh index 6ab052938..07b989ee0 100755 --- a/gen-rn/gen-rn.sh +++ b/gen-rn/gen-rn.sh @@ -67,7 +67,16 @@ check_prerequisites() { # Check for Node.js (for the AI script) if ! command -v node &> /dev/null; then print_error "Node.js is required but not installed." - echo "Please install Node.js from https://nodejs.org/" + echo "Please install Node.js 18 or later from https://nodejs.org/" + exit 1 + fi + + # Check Node.js version (requires 18+ for native fetch support) + NODE_VERSION=$(node --version | cut -d'v' -f2 | cut -d'.' -f1) + if [ "$NODE_VERSION" -lt 18 ]; then + print_error "Node.js 18 or later is required (found: $(node --version))" + echo "The script uses native fetch() API which requires Node.js 18+" + echo "Please upgrade Node.js from https://nodejs.org/" exit 1 fi @@ -97,10 +106,10 @@ setup_environment() { # Get available tags echo "Available git tags:" echo "Recent tags:" - git tag --sort=-version:refname | grep -E "^[0-9]" | head -10 + git tag --sort=-version:refname | grep -E "^[0-9]" | head -10 || true echo "" echo "Legacy tags:" - git tag --sort=-version:refname | grep "^v" | head -5 + git tag --sort=-version:refname | grep "^v" | head -5 || true echo "" # Get current tag or prompt user @@ -138,14 +147,27 @@ setup_environment() { else # Look for non-v-prefixed tags in the same version series VERSION_SERIES=$(echo "$TAG_NAME" | cut -d. -f1-2) # e.g., "4.6" from "4.6.4" - + + # Escape dots in tag name for regex matching + ESCAPED_TAG_NAME=$(echo "$TAG_NAME" | sed 's/\./\\./g') + # Try to find previous tag in same series first - PREVIOUS_TAG=$(git tag | grep "^${VERSION_SERIES}\." | sort -V | grep -B1 "^${TAG_NAME}$" | head -n1) - + PREVIOUS_TAG=$(git tag | grep "^${VERSION_SERIES}\." | sort -V | grep -B1 "^${ESCAPED_TAG_NAME}$" | head -n1) + + # Guard against self-comparison + if [ "$PREVIOUS_TAG" = "$TAG_NAME" ]; then + PREVIOUS_TAG="" + fi + # If no same-series tag found, try broader search if [ -z "$PREVIOUS_TAG" ]; then MAJOR_VERSION=$(echo "$TAG_NAME" | cut -d. -f1) # e.g., "4" from "4.6.4" - PREVIOUS_TAG=$(git tag | grep "^${MAJOR_VERSION}\." | sort -V | grep -B1 "^${TAG_NAME}$" | head -n1) + PREVIOUS_TAG=$(git tag | grep "^${MAJOR_VERSION}\." | sort -V | grep -B1 "^${ESCAPED_TAG_NAME}$" | head -n1) + + # Guard against self-comparison + if [ "$PREVIOUS_TAG" = "$TAG_NAME" ]; then + PREVIOUS_TAG="" + fi fi # If still no tag, fall back to any non-v tag @@ -466,14 +488,14 @@ main() { echo "" check_prerequisites - setup_environment "$1" "$2" + setup_environment "${1:-}" "${2:-}" gather_release_info generate_ai_release_notes display_results } # Script usage help -if [ "$1" = "--help" ] || [ "$1" = "-h" ]; then +if [ "${1:-}" = "--help" ] || [ "${1:-}" = "-h" ]; then echo "XMTP SDK Release Notes Generator" echo "" echo "Generates AI-powered release notes for XMTP SDK releases using git history analysis." @@ -490,7 +512,7 @@ if [ "$1" = "--help" ] || [ "$1" = "-h" ]; then echo "" echo "Requirements:" echo " - Git repository with release tags" - echo " - Node.js installed" + echo " - Node.js 18 or later (for native fetch API support)" echo " - ANTHROPIC_API_KEY environment variable set" echo " - Internet connection (for AI API)" echo "" From 605b21ae73ab127937396e2f807169664eca8feb Mon Sep 17 00:00:00 2001 From: Jennifer Hasegawa <5481259+jhaaaa@users.noreply.github.com> Date: Wed, 17 Dec 2025 09:59:46 -0800 Subject: [PATCH 5/8] more fixes --- gen-rn/gen-rn.sh | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/gen-rn/gen-rn.sh b/gen-rn/gen-rn.sh index 07b989ee0..59786f3fb 100755 --- a/gen-rn/gen-rn.sh +++ b/gen-rn/gen-rn.sh @@ -73,7 +73,7 @@ check_prerequisites() { # Check Node.js version (requires 18+ for native fetch support) NODE_VERSION=$(node --version | cut -d'v' -f2 | cut -d'.' -f1) - if [ "$NODE_VERSION" -lt 18 ]; then + if ! [[ "$NODE_VERSION" =~ ^[0-9]+$ ]] || [ "$NODE_VERSION" -lt 18 ]; then print_error "Node.js 18 or later is required (found: $(node --version))" echo "The script uses native fetch() API which requires Node.js 18+" echo "Please upgrade Node.js from https://nodejs.org/" @@ -142,17 +142,20 @@ setup_environment() { # First, determine if current tag has 'v' prefix if [[ "$TAG_NAME" == v* ]]; then - # Look for other v-prefixed tags - PREVIOUS_TAG=$(git tag --sort=-version:refname | grep "^v" | grep -v "$TAG_NAME" | head -n 1) + # Look for other v-prefixed tags (use grep -F for fixed-string matching) + PREVIOUS_TAG=$(git tag --sort=-version:refname | grep "^v" | grep -Fv "$TAG_NAME" | head -n 1) + + # Guard against self-comparison + if [ "$PREVIOUS_TAG" = "$TAG_NAME" ]; then + PREVIOUS_TAG="" + fi else # Look for non-v-prefixed tags in the same version series VERSION_SERIES=$(echo "$TAG_NAME" | cut -d. -f1-2) # e.g., "4.6" from "4.6.4" - # Escape dots in tag name for regex matching - ESCAPED_TAG_NAME=$(echo "$TAG_NAME" | sed 's/\./\\./g') - # Try to find previous tag in same series first - PREVIOUS_TAG=$(git tag | grep "^${VERSION_SERIES}\." | sort -V | grep -B1 "^${ESCAPED_TAG_NAME}$" | head -n1) + # Use git tag --sort for cross-platform compatibility and grep -F for fixed-string matching + PREVIOUS_TAG=$(git tag --sort=version:refname | grep "^${VERSION_SERIES}\." | grep -F -B1 "$TAG_NAME" | head -n1) # Guard against self-comparison if [ "$PREVIOUS_TAG" = "$TAG_NAME" ]; then @@ -162,7 +165,7 @@ setup_environment() { # If no same-series tag found, try broader search if [ -z "$PREVIOUS_TAG" ]; then MAJOR_VERSION=$(echo "$TAG_NAME" | cut -d. -f1) # e.g., "4" from "4.6.4" - PREVIOUS_TAG=$(git tag | grep "^${MAJOR_VERSION}\." | sort -V | grep -B1 "^${ESCAPED_TAG_NAME}$" | head -n1) + PREVIOUS_TAG=$(git tag --sort=version:refname | grep "^${MAJOR_VERSION}\." | grep -F -B1 "$TAG_NAME" | head -n1) # Guard against self-comparison if [ "$PREVIOUS_TAG" = "$TAG_NAME" ]; then @@ -172,7 +175,12 @@ setup_environment() { # If still no tag, fall back to any non-v tag if [ -z "$PREVIOUS_TAG" ]; then - PREVIOUS_TAG=$(git tag --sort=-version:refname | grep -v "^v" | grep -v "$TAG_NAME" | head -n 1) + PREVIOUS_TAG=$(git tag --sort=-version:refname | grep -v "^v" | grep -Fv "$TAG_NAME" | head -n 1) + + # Guard against self-comparison + if [ "$PREVIOUS_TAG" = "$TAG_NAME" ]; then + PREVIOUS_TAG="" + fi fi fi @@ -259,7 +267,7 @@ gather_release_info() { for file in build.gradle settings.gradle gradle.properties library/build.gradle example/build.gradle README.md CHANGELOG.md; do if [ -f "$file" ]; then if git rev-list "$PREVIOUS_TAG..$TAG_NAME" &>/dev/null; then - if git diff --name-only "$PREVIOUS_TAG..$TAG_NAME" | grep -q "^$file$"; then + if git diff --name-only "$PREVIOUS_TAG..$TAG_NAME" | grep -Fxq "$file"; then echo "Changes in $file:" >> "$info_file" if ! git diff "$PREVIOUS_TAG..$TAG_NAME" -- "$file" >> "$info_file" 2>&1; then print_warning "Failed to get diff for $file" From 14e9d6ceb733ee7e9e5b99aecd0b26d7c64f77a7 Mon Sep 17 00:00:00 2001 From: Jennifer Hasegawa <5481259+jhaaaa@users.noreply.github.com> Date: Wed, 17 Dec 2025 11:03:01 -0800 Subject: [PATCH 6/8] add gitignore --- .gitignore | 5 ++++- gen-rn/README.md | 57 ++++++++++++++++++------------------------------ 2 files changed, 25 insertions(+), 37 deletions(-) diff --git a/.gitignore b/.gitignore index de3252287..3ed669a39 100644 --- a/.gitignore +++ b/.gitignore @@ -96,4 +96,7 @@ lint/tmp/ # OSX .DS_Store -.kotlin \ No newline at end of file +.kotlin + +# Release notes generator output +gen-rn/output/ \ No newline at end of file diff --git a/gen-rn/README.md b/gen-rn/README.md index 45c8ea249..39b878577 100644 --- a/gen-rn/README.md +++ b/gen-rn/README.md @@ -2,6 +2,9 @@ AI-powered release notes generation for XMTP SDK releases using git history analysis and Claude AI. +> [!IMPORTANT] +> Generated release notes must be reviewed by humans before publication. The AI provides a starting point, not a final product. + ## Purpose 1. **Engineering sanity check**: Help engineers understand exactly what's included in a release @@ -20,11 +23,11 @@ The script supports the following environment variables: - `ANTHROPIC_API_KEY` (required): Your Anthropic API key for Claude AI - `MAX_TOKENS` (optional): Maximum tokens for AI response (default: 4000, range: 1000-8000) -Example with custom token limit: -```bash -export MAX_TOKENS=6000 -./gen-rn.sh 4.6.4 -``` +### API key handling + +- Never commit your `ANTHROPIC_API_KEY` to version control +- Store your API key in your shell profile as described in Setup +- Rotate your API key if it's accidentally exposed ## Setup @@ -40,31 +43,26 @@ git fetch --tags # Make script executable chmod +x gen-rn.sh -# Generate release notes -./gen-rn.sh 4.6.4 +# Optionally, set custom token limit for current terminal session +export MAX_TOKENS=6000 ``` ## Usage -### Interactive mode - ```bash -./gen-rn.sh -# Displays available tags and enables you to enter a tag. -# Compares against auto-detected previous tag. -``` - -### Specific release +# Specify both tags to show what changed between releases +# This example generates release notes for 4.6.4 based on updates since 4.6.3 +./gen-rn.sh 4.6.4 4.6.3 -```bash # Generate release notes for 4.6.4 (compares against auto-detected previous tag, likely 4.6.3) ./gen-rn.sh 4.6.4 -# Specify both tags for comparison (show what changed from 4.6.3 to 4.6.4) -./gen-rn.sh 4.6.4 4.6.3 +./gen-rn.sh +# Displays available tags and enables you to enter a tag. +# Compares against auto-detected previous tag. -# Use latest tag (compares latest against its predecessor) -./gen-rn.sh latest +# Help +./gen-rn.sh --help ``` ### Supported release types @@ -82,21 +80,8 @@ The script automatically detects release types based on tag naming: ./gen-rn.sh 4.6.7-dev.abc123 4.6.7 # → Release type: development ``` -### Help +### Tag name validation -```bash -./gen-rn.sh --help -``` - -## Security - -### API Key Handling -- Never commit your `ANTHROPIC_API_KEY` to version control -- Store your API key in your shell profile (e.g., `~/.zshrc`, `~/.bashrc`) -- Consider using a secrets manager for production environments -- Rotate your API key if it's accidentally exposed - -### Input Validation The script validates all tag names to prevent command injection attacks. Tag names must contain only: - Letters (a-z, A-Z) - Numbers (0-9) @@ -117,8 +102,8 @@ output/ └── ai-script.js # Temporary AI generation script ``` -> [!IMPORTANT] -> Generated release notes must be reviewed by humans before publication. The AI provides a starting point, not a final product. +> [!NOTE] +> The `gen-rn/output/` directory is automatically excluded from version control via `.gitignore`. Generated files will not be committed to the repository. ## Troubleshooting From 707b874249f14f79e6590a7919efc9ed273dd440 Mon Sep 17 00:00:00 2001 From: Jennifer Hasegawa <5481259+jhaaaa@users.noreply.github.com> Date: Wed, 17 Dec 2025 11:10:44 -0800 Subject: [PATCH 7/8] fixes --- gen-rn/README.md | 9 +++-- gen-rn/gen-rn.sh | 101 +++++++++++++++++++++++++++++++++-------------- 2 files changed, 76 insertions(+), 34 deletions(-) diff --git a/gen-rn/README.md b/gen-rn/README.md index 39b878577..b1b68c78b 100644 --- a/gen-rn/README.md +++ b/gen-rn/README.md @@ -27,7 +27,8 @@ The script supports the following environment variables: - Never commit your `ANTHROPIC_API_KEY` to version control - Store your API key in your shell profile as described in Setup -- Rotate your API key if it's accidentally exposed +- Rotate your API key immediately if it's accidentally exposed or committed +- The temporary `ai-script.js` file is automatically deleted after execution for security ## Setup @@ -98,12 +99,12 @@ After running, check the `output/` directory: output/ ├── release-notes.md # Main AI-generated release notes ├── release-data.txt # Raw git analysis data (for debugging) -├── pr-refs.txt # PR numbers found in commits -└── ai-script.js # Temporary AI generation script +└── pr-refs.txt # PR numbers found in commits ``` > [!NOTE] -> The `gen-rn/output/` directory is automatically excluded from version control via `.gitignore`. Generated files will not be committed to the repository. +> - The `gen-rn/output/` directory is automatically excluded from version control via `.gitignore` +> - The temporary `ai-script.js` file is automatically deleted after execution for security ## Troubleshooting diff --git a/gen-rn/gen-rn.sh b/gen-rn/gen-rn.sh index 59786f3fb..141378417 100755 --- a/gen-rn/gen-rn.sh +++ b/gen-rn/gen-rn.sh @@ -10,6 +10,17 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" OUTPUT_DIR="./output" MAX_TOKENS="${MAX_TOKENS:-4000}" # Configurable via environment variable, default 4000 +# Validate MAX_TOKENS is numeric and within range +if [[ "$MAX_TOKENS" =~ ^[0-9]+$ ]]; then + if [ "$MAX_TOKENS" -lt 1000 ] || [ "$MAX_TOKENS" -gt 8000 ]; then + echo "⚠ Warning: MAX_TOKENS should be between 1000-8000, got $MAX_TOKENS. Using default 4000." + MAX_TOKENS=4000 + fi +else + echo "⚠ Warning: MAX_TOKENS must be numeric, got '$MAX_TOKENS'. Using default 4000." + MAX_TOKENS=4000 +fi + # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' @@ -46,8 +57,8 @@ validate_tag_name() { exit 1 fi - # Check if tag exists in repository - if ! git rev-parse "$tag" >/dev/null 2>&1; then + # Check if tag exists in repository (verify it's actually a tag, not a branch or commit) + if ! git rev-parse -q --verify "refs/tags/$tag" >/dev/null 2>&1; then print_error "Tag does not exist: $tag" echo "Use 'git tag' to see available tags" exit 1 @@ -142,8 +153,8 @@ setup_environment() { # First, determine if current tag has 'v' prefix if [[ "$TAG_NAME" == v* ]]; then - # Look for other v-prefixed tags (use grep -F for fixed-string matching) - PREVIOUS_TAG=$(git tag --sort=-version:refname | grep "^v" | grep -Fv "$TAG_NAME" | head -n 1) + # Look for other v-prefixed tags (use grep -Fx for exact fixed-string matching) + PREVIOUS_TAG=$(git tag --sort=-version:refname | grep "^v" | grep -Fxv "$TAG_NAME" | head -n 1) # Guard against self-comparison if [ "$PREVIOUS_TAG" = "$TAG_NAME" ]; then @@ -153,39 +164,42 @@ setup_environment() { # Look for non-v-prefixed tags in the same version series VERSION_SERIES=$(echo "$TAG_NAME" | cut -d. -f1-2) # e.g., "4.6" from "4.6.4" - # Try to find previous tag in same series first - # Use git tag --sort for cross-platform compatibility and grep -F for fixed-string matching - PREVIOUS_TAG=$(git tag --sort=version:refname | grep "^${VERSION_SERIES}\." | grep -F -B1 "$TAG_NAME" | head -n1) - - # Guard against self-comparison - if [ "$PREVIOUS_TAG" = "$TAG_NAME" ]; then - PREVIOUS_TAG="" - fi + # Try to find previous tag in same series first (portable approach without grep -B) + PREVIOUS_TAG=$(git tag --sort=version:refname | grep "^${VERSION_SERIES}\." | { + local prev="" + while IFS= read -r tag; do + if [ "$tag" = "$TAG_NAME" ]; then + echo "$prev" + break + fi + prev="$tag" + done + }) # If no same-series tag found, try broader search if [ -z "$PREVIOUS_TAG" ]; then MAJOR_VERSION=$(echo "$TAG_NAME" | cut -d. -f1) # e.g., "4" from "4.6.4" - PREVIOUS_TAG=$(git tag --sort=version:refname | grep "^${MAJOR_VERSION}\." | grep -F -B1 "$TAG_NAME" | head -n1) - - # Guard against self-comparison - if [ "$PREVIOUS_TAG" = "$TAG_NAME" ]; then - PREVIOUS_TAG="" - fi + PREVIOUS_TAG=$(git tag --sort=version:refname | grep "^${MAJOR_VERSION}\." | { + local prev="" + while IFS= read -r tag; do + if [ "$tag" = "$TAG_NAME" ]; then + echo "$prev" + break + fi + prev="$tag" + done + }) fi - + # If still no tag, fall back to any non-v tag if [ -z "$PREVIOUS_TAG" ]; then - PREVIOUS_TAG=$(git tag --sort=-version:refname | grep -v "^v" | grep -Fv "$TAG_NAME" | head -n 1) - - # Guard against self-comparison - if [ "$PREVIOUS_TAG" = "$TAG_NAME" ]; then - PREVIOUS_TAG="" - fi + PREVIOUS_TAG=$(git tag --sort=-version:refname | grep -v "^v" | grep -Fxv "$TAG_NAME" | head -n 1) fi fi if [ -z "$PREVIOUS_TAG" ]; then - PREVIOUS_TAG=$(git rev-list --max-parents=0 HEAD) + # Get first root commit (handles repos with multiple root commits) + PREVIOUS_TAG=$(git rev-list --max-parents=0 HEAD | head -n 1) print_warning "No previous tag found, comparing against first commit" else echo "Using previous tag: $PREVIOUS_TAG" @@ -396,12 +410,30 @@ FORMAT REQUIREMENTS: if (!response.ok) { const errorText = await response.text(); - throw new Error(`HTTP error! status: ${response.status}, body: ${errorText}`); + + // Provide actionable error messages for common failures + if (response.status === 401) { + throw new Error(`Authentication failed (401). Check your ANTHROPIC_API_KEY is valid and not expired.`); + } else if (response.status === 429) { + throw new Error(`Rate limit exceeded (429). Wait a few minutes and try again, or reduce MAX_TOKENS.`); + } else if (response.status === 400) { + throw new Error(`Bad request (400). This may be due to invalid MAX_TOKENS or request format. Error: ${errorText.substring(0, 200)}`); + } else { + throw new Error(`HTTP error! status: ${response.status}, body: ${errorText.substring(0, 300)}`); + } } const data = await response.json(); - const releaseNotes = data.content - .map(item => (item.type === "text" ? item.text : "")) + + // Validate API response structure + const content = Array.isArray(data?.content) ? data.content : null; + if (!content || content.length === 0) { + const responsePreview = JSON.stringify(data).substring(0, 200); + throw new Error(`Unexpected API response: missing or invalid 'content' array. Response: ${responsePreview}`); + } + + const releaseNotes = content + .map(item => (item && item.type === "text" ? item.text : "")) .filter(Boolean) .join("\n"); @@ -458,6 +490,9 @@ EOF else print_warning "AI generation failed, but fallback content was created" fi + + # Clean up temporary AI script for security + rm -f "$ai_script" } # Function to display results @@ -465,7 +500,13 @@ display_results() { print_header "Release Notes Generated" echo "📁 Generated files in $OUTPUT_DIR/:" - ls -la "$OUTPUT_DIR/" | awk '/^-/ && $9 != "" {print " " $9 " (" $5 " bytes)"}' + # List files with sizes (handles filenames with spaces correctly) + for file in "$OUTPUT_DIR"/*; do + if [ -f "$file" ]; then + size=$(wc -c < "$file") + printf " %s (%s bytes)\n" "$(basename "$file")" "$size" + fi + done echo "" if [ -f "$OUTPUT_DIR/release-notes.md" ]; then From a2f5dbf2178691805ccc5ecb01ac4938b49c1956 Mon Sep 17 00:00:00 2001 From: Jennifer Hasegawa <5481259+jhaaaa@users.noreply.github.com> Date: Wed, 17 Dec 2025 15:50:04 -0800 Subject: [PATCH 8/8] add from base main option --- gen-rn/README.md | 33 ++++++++++++++++++++++++++------- gen-rn/gen-rn.sh | 44 ++++++++++++++++++++++++++++++++++++-------- 2 files changed, 62 insertions(+), 15 deletions(-) diff --git a/gen-rn/README.md b/gen-rn/README.md index b1b68c78b..1f3c41ff8 100644 --- a/gen-rn/README.md +++ b/gen-rn/README.md @@ -50,22 +50,41 @@ export MAX_TOKENS=6000 ## Usage -```bash -# Specify both tags to show what changed between releases -# This example generates release notes for 4.6.4 based on updates since 4.6.3 -./gen-rn.sh 4.6.4 4.6.3 +### Basic usage -# Generate release notes for 4.6.4 (compares against auto-detected previous tag, likely 4.6.3) +```bash +# Auto-detect previous tag and generate delta ./gen-rn.sh 4.6.4 +# Specify both tags to compare (what's new in 4.6.4 vs 4.6.3) +./gen-rn.sh 4.6.4 4.6.3 + +# Interactive mode - prompts for tag selection +# Detects previous tag and generate delta ./gen-rn.sh -# Displays available tags and enables you to enter a tag. -# Compares against auto-detected previous tag. # Help ./gen-rn.sh --help ``` +### Custom base comparison with `--from-base` + +Use the `--from-base` flag to compare a release against any git reference (branch, tag, or commit): + +```bash +# Show everything in 4.6.4 that's not in main +./gen-rn.sh 4.6.4 --from-base main + +# Show full content of a release candidate +./gen-rn.sh 4.6.4-rc.1 --from-base main + +# Show all changes since a specific older tag +./gen-rn.sh 4.6.4 --from-base 4.5.0 + +# Compare against a branch +./gen-rn.sh 4.6.4 --from-base release/4.6 +``` + ### Supported release types The script automatically detects release types based on tag naming: diff --git a/gen-rn/gen-rn.sh b/gen-rn/gen-rn.sh index 141378417..64a3f1254 100755 --- a/gen-rn/gen-rn.sh +++ b/gen-rn/gen-rn.sh @@ -139,12 +139,36 @@ setup_environment() { fi fi + # Check if user accidentally used --from-base as tag name + if [ "$TAG_NAME" = "--from-base" ]; then + print_error "Missing tag name before --from-base flag" + echo "Correct usage: ./gen-rn.sh --from-base " + echo "Example: ./gen-rn.sh 4.6.4 --from-base main" + exit 1 + fi + # Validate tag name validate_tag_name "$TAG_NAME" - - # Get previous tag with smart detection (from working local-test-runner.sh) + + # Get previous tag with smart detection or --from-base option PREVIOUS_TAG="" # Initialize to empty string for set -u compatibility - if [ "${2:-}" != "" ]; then + + # Check if --from-base flag is used + if [ "${2:-}" = "--from-base" ]; then + if [ -z "${3:-}" ]; then + print_error "--from-base requires a git reference (tag, branch, or commit)" + echo "Example: ./gen-rn.sh $TAG_NAME --from-base main" + exit 1 + fi + PREVIOUS_TAG="$3" + # Validate that the base reference exists in git + if ! git rev-parse "$PREVIOUS_TAG" >/dev/null 2>&1; then + print_error "Base reference does not exist: $PREVIOUS_TAG" + echo "Use 'git branch -a' or 'git tag' to see available references" + exit 1 + fi + echo "Using custom base reference: $PREVIOUS_TAG" + elif [ "${2:-}" != "" ]; then PREVIOUS_TAG="$2" # Validate previous tag validate_tag_name "$PREVIOUS_TAG" @@ -537,7 +561,7 @@ main() { echo "" check_prerequisites - setup_environment "${1:-}" "${2:-}" + setup_environment "${1:-}" "${2:-}" "${3:-}" gather_release_info generate_ai_release_notes display_results @@ -551,13 +575,17 @@ if [ "${1:-}" = "--help" ] || [ "${1:-}" = "-h" ]; then echo "" echo "Usage:" echo " $0 [tag_name] [previous_tag]" + echo " $0 [tag_name] --from-base " echo " $0 --help|-h" echo "" echo "Examples:" - echo " $0 # Interactive mode - will prompt for tag" - echo " $0 4.6.4 # Generate notes for v4.6.4 (auto-detect previous)" - echo " $0 4.6.4 4.6.3 # Generate notes comparing 4.6.4 to 4.6.3" - echo " $0 latest # Generate notes for most recent tag" + echo " $0 # Interactive mode - will prompt for tag" + echo " $0 4.6.4 # Generate notes for 4.6.4 (auto-detect previous)" + echo " $0 4.6.4 4.6.3 # Compare 4.6.4 to 4.6.3 (what's new)" + echo " $0 4.6.4 --from-base main # Show everything in 4.6.4 not in main" + echo " $0 4.6.4 --from-base 4.5.0 # Show everything since 4.5.0" + echo " $0 4.6.4-rc.1 --from-base main # Full content of RC release" + echo " $0 latest # Generate notes for most recent tag" echo "" echo "Requirements:" echo " - Git repository with release tags"