#!/bin/bash # # generate_changelog.sh # # Generates a weekly changelog from merged PRs, uses Claude to create a synopsis, # and sends an HTML email via Fastmail JMAP API. # # Usage: ./scripts/generate_changelog.sh [--dry-run] # # Configuration files (in ~/.config/eagle0/): # fastmail_token - API token (required) # changelog_recipient - Email addresses, one per line (optional, defaults to sender) # # To set up: # mkdir -p ~/.config/eagle0 # echo 'your-token' > ~/.config/eagle0/fastmail_token # chmod 600 ~/.config/eagle0/fastmail_token # # # Optional: configure recipients (one per line, # for comments) # cat > ~/.config/eagle0/changelog_recipient << EOF # alice@example.com # bob@example.com # EOF # # The script tracks its last run using a git tag 'changelog-last-run'. # On first run (no tag), it defaults to the previous Friday at 4pm. set -euo pipefail # Ensure homebrew binaries are in PATH export PATH="/opt/homebrew/bin:$PATH" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" TAG_NAME="changelog-last-run" DRY_RUN=false FASTMAIL_API="https://api.fastmail.com/jmap/api/" CONFIG_DIR="$HOME/.config/eagle0" TOKEN_FILE="$CONFIG_DIR/fastmail_token" RECIPIENT_FILE="$CONFIG_DIR/changelog_recipient" # Load API token from file or environment load_api_token() { # Environment variable takes precedence if [[ -n "${FASTMAIL_API_TOKEN:-}" ]]; then return 0 fi # Try loading from config file if [[ -f "$TOKEN_FILE" ]]; then FASTMAIL_API_TOKEN=$(cat "$TOKEN_FILE" | tr -d '[:space:]') if [[ -n "$FASTMAIL_API_TOKEN" ]]; then echo "Loaded API token from $TOKEN_FILE" export FASTMAIL_API_TOKEN return 0 fi fi return 1 } # Load recipient emails from config file (one per line) # Returns JSON array fragment like: {"email": "a@b.com"}, {"email": "c@d.com"} load_recipients_json() { local recipients="" if [[ -f "$RECIPIENT_FILE" ]]; then while IFS= read -r line || [[ -n "$line" ]]; do # Skip empty lines and comments line=$(echo "$line" | tr -d '[:space:]') [[ -z "$line" || "$line" == \#* ]] && continue if [[ -n "$recipients" ]]; then recipients="$recipients, " fi recipients="$recipients{\"email\": \"$line\"}" done < "$RECIPIENT_FILE" fi echo "$recipients" } # Get human-readable list of recipients load_recipients_display() { if [[ -f "$RECIPIENT_FILE" ]]; then grep -v '^#' "$RECIPIENT_FILE" | grep -v '^[[:space:]]*$' | tr '\n' ', ' | sed 's/, $//' fi } # Parse arguments while [[ $# -gt 0 ]]; do case $1 in --dry-run) DRY_RUN=true shift ;; *) echo "Unknown option: $1" echo "Usage: $0 [--dry-run]" exit 1 ;; esac done cd "$REPO_ROOT" # Get the cutoff date - either from tag or previous Friday 4pm get_cutoff_date() { # Try to get the date from the tag if git rev-parse "$TAG_NAME" >/dev/null 2>&1; then # Get the commit date of the tagged commit git log -1 --format="%aI" "$TAG_NAME" else # Calculate previous Friday at 4pm # Get current day of week (1=Monday, 7=Sunday) local dow=$(date +%u) local days_since_friday if [[ $dow -ge 5 ]]; then # Friday (5), Saturday (6), or Sunday (7) days_since_friday=$((dow - 5)) else # Monday (1) through Thursday (4) days_since_friday=$((dow + 2)) fi # Get previous Friday at 4pm in ISO format if [[ "$(uname)" == "Darwin" ]]; then date -v-"${days_since_friday}d" -v16H -v0M -v0S +"%Y-%m-%dT%H:%M:%S%z" else date -d "$days_since_friday days ago 16:00:00" --iso-8601=seconds fi fi } # Fetch merged PRs since the cutoff date fetch_merged_prs() { local since_date="$1" local output_file="$2" echo "Fetching PRs merged since: $since_date" # Use gh to search for merged PRs gh pr list \ --state merged \ --base main \ --json number,title,body,mergedAt,author \ --jq ".[] | select(.mergedAt >= \"$since_date\")" \ > "$output_file.json" # Format the output nicely echo "# Merged PRs since $since_date" > "$output_file" echo "" >> "$output_file" # Process each PR jq -r ' "## PR #\(.number): \(.title)\n" + "Author: \(.author.login)\n" + "Merged: \(.mergedAt)\n\n" + "### Description\n" + (.body // "(No description)") + "\n\n---\n" ' "$output_file.json" >> "$output_file" # Count PRs local pr_count=$(jq -s 'length' "$output_file.json") echo "Found $pr_count merged PRs" rm -f "$output_file.json" if [[ $pr_count -eq 0 ]]; then echo "No PRs found since $since_date" return 1 fi return 0 } # Generate synopsis using Claude generate_synopsis() { local input_file="$1" local output_file="$2" echo "Generating synopsis with Claude..." # Create a prompt file to avoid shell escaping issues local prompt_file="/tmp/eagle0_prompt_$$.txt" # Get repo URL for PR links local repo_url=$(gh repo view --json url -q '.url') cat > "$prompt_file" < title (e.g., "Eagle0 Weekly Update") 2.

BLUF

(Bottom Line Up Front) - A short prose paragraph (2-4 sentences) highlighting the 1-3 most important changes this week and what to look for when testing. This should be conversational and help readers quickly understand what matters most. 3. Synopsis sections (

headings with bullet point summaries) 4.
divider 5.

PR Details

with the same groupings, but smaller (

headings) and listing PR links - Format each PR as: #NUMBER: Title Guidelines for the SYNOPSIS sections: - Group related changes together under clear headings (use

tags) - Use bullet points (