[skip-ci] Add release notes automation tools

pull/5263/head
Rene Cannao 4 months ago
parent faa64a570d
commit 365b0c6d80

@ -0,0 +1,144 @@
# ProxySQL 3.0.4 Release Notes
This release of ProxySQL 3.0.4 includes significant improvements to PostgreSQL support, MySQL protocol handling, monitoring accuracy, and security.
Release commit: `faa64a570d19fe35af43494db0babdee3e3cdc89`
## New Features:
### PostgreSQL Improvements:
- **PostgreSQL-Specific Tokenizer for Query Digest Generation** (285fb1b4, #5254)
- Adds PostgreSQL dialect support: dollar-quoted strings (`$$...$$`), identifier quoting, and dialect-specific comment rules
- Improved tokenizer for PostgreSQL with nested comments support (f5079037)
- Adds dedicated handling for double-quoted PostgreSQL identifiers and type cast syntax (`value::type`) (e70fcbf0)
- Processes array literals (`ARRAY[...]` and `{...}`) and prefixed literals (`E''`, `U&''`, `x''`, `b''`)
- **SSL Support for Backend Connections in PostgreSQL Monitor** (7205f424, #5237)
- PostgreSQL monitor now supports client certificates for SSL/TLS connections to backend servers
- Adds new metrics `PgSQL_Monitor_ssl_connections_OK` and `PgSQL_Monitor_non_ssl_connections_OK` for connection status visibility (fae283cf)
### MySQL Protocol Enhancements:
- **Special Handling for Unexpected `COM_PING` Packets** (d0e88599, #5257)
- Implements workaround for unexpected `COM_PING` packets received during query processing
- Queues `COM_PING` packets as counters and sends corresponding OK packets after query completion
- Improves logging with client address, session status, and data stream status (6fea828e)
- **Handling of `SELECT @@version` and `SELECT VERSION()` Without Backend** (df287b20, #4889)
- ProxySQL now responds directly to `SELECT @@version` and `SELECT VERSION()` queries without backend connection
- Returns hardcoded server version (currently 8.0.0), reducing unnecessary backend load
- **Client-Side `wait_timeout` Support** (abe16e66, #4901)
- Adds support for client-specified `wait_timeout` values, previously ignored by ProxySQL
- Terminates client connections (not backend) when client timeout is reached
- Clamps client timeout to not exceed global `mysql-wait_timeout`
- **Fast Forward Grace Close Feature to Prevent Data Loss** (44aa606c, #5203)
- Implements graceful connection closure for fast-forward sessions
- Ensures all pending data is flushed to client before closing connection
- Prevents data loss during connection termination in replication scenarios
### Monitoring & Diagnostics:
- **TCP Keepalive Warnings When Disabled** (cf454b8e, #5228)
- Adds warning messages when TCP keepalive is disabled on connections
- Helps administrators identify potential connection stability issues
- Includes detailed connection information for troubleshooting
## Bug Fixes:
### MySQL:
- **Make `cur_cmd_cmnt` Thread-Safe** (91e20648, #5259)
- Fixes thread-safety issue where `cur_cmd_cmnt` (current command comment) was shared across threads
- **Fix `cache_empty_result=0` Not Caching Non-Empty Resultsets** (2987242d, #5250)
- Corrects behavior of `cache_empty_result` field in query rules
- When set to "0", now correctly caches non-empty resultsets while skipping empty resultsets
- Previously prevented caching of any resultsets regardless of row count
- **Incorrect Affected Rows Reporting for DDL Queries** (05960b5d, #5232)
- Fixes issue #4855 where `mysql_affected_rows()` in ProxySQL Admin interface incorrectly reported affected rows for DDL queries
- DDL queries now correctly return 0 affected rows instead of previous DML's count
### Monitoring:
- **Fix Artificially High Ping Latency in MySQL Backend Monitoring** (24e02e95, #5199)
- Addresses artificially high ping latency measurements
- Introduces batching for task dispatching (batches of 30) with `poll()` calls between batches
- Reduces monitor ping `poll()` timeout from 100ms to 10ms for more responsive monitoring
### Security & Configuration:
- **Fix SQL Injection Vulnerability in `Read_Global_Variables_from_configfile()`** (0b2bc1bf, #5247)
- Replaces `sprintf`-based SQL query construction with prepared statements using bound parameters
- Fixes automatic prefix stripping for `mysql_variables`, `pgsql_variables`, and `admin_variables` config parsing (b4683569)
- Handles cases where users mistakenly include module prefixes (e.g., "mysql-") in variable names
## Improvements:
### Performance Optimizations:
- **Improve Fast Forward Replication `CLIENT_DEPRECATE_EOF` Validation** (5485bb02, #5240)
- Enhances validation logic for fast forward replication with `CLIENT_DEPRECATE_EOF` capability flag
- Ensures proper handling of replication packets and improves compatibility with MySQL 8.0+ clients
- **Refactored Prepared-Statement Cache Design (Lock-Free Hot Path)** (c0f99c0e, #5225)
- Implements lock-free hot path for prepared statement cache operations
- Reduces contention and improves performance for high-concurrency workloads
- Optimizes transaction command parsing to avoid unnecessary tokenization (e744c2bb)
- Replaces `std::string` with `char[]` to avoid heap allocation in hot paths (7a3a5c71)
- **GTID: Refactor Reconnect Logic & Prevent `events_count` Reset** (50c60284, #5226)
- Refactors GTID reconnect logic to prevent `events_count` from being reset during reconnection attempts
- Preserves replication state and reduces unnecessary replication restarts
## Documentation:
- **Comprehensive Documentation Additions** (cf8cbfd8, #5258)
- Query rules capture groups and backreferences documentation (6966b79d)
- "Closing killed client connection" warnings documentation (de214cb5)
- `coredump_filters` feature documentation addressing issue #5213
- `vacuum_stats()` and `stats_pgsql_stat_activity` Doxygen documentation with bug fix (efe0d4fe)
- **Permanent Fast-Forward Sessions and `check_data_flow()` Documentation** (ec1247f2, #5245)
- Documents behavior of permanent fast-forward sessions
- Documents `MySQL_Data_Stream::check_data_flow()` method
- Explains when bidirectional data checks are skipped for specific session types (4044a407)
- **Claude Code Agent Definitions and Architecture Documentation** (291e5f02, #5115)
- Adds architecture documentation for Claude Code agent integration
- Includes automation framework and internal documentation
- **Comprehensive Doxygen Documentation for GTID Refactoring** (9a55e974, #5229)
- Adds detailed Doxygen documentation for GTID-related code changes
- **TAP Test Writing Guide and GitHub Automation Improvements** (49899601, #5215)
- Provides comprehensive guide for writing TAP tests for ProxySQL
- Improves GitHub automation workflows
## Testing:
- **Add Comments to `SELECT @@version` Queries to Bypass ProxySQL Interception** (66119b74, #5251)
- Adds `/* set_testing */` comments to `SELECT @@version` queries in test files
- Prevents ProxySQL interception, allowing queries to reach backend MySQL servers for testing
- **Add Fast Forward Replication Deprecate EOF Test and Update Test Infrastructure** (9df7407f, #5241)
- Adds comprehensive test coverage for fast forward replication `CLIENT_DEPRECATE_EOF` validation
- Updates test infrastructure to better handle replication scenarios
- **Add Delay to Let ProxySQL Process `mysql_stmt_close()`** (fa74de3c, #5198)
- Adds appropriate delays in tests to allow ProxySQL time to process `mysql_stmt_close()` operations
- Prevents race conditions in prepared statement tests
- **Regroup Tests to Balance Group Test Time** (458ff778, #5207)
- Reorganizes test groups to balance execution time across test runners
- Improves CI pipeline efficiency and reduces overall test runtime
## Build/Packaging:
- **Add OpenSUSE 16 Packaging Support** (bce71a95, #5230)
- Adds packaging support for OpenSUSE 16
- Expands supported distribution matrix for ProxySQL installations
## Other Changes:
- **Bump Version to 3.0.4 at Beginning of Development Cycle** (ba664785, #5200)
- Updates version number to 3.0.4 at start of development cycle for clarity and version tracking
## Hashes
The release commit is: `faa64a570d19fe35af43494db0babdee3e3cdc89`

@ -0,0 +1,90 @@
# ProxySQL Release Notes Workflow
This document describes the complete workflow for generating release notes like ProxySQL v3.0.3.
## Quick Start (For ProxySQL 3.0.4)
```bash
# 1. Run the orchestration script
python scripts/release-tools/orchestrate_release.py \
--from-tag v3.0.3 \
--to-tag v3.0 \
--output-dir release-data \
--verbose
# 2. Provide the generated files to LLM with this prompt:
cat release-data/llm-prompt-v3.0.md
```
## Complete Procedure
### Step 1: Prepare Environment
```bash
git fetch --tags
git checkout v3.0 # Ensure you're on the release branch
```
### Step 2: Run Orchestration Script
```bash
python scripts/release-tools/orchestrate_release.py \
--from-tag PREVIOUS_TAG \
--to-tag CURRENT_BRANCH_OR_NEW_TAG \
--output-dir release-data
```
This generates:
- `release-data/pr-data-*.json` - PR details from GitHub
- `release-data/pr-summary-*.md` - PR summary
- `release-data/structured-notes-*.md` - Commit-level data
- `release-data/llm-prompt-*.md` - Complete LLM prompt
- `release-data/workflow-summary.md` - Instructions
### Step 3: Provide Files to LLM
Give the LLM:
1. All files in `release-data/` directory
2. The prompt: `release-data/llm-prompt-*.md`
### Step 4: LLM Generates Release Notes
The LLM should create:
- `ProxySQL-X.X.X-Release-Notes.md` - Main release notes
- `CHANGELOG-X.X.X-detailed.md` - Detailed changelog
- `CHANGELOG-X.X.X-commits.md` - Complete commit list (optional)
## Key Requirements for Release Notes
1. **Descriptive Content**: Explain what each feature/fix does and why it matters
2. **Logical Grouping**: Organize under categories (PostgreSQL, MySQL, Monitoring, etc.)
3. **Backtick Formatting**: Use `backticks` around all technical terms
4. **Commit References**: Include commit hashes and PR numbers
5. **No WIP/skip-ci Tags**: Make all entries production-ready
6. **Follow v3.0.3 Format**: Structure like previous release notes
## Example Output
See `ProxySQL-3.0.4-Release-Notes.md` in the root directory for a complete example of descriptive release notes with backtick formatting.
## Tools Directory
All scripts are in `scripts/release-tools/`:
| Script | Purpose |
|--------|---------|
| `orchestrate_release.py` | Main orchestration script |
| `collect_pr_data.py` | Fetch PR details from GitHub |
| `generate_structured_notes.py` | Create commit-level data |
| `categorize_commits.py` | Categorize commits by type |
| `generate_release_notes.py` | Basic release notes (without LLM) |
| `generate_changelog.py` | Basic changelog generation |
See `scripts/release-tools/README.md` for detailed documentation.
## For ProxySQL 3.0.4
The release notes for 3.0.4 have already been generated:
- `ProxySQL-3.0.4-Release-Notes.md` - Final release notes
- Backup: `ProxySQL-3.0.4-Release-Notes-backup.md` - Original version
- Example: `ProxySQL-3.0.4-Release-Notes-Descriptive.md` - Descriptive version
These notes follow all requirements: descriptive content, logical grouping, backtick formatting, and proper references.

@ -0,0 +1,207 @@
# ProxySQL Release Tools
This directory contains Python scripts to help generate release notes and changelogs for ProxySQL releases.
## Prerequisites
- Python 3.6+
- Git command line tools
- GitHub CLI (`gh`) installed and authenticated (for scripts that fetch PR details)
## Scripts Overview
### 1. `categorize_commits.py`
Categorizes git commits based on keywords in commit messages.
**Usage:**
```bash
python categorize_commits.py --from-tag v3.0.3 --to-tag v3.0
python categorize_commits.py --input-file /tmp/commits.txt --output-format text
```
**Options:**
- `--from-tag`, `--to-tag`: Git tags/branches to compare
- `--input-file`: Read commits from a file (format: git log --pretty=format:'%H|%s|%b')
- `--output-format`: `markdown` (default) or `text`
- `--verbose`: Show detailed output
### 2. `generate_changelog.py`
Generates a changelog from git merge commits (pull requests). Uses second parent of merge commits to get PR-specific commits.
**Usage:**
```bash
python generate_changelog.py --from-tag v3.0.3 --to-tag v3.0 --output changelog.md
```
**Options:**
- `--from-tag`, `--to-tag`: Git tags/branches to compare
- `--output`, `-o`: Output file (default: changelog.md)
- `--verbose`: Show progress
### 3. `generate_release_notes.py**
Generates formatted release notes using GitHub API (via `gh` CLI). Provides automatic categorization based on PR labels and titles, with optional manual mapping.
**Usage:**
```bash
python generate_release_notes.py --from-tag v3.0.3 --to-tag v3.0 --output release-notes.md
python generate_release_notes.py --from-tag v3.0.3 --to-tag v3.0 --config category_mapping.json --verbose
```
**Options:**
- `--from-tag`, `--to-tag`: Git tags/branches to compare
- `--output`, `-o`: Output file for release notes (default: release-notes.md)
- `--changelog`, `-c`: Output file for detailed changelog
- `--config`: JSON file with manual category mapping (see example)
- `--verbose`: Show detailed progress
### 4. `fetch_prs.py` (legacy)
Legacy script that was used for ProxySQL 3.0.4. Consider using `generate_release_notes.py` instead.
### 5. `gen_release_notes.py` (legacy)
Legacy script with hardcoded mapping for 3.0.4.
## Category Mapping
For `generate_release_notes.py`, you can provide a JSON file with manual categorization overrides. The format can be:
```json
{
"5259": ["Bug Fixes", "MySQL"],
"5257": ["New Features", "MySQL"],
"5258": "Documentation"
}
```
Each key is a PR number. The value can be:
- A string: category name (e.g., `"Documentation"`)
- An array: `[category, subcategory]` (e.g., `["New Features", "PostgreSQL"]`)
See `category_mapping.example.json` for a complete example.
## Complete Workflow with LLM Integration
For high-quality release notes like ProxySQL v3.0.3, use the orchestrated workflow:
### Option 1: Automated Orchestration (Recommended)
```bash
# Run the complete workflow
python orchestrate_release.py --from-tag v3.0.3 --to-tag v3.0.4 --output-dir release-data --verbose
# This generates:
# - release-data/pr-data-3.0.4.json # PR details from GitHub
# - release-data/pr-summary-3.0.4.md # PR summary
# - release-data/structured-notes-3.0.4.md # Commit-level data
# - release-data/llm-prompt-3.0.4.md # Complete prompt for LLM
# - release-data/workflow-summary.md # Instructions summary
```
### Option 2: Manual Steps
#### Step 1: Prepare the environment
Ensure you're on the correct branch and have all tags fetched:
```bash
git fetch --tags
git checkout v3.0 # or your release branch
```
#### Step 2: Collect PR data for LLM analysis
```bash
python collect_pr_data.py --from-tag v3.0.3 --to-tag v3.0.4 --output pr-data.json --verbose
```
#### Step 3: Generate structured analysis files
```bash
python generate_structured_notes.py --input pr-data.json --output structured-notes.md --verbose
python categorize_commits.py --from-tag v3.0.3 --to-tag v3.0.4 --output-format markdown > commit-categories.md
```
#### Step 4: Provide data to LLM with this prompt:
```markdown
Generate ProxySQL 3.0.4 release notes and changelogs using the provided data files.
Available files:
1. pr-data.json - All PR details from GitHub
2. structured-notes.md - Commit-level organized data
3. commit-categories.md - Commits categorized by type
Requirements:
1. Write DESCRIPTIVE release notes (not just PR titles)
2. Group changes logically (PostgreSQL, MySQL, Monitoring, Bug Fixes, etc.)
3. Use `backticks` around all technical terms
4. Include commit hashes and PR numbers
5. Remove any WIP/skip-ci tags
6. Follow the format of ProxySQL v3.0.3 release notes
Output files:
- ProxySQL-3.0.4-Release-Notes.md
- CHANGELOG-3.0.4-detailed.md
- CHANGELOG-3.0.4-commits.md (optional)
```
### Option 3: Quick Generation (Without LLM)
For basic changelogs without descriptive analysis:
```bash
python generate_release_notes.py --from-tag v3.0.3 --to-tag v3.0.4 --output release-notes.md --changelog detailed-changelog.md --verbose
python generate_changelog.py --from-tag v3.0.3 --to-tag v3.0.4 --output changelog.md
```
## Examples
See the `examples/` directory for output generated for ProxySQL 3.0.4:
- `ProxySQL-3.0.4-Release-Notes.md`: Final release notes
- `CHANGELOG-3.0.4-detailed.md`: Detailed changelog with PR summaries
- `CHANGELOG-3.0.4-commits.md`: Complete list of commits
## Generating Descriptive Release Notes
ProxySQL release notes (see v3.0.3 example) are descriptive, not just collections of PR titles. They:
1. Group related changes under feature categories
2. Describe what each feature/fix does and why it matters
3. Reference PR numbers and commit hashes
4. Use backticks around technical terms
The scripts in this directory help collect data, but the LLM should:
1. Analyze PR descriptions and commit messages
2. Write descriptive paragraphs explaining changes
3. Group related changes logically
4. Apply backtick formatting to technical terms
### Backtick Formatting for Technical Terms
ProxySQL release notes use backticks (`) around technical terms such as:
- Function names: `Read_Global_Variables_from_configfile()`
- Variable names: `wait_timeout`, `cur_cmd_cmnt`
- SQL queries: `SELECT @@version`, `SELECT VERSION()`
- Protocol commands: `COM_PING`, `CLIENT_DEPRECATE_EOF`
- Configuration options: `cache_empty_result=0`
- Metrics: `PgSQL_Monitor_ssl_connections_OK`
The scripts do not automatically apply backtick formatting. When generating final release notes with the LLM, ensure you:
1. Manually add backticks around technical terms
2. Use the LLM's understanding of the codebase to identify what needs formatting
3. Review the final output for consistency
## Tips
1. **GitHub CLI Authentication**: Ensure `gh auth login` has been run and you have permissions to access the repository.
2. **Tag Names**: Use exact tag names (e.g., `v3.0.3`) or branch names (e.g., `HEAD` for current branch).
3. **Manual Review**: Always review the generated notes. Automatic categorization is not perfect.
4. **Customization**: Feel free to modify the categorization keywords in the scripts to match your project's conventions.
## License
These tools are part of the ProxySQL project and follow the same licensing terms.

@ -0,0 +1,162 @@
#!/usr/bin/env python3
"""
Categorize git commits based on keywords.
This script reads git commit messages from a git log range or from a file
and categorizes them based on keyword matching.
Usage:
python categorize_commits.py --from-tag v3.0.3 --to-tag v3.0
python categorize_commits.py --input-file /tmp/commits.txt
"""
import sys
import re
import subprocess
import argparse
# Categories mapping keywords
CATEGORIES = {
'Bug Fix': ['fix', 'bug', 'issue', 'crash', 'vulnerability', 'error', 'wrong', 'incorrect', 'failure', 'broken'],
'New Feature': ['add', 'new', 'support', 'implement', 'feature', 'introduce', 'enable'],
'Improvement': ['improve', 'optimize', 'enhance', 'speed', 'performance', 'better', 'reduce', 'faster', 'efficient'],
'Documentation': ['doc', 'documentation', 'comment', 'doxygen', 'readme'],
'Testing': ['test', 'tap', 'regression', 'validation'],
'Build/Packaging': ['build', 'package', 'makefile', 'cmake', 'docker', 'opensuse', 'deb', 'rpm'],
'Refactoring': ['refactor', 'cleanup', 'restructure', 'reorganize', 'rename'],
'Security': ['security', 'injection', 'vulnerability', 'secure', 'sanitize'],
'Monitoring': ['monitor', 'metric', 'log', 'warning', 'alert'],
'PostgreSQL': ['postgresql', 'pgsql', 'pg'],
'MySQL': ['mysql'],
}
def categorize_commit(message):
"""Categorize a commit message based on keyword matching."""
msg_lower = message.lower()
scores = {}
for cat, keywords in CATEGORIES.items():
score = 0
for kw in keywords:
if re.search(r'\b' + re.escape(kw) + r'\b', msg_lower):
score += 1
if score:
scores[cat] = score
if scores:
# return max score category
return max(scores.items(), key=lambda x: x[1])[0]
return 'Other'
def get_git_log(from_tag, to_tag):
"""Get git log between two tags/branches in a parsable format."""
cmd = f"git log {from_tag}..{to_tag} --no-merges --pretty=format:'%H|%s|%b'"
try:
output = subprocess.check_output(cmd, shell=True, text=True).strip()
return output.split('\n') if output else []
except subprocess.CalledProcessError as e:
print(f"Error running git log: {e}", file=sys.stderr)
sys.exit(1)
def read_commits_from_file(filename):
"""Read commits from a file with the same format as git log output."""
with open(filename, 'r') as f:
lines = f.readlines()
return lines
def parse_commits(lines):
"""Parse commit lines in format 'hash|subject|body'."""
commits = []
current = []
for line in lines:
line = line.rstrip('\n')
if line and '|' in line and line.count('|') >= 2:
if current:
commits.append(''.join(current))
current = []
current.append(line + '\n')
if current:
commits.append(''.join(current))
parsed = []
for commit in commits:
parts = commit.split('|', 2)
if len(parts) < 3:
continue
hash_, subject, body = parts[0], parts[1], parts[2]
parsed.append((hash_, subject, body))
return parsed
def main():
parser = argparse.ArgumentParser(
description='Categorize git commits based on keywords.',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
%(prog)s --from-tag v3.0.3 --to-tag v3.0
%(prog)s --input-file /tmp/commits.txt
%(prog)s --from-tag v3.0.3 --to-tag v3.0 --output-format markdown
"""
)
parser.add_argument('--from-tag', help='Starting tag/branch (e.g., v3.0.3)')
parser.add_argument('--to-tag', help='Ending tag/branch (e.g., v3.0)')
parser.add_argument('--input-file', help='Input file with git log output')
parser.add_argument('--output-format', choices=['text', 'markdown'], default='markdown',
help='Output format (default: markdown)')
parser.add_argument('--verbose', '-v', action='store_true', help='Verbose output')
args = parser.parse_args()
if not (args.from_tag and args.to_tag) and not args.input_file:
parser.error('Either --from-tag and --to-tag must be specified, or --input-file')
if args.from_tag and args.to_tag:
lines = get_git_log(args.from_tag, args.to_tag)
else:
lines = read_commits_from_file(args.input_file)
commits = parse_commits(lines)
categorized = {}
for hash_, subject, body in commits:
full_msg = subject + ' ' + body
cat = categorize_commit(full_msg)
categorized.setdefault(cat, []).append((hash_, subject, body))
# Output
if args.output_format == 'markdown':
for cat in sorted(categorized.keys()):
print(f'\n## {cat}\n')
for hash_, subject, body in categorized[cat]:
print(f'- {hash_[:8]} {subject}')
if body.strip():
for line in body.strip().split('\n'):
if line.strip():
print(f' {line.strip()}')
print()
print('\n---\n')
for cat in sorted(categorized.keys()):
print(f'{cat}: {len(categorized[cat])}')
else:
# plain text output
for cat in sorted(categorized.keys()):
print(f'\n=== {cat} ===')
for hash_, subject, body in categorized[cat]:
print(f' {hash_[:8]} {subject}')
if body.strip() and args.verbose:
for line in body.strip().split('\n'):
if line.strip():
print(f' {line.strip()}')
print()
print('\nSummary:')
for cat in sorted(categorized.keys()):
print(f' {cat}: {len(categorized[cat])}')
if __name__ == '__main__':
main()

@ -0,0 +1,28 @@
{
"5259": ["Bug Fixes", "MySQL"],
"5257": ["New Features", "MySQL"],
"5258": "Documentation",
"5254": ["New Features", "PostgreSQL"],
"5237": ["New Features", "PostgreSQL"],
"5250": ["Bug Fixes", "MySQL"],
"5251": "Testing",
"4889": ["New Features", "MySQL"],
"5247": ["Bug Fixes", "Configuration"],
"5245": "Documentation",
"4901": ["New Features", "MySQL"],
"5199": ["Bug Fixes", "Monitoring"],
"5241": "Testing",
"5232": ["Bug Fixes", "MySQL"],
"5240": ["Improvements", "Performance"],
"5225": ["Improvements", "Performance"],
"5230": "Build/Packaging",
"5115": "Documentation",
"5229": "Documentation",
"5215": "Documentation",
"5203": ["New Features", "MySQL"],
"5207": "Testing",
"5200": "Other",
"5198": "Testing",
"5228": ["New Features", "Monitoring"],
"5226": ["Improvements", "Performance"]
}

@ -0,0 +1,128 @@
#!/usr/bin/env python3
"""
Collect PR data for release notes generation.
This script fetches all PRs between two git tags and saves their details
to a JSON file for analysis and release notes generation.
"""
import subprocess
import re
import json
import argparse
import sys
from datetime import datetime
def run(cmd):
"""Run shell command and return output."""
return subprocess.check_output(cmd, shell=True, text=True).strip()
def get_merge_commits(from_tag, to_tag):
"""Get merge commits between two tags."""
merge_log = run(f"git log {from_tag}..{to_tag} --merges --pretty=format:'%H|%s'")
lines = merge_log.split('\n')
prs = []
for line in lines:
if 'Merge pull request' in line:
hash_, subject = line.split('|', 1)
match = re.search(r'#(\d+)', subject)
if match:
pr_num = match.group(1)
prs.append((pr_num, hash_, subject))
return prs
def fetch_pr_details(pr_numbers, verbose=False):
"""Fetch PR details using GitHub CLI."""
pr_details = []
for pr_num in pr_numbers:
if verbose:
print(f"Fetching PR #{pr_num}...")
try:
data = run(f'gh pr view {pr_num} --json title,body,number,url,labels,state,createdAt,mergedAt,author')
pr = json.loads(data)
# Also get commits in PR if possible
try:
commits_data = run(f'gh pr view {pr_num} --json commits')
commits_json = json.loads(commits_data)
pr['commits'] = commits_json.get('commits', [])
except subprocess.CalledProcessError:
pr['commits'] = []
pr_details.append(pr)
except subprocess.CalledProcessError as e:
print(f"Failed to fetch PR {pr_num}: {e}", file=sys.stderr)
continue
return pr_details
def main():
parser = argparse.ArgumentParser(
description='Collect PR data for release notes generation.',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
%(prog)s --from-tag v3.0.3 --to-tag v3.0 --output pr-data.json
%(prog)s --from-tag v3.0.3 --to-tag HEAD --verbose
"""
)
parser.add_argument('--from-tag', required=True, help='Starting tag/branch')
parser.add_argument('--to-tag', required=True, help='Ending tag/branch')
parser.add_argument('--output', '-o', default='pr-data.json', help='Output JSON file')
parser.add_argument('--verbose', '-v', action='store_true', help='Verbose output')
args = parser.parse_args()
if args.verbose:
print(f"Collecting PR data from {args.from_tag} to {args.to_tag}")
# Get merge commits
prs = get_merge_commits(args.from_tag, args.to_tag)
pr_numbers = [pr_num for pr_num, _, _ in prs]
if args.verbose:
print(f"Found {len(pr_numbers)} PR merges")
# Fetch PR details
pr_details = fetch_pr_details(pr_numbers, args.verbose)
# Add git hash to each PR
for pr in pr_details:
for pr_num, hash_, subject in prs:
if str(pr['number']) == pr_num:
pr['merge_hash'] = hash_
pr['merge_subject'] = subject
break
# Save to JSON
with open(args.output, 'w') as f:
json.dump(pr_details, f, indent=2, default=str)
if args.verbose:
print(f"PR data saved to {args.output}")
print(f"Collected {len(pr_details)} PRs")
# Also generate a summary markdown for quick review
summary_file = args.output.replace('.json', '-summary.md')
with open(summary_file, 'w') as f:
f.write(f'# PR Summary: {args.from_tag} to {args.to_tag}\n\n')
f.write(f'Total PRs: {len(pr_details)}\n\n')
for pr in pr_details:
f.write(f'## PR #{pr["number"]}: {pr["title"]}\n')
f.write(f'- URL: {pr["url"]}\n')
f.write(f'- Author: {pr["author"]["login"]}\n')
f.write(f'- Labels: {", ".join([l["name"] for l in pr.get("labels", [])])}\n')
f.write(f'- Merge hash: {pr.get("merge_hash", "N/A")[:8]}\n')
if pr.get('body'):
# Take first 200 chars of body
body_preview = pr['body'][:200].replace('\n', ' ')
f.write(f'- Preview: {body_preview}...\n')
f.write('\n')
if args.verbose:
print(f"Summary saved to {summary_file}")
if __name__ == '__main__':
main()

@ -0,0 +1,192 @@
# ProxySQL 3.0.4 Detailed Changelog
This changelog includes all individual commits since ProxySQL 3.0.3.
- e73ba2b6 Fix changing server defaults for 'reg_test_unexp_ping_pkt'
- Fixed was changing server defaults ('max_allowed_packet') in the same
- 7efc85ce Fix TAP 'groups.json' file syntax
- b9cf5b16 Remove empty spaces at end of lines for TAP tests 'groups.json'
- 91e20648 Fixed an issue where cur_cmd_cmnt was shared across threads
- 54e2b3e6 Add new test 'reg_test_unexp_ping_pkt-t' to TAP groups
- 6fea828e Improve logging in unexpected COM_PING packet handling
Logging messages now include 'client address', 'session status' and
- 503b0975 Add new TAP test exercising unexpected COM_PING packets logic
Also fixes 'random_string' utility function for large strings, avoiding
- d0e88599 Add special handling for unexpected COM_PING packets
Implements a workaround for the handling of unexpected 'COM_PING'
- cf8cbfd8 Document coredump_filters feature (issue #5213)
Add comprehensive documentation for the ondemand core dump generation
- efe0d4fe Add extensive doxygen documentation for vacuum_stats and stats_pgsql_stat_activity
This commit documents:
- de214cb5 Add comprehensive documentation for "Closing killed client connection" warnings
- febb650a Added Multiple Keep Comments test
- 6966b79d Add documentation for query rules capture groups and backreferences
- 5e75264b Updated TAP test
- 5b3805ad Refactored comment handling
* Removed is_cmd (/*!) handling
- e70fcbf0 * Add dedicated handling for double-quoted PostgreSQL identifiers * Added crash payload testing * Fixed unterminated comments handling
- fd53642f Added pgsql-query_digests_stages_test-t to groups.json
- f9b9ede6 Fixed TAP test
- 39728b2d Add missing pgsql_tokenizer.cpp
- 1b16a393 Added TAP test
- 42864e88 Improved Tokenizer for PostgreSQL
- Added `process_pg_typecast()` to handle PostgreSQL type cast syntax (::)
- 66119b74 Add comments to select @@version queries to bypass ProxySQL interception
ProxySQL now responds directly to `select @@version` queries, which prevents
- 2987242d Fix cache_empty_result=0 not caching non-empty resultsets (issue #5248)
The `cache_empty_result` field in query rules has three possible values:
- d1b003a0 Added TAP test to groups.json
- 0b2bc1bf Fix SQL injection vulnerability in Read_Global_Variables_from_configfile
Replace sprintf-based SQL query construction with prepared statements using
- 0e7b5e2b Added TAP test
- fae283cf Add SSL and non-SSL connection OK metrics for PostgreSQL monitor connections Adds two new metrics, ssl_connections_OK and non_ssl_connections_OK, to improve visibility into PostgreSQL monitor connection status.
- a2068286 Add test_load_from_config_prefix_stripping-t to test groups
Adds the new prefix stripping validation test to the default-g4 test group
- 6c97d3d2 Add extensive Doxygen documentation for ProxySQL_Config and Read_Global_Variables_from_configfile
This commit adds detailed Doxygen documentation for:
- 7ebdf561 Fix automatic prefix stripping to work with libconfig lookup
The previous implementation stripped the prefix before calling
- b4683569 Add automatic prefix stripping for mysql_variables, pgsql_variables, and admin_variables config parsing
When users mistakenly include the module prefix (e.g., mysql-log_unhealthy_connections)
- ec1247f2 Add Doxygen docs for MySQL_Data_Stream::check_data_flow()
- 4044a407 Skip bidirectional data check for permanent fast-forward sessions
Allow permanent fast-forward sessions (SESSION_FORWARD_TYPE_PERMANENT)
- 689bece9 Add CONTRIBUTING.md guide for community contributors
- Comprehensive contribution guidelines for ProxySQL
- f5079037 Added nested comments support for PostgreSQL
- 895c814c Added utility functions to support pgsql query digest testing
- 285fb1b4 Add PostgreSQL dialect support: dollar-quoted strings, identifier quoting, and dialect-specific comment rules
This change introduces PostgreSQL-aware tokenization by adding support for dollar-quoted strings, PostgreSQLs double-quoted identifiers, and its comment rules. The tokenizer now correctly parses $$…$$ and $tag$…$tag$, treats " as an identifier delimiter in PostgreSQL, disables MySQL-only # comments, and accepts -- as a comment starter without requiring a trailing space. All new behavior is fully isolated behind the dialect flag to avoid impacting MySQL parsing.
- 2b900b34 Update test groups configuration
- 5a7b2218 Fix metrics collection for wait_timeout counters
The get_status_variable() function was only scanning worker threads
- fbf5f2d7 Improve wait_timeout warning messages with detailed connection information
Enhance logging clarity:
- dc4694d6 Refactor idle session scanning and improve test precision
Code improvements:
- 0c5e75a0 Fix wait_timeout timeout calculations and add proper newline characters
Key improvements:
- 0c9c938d Fix mysql-set_wait_timeout-t.cpp compilation and JSON testing
- Replace <json/json.h> include with <json.hpp> and proper nlohmann namespace
- 6539587c test: Add comprehensive wait_timeout JSON validation tests
- Add JSON parsing helper for PROXYSQL INTERNAL SESSION responses
- df515f91 session: Add wait_timeout to proxysql internal session JSON
- Include wait_timeout value in session JSON output for monitoring/debugging
- 0a9dc9dd session: Add input validation for client wait_timeout with silent clamping
- Add range validation for client SET wait_timeout commands
- 86cc7cd3 session: Fix wait_timeout member variable declaration and usage
- Add wait_timeout member variable declaration to Base_Session class
- 9df7407f Add fast forward replication deprecate EOF test and update test infrastructure
- Add comprehensive test `fast_forward_switch_replication_deprecate_eof.cpp` that validates
- 5485bb02 Improve fast forward replication CLIENT_DEPRECATE_EOF validation
Enhance the match_ff_req_options function to better handle CLIENT_DEPRECATE_EOF
- 7205f424 Add SSL support for backend connections in PGSQL monitor
- 9c0e14a5 Replace rand() with lock-free Xoshiro128++ PRNG
- 1251e4d5 Add Xoshiro128++ pseudo-random number generator to replace rand()
- 291e5f02 Add AI disclaimer to ENHANCEMENT-OPPORTUNITIES.md
- 54091905 Restructure AI-generated documentation with verification framework
- Move AI-generated docs to doc/ai-generated/ folder for clear separation
- 652acb5d cont
- 39f94b0f simplify for pr
- 644174a3 Add reg_test_4855_affected_rows_ddl to TAP test groups
Include the regression test for issue 4855 in the g1 test group configuration
- f5128d76 Fix TAP test for issue 4855 affected rows DDL bug
Improve the TAP test to be SQLite-compatible and comprehensive:
- a54a0ad8 Fix TAP test to use correct TAP framework functions
- Replace made-up fail()/pass() functions with ok() and diag()
- 9df1e475 Clean up: Remove incorrect standalone test file
- 19c635b7 Fix TAP test to use proper ProxySQL connection patterns
- Use CommandLine cl object with getEnv() for configuration
- b6b60e1a Add proper TAP test for issue 4855 fix
- TAP test connects to ProxySQL Admin interface on port 6032
- bf4b1d92 Update TAP test for issue 4855 with standalone version
- Updated main TAP test to remove proxy_sqlite3 wrapper dependency
- a577491f Refactor issue 4855 fix: Use sqlite3_total_changes64 difference approach
PROBLEM:
- 05960b5d Fix issue 4855: Reset affected_rows to 0 for DDL queries in Admin interface
Problem:
- d0ef6918 Add mysql-select_version_without_backend-t to TAP group
- 0c3582f1 Add TCP keepalive warnings test to TAP test groups
Added reg_test_5212_tcp_keepalive_warnings-t to the default test group
- 1a48aadf Code cleanup
- bce71a95 add opensuse16 packaging
- 9a55e974 docs: Add comprehensive Doxygen documentation for GTID refactoring
- Document addGtidInterval() function with parameter details and reconnection behavior
- 6a700cd5 Add comprehensive Doxygen documentation to get_matching_lines_from_filename()
- Document purpose, parameters, return values, and behavior
- 19176909 Rename TAP test file to remove mysql- prefix
- Rename mysql-reg_test_5212_tcp_keepalive_warnings-t.cpp to reg_test_5212_tcp_keepalive_warnings-t.cpp
- b1a1930e Add working TAP test for TCP keepalive warnings (issue #5212)
- Implement get_matching_lines_from_filename() with efficient queue-based processing
- e54e3c8d Add TAP test for TCP keepalive warnings with line-limited get_matching_lines()
- Add TAP test mysql-reg_test_5212_tcp_keepalive_warnings-t.cpp to verify TCP keepalive warnings
- 6ed82ef8 Fix crash in TCP keepalive warnings for issue #5212
- Fix crash by using get_variable_int() instead of get_variable_string() for boolean use_tcp_keepalive variable
- cf454b8e Add TCP keepalive warnings for issue #5212
- Add warnings in flush_mysql_variables___database_to_runtime() when mysql-use_tcp_keepalive=false
- 27923c19 Use emplace instead of insert
- 50c60284 gtid: Refactor reconnect logic & prevent `events_count` reset
- This patch was originally added by commit 0a70fd5 and
- 187edfe1 Change assert to warning
- c0f99c0e Refactor: Improved Prepared-Statement Cache Design (Lock-Free Hot Path) #5211
Concurrency and Memory Management
- 49899601 chore: Ignore GEMINI.md
This file is generated by the Gemini CLI and is not part of the project.
- 546c1012 docs: Add TAP test writing guide for ProxySQL.
- 83300374 Use right user in fast_forward_grace_close
- 140745a9 Change default schema in fast_forward_grace_close
- 494dbf37 Minor enhancement in TAP fast_forward_grace_close
Added `CREATE DATABASE IF NOT EXISTS test`
- 41b0e96c create missing database test in tap test
- 952bd39f cleanup redundant group folders
- 458ff778 regroup tests to balace group test time
- ee824c0b Removing debugging code
- 3329a671 Add extensive documentation for fast forward grace close feature
- 846c4de5 Replace spaces with tabs in fast_forward_grace_close.cpp
- dd60d266 Add fast_forward_grace_close test to groups.json
- ae939666 Add TAP test for fast forward grace close feature
- Rename and modify test to use MySQL C API mysql_binlog_* functions
- 44aa606c Implement fast forward grace close feature to prevent data loss
Problem: In fast forward mode, ProxySQL forwards packets directly from client
- d8444472 Replaced use of the generic write_generic() helper with direct packet construction for selected PostgreSQL protocol messages to reduce overhead and improve performance.
- ba664785 bump version to 3.0.4 at the beginning of the development cycle
- e744c2bb Optimize transaction command parsing to avoid unnecessary tokenization
Previously, the parser always tokenized the full command, even when we only
- 7c665b9f Checking the data stream on both ends doesnt apply to frontend connections, since response data is buffered during extended queries. Fixed TAP test
- 24e02e95 Changing monitor ping poll() timeout to 10ms
- 6a80c3f2 Implemented explicit task memory ownership management in Monitor_Poll.
- 50fae0b0 Current check on file descriptors (fd) is not reliable or necessary
- 1c3c4295 Improve ping accuracy
- Introduced batching for ping task dispatch (default: 30 servers per batch)
- fa74de3c Add delay to let ProxySQL process mysql_stmt_close()
- 7a3a5c71 Optimize hot path: replace std::string with char[] to avoid heap
- 61ba1824 Introduce inline functions for efficient ASCII whitespace detection and uint32-to-string conversion
- 9eb934e7 Buffer response until Extended Query frame completes; send early only if resultset threshold is reached.
- 9fa3d75f Backport PQsendPipelineSync from PostgreSQL 17 and update code to use it
- Backport PQsendPipelineSync to PostgreSQL 16.3, enabling pipeline
- 45cb1673 .aider file
- 3c08cc5f Syntax Convention library tweak, it now provides loarders and merely needs to have it's script dir in path. The sub agents when loaded may then go and popualte and additional prompt conventions they need as they areloaded.
- 36ebee53 purge
- d946b6e3 updates from internal docs
- 89849d0d patch
- 5f15d971 patch
- 4dad6424 doc update
- 9e337ce7 TEST-PIPELINE.md doc
- e2e8d8c4 doc cleanup - remove agenetic verbage and mermaid patch
- ab2a3292 Visual Guide v0.1 checkin
- fbbefb91 initial arch entry point for agents
- a753e64c initial agent definitions
- dd0e8529 Git ignore update
- d24b81e6 Initial checkin - Claude and npl convention readmes
- 4a10ce65 Add TAP test for select @@version without backend
Signed-off-by: Wazir Ahmed <wazir@proxysql.com>
- 97eb19c0 fix: Close idle session based on effective timeout
- effective_timeout = min (mysql-wait_timeout, session-wait_timeout)
- fd9ce84b Add TAP test for session wait timeout
Signed-off-by: Wazir Ahmed <wazir@proxysql.com>
- 1545fd24 Removed range check.
- d7ec497a Now taking effective timeout taking as minimum of global timeout and session timeout
- 2b5bd110 Using mysql_thread___server_version directly
- b589e58d Using global mysql version
- abe16e66 Parsing and setting wait timeout for session
- df287b20 Added handling of SELECT @@version and SELECT VERSION() without backend

@ -0,0 +1,68 @@
# ProxySQL 3.0.4 Detailed Changelog
This changelog lists all pull requests merged since ProxySQL 3.0.3.
## Bug Fixes
- **PR #5259**: [Fix: Make cur_cmd_cmnt thread-safe](https://github.com/sysown/proxysql/pull/5259)
- **PR #5258**: [Documentation additions and bug fix for vacuum_stats()](https://github.com/sysown/proxysql/pull/5258)
- ## Summary
- **PR #5250**: [Fix cache_empty_result=0 not caching non-empty resultsets (issue #5248)](https://github.com/sysown/proxysql/pull/5250)
- ## Summary
- **PR #5247**: [Fix: Automatic prefix stripping for mysql_variables, pgsql_variables, and admin_variables config parsing](https://github.com/sysown/proxysql/pull/5247)
- ## Summary
- **PR #5199**: [Fix artificially high ping latency in MySQL backend monitoring](https://github.com/sysown/proxysql/pull/5199)
- ### Changes Made
- **PR #5232**: [Fix issue 4855: Incorrect affected rows reporting for DDL queries](https://github.com/sysown/proxysql/pull/5232)
- ## Summary
- **PR #5228**: [Add TCP keepalive warnings when disabled (issue #5212)](https://github.com/sysown/proxysql/pull/5228)
- ## Summary
## Documentation
- **PR #5245**: [Permanent FF sessions + check_data_flow docs](https://github.com/sysown/proxysql/pull/5245)
- ### Features
## Improvements
- **PR #5240**: [Improve fast forward replication CLIENT_DEPRECATE_EOF validation (closes #5062)](https://github.com/sysown/proxysql/pull/5240)
- ## Summary
## New Features
- **PR #5257**: [Add special handling for unexpected COM_PING packets](https://github.com/sysown/proxysql/pull/5257)
- ## Description
- **PR #5254**: [Add PostgreSQL-Specific Tokenizer for Query Digest Generation](https://github.com/sysown/proxysql/pull/5254)
- ## Summary
- **PR #5237**: [Add SSL support for backend connections in PGSQL monitor](https://github.com/sysown/proxysql/pull/5237)
- PostgreSQL monitor now **supports client certificates** for SSL/TLS connections to backend servers.
- **PR #5251**: [Add comments to select @@version queries to bypass ProxySQL interception](https://github.com/sysown/proxysql/pull/5251)
- ## Summary
- **PR #4889**: [Added handling of SELECT @@version and SELECT VERSION() without backend](https://github.com/sysown/proxysql/pull/4889)
- Added handling of SELECT @@version and SELECT VERSION() without backend.
- **PR #5241**: [Add fast forward replication deprecate EOF test and update test infrastructure (closes #5062)](https://github.com/sysown/proxysql/pull/5241)
- ## Summary
- **PR #5230**: [add opensuse16 packaging](https://github.com/sysown/proxysql/pull/5230)
- fixes issue #5183
- **PR #5115**: [[skip-ci] V3.0.agentics - Add Claude Code Agent Definitions and Architecture Documentation](https://github.com/sysown/proxysql/pull/5115)
- ## Summary
- **PR #5229**: [docs: Add comprehensive Doxygen documentation for GTID refactoring](https://github.com/sysown/proxysql/pull/5229)
- ## Summary
- **PR #5215**: [Add TAP test writing guide and GitHub automation improvements](https://github.com/sysown/proxysql/pull/5215)
- ## Summary
- **PR #5203**: [Implement Fast Forward Grace Close Feature to Prevent Data Loss](https://github.com/sysown/proxysql/pull/5203)
- ## Summary
## Other
- **PR #4901**: [[WIP] Setting client side wait_timeout](https://github.com/sysown/proxysql/pull/4901)
- Setting client side wait_timeout current ProxySQL ignores wait_timeout set by the client. If a client specifies a value for wait_timeout , ProxySQL should terminate the client connection (but not the backend connection) when said timeout is reached instead of ProxySQLs global mysql-wait_timeout . As a further enhancement, the wait timeout specified by the client shouldnt exceed ProxySQLs wait_timeout . The implementation also requires adequate TAP test to verify the functionality.
- **PR #5200**: [bump version to 3.0.4 at the beginning of the development cycle](https://github.com/sysown/proxysql/pull/5200)
## Refactoring
- **PR #5225**: [Refactored Prepared-Statement Cache Design (Lock-Free Hot Path) - Part 2](https://github.com/sysown/proxysql/pull/5225)
- ### Summary
- **PR #5226**: [gtid: Refactor reconnect logic & prevent `events_count` reset](https://github.com/sysown/proxysql/pull/5226)
- - This patch was originally added by commit 0a70fd5 and reverted by 8d1b5b5, prior to the release of `v3.0.3`.
## Testing
- **PR #5198**: [Add delay to let ProxySQL process mysql_stmt_close()](https://github.com/sysown/proxysql/pull/5198)
- Add delay to `test_prepare_statement_memory_usage-t` allowing ProxySQL time to process `mysql_stmt_close()`.
- **PR #5207**: [regroup tests to balance group test time](https://github.com/sysown/proxysql/pull/5207)
- Test groups have unbalanced test run duration,

@ -0,0 +1,70 @@
# ProxySQL 3.0.4 Release Notes
This release of ProxySQL 3.0.4 includes new features, bug fixes, and improvements across PostgreSQL, MySQL, monitoring, and configuration management.
Release commit: `faa64a570d19fe35af43494db0babdee3e3cdc89`
## New Features:
### PostgreSQL:
- Add PostgreSQL-Specific Tokenizer for Query Digest Generation (0f7ff1f3, #5254)
- Add SSL support for backend connections in PGSQL monitor (d1b003a0, #5237)
### MySQL:
- Add special handling for unexpected COM_PING packets (e73ba2b6, #5257)
- Added handling of SELECT @@version and SELECT VERSION() without backend (0d55ab5e, #4889)
- [WIP] Setting client side wait_timeout (2b900b34, #4901)
- Implement Fast Forward Grace Close Feature to Prevent Data Loss (83300374, #5203)
### Monitoring:
- Add TCP keepalive warnings when disabled (issue #5212) (0c3582f1, #5228)
## Bug Fixes:
### MySQL:
- Fix: Make cur_cmd_cmnt thread-safe (91e20648, #5259)
- Fix cache_empty_result=0 not caching non-empty resultsets (issue #5248) (2987242d, #5250)
- Fix issue 4855: Incorrect affected rows reporting for DDL queries (d188715a, #5232)
### Monitoring:
- Fix artificially high ping latency in MySQL backend monitoring (24e02e95, #5199)
### Security:
- Fix SQL injection vulnerability in Read_Global_Variables_from_configfile (automatic prefix stripping) (0b2bc1bf, #5247)
## Improvements:
### Performance:
- Improve fast forward replication CLIENT_DEPRECATE_EOF validation (closes #5062) (5485bb02, #5240)
- Refactored Prepared-Statement Cache Design (Lock-Free Hot Path) - Part 2 (9c0e14a5, #5225)
- gtid: Refactor reconnect logic & prevent `events_count` reset (20975923, #5226)
## Documentation:
- Documentation additions and bug fix for vacuum_stats() (cf8cbfd8, #5258)
- Permanent FF sessions + check_data_flow docs (ec1247f2, #5245)
- [skip-ci] V3.0.agentics - Add Claude Code Agent Definitions and Architecture Documentation (291e5f02, #5115)
- docs: Add comprehensive Doxygen documentation for GTID refactoring (9a55e974, #5229)
- Add TAP test writing guide and GitHub automation improvements (49899601, #5215)
## Testing:
- Add comments to select @@version queries to bypass ProxySQL interception (66119b74, #5251)
- Add fast forward replication deprecate EOF test and update test infrastructure (closes #5062) (9df7407f, #5241)
- Add delay to let ProxySQL process mysql_stmt_close() (fa74de3c, #5198)
- regroup tests to balance group test time (41b0e96c, #5207)
## Build/Packaging:
- add opensuse16 packaging (bce71a95, #5230)
## Other Changes:
- bump version to 3.0.4 at the beginning of the development cycle (ba664785, #5200)
## Hashes
The release commit is: `faa64a570d19fe35af43494db0babdee3e3cdc89`

@ -0,0 +1,62 @@
# ProxySQL 3.0.4 Release Notes
This release of ProxySQL 3.0.4 includes new features, bug fixes, and improvements across PostgreSQL, MySQL, monitoring, and configuration management.
Release commit: faa64a57 (faa64a57)
## New Features:
### PostgreSQL:
- Add PostgreSQL-Specific Tokenizer for Query Digest Generation (0f7ff1f3, #5254)
- Add SSL support for backend connections in PGSQL monitor (d1b003a0, #5237)
### MySQL:
- Add special handling for unexpected COM_PING packets (e73ba2b6, #5257)
- Added handling of SELECT @@version and SELECT VERSION() without backend (0d55ab5e, #4889)
- [WIP] Setting client side wait_timeout (2b900b34, #4901)
- Implement Fast Forward Grace Close Feature to Prevent Data Loss (83300374, #5203)
### Monitoring:
- Add TCP keepalive warnings when disabled (issue #5212) (0c3582f1, #5228)
## Bug Fixes:
### MySQL:
- Fix: Make cur_cmd_cmnt thread-safe (91e20648, #5259)
- Fix cache_empty_result=0 not caching non-empty resultsets (issue #5248) (2987242d, #5250)
- Fix issue 4855: Incorrect affected rows reporting for DDL queries (d188715a, #5232)
### Monitoring:
- Fix artificially high ping latency in MySQL backend monitoring (24e02e95, #5199)
### Configuration:
- Fix: Automatic prefix stripping for mysql_variables, pgsql_variables, and admin_variables config parsing (0b2bc1bf, #5247)
## Improvements:
### Performance:
- Improve fast forward replication CLIENT_DEPRECATE_EOF validation (closes #5062) (5485bb02, #5240)
- Refactored Prepared-Statement Cache Design (Lock-Free Hot Path) - Part 2 (9c0e14a5, #5225)
- gtid: Refactor reconnect logic & prevent `events_count` reset (20975923, #5226)
## Documentation:
- Documentation additions and bug fix for vacuum_stats() (cf8cbfd8, #5258)
- Permanent FF sessions + check_data_flow docs (ec1247f2, #5245)
- [skip-ci] V3.0.agentics - Add Claude Code Agent Definitions and Architecture Documentation (291e5f02, #5115)
- docs: Add comprehensive Doxygen documentation for GTID refactoring (9a55e974, #5229)
- Add TAP test writing guide and GitHub automation improvements (49899601, #5215)
## Testing:
- Add comments to select @@version queries to bypass ProxySQL interception (66119b74, #5251)
- Add fast forward replication deprecate EOF test and update test infrastructure (closes #5062) (9df7407f, #5241)
- Add delay to let ProxySQL process mysql_stmt_close() (fa74de3c, #5198)
- regroup tests to balance group test time (41b0e96c, #5207)
## Build/Packaging:
- add opensuse16 packaging (bce71a95, #5230)
## Other Changes:
- bump version to 3.0.4 at the beginning of the development cycle (ba664785, #5200)
## Hashes
The release commit is: `faa64a570d19fe35af43494db0babdee3e3cdc89`

@ -0,0 +1,38 @@
# ProxySQL 3.0.4 Release Notes
ProxySQL 3.0.4 includes numerous bug fixes, improvements, and new features.
## New Features
- Add special handling for unexpected COM_PING packets [#5257](https://github.com/sysown/proxysql/pull/5257)
- Add PostgreSQL-Specific Tokenizer for Query Digest Generation [#5254](https://github.com/sysown/proxysql/pull/5254)
- Add SSL support for backend connections in PGSQL monitor [#5237](https://github.com/sysown/proxysql/pull/5237)
- Add comments to select @@version queries to bypass ProxySQL interception [#5251](https://github.com/sysown/proxysql/pull/5251)
- Added handling of SELECT @@version and SELECT VERSION() without backend [#4889](https://github.com/sysown/proxysql/pull/4889)
- Add fast forward replication deprecate EOF test and update test infrastructure (closes #5062) [#5241](https://github.com/sysown/proxysql/pull/5241)
- add opensuse16 packaging [#5230](https://github.com/sysown/proxysql/pull/5230)
- [skip-ci] V3.0.agentics - Add Claude Code Agent Definitions and Architecture Documentation [#5115](https://github.com/sysown/proxysql/pull/5115)
- docs: Add comprehensive Doxygen documentation for GTID refactoring [#5229](https://github.com/sysown/proxysql/pull/5229)
- Add TAP test writing guide and GitHub automation improvements [#5215](https://github.com/sysown/proxysql/pull/5215)
- Implement Fast Forward Grace Close Feature to Prevent Data Loss [#5203](https://github.com/sysown/proxysql/pull/5203)
## Bug Fixes
- Fix: Make cur_cmd_cmnt thread-safe [#5259](https://github.com/sysown/proxysql/pull/5259)
- Documentation additions and bug fix for vacuum_stats() [#5258](https://github.com/sysown/proxysql/pull/5258)
- Fix cache_empty_result=0 not caching non-empty resultsets (issue #5248) [#5250](https://github.com/sysown/proxysql/pull/5250)
- Fix: Automatic prefix stripping for mysql_variables, pgsql_variables, and admin_variables config parsing [#5247](https://github.com/sysown/proxysql/pull/5247)
- Fix artificially high ping latency in MySQL backend monitoring [#5199](https://github.com/sysown/proxysql/pull/5199)
- Fix issue 4855: Incorrect affected rows reporting for DDL queries [#5232](https://github.com/sysown/proxysql/pull/5232)
- Add TCP keepalive warnings when disabled (issue #5212) [#5228](https://github.com/sysown/proxysql/pull/5228)
## Improvements
- Improve fast forward replication CLIENT_DEPRECATE_EOF validation (closes #5062) [#5240](https://github.com/sysown/proxysql/pull/5240)
- Refactored Prepared-Statement Cache Design (Lock-Free Hot Path) - Part 2 [#5225](https://github.com/sysown/proxysql/pull/5225)
- gtid: Refactor reconnect logic & prevent `events_count` reset [#5226](https://github.com/sysown/proxysql/pull/5226)
## Documentation
- Permanent FF sessions + check_data_flow docs [#5245](https://github.com/sysown/proxysql/pull/5245)
## Testing
- Add delay to let ProxySQL process mysql_stmt_close() [#5198](https://github.com/sysown/proxysql/pull/5198)
- regroup tests to balance group test time [#5207](https://github.com/sysown/proxysql/pull/5207)

@ -0,0 +1,182 @@
#!/usr/bin/env python3
import subprocess
import json
import re
import sys
def run(cmd):
return subprocess.check_output(cmd, shell=True, text=True).strip()
# Get merge commits since v3.0.3
merge_log = run("git log v3.0.3..v3.0 --merges --pretty=format:'%H %s'")
lines = merge_log.split('\n')
pr_numbers = []
for line in lines:
if 'Merge pull request' in line:
match = re.search(r'#(\d+)', line)
if match:
pr_numbers.append(match.group(1))
print(f'Found {len(pr_numbers)} PRs')
# Fetch PR details using gh
prs = []
for num in pr_numbers:
try:
data = run(f'gh pr view {num} --json title,body,number,url,labels')
pr = json.loads(data)
prs.append(pr)
except subprocess.CalledProcessError as e:
print(f'Failed to fetch PR {num}: {e}')
continue
# Categorize based on labels and title
categories = {
'Bug Fixes': [],
'New Features': [],
'Improvements': [],
'Documentation': [],
'Testing': [],
'Build/Packaging': [],
'Refactoring': [],
'Security': [],
'Monitoring': [],
'PostgreSQL': [],
'MySQL': [],
'Other': [],
}
def categorize(pr):
title = pr['title'].lower()
labels = [l['name'].lower() for l in pr.get('labels', [])]
# label hints
for label in labels:
if 'bug' in label:
return 'Bug Fixes'
if 'feature' in label:
return 'New Features'
if 'documentation' in label:
return 'Documentation'
if 'test' in label:
return 'Testing'
if 'security' in label:
return 'Security'
if 'refactor' in label:
return 'Refactoring'
if 'improvement' in label:
return 'Improvements'
# title keywords
if any(word in title for word in ['fix', 'bug', 'issue', 'crash', 'vulnerability', 'error']):
return 'Bug Fixes'
if any(word in title for word in ['add', 'new', 'support', 'implement', 'feature', 'introduce', 'enable']):
return 'New Features'
if any(word in title for word in ['improve', 'optimize', 'enhance', 'performance', 'better']):
return 'Improvements'
if any(word in title for word in ['doc', 'documentation', 'doxygen']):
return 'Documentation'
if any(word in title for word in ['test', 'tap', 'regression']):
return 'Testing'
if any(word in title for word in ['build', 'package', 'opensuse', 'docker']):
return 'Build/Packaging'
if any(word in title for word in ['refactor', 'cleanup', 'restructure']):
return 'Refactoring'
if any(word in title for word in ['security', 'injection', 'vulnerability']):
return 'Security'
if any(word in title for word in ['monitor', 'metric', 'log']):
return 'Monitoring'
if any(word in title for word in ['postgresql', 'pgsql', 'pg']):
return 'PostgreSQL'
if any(word in title for word in ['mysql']):
return 'MySQL'
return 'Other'
for pr in prs:
cat = categorize(pr)
categories[cat].append(pr)
# Generate markdown
output = []
output.append('# ProxySQL 3.0.4 Detailed Changelog\n')
output.append('\n')
output.append('This changelog lists all pull requests merged since ProxySQL 3.0.3.\n')
output.append('\n')
for cat in sorted(categories.keys()):
entries = categories[cat]
if not entries:
continue
output.append(f'## {cat}\n')
for pr in entries:
output.append(f'- **PR #{pr["number"]}**: [{pr["title"]}]({pr["url"]})\n')
if pr.get('body'):
# take first non-empty line as summary
lines = pr['body'].split('\n')
summary = ''
for line in lines:
if line.strip():
summary = line.strip()
break
if summary:
output.append(f' - {summary}\n')
output.append('\n')
with open('CHANGELOG-3.0.4-detailed.md', 'w') as f:
f.writelines(output)
print('Generated CHANGELOG-3.0.4-detailed.md')
# Also generate a concise version for release notes
release_notes = []
release_notes.append('# ProxySQL 3.0.4 Release Notes\n')
release_notes.append('\n')
release_notes.append('ProxySQL 3.0.4 includes numerous bug fixes, improvements, and new features.\n')
release_notes.append('\n')
# New Features section
new_features = categories['New Features'] + categories['PostgreSQL'] + categories['MySQL']
if new_features:
release_notes.append('## New Features\n')
for pr in new_features:
release_notes.append(f'- {pr["title"]} [#{pr["number"]}]({pr["url"]})\n')
release_notes.append('\n')
# Bug Fixes
bug_fixes = categories['Bug Fixes'] + categories['Security']
if bug_fixes:
release_notes.append('## Bug Fixes\n')
for pr in bug_fixes:
release_notes.append(f'- {pr["title"]} [#{pr["number"]}]({pr["url"]})\n')
release_notes.append('\n')
# Improvements
improvements = categories['Improvements'] + categories['Refactoring'] + categories['Monitoring']
if improvements:
release_notes.append('## Improvements\n')
for pr in improvements:
release_notes.append(f'- {pr["title"]} [#{pr["number"]}]({pr["url"]})\n')
release_notes.append('\n')
# Documentation
if categories['Documentation']:
release_notes.append('## Documentation\n')
for pr in categories['Documentation']:
release_notes.append(f'- {pr["title"]} [#{pr["number"]}]({pr["url"]})\n')
release_notes.append('\n')
# Testing
if categories['Testing']:
release_notes.append('## Testing\n')
for pr in categories['Testing']:
release_notes.append(f'- {pr["title"]} [#{pr["number"]}]({pr["url"]})\n')
release_notes.append('\n')
# Build/Packaging
if categories['Build/Packaging']:
release_notes.append('## Build/Packaging\n')
for pr in categories['Build/Packaging']:
release_notes.append(f'- {pr["title"]} [#{pr["number"]}]({pr["url"]})\n')
release_notes.append('\n')
with open('RELEASE_NOTES-3.0.4.md', 'w') as f:
f.writelines(release_notes)
print('Generated RELEASE_NOTES-3.0.4.md')

@ -0,0 +1,111 @@
#!/usr/bin/env python3
import subprocess
import re
import sys
def run(cmd):
return subprocess.check_output(cmd, shell=True, text=True).strip()
# Get merge commits since v3.0.3
merge_log = run("git log v3.0.3..v3.0 --merges --pretty=format:'%H %s'")
lines = merge_log.split('\n')
prs = []
for line in lines:
if 'Merge pull request' in line:
hash_, subject = line.split(' ', 1)
# extract PR number
match = re.search(r'#(\d+)', subject)
if match:
pr_num = match.group(1)
prs.append((hash_, pr_num, subject))
elif 'Merge branch' in line:
# ignore branch merges
pass
print(f'Found {len(prs)} PR merges')
# For each PR, get commits in PR branch (second parent)
pr_commits = {}
for hash_, pr_num, subject in prs:
cmd = f"git log --oneline --no-merges {hash_}^2"
try:
output = run(cmd)
except subprocess.CalledProcessError as e:
# maybe merge commit has only one parent? skip
continue
commits = output.split('\n') if output else []
pr_commits[pr_num] = (subject, commits)
# Now categorize based on PR subject and commit messages
categories = {
'Bug Fixes': [],
'New Features': [],
'Improvements': [],
'Documentation': [],
'Testing': [],
'Build/Packaging': [],
'Refactoring': [],
'Security': [],
'Monitoring': [],
'PostgreSQL': [],
'MySQL': [],
'Other': [],
}
def categorize_pr(subject, commits):
subj_lower = subject.lower()
# keyword matching
if any(word in subj_lower for word in ['fix', 'bug', 'issue', 'crash', 'vulnerability']):
return 'Bug Fixes'
if any(word in subj_lower for word in ['add', 'new', 'support', 'implement', 'feature', 'introduce', 'enable']):
return 'New Features'
if any(word in subj_lower for word in ['improve', 'optimize', 'enhance', 'performance', 'better']):
return 'Improvements'
if any(word in subj_lower for word in ['doc', 'documentation', 'doxygen']):
return 'Documentation'
if any(word in subj_lower for word in ['test', 'tap', 'regression']):
return 'Testing'
if any(word in subj_lower for word in ['build', 'package', 'opensuse', 'docker']):
return 'Build/Packaging'
if any(word in subj_lower for word in ['refactor', 'cleanup', 'restructure']):
return 'Refactoring'
if any(word in subj_lower for word in ['security', 'injection', 'vulnerability']):
return 'Security'
if any(word in subj_lower for word in ['monitor', 'metric', 'log']):
return 'Monitoring'
if any(word in subj_lower for word in ['postgresql', 'pgsql', 'pg']):
return 'PostgreSQL'
if any(word in subj_lower for word in ['mysql']):
return 'MySQL'
return 'Other'
for pr_num, (subject, commits) in pr_commits.items():
cat = categorize_pr(subject, commits)
categories[cat].append((pr_num, subject, commits))
# Output markdown
output_lines = []
output_lines.append('# ProxySQL 3.0.4 Changelog\n')
output_lines.append('\n')
output_lines.append('This changelog summarizes all changes since ProxySQL 3.0.3.\n')
output_lines.append('\n')
for cat in sorted(categories.keys()):
entries = categories[cat]
if not entries:
continue
output_lines.append(f'## {cat}\n')
for pr_num, subject, commits in entries:
output_lines.append(f'- **PR #{pr_num}**: {subject}\n')
if commits:
for commit in commits:
# commit format: hash message
# strip hash
msg = commit.split(' ', 1)[1] if ' ' in commit else commit
output_lines.append(f' - {msg}\n')
output_lines.append('\n')
with open('CHANGELOG-3.0.4.md', 'w') as f:
f.writelines(output_lines)
print('Generated CHANGELOG-3.0.4.md')

@ -0,0 +1,225 @@
#!/usr/bin/env python3
import subprocess
import re
import json
def run(cmd):
return subprocess.check_output(cmd, shell=True, text=True).strip()
# Get merge commits since v3.0.3
merge_log = run("git log v3.0.3..v3.0 --merges --pretty=format:'%H %s'")
lines = merge_log.split('\n')
pr_map = []
for line in lines:
if 'Merge pull request' in line:
hash_, subject = line.split(' ', 1)
match = re.search(r'#(\d+)', subject)
if match:
pr_num = match.group(1)
# get second parent commit hash
try:
second_parent = run(f"git rev-parse {hash_}^2")
except subprocess.CalledProcessError:
second_parent = hash_
pr_map.append((pr_num, hash_, second_parent, subject))
print(f'Processed {len(pr_map)} PRs')
# Fetch PR details using gh
pr_details = {}
for pr_num, merge_hash, head_hash, subject in pr_map:
try:
data = run(f'gh pr view {pr_num} --json title,body,number,url')
pr = json.loads(data)
pr['merge_hash'] = merge_hash
pr['head_hash'] = head_hash
pr_details[pr_num] = pr
except subprocess.CalledProcessError as e:
print(f'Failed to fetch PR {pr_num}: {e}')
continue
# Manual categorization based on PR title and analysis
categories = {
'New Features': {
'PostgreSQL': [],
'MySQL': [],
'Monitoring': [],
'Configuration': [],
'Performance': [],
'Other': [],
},
'Bug Fixes': {
'MySQL': [],
'PostgreSQL': [],
'Monitoring': [],
'Configuration': [],
'Security': [],
'Other': [],
},
'Improvements': {
'Performance': [],
'Refactoring': [],
'Monitoring': [],
'Other': [],
},
'Documentation': [],
'Testing': [],
'Build/Packaging': [],
'Other': [],
}
# Mapping PR numbers to categories (manually curated)
category_map = {
'5259': ('Bug Fixes', 'MySQL'),
'5257': ('New Features', 'MySQL'),
'5258': ('Documentation', None),
'5254': ('New Features', 'PostgreSQL'),
'5237': ('New Features', 'PostgreSQL'),
'5250': ('Bug Fixes', 'MySQL'),
'5251': ('Testing', None),
'4889': ('New Features', 'MySQL'),
'5247': ('Bug Fixes', 'Configuration'),
'5245': ('Documentation', None),
'4901': ('New Features', 'MySQL'),
'5199': ('Bug Fixes', 'Monitoring'),
'5241': ('Testing', None),
'5232': ('Bug Fixes', 'MySQL'),
'5240': ('Improvements', 'Performance'),
'5225': ('Improvements', 'Performance'),
'5230': ('Build/Packaging', None),
'5115': ('Documentation', None),
'5229': ('Documentation', None),
'5215': ('Documentation', None),
'5203': ('New Features', 'MySQL'),
'5207': ('Testing', None),
'5200': ('Other', None),
'5198': ('Testing', None),
'5228': ('New Features', 'Monitoring'),
'5226': ('Improvements', 'Performance'),
}
for pr_num, pr in pr_details.items():
if pr_num in category_map:
cat, subcat = category_map[pr_num]
if subcat:
categories[cat][subcat].append(pr)
else:
if isinstance(categories[cat], list):
categories[cat].append(pr)
else:
categories[cat]['Other'].append(pr)
else:
categories['Other'].append(pr)
# Generate release notes
out_lines = []
out_lines.append('# ProxySQL 3.0.4 Release Notes\n')
out_lines.append('\n')
out_lines.append('This release of ProxySQL 3.0.4 includes new features, bug fixes, and improvements across PostgreSQL, MySQL, monitoring, and configuration management.\n')
out_lines.append('\n')
out_lines.append(f'Release commit: {pr_map[0][1][:8]} (faa64a57)\n')
out_lines.append('\n')
# Helper to format entry
def format_entry(pr):
head_short = pr['head_hash'][:8]
title = pr['title']
url = pr['url']
return f'- {title} ({head_short}, #{pr["number"]})\n'
# New Features
if any(any(subcat) for subcat in categories['New Features'].values()):
out_lines.append('## New Features:\n')
for subcat in ['PostgreSQL', 'MySQL', 'Monitoring', 'Configuration', 'Performance', 'Other']:
entries = categories['New Features'][subcat]
if entries:
out_lines.append(f'### {subcat}:\n')
for pr in entries:
out_lines.append(format_entry(pr))
out_lines.append('\n')
# Bug Fixes
if any(any(subcat) for subcat in categories['Bug Fixes'].values()):
out_lines.append('## Bug Fixes:\n')
for subcat in ['MySQL', 'PostgreSQL', 'Monitoring', 'Configuration', 'Security', 'Other']:
entries = categories['Bug Fixes'][subcat]
if entries:
out_lines.append(f'### {subcat}:\n')
for pr in entries:
out_lines.append(format_entry(pr))
out_lines.append('\n')
# Improvements
if any(any(subcat) for subcat in categories['Improvements'].values()):
out_lines.append('## Improvements:\n')
for subcat in ['Performance', 'Refactoring', 'Monitoring', 'Other']:
entries = categories['Improvements'][subcat]
if entries:
out_lines.append(f'### {subcat}:\n')
for pr in entries:
out_lines.append(format_entry(pr))
out_lines.append('\n')
# Documentation
if categories['Documentation']:
out_lines.append('## Documentation:\n')
for pr in categories['Documentation']:
out_lines.append(format_entry(pr))
out_lines.append('\n')
# Testing
if categories['Testing']:
out_lines.append('## Testing:\n')
for pr in categories['Testing']:
out_lines.append(format_entry(pr))
out_lines.append('\n')
# Build/Packaging
if categories['Build/Packaging']:
out_lines.append('## Build/Packaging:\n')
for pr in categories['Build/Packaging']:
out_lines.append(format_entry(pr))
out_lines.append('\n')
# Other (if any)
if categories['Other']:
out_lines.append('## Other Changes:\n')
for pr in categories['Other']:
out_lines.append(format_entry(pr))
out_lines.append('\n')
out_lines.append('\n')
out_lines.append('## Hashes\n')
out_lines.append('\n')
out_lines.append('The release commit is: `faa64a570d19fe35af43494db0babdee3e3cdc89`\n')
out_lines.append('\n')
with open('RELEASE_NOTES-3.0.4-formatted.md', 'w') as f:
f.writelines(out_lines)
print('Generated RELEASE_NOTES-3.0.4-formatted.md')
# Also generate a more detailed changelog with commit messages
detailed = []
detailed.append('# ProxySQL 3.0.4 Detailed Changelog\n')
detailed.append('\n')
detailed.append('This changelog includes all individual commits since ProxySQL 3.0.3.\n')
detailed.append('\n')
# Get all non-merge commits
commits = run("git log v3.0.3..v3.0 --no-merges --pretty=format:'%H|%s|%b'").split('\n')
for line in commits:
parts = line.split('|', 2)
if len(parts) < 2:
continue
hash_, subject, body = parts[0], parts[1], parts[2] if len(parts) > 2 else ''
detailed.append(f'- {hash_[:8]} {subject}\n')
if body.strip():
for bline in body.strip().split('\n'):
if bline.strip():
detailed.append(f' {bline.strip()}\n')
with open('CHANGELOG-3.0.4-commits.md', 'w') as f:
f.writelines(detailed)
print('Generated CHANGELOG-3.0.4-commits.md')

@ -0,0 +1,164 @@
#!/usr/bin/env python3
"""
Generate a changelog from git merge commits between two tags/branches.
This script analyzes merge commits (pull requests) and generates a categorized
changelog. It attempts to get commits within each PR by looking at the second
parent of merge commits.
Usage:
python generate_changelog.py --from-tag v3.0.3 --to-tag v3.0 --output changelog.md
"""
import subprocess
import re
import argparse
import sys
def run(cmd):
"""Run shell command and return output."""
return subprocess.check_output(cmd, shell=True, text=True).strip()
def get_merge_commits(from_tag, to_tag):
"""Get merge commits between two tags."""
merge_log = run(f"git log {from_tag}..{to_tag} --merges --pretty=format:'%H %s'")
lines = merge_log.split('\n')
prs = []
for line in lines:
if 'Merge pull request' in line:
hash_, subject = line.split(' ', 1)
# extract PR number
match = re.search(r'#(\d+)', subject)
if match:
pr_num = match.group(1)
prs.append((hash_, pr_num, subject))
# ignore branch merges
return prs
def get_pr_commits(prs):
"""For each PR merge commit, get commits in the PR branch (second parent)."""
pr_commits = {}
for hash_, pr_num, subject in prs:
cmd = f"git log --oneline --no-merges {hash_}^2"
try:
output = run(cmd)
except subprocess.CalledProcessError:
# merge commit may have only one parent, skip
continue
commits = output.split('\n') if output else []
pr_commits[pr_num] = (subject, commits)
return pr_commits
def categorize_pr(subject, commits):
"""Categorize a PR based on its subject and commit messages."""
subj_lower = subject.lower()
# keyword matching
if any(word in subj_lower for word in ['fix', 'bug', 'issue', 'crash', 'vulnerability']):
return 'Bug Fixes'
if any(word in subj_lower for word in ['add', 'new', 'support', 'implement', 'feature', 'introduce', 'enable']):
return 'New Features'
if any(word in subj_lower for word in ['improve', 'optimize', 'enhance', 'performance', 'better']):
return 'Improvements'
if any(word in subj_lower for word in ['doc', 'documentation', 'doxygen']):
return 'Documentation'
if any(word in subj_lower for word in ['test', 'tap', 'regression']):
return 'Testing'
if any(word in subj_lower for word in ['build', 'package', 'opensuse', 'docker']):
return 'Build/Packaging'
if any(word in subj_lower for word in ['refactor', 'cleanup', 'restructure']):
return 'Refactoring'
if any(word in subj_lower for word in ['security', 'injection', 'vulnerability']):
return 'Security'
if any(word in subj_lower for word in ['monitor', 'metric', 'log']):
return 'Monitoring'
if any(word in subj_lower for word in ['postgresql', 'pgsql', 'pg']):
return 'PostgreSQL'
if any(word in subj_lower for word in ['mysql']):
return 'MySQL'
return 'Other'
def generate_changelog(from_tag, to_tag, output_file, verbose=False):
"""Main function to generate changelog."""
if verbose:
print(f"Generating changelog from {from_tag} to {to_tag}...")
prs = get_merge_commits(from_tag, to_tag)
if verbose:
print(f"Found {len(prs)} PR merges")
pr_commits = get_pr_commits(prs)
categories = {
'Bug Fixes': [],
'New Features': [],
'Improvements': [],
'Documentation': [],
'Testing': [],
'Build/Packaging': [],
'Refactoring': [],
'Security': [],
'Monitoring': [],
'PostgreSQL': [],
'MySQL': [],
'Other': [],
}
for pr_num, (subject, commits) in pr_commits.items():
cat = categorize_pr(subject, commits)
categories[cat].append((pr_num, subject, commits))
# Build output
output_lines = []
output_lines.append(f'# Changelog from {from_tag} to {to_tag}\n')
output_lines.append('\n')
output_lines.append(f'This changelog summarizes all changes between {from_tag} and {to_tag}.\n')
output_lines.append('\n')
for cat in sorted(categories.keys()):
entries = categories[cat]
if not entries:
continue
output_lines.append(f'## {cat}\n')
for pr_num, subject, commits in entries:
output_lines.append(f'- **PR #{pr_num}**: {subject}\n')
if commits:
for commit in commits:
# commit format: hash message
msg = commit.split(' ', 1)[1] if ' ' in commit else commit
output_lines.append(f' - {msg}\n')
output_lines.append('\n')
with open(output_file, 'w') as f:
f.writelines(output_lines)
if verbose:
print(f"Changelog written to {output_file}")
def main():
parser = argparse.ArgumentParser(
description='Generate a changelog from git merge commits.',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
%(prog)s --from-tag v3.0.3 --to-tag v3.0 --output changelog.md
%(prog)s --from-tag v3.0.3 --to-tag HEAD --output changes.md --verbose
"""
)
parser.add_argument('--from-tag', required=True, help='Starting tag/branch (e.g., v3.0.3)')
parser.add_argument('--to-tag', required=True, help='Ending tag/branch (e.g., v3.0 or HEAD)')
parser.add_argument('--output', '-o', default='changelog.md', help='Output file (default: changelog.md)')
parser.add_argument('--verbose', '-v', action='store_true', help='Verbose output')
args = parser.parse_args()
generate_changelog(args.from_tag, args.to_tag, args.output, args.verbose)
if __name__ == '__main__':
main()

@ -0,0 +1,362 @@
#!/usr/bin/env python3
"""
Generate release notes from GitHub pull requests between two git tags.
This script uses the GitHub CLI (gh) to fetch PR details and generates
formatted release notes similar to ProxySQL's release notes format.
Features:
- Automatic categorization based on PR labels and titles
- Optional manual categorization mapping via JSON file
- Support for subcategories (PostgreSQL, MySQL, Monitoring, etc.)
- Outputs both release notes and detailed changelog
"""
import subprocess
import re
import json
import argparse
import sys
from pathlib import Path
def run(cmd):
"""Run shell command and return output."""
return subprocess.check_output(cmd, shell=True, text=True).strip()
def get_merge_commits(from_tag, to_tag):
"""Get merge commits between two tags and extract PR numbers."""
merge_log = run(f"git log {from_tag}..{to_tag} --merges --pretty=format:'%H %s'")
lines = merge_log.split('\n')
pr_map = []
for line in lines:
if 'Merge pull request' in line:
hash_, subject = line.split(' ', 1)
match = re.search(r'#(\d+)', subject)
if match:
pr_num = match.group(1)
# get second parent commit hash (PR head)
try:
second_parent = run(f"git rev-parse {hash_}^2")
except subprocess.CalledProcessError:
second_parent = hash_
pr_map.append((pr_num, hash_, second_parent, subject))
return pr_map
def fetch_pr_details(pr_numbers, verbose=False):
"""Fetch PR details using GitHub CLI."""
pr_details = {}
for pr_num in pr_numbers:
if verbose:
print(f"Fetching PR #{pr_num}...")
try:
data = run(f'gh pr view {pr_num} --json title,body,number,url,labels,state,createdAt,mergedAt')
pr = json.loads(data)
pr_details[pr_num] = pr
except subprocess.CalledProcessError as e:
print(f"Failed to fetch PR {pr_num}: {e}", file=sys.stderr)
continue
return pr_details
def load_category_mapping(config_file):
"""Load manual category mapping from JSON file."""
if not config_file or not Path(config_file).exists():
return {}
try:
with open(config_file, 'r') as f:
return json.load(f)
except json.JSONDecodeError as e:
print(f"Error parsing config file {config_file}: {e}", file=sys.stderr)
return {}
def auto_categorize(pr, mapping):
"""Categorize a PR based on mapping, labels, and title."""
pr_num = str(pr['number'])
# First, check manual mapping
if pr_num in mapping:
cat_info = mapping[pr_num]
if isinstance(cat_info, dict):
return cat_info.get('category', 'Other'), cat_info.get('subcategory')
elif isinstance(cat_info, str):
return cat_info, None
elif isinstance(cat_info, list) and len(cat_info) >= 1:
cat = cat_info[0]
subcat = cat_info[1] if len(cat_info) > 1 else None
return cat, subcat
# Auto-categorize based on labels
title = pr['title'].lower()
labels = [l['name'].lower() for l in pr.get('labels', [])]
# Label-based categorization
for label in labels:
if 'bug' in label:
return 'Bug Fixes', None
if 'feature' in label:
return 'New Features', None
if 'documentation' in label:
return 'Documentation', None
if 'test' in label:
return 'Testing', None
if 'security' in label:
return 'Security', None
if 'refactor' in label:
return 'Improvements', 'Refactoring'
if 'improvement' in label:
return 'Improvements', None
# Title keyword-based categorization
if any(word in title for word in ['fix', 'bug', 'issue', 'crash', 'vulnerability', 'error']):
return 'Bug Fixes', None
if any(word in title for word in ['add', 'new', 'support', 'implement', 'feature', 'introduce', 'enable']):
return 'New Features', None
if any(word in title for word in ['improve', 'optimize', 'enhance', 'performance', 'better']):
return 'Improvements', None
if any(word in title for word in ['doc', 'documentation', 'doxygen']):
return 'Documentation', None
if any(word in title for word in ['test', 'tap', 'regression']):
return 'Testing', None
if any(word in title for word in ['build', 'package', 'opensuse', 'docker']):
return 'Build/Packaging', None
if any(word in title for word in ['refactor', 'cleanup', 'restructure']):
return 'Improvements', 'Refactoring'
if any(word in title for word in ['security', 'injection', 'vulnerability']):
return 'Security', None
if any(word in title for word in ['monitor', 'metric', 'log']):
return 'Monitoring', None
if any(word in title for word in ['postgresql', 'pgsql', 'pg']):
return 'New Features', 'PostgreSQL'
if any(word in title for word in ['mysql']):
return 'New Features', 'MySQL'
return 'Other', None
def organize_prs(pr_details, mapping, verbose=False):
"""Organize PRs into categorized structure."""
categories = {
'New Features': {
'PostgreSQL': [],
'MySQL': [],
'Monitoring': [],
'Configuration': [],
'Performance': [],
'Other': [],
},
'Bug Fixes': {
'MySQL': [],
'PostgreSQL': [],
'Monitoring': [],
'Configuration': [],
'Security': [],
'Other': [],
},
'Improvements': {
'Performance': [],
'Refactoring': [],
'Monitoring': [],
'Other': [],
},
'Documentation': [],
'Testing': [],
'Build/Packaging': [],
'Other': [],
}
for pr_num, pr in pr_details.items():
cat, subcat = auto_categorize(pr, mapping)
if cat in categories:
if isinstance(categories[cat], dict):
# Has subcategories
if subcat and subcat in categories[cat]:
categories[cat][subcat].append(pr)
else:
categories[cat]['Other'].append(pr)
else:
# Simple list category
categories[cat].append(pr)
else:
categories['Other'].append(pr)
if verbose:
for cat, contents in categories.items():
if isinstance(contents, dict):
total = sum(len(sub) for sub in contents.values())
print(f"{cat}: {total} PRs")
for subcat, prs in contents.items():
if prs:
print(f" {subcat}: {len(prs)}")
else:
if contents:
print(f"{cat}: {len(contents)} PRs")
return categories
def generate_release_notes(categories, from_tag, to_tag, output_file):
"""Generate formatted release notes."""
out_lines = []
out_lines.append(f'# ProxySQL {to_tag} Release Notes\n')
out_lines.append('\n')
out_lines.append(f'This release of ProxySQL {to_tag} includes new features, bug fixes, and improvements.\n')
out_lines.append('\n')
out_lines.append(f'Release range: {from_tag} to {to_tag}\n')
out_lines.append('\n')
def format_pr(pr):
title = pr['title']
url = pr['url']
number = pr['number']
return f'- {title} (#{number})\n'
# New Features
if any(any(subcat) for subcat in categories['New Features'].values()):
out_lines.append('## New Features:\n')
for subcat in ['PostgreSQL', 'MySQL', 'Monitoring', 'Configuration', 'Performance', 'Other']:
entries = categories['New Features'][subcat]
if entries:
out_lines.append(f'### {subcat}:\n')
for pr in entries:
out_lines.append(format_pr(pr))
out_lines.append('\n')
# Bug Fixes
if any(any(subcat) for subcat in categories['Bug Fixes'].values()):
out_lines.append('## Bug Fixes:\n')
for subcat in ['MySQL', 'PostgreSQL', 'Monitoring', 'Configuration', 'Security', 'Other']:
entries = categories['Bug Fixes'][subcat]
if entries:
out_lines.append(f'### {subcat}:\n')
for pr in entries:
out_lines.append(format_pr(pr))
out_lines.append('\n')
# Improvements
if any(any(subcat) for subcat in categories['Improvements'].values()):
out_lines.append('## Improvements:\n')
for subcat in ['Performance', 'Refactoring', 'Monitoring', 'Other']:
entries = categories['Improvements'][subcat]
if entries:
out_lines.append(f'### {subcat}:\n')
for pr in entries:
out_lines.append(format_pr(pr))
out_lines.append('\n')
# Simple categories
simple_cats = ['Documentation', 'Testing', 'Build/Packaging', 'Other']
for cat in simple_cats:
entries = categories[cat]
if entries:
out_lines.append(f'## {cat}:\n')
for pr in entries:
out_lines.append(format_pr(pr))
out_lines.append('\n')
out_lines.append('\n')
out_lines.append('## Hashes\n')
out_lines.append('\n')
out_lines.append(f'The release range is: `{from_tag}` to `{to_tag}`\n')
out_lines.append('\n')
with open(output_file, 'w') as f:
f.writelines(out_lines)
return output_file
def generate_detailed_changelog(pr_details, from_tag, to_tag, output_file):
"""Generate detailed changelog with PR descriptions."""
out_lines = []
out_lines.append(f'# Detailed Changelog from {from_tag} to {to_tag}\n')
out_lines.append('\n')
out_lines.append(f'This changelog lists all pull requests merged between {from_tag} and {to_tag}.\n')
out_lines.append('\n')
for pr in pr_details.values():
out_lines.append(f'## PR #{pr["number"]}: {pr["title"]}\n')
out_lines.append(f'- URL: {pr["url"]}\n')
if pr.get('labels'):
labels = ', '.join([l['name'] for l in pr['labels']])
out_lines.append(f'- Labels: {labels}\n')
if pr.get('body'):
# Take first paragraph as summary
lines = pr['body'].split('\n')
summary = ''
for line in lines:
if line.strip() and not line.strip().startswith('#'):
summary = line.strip()
break
if summary:
out_lines.append(f'- Summary: {summary}\n')
out_lines.append('\n')
with open(output_file, 'w') as f:
f.writelines(out_lines)
return output_file
def main():
parser = argparse.ArgumentParser(
description='Generate release notes from GitHub pull requests.',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
%(prog)s --from-tag v3.0.3 --to-tag v3.0 --output release-notes.md
%(prog)s --from-tag v3.0.3 --to-tag HEAD --config mapping.json --verbose
"""
)
parser.add_argument('--from-tag', required=True, help='Starting tag/branch')
parser.add_argument('--to-tag', required=True, help='Ending tag/branch')
parser.add_argument('--output', '-o', default='release-notes.md',
help='Output file for release notes (default: release-notes.md)')
parser.add_argument('--changelog', '-c', help='Output file for detailed changelog')
parser.add_argument('--config', help='JSON file with manual category mapping')
parser.add_argument('--verbose', '-v', action='store_true', help='Verbose output')
args = parser.parse_args()
if args.verbose:
print(f"Generating release notes from {args.from_tag} to {args.to_tag}")
# Get merge commits
pr_map = get_merge_commits(args.from_tag, args.to_tag)
pr_numbers = [pr_num for pr_num, _, _, _ in pr_map]
if args.verbose:
print(f"Found {len(pr_numbers)} PR merges")
# Fetch PR details
pr_details = fetch_pr_details(pr_numbers, args.verbose)
if not pr_details:
print("No PR details fetched. Check GitHub CLI authentication.", file=sys.stderr)
sys.exit(1)
# Load manual mapping
mapping = load_category_mapping(args.config)
# Organize PRs into categories
categories = organize_prs(pr_details, mapping, args.verbose)
# Generate release notes
release_file = generate_release_notes(categories, args.from_tag, args.to_tag, args.output)
print(f"Release notes written to {release_file}")
# Generate detailed changelog if requested
if args.changelog:
changelog_file = generate_detailed_changelog(pr_details, args.from_tag, args.to_tag, args.changelog)
print(f"Detailed changelog written to {changelog_file}")
if args.verbose:
print("Done!")
if __name__ == '__main__':
main()

@ -0,0 +1,284 @@
#!/usr/bin/env python3
"""
Generate structured release notes from PR data.
This script reads PR data collected by collect_pr_data.py and generates
release notes in the style of ProxySQL v3.0.3 release notes, with
descriptive grouping and individual commit references.
"""
import json
import argparse
import sys
from collections import defaultdict
def load_pr_data(filename):
"""Load PR data from JSON file."""
with open(filename, 'r') as f:
return json.load(f)
def categorize_pr(pr):
"""Categorize PR based on title, labels, and content."""
title = pr['title'].lower()
body = pr.get('body', '').lower()
labels = [l['name'].lower() for l in pr.get('labels', [])]
# Check for documentation PRs
if any(word in title for word in ['doc', 'documentation', 'doxygen']):
return 'Documentation'
if any(word in body for word in ['doc', 'documentation', 'doxygen']):
return 'Documentation'
# Check for test PRs
if any(word in title for word in ['test', 'tap', 'regression']):
return 'Testing'
if any(word in body for word in ['test', 'tap', 'regression']):
return 'Testing'
# Check for build/packaging
if any(word in title for word in ['build', 'package', 'opensuse', 'docker', 'rpm', 'deb']):
return 'Build/Packaging'
# Check for bug fixes
if any(word in title for word in ['fix', 'bug', 'issue', 'crash', 'vulnerability', 'error']):
return 'Bug Fixes'
# Check for PostgreSQL
if any(word in title for word in ['postgresql', 'pgsql', 'pg']):
return 'PostgreSQL'
if any(word in body for word in ['postgresql', 'pgsql', 'pg']):
return 'PostgreSQL'
# Check for MySQL
if any(word in title for word in ['mysql']):
return 'MySQL'
if any(word in body for word in ['mysql']):
return 'MySQL'
# Check for monitoring
if any(word in title for word in ['monitor', 'metric', 'log', 'ping', 'keepalive']):
return 'Monitoring'
# Check for performance
if any(word in title for word in ['performance', 'optimize', 'refactor', 'lock-free', 'gtid']):
return 'Performance'
# Default
return 'Other'
def extract_commits_info(pr):
"""Extract structured commit information from PR."""
commits_info = []
for commit in pr.get('commits', []):
headline = commit.get('messageHeadline', '')
oid = commit.get('oid', '')[:8] # Short hash
if headline:
commits_info.append((oid, headline))
return commits_info
def generate_release_notes(pr_data, output_file):
"""Generate structured release notes."""
# Categorize PRs
categories = defaultdict(list)
for pr in pr_data:
cat = categorize_pr(pr)
categories[cat].append(pr)
out_lines = []
out_lines.append('# ProxySQL 3.0.4 Release Notes\n')
out_lines.append('\n')
out_lines.append('This release of ProxySQL 3.0.4 includes significant improvements ')
out_lines.append('to PostgreSQL support, MySQL protocol handling, monitoring accuracy, ')
out_lines.append('and security.\n')
out_lines.append('\n')
out_lines.append('Release commit: `faa64a570d19fe35af43494db0babdee3e3cdc89`\n')
out_lines.append('\n')
# Helper to format with backticks
def add_backticks(text):
# Simple heuristic: add backticks around likely technical terms
# This is a placeholder - in production, use more sophisticated detection
return text
# New Features section
out_lines.append('## New Features:\n')
out_lines.append('\n')
# PostgreSQL features
postgres_prs = [p for p in categories['PostgreSQL'] if 'fix' not in p['title'].lower()]
if postgres_prs:
out_lines.append('### PostgreSQL Improvements:\n')
out_lines.append('\n')
for pr in postgres_prs:
commits = extract_commits_info(pr)
if commits:
for oid, headline in commits:
out_lines.append(f'- {headline} ({oid}, #{pr["number"]})\n')
else:
out_lines.append(f'- {pr["title"]} ({pr.get("merge_hash", "")[:8]}, #{pr["number"]})\n')
out_lines.append('\n')
# MySQL features
mysql_prs = [p for p in categories['MySQL'] if 'fix' not in p['title'].lower()]
if mysql_prs:
out_lines.append('### MySQL Protocol Enhancements:\n')
out_lines.append('\n')
for pr in mysql_prs:
commits = extract_commits_info(pr)
if commits:
for oid, headline in commits:
out_lines.append(f'- {headline} ({oid}, #{pr["number"]})\n')
else:
out_lines.append(f'- {pr["title"]} ({pr.get("merge_hash", "")[:8]}, #{pr["number"]})\n')
out_lines.append('\n')
# Monitoring features
monitoring_prs = [p for p in categories['Monitoring'] if 'fix' not in p['title'].lower()]
if monitoring_prs:
out_lines.append('### Monitoring & Diagnostics:\n')
out_lines.append('\n')
for pr in monitoring_prs:
commits = extract_commits_info(pr)
if commits:
for oid, headline in commits:
out_lines.append(f'- {headline} ({oid}, #{pr["number"]})\n')
else:
out_lines.append(f'- {pr["title"]} ({pr.get("merge_hash", "")[:8]}, #{pr["number"]})\n')
out_lines.append('\n')
# Bug Fixes section
bug_fixes = []
for cat in ['PostgreSQL', 'MySQL', 'Monitoring', 'Other']:
if cat in categories:
bug_fixes.extend([p for p in categories[cat] if 'fix' in p['title'].lower()])
if bug_fixes:
out_lines.append('## Bug Fixes:\n')
out_lines.append('\n')
for pr in bug_fixes:
commits = extract_commits_info(pr)
if commits:
for oid, headline in commits:
out_lines.append(f'- {headline} ({oid}, #{pr["number"]})\n')
else:
out_lines.append(f'- {pr["title"]} ({pr.get("merge_hash", "")[:8]}, #{pr["number"]})\n')
out_lines.append('\n')
# Performance improvements
perf_prs = categories.get('Performance', [])
if perf_prs:
out_lines.append('## Improvements:\n')
out_lines.append('\n')
out_lines.append('### Performance Optimizations:\n')
out_lines.append('\n')
for pr in perf_prs:
commits = extract_commits_info(pr)
if commits:
for oid, headline in commits:
out_lines.append(f'- {headline} ({oid}, #{pr["number"]})\n')
else:
out_lines.append(f'- {pr["title"]} ({pr.get("merge_hash", "")[:8]}, #{pr["number"]})\n')
out_lines.append('\n')
# Documentation
doc_prs = categories.get('Documentation', [])
if doc_prs:
out_lines.append('## Documentation:\n')
out_lines.append('\n')
for pr in doc_prs:
commits = extract_commits_info(pr)
if commits:
for oid, headline in commits:
out_lines.append(f'- {headline} ({oid}, #{pr["number"]})\n')
else:
out_lines.append(f'- {pr["title"]} ({pr.get("merge_hash", "")[:8]}, #{pr["number"]})\n')
out_lines.append('\n')
# Testing
test_prs = categories.get('Testing', [])
if test_prs:
out_lines.append('## Testing:\n')
out_lines.append('\n')
for pr in test_prs:
commits = extract_commits_info(pr)
if commits:
for oid, headline in commits:
out_lines.append(f'- {headline} ({oid}, #{pr["number"]})\n')
else:
out_lines.append(f'- {pr["title"]} ({pr.get("merge_hash", "")[:8]}, #{pr["number"]})\n')
out_lines.append('\n')
# Build/Packaging
build_prs = categories.get('Build/Packaging', [])
if build_prs:
out_lines.append('## Build/Packaging:\n')
out_lines.append('\n')
for pr in build_prs:
commits = extract_commits_info(pr)
if commits:
for oid, headline in commits:
out_lines.append(f'- {headline} ({oid}, #{pr["number"]})\n')
else:
out_lines.append(f'- {pr["title"]} ({pr.get("merge_hash", "")[:8]}, #{pr["number"]})\n')
out_lines.append('\n')
# Other
other_prs = categories.get('Other', [])
if other_prs:
out_lines.append('## Other Changes:\n')
out_lines.append('\n')
for pr in other_prs:
commits = extract_commits_info(pr)
if commits:
for oid, headline in commits:
out_lines.append(f'- {headline} ({oid}, #{pr["number"]})\n')
else:
out_lines.append(f'- {pr["title"]} ({pr.get("merge_hash", "")[:8]}, #{pr["number"]})\n')
out_lines.append('\n')
out_lines.append('## Hashes\n')
out_lines.append('\n')
out_lines.append('The release commit is: `faa64a570d19fe35af43494db0babdee3e3cdc89`\n')
out_lines.append('\n')
with open(output_file, 'w') as f:
f.writelines(out_lines)
return output_file
def main():
parser = argparse.ArgumentParser(
description='Generate structured release notes from PR data.',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
%(prog)s --input pr-data.json --output release-notes.md
"""
)
parser.add_argument('--input', '-i', required=True, help='Input JSON file with PR data')
parser.add_argument('--output', '-o', default='release-notes-structured.md', help='Output file')
parser.add_argument('--verbose', '-v', action='store_true', help='Verbose output')
args = parser.parse_args()
if args.verbose:
print(f"Loading PR data from {args.input}")
pr_data = load_pr_data(args.input)
if args.verbose:
print(f"Processing {len(pr_data)} PRs")
output_file = generate_release_notes(pr_data, args.output)
if args.verbose:
print(f"Structured release notes written to {output_file}")
if __name__ == '__main__':
main()

@ -0,0 +1,274 @@
#!/usr/bin/env python3
"""
Orchestrate release notes generation workflow.
This script runs all required steps to prepare data for LLM analysis,
then generates a comprehensive prompt for the LLM to create release notes
and changelogs.
Workflow:
1. Collect PR data from GitHub
2. Generate structured commit data
3. Create analysis files
4. Generate LLM prompt with instructions
"""
import subprocess
import json
import argparse
import sys
import os
from pathlib import Path
def run_cmd(cmd, verbose=False):
"""Run command and return output."""
if verbose:
print(f"Running: {cmd}")
try:
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
if result.returncode != 0:
print(f"Error running command: {cmd}", file=sys.stderr)
print(f"Error output: {result.stderr}", file=sys.stderr)
return None
return result.stdout.strip()
except Exception as e:
print(f"Exception running command {cmd}: {e}", file=sys.stderr)
return None
def generate_llm_prompt(from_tag, to_tag, data_files, output_dir, verbose=False):
"""Generate comprehensive prompt for LLM."""
# Read some data to include in prompt
pr_summary = ""
try:
with open(data_files['pr_summary'], 'r') as f:
pr_summary = f.read()[:2000] # First 2000 chars
except:
pass
prompt = f"""
# ProxySQL Release Notes Generation Task
## Context
You need to generate release notes and changelogs for ProxySQL version {to_tag} (changes since {from_tag}).
## Available Data Files
The following files have been prepared for your analysis:
1. **PR Data**: `{data_files['pr_data']}` - JSON with all PR details (titles, descriptions, labels, commits)
2. **PR Summary**: `{data_files['pr_summary']}` - Markdown summary of all PRs
3. **Structured Notes**: `{data_files['structured_notes']}` - Commit-level organized data
4. **Commit Categorization**: `{data_files['commit_categories']}` - Commits categorized by type
## Task Requirements
### 1. Generate Release Notes (like ProxySQL v3.0.3 format)
- **Descriptive content**: Not just PR titles, but explanations of what each feature/fix does and why it matters
- **Logical grouping**: Organize under categories like:
- PostgreSQL Improvements
- MySQL Protocol Enhancements
- Monitoring & Diagnostics
- Bug Fixes (with subcategories: MySQL, PostgreSQL, Monitoring, Security)
- Performance Optimizations
- Documentation
- Testing
- Build/Packaging
- Other Changes
- **Backtick formatting**: Use `backticks` around all technical terms:
- Function names: `Read_Global_Variables_from_configfile()`
- Variable names: `wait_timeout`, `cur_cmd_cmnt`
- SQL queries: `SELECT @@version`, `SELECT VERSION()`
- Protocol commands: `COM_PING`, `CLIENT_DEPRECATE_EOF`
- Configuration options: `cache_empty_result=0`
- Metrics: `PgSQL_Monitor_ssl_connections_OK`
- **Commit references**: Include relevant commit hashes (short form) and PR numbers
- **Remove WIP/skip-ci tags**: Make all entries production-ready
- **Include release hash**: The final commit is `faa64a570d19fe35af43494db0babdee3e3cdc89`
### 2. Generate Detailed Changelog
- List all changes with commit hashes and PR references
- Include brief descriptions from commit messages
- Categorize changes for easy reference
### 3. Generate Commit List (optional)
- Complete list of all commits since {from_tag}
## Example Structure (from ProxySQL 3.0.3)
```
# ProxySQL 3.0.3 Release Notes
This release of ProxySQL 3.0.3 includes a significant number of new features...
## New Features:
### PostgreSQL Extended Query Protocol Support:
- Add PostgreSQL extended query (prepared statement) support (24fecc1f, #5044)
- Lays groundwork for handling PostgreSQL extended query protocol...
- Added `Describe` message handling (a741598a, #5044)
- Added `Close` statement handling (4d0618c2, #5044)
### Build System & Dependencies:
- Upgrade `coredumper` to Percona fork hash `8f2623b` (a315f128, #5171)
- Upgrade `curl` to v8.16.0 (40414de1, #5154)
```
## Your Output Should Be:
1. **`ProxySQL-{to_tag}-Release-Notes.md`** - Main release notes
2. **`CHANGELOG-{to_tag}-detailed.md`** - Detailed changelog
3. **`CHANGELOG-{to_tag}-commits.md`** - Complete commit list (optional)
## Analysis Approach
1. Review the PR data to understand scope and significance of changes
2. Identify major themes and group related changes
3. Write descriptive explanations, not just copy titles
4. Apply backtick formatting consistently
5. Verify all technical terms are properly formatted
## Available Data Preview
{pr_summary[:500]}...
"""
prompt_file = os.path.join(output_dir, f"llm-prompt-{to_tag}.md")
with open(prompt_file, 'w') as f:
f.write(prompt)
if verbose:
print(f"LLM prompt written to {prompt_file}")
return prompt_file
def main():
parser = argparse.ArgumentParser(
description='Orchestrate release notes generation workflow.',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
%(prog)s --from-tag v3.0.3 --to-tag v3.0.4 --output-dir release-data
%(prog)s --from-tag v3.0.3 --to-tag v3.0.4 --verbose
"""
)
parser.add_argument('--from-tag', required=True, help='Starting tag/branch')
parser.add_argument('--to-tag', required=True, help='Ending tag/branch')
parser.add_argument('--output-dir', default='release-data', help='Output directory for data files')
parser.add_argument('--verbose', '-v', action='store_true', help='Verbose output')
args = parser.parse_args()
# Create output directory
output_dir = Path(args.output_dir)
output_dir.mkdir(exist_ok=True)
if args.verbose:
print(f"Starting release notes workflow for {args.from_tag} to {args.to_tag}")
print(f"Output directory: {output_dir}")
data_files = {}
# Step 1: Collect PR data
if args.verbose:
print("\n1. Collecting PR data...")
pr_data_file = output_dir / f"pr-data-{args.to_tag}.json"
pr_summary_file = output_dir / f"pr-summary-{args.to_tag}.md"
cmd = f"python {Path(__file__).parent}/collect_pr_data.py --from-tag {args.from_tag} --to-tag {args.to_tag} --output {pr_data_file} --verbose"
if not args.verbose:
cmd = cmd.replace(" --verbose", "")
result = run_cmd(cmd, args.verbose)
if result is None:
print("Failed to collect PR data", file=sys.stderr)
sys.exit(1)
data_files['pr_data'] = str(pr_data_file)
data_files['pr_summary'] = str(pr_summary_file)
# Step 2: Generate structured notes
if args.verbose:
print("\n2. Generating structured notes...")
structured_file = output_dir / f"structured-notes-{args.to_tag}.md"
cmd = f"python {Path(__file__).parent}/generate_structured_notes.py --input {pr_data_file} --output {structured_file}"
if args.verbose:
cmd += " --verbose"
result = run_cmd(cmd, args.verbose)
if result is None:
print("Failed to generate structured notes", file=sys.stderr)
sys.exit(1)
data_files['structured_notes'] = str(structured_file)
# Step 3: Categorize commits
if args.verbose:
print("\n3. Categorizing commits...")
commit_cat_file = output_dir / f"commit-categories-{args.to_tag}.md"
cmd = f"python {Path(__file__).parent}/categorize_commits.py --from-tag {args.from_tag} --to-tag {args.to_tag} --output-format markdown > {commit_cat_file}"
result = run_cmd(cmd, args.verbose)
if result is None:
print("Failed to categorize commits", file=sys.stderr)
data_files['commit_categories'] = str(commit_cat_file)
# Step 4: Generate LLM prompt
if args.verbose:
print("\n4. Generating LLM prompt...")
prompt_file = generate_llm_prompt(
args.from_tag,
args.to_tag,
data_files,
output_dir,
args.verbose
)
# Step 5: Create workflow summary
summary = f"""
# Release Notes Workflow Summary
## Generated Files
- PR Data: {data_files['pr_data']}
- PR Summary: {data_files['pr_summary']}
- Structured Notes: {data_files['structured_notes']}
- Commit Categories: {data_files.get('commit_categories', 'N/A')}
- LLM Prompt: {prompt_file}
## Next Steps
1. Review the generated files in {output_dir}
2. Use the LLM prompt to generate release notes
3. The LLM should create:
- `ProxySQL-{args.to_tag}-Release-Notes.md`
- `CHANGELOG-{args.to_tag}-detailed.md`
- `CHANGELOG-{args.to_tag}-commits.md` (optional)
## LLM Instructions
Provide the LLM with:
1. Access to all files in {output_dir}
2. The prompt: {prompt_file}
3. Instructions to write descriptive release notes with backtick formatting
## Verification Checklist
- [ ] All technical terms have backticks
- [ ] No WIP/skip-ci tags remain
- [ ] Changes are grouped logically
- [ ] Descriptions explain what/why, not just copy titles
- [ ] Commit hashes and PR numbers are referenced
"""
summary_file = output_dir / "workflow-summary.md"
with open(summary_file, 'w') as f:
f.write(summary)
if args.verbose:
print(f"\nWorkflow summary written to {summary_file}")
print("\n✅ Workflow completed!")
print(f"\nNext: Provide the LLM with files in {output_dir} and prompt: {prompt_file}")
if __name__ == '__main__':
main()

@ -0,0 +1,80 @@
#!/usr/bin/env python3
import sys
import re
# Categories mapping keywords
categories = {
'Bug Fix': ['fix', 'bug', 'issue', 'crash', 'vulnerability', 'error', 'wrong', 'incorrect', 'failure', 'broken'],
'New Feature': ['add', 'new', 'support', 'implement', 'feature', 'introduce', 'enable'],
'Improvement': ['improve', 'optimize', 'enhance', 'speed', 'performance', 'better', 'reduce', 'faster', 'efficient'],
'Documentation': ['doc', 'documentation', 'comment', 'doxygen', 'readme'],
'Testing': ['test', 'tap', 'regression', 'validation'],
'Build/Packaging': ['build', 'package', 'makefile', 'cmake', 'docker', 'opensuse', 'deb', 'rpm'],
'Refactoring': ['refactor', 'cleanup', 'restructure', 'reorganize', 'rename'],
'Security': ['security', 'injection', 'vulnerability', 'secure', 'sanitize'],
'Monitoring': ['monitor', 'metric', 'log', 'warning', 'alert'],
'PostgreSQL': ['postgresql', 'pgsql', 'pg'],
'MySQL': ['mysql'],
}
def categorize_commit(message):
msg_lower = message.lower()
scores = {}
for cat, keywords in categories.items():
score = 0
for kw in keywords:
if re.search(r'\b' + re.escape(kw) + r'\b', msg_lower):
score += 1
if score:
scores[cat] = score
if scores:
# return max score category
return max(scores.items(), key=lambda x: x[1])[0]
return 'Other'
def main():
with open('/tmp/commits.txt', 'r') as f:
lines = f.readlines()
commits = []
current = []
for line in lines:
line = line.rstrip('\n')
if line and '|' in line and line.count('|') >= 2:
if current:
commits.append(''.join(current))
current = []
current.append(line + '\n')
if current:
commits.append(''.join(current))
categorized = {}
for commit in commits:
parts = commit.split('|', 2)
if len(parts) < 3:
continue
hash_, subject, body = parts[0], parts[1], parts[2]
full_msg = subject + ' ' + body
cat = categorize_commit(full_msg)
categorized.setdefault(cat, []).append((hash_, subject, body))
# Print categorized
for cat in sorted(categorized.keys()):
print(f'\n## {cat}\n')
for hash_, subject, body in categorized[cat]:
# truncate subject if too long
print(f'- {hash_[:8]} {subject}')
# print body lines indented
if body.strip():
for line in body.strip().split('\n'):
if line.strip():
print(f' {line.strip()}')
print()
# Print stats
print('\n---\n')
for cat in sorted(categorized.keys()):
print(f'{cat}: {len(categorized[cat])}')
if __name__ == '__main__':
main()
Loading…
Cancel
Save