diff --git a/sgl-router/Makefile b/sgl-router/Makefile index 9336ad0fa0d1..5e6cf3f2cbb9 100644 --- a/sgl-router/Makefile +++ b/sgl-router/Makefile @@ -19,7 +19,8 @@ else endif .PHONY: help build test clean docs check fmt dev-setup pre-commit setup-sccache sccache-stats sccache-clean sccache-stop \ - python-dev python-build python-build-release python-install python-clean python-test python-check + python-dev python-build python-build-release python-install python-clean python-test python-check \ + release-notes help: ## Show this help message @echo "Model Gateway Development Commands" @@ -132,3 +133,27 @@ python-check: ## Check Python package with twine dev: python-dev ## Quick development setup (build Python bindings in dev mode) install: python-install ## Build and install everything + +# Release management +release-notes: ## Generate release notes for gateway (usage: make release-notes PREV=gateway-v0.2.2 CURR=gateway-v1.0.0) + @if [ -z "$(PREV)" ] || [ -z "$(CURR)" ]; then \ + echo "Usage: make release-notes PREV= CURR="; \ + echo "Example: make release-notes PREV=gateway-v0.2.2 CURR=gateway-v1.0.0"; \ + echo ""; \ + echo "Options:"; \ + echo " OUTPUT= Save to file (default: stdout)"; \ + echo " CREATE_RELEASE=1 Create GitHub draft release via gh CLI (default: draft)"; \ + echo " DRAFT=0 Publish release immediately (skip draft)"; \ + exit 1; \ + fi + @ARGS="$(PREV) $(CURR)"; \ + if [ -n "$(OUTPUT)" ]; then \ + ARGS="$$ARGS --output $(OUTPUT)"; \ + fi; \ + if [ "$(CREATE_RELEASE)" = "1" ]; then \ + ARGS="$$ARGS --create-release"; \ + if [ "$(DRAFT)" = "0" ]; then \ + ARGS="$$ARGS --no-draft"; \ + fi; \ + fi; \ + ./scripts/generate_gateway_release_notes.sh $$ARGS diff --git a/sgl-router/README.md b/sgl-router/README.md index 3eede1d0c1bc..2830918dfbf6 100644 --- a/sgl-router/README.md +++ b/sgl-router/README.md @@ -587,4 +587,59 @@ For production builds, use `maturin build --release --out dist` from the `bindin --- +## Release Management + +### Creating Gateway Releases + +Create releases for the Gateway/Router component with filtered commits: + +```bash +# Using make +make release-notes PREV=gateway-v0.2.2 CURR=gateway-v1.0.0 + +# Save to file +make release-notes PREV=gateway-v0.2.2 CURR=gateway-v1.0.0 OUTPUT=RELEASE_NOTES.md + +# Create draft release (requires gh CLI, DEFAULT behavior) +make release-notes PREV=gateway-v0.2.2 CURR=gateway-v1.0.0 CREATE_RELEASE=1 + +# Publish release immediately (requires gh CLI) +make release-notes PREV=gateway-v0.2.2 CURR=gateway-v1.0.0 CREATE_RELEASE=1 DRAFT=0 +``` + +**Tag Naming**: Use `gateway-*` or `router-*` prefixes to avoid triggering unrelated CI workflows. + +### Release Workflow + +1. **Create and push tag**: + ```bash + git tag -a gateway-v1.0.0 -m "Gateway release v1.0.0" + git push origin gateway-v1.0.0 + ``` + +2. **Generate release notes** (automatically filters gateway-related commits): + ```bash + make release-notes PREV=gateway-v0.2.2 CURR=gateway-v1.0.0 + ``` + +3. **Create GitHub release**: + ```bash + # Create draft (DEFAULT - review before publishing) + make release-notes PREV=gateway-v0.2.2 CURR=gateway-v1.0.0 CREATE_RELEASE=1 + + # Or publish immediately (skip draft) + make release-notes PREV=gateway-v0.2.2 CURR=gateway-v1.0.0 CREATE_RELEASE=1 DRAFT=0 + ``` + +### Filtered Paths + +Release notes only include commits touching: +- `sgl-router/` - Router codebase +- `python/sglang/srt/grpc/` - gRPC protocol +- `python/sglang/srt/entrypoints/grpc_server.py` - gRPC server + +The script automatically extracts author attribution, PR links, and identifies new contributors. + +--- + SGLang Model Gateway continues to evolve alongside the core SGLang runtime. Contributions should keep CLI flags, documentation, and Python bindings in sync with the Rust implementation. diff --git a/sgl-router/scripts/generate_gateway_release_notes.sh b/sgl-router/scripts/generate_gateway_release_notes.sh new file mode 100755 index 000000000000..bb7f6ba01566 --- /dev/null +++ b/sgl-router/scripts/generate_gateway_release_notes.sh @@ -0,0 +1,312 @@ +#!/bin/bash +# Generate release notes for SGLang Gateway/Router +# Only includes commits that affect gateway-related paths + +set -e + +# Configuration +GATEWAY_PATHS=( + "sgl-router" + "python/sglang/srt/grpc" + "python/sglang/srt/entrypoints/grpc_server.py" +) + +# Colors for output +GREEN='\033[0;32m' +BLUE='\033[0;34m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Function to display usage +usage() { + echo "Usage: $0 " + echo "" + echo "Example: $0 gateway-v0.2.2 gateway-v0.2.3" + echo "" + echo "Options:" + echo " -o, --output FILE Save output to file (default: stdout)" + echo " -f, --format FORMAT Output format: markdown|github|plain (default: markdown)" + echo " --create-release Create GitHub release using gh CLI (default: draft)" + echo " --draft Create as draft release (default when using --create-release)" + echo " --no-draft Publish release immediately (skip draft)" + echo " -h, --help Show this help message" + exit 1 +} + +# Parse arguments +OUTPUT_FILE="" +FORMAT="markdown" +CREATE_RELEASE=false +DRAFT_RELEASE="default" # Default to draft unless explicitly disabled +PREV_TAG="" +CURR_TAG="" + +while [[ $# -gt 0 ]]; do + case $1 in + -o|--output) + OUTPUT_FILE="$2" + shift 2 + ;; + -f|--format) + FORMAT="$2" + shift 2 + ;; + --create-release) + CREATE_RELEASE=true + shift + ;; + --draft) + DRAFT_RELEASE=true + shift + ;; + --no-draft) + DRAFT_RELEASE=false + shift + ;; + -h|--help) + usage + ;; + *) + if [[ -z "$PREV_TAG" ]]; then + PREV_TAG="$1" + elif [[ -z "$CURR_TAG" ]]; then + CURR_TAG="$1" + else + echo "Error: Too many arguments" + usage + fi + shift + ;; + esac +done + +# Validate arguments +if [[ -z "$PREV_TAG" ]] || [[ -z "$CURR_TAG" ]]; then + echo "Error: Both previous and current tags are required" + usage +fi + +# Navigate to repo root (main sglang repo, not sgl-router) +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +cd "$REPO_ROOT" + +echo -e "${BLUE}Generating Gateway/Router release notes...${NC}" >&2 +echo -e "${BLUE}Previous: $PREV_TAG${NC}" >&2 +echo -e "${BLUE}Current: $CURR_TAG${NC}" >&2 +echo "" >&2 + +# Verify tags exist +if ! git rev-parse "$PREV_TAG" >/dev/null 2>&1; then + echo -e "${YELLOW}Warning: Tag $PREV_TAG not found, using initial commit${NC}" >&2 + PREV_TAG=$(git rev-list --max-parents=0 HEAD) +fi + +if ! git rev-parse "$CURR_TAG" >/dev/null 2>&1; then + echo -e "${YELLOW}Warning: Tag $CURR_TAG not found, using HEAD${NC}" >&2 + CURR_TAG="HEAD" +fi + +# Build path filter arguments +PATH_ARGS=() +for path in "${GATEWAY_PATHS[@]}"; do + PATH_ARGS+=("--" "$path") +done + +# Get filtered commit list +COMMITS=$(git log "$PREV_TAG..$CURR_TAG" --oneline --no-merges "${PATH_ARGS[@]}") + +if [[ -z "$COMMITS" ]]; then + echo -e "${YELLOW}No commits found for gateway paths between $PREV_TAG and $CURR_TAG${NC}" >&2 + exit 0 +fi + +COMMIT_COUNT=$(echo "$COMMITS" | wc -l | tr -d ' ') +echo -e "${GREEN}Found $COMMIT_COUNT gateway-related commits${NC}" >&2 + +# Get contributors +echo -e "${BLUE}Analyzing contributors...${NC}" >&2 + +# Get all contributors in this release (with commit count) +CONTRIBUTORS=$(git log "$PREV_TAG..$CURR_TAG" --format='%aN <%aE>' --no-merges "${PATH_ARGS[@]}" | sort | uniq -c | sort -rn) + +# Get all contributors before this release (from initial commit up to PREV_TAG) +# Using $(git rev-list --max-parents=0 HEAD) to get initial commit ensures we check entire history +INITIAL_COMMIT=$(git rev-list --max-parents=0 HEAD | tail -1) +PREV_CONTRIBUTORS=$(git log "$INITIAL_COMMIT..$PREV_TAG" --format='%aN <%aE>' --no-merges "${PATH_ARGS[@]}" | sort | uniq) + +# Find new contributors +NEW_CONTRIBUTORS="" +while IFS= read -r line; do + contributor=$(echo "$line" | sed 's/^[[:space:]]*[0-9]*[[:space:]]*//') + if [[ -n "$contributor" ]] && ! echo "$PREV_CONTRIBUTORS" | grep -Fxq "$contributor"; then + NEW_CONTRIBUTORS="$NEW_CONTRIBUTORS$contributor"$'\n' + fi +done <<< "$CONTRIBUTORS" + +CONTRIBUTOR_COUNT=$(echo "$CONTRIBUTORS" | grep -c '^' || echo 0) +NEW_CONTRIBUTOR_COUNT=$(echo "$NEW_CONTRIBUTORS" | grep -c '^' || echo 0) + +echo -e "${GREEN}Found $CONTRIBUTOR_COUNT contributors ($NEW_CONTRIBUTOR_COUNT new)${NC}" >&2 +echo "" >&2 + +# Generate release notes based on format +generate_notes() { + case $FORMAT in + markdown|github) + echo "## What's Changed in Gateway" + echo "" + echo "### Gateway Changes ($COMMIT_COUNT commits)" + echo "" + + # Categorize commits with author attribution + echo "$COMMITS" | while IFS= read -r line; do + commit_hash=$(echo "$line" | awk '{print $1}') + commit_msg=$(echo "$line" | cut -d' ' -f2-) + + # Get PR number from commit message + pr_num=$(echo "$commit_msg" | grep -o '(#[0-9]*' | grep -o '[0-9]*' | head -1) + + # Try to get GitHub username from PR if gh CLI is available + gh_user="" + if [[ -n "$pr_num" ]] && command -v gh &> /dev/null; then + gh_user=$(gh pr view "$pr_num" --json author --jq '.author.login' 2>/dev/null || echo "") + fi + + # Fallback: try to extract from email (works for users.noreply.github.com emails) + if [[ -z "$gh_user" ]]; then + email=$(git show -s --format='%aE' "$commit_hash") + gh_user=$(echo "$email" | sed 's/@users\.noreply\.github\.com$//' | sed 's/^[0-9]*+//') + # If still contains @, it's not a GitHub username + if [[ "$gh_user" == *"@"* ]]; then + gh_user="" + fi + fi + + # Format author link + if [[ -n "$gh_user" ]]; then + author_link="by @$gh_user" + else + # Final fallback: use full name + author=$(git show -s --format='%aN' "$commit_hash") + author_link="by $author" + fi + + # Format PR link + if [[ -n "$pr_num" ]]; then + pr_link="in https://github.com/sgl-project/sglang/pull/$pr_num" + else + pr_link="" + fi + + echo "- $commit_msg $author_link $pr_link" + done + + # New Contributors section + if [[ -n "$NEW_CONTRIBUTORS" ]] && [[ "$NEW_CONTRIBUTOR_COUNT" -gt 0 ]]; then + echo "" + echo "### New Contributors" + echo "" + while IFS= read -r contributor; do + if [[ -n "$contributor" ]]; then + # Extract name and email + name=$(echo "$contributor" | sed 's/ <.*//') + email=$(echo "$contributor" | sed 's/.*<\(.*\)>/\1/') + + # Get their first commit + first_commit=$(git log "$PREV_TAG..$CURR_TAG" --author="$contributor" --format='%h' --reverse --no-merges "${PATH_ARGS[@]}" | head -1) + + # Try to get GitHub username from first commit's PR + gh_user="" + if command -v gh &> /dev/null; then + commit_msg=$(git log --format=%s -n 1 "$first_commit") + pr_num=$(echo "$commit_msg" | grep -o '(#[0-9]*' | grep -o '[0-9]*' | head -1) + if [[ -n "$pr_num" ]]; then + gh_user=$(gh pr view "$pr_num" --json author --jq '.author.login' 2>/dev/null || echo "") + fi + fi + + # Fallback: try to get GitHub username from email + if [[ -z "$gh_user" ]]; then + gh_user=$(echo "$email" | sed 's/@users\.noreply\.github\.com$//' | sed 's/^[0-9]*+//') + # If still contains @, it's not a GitHub username + if [[ "$gh_user" == *"@"* ]]; then + gh_user="" + fi + fi + + if [[ -n "$gh_user" ]]; then + echo "* @$gh_user made their first contribution in https://github.com/sgl-project/sglang/commit/$first_commit" + else + echo "* $name made their first contribution in https://github.com/sgl-project/sglang/commit/$first_commit" + fi + fi + done <<< "$NEW_CONTRIBUTORS" + fi + + echo "" + echo "### Paths Included" + echo "" + for path in "${GATEWAY_PATHS[@]}"; do + echo "- \`$path\`" + done + echo "" + echo "**Full Changelog**: https://github.com/sgl-project/sglang/compare/$PREV_TAG...$CURR_TAG" + ;; + plain) + echo "Gateway/Router Release Notes: $CURR_TAG" + echo "==========================================" + echo "" + echo "$COMMITS" + echo "" + echo "Contributors: $CONTRIBUTOR_COUNT ($NEW_CONTRIBUTOR_COUNT new)" + ;; + esac +} + +# Output release notes +if [[ -n "$OUTPUT_FILE" ]]; then + generate_notes > "$OUTPUT_FILE" + echo -e "${GREEN}Release notes saved to: $OUTPUT_FILE${NC}" >&2 +else + generate_notes +fi + +# Create GitHub release if requested +if [[ "$CREATE_RELEASE" == true ]]; then + if ! command -v gh &> /dev/null; then + echo -e "${YELLOW}Error: gh CLI not found. Install from https://cli.github.com/${NC}" >&2 + exit 1 + fi + + NOTES_FILE=$(mktemp) + generate_notes > "$NOTES_FILE" + + # Default to draft if not explicitly set to false + if [[ "$DRAFT_RELEASE" == "default" ]]; then + DRAFT_RELEASE=true + fi + + echo "" >&2 + if [[ "$DRAFT_RELEASE" == true ]]; then + echo -e "${BLUE}Creating GitHub DRAFT release...${NC}" >&2 + else + echo -e "${BLUE}Creating GitHub release (publishing immediately)...${NC}" >&2 + fi + + # Build gh command with optional --draft flag + GH_ARGS=("$CURR_TAG" --title "Gateway/Router $CURR_TAG" --notes-file "$NOTES_FILE" --repo sgl-project/sglang) + if [[ "$DRAFT_RELEASE" == true ]]; then + GH_ARGS+=(--draft) + fi + + gh release create "${GH_ARGS[@]}" + + rm -f "$NOTES_FILE" + if [[ "$DRAFT_RELEASE" == true ]]; then + echo -e "${GREEN}Draft release created successfully!${NC}" >&2 + echo -e "${YELLOW}Visit https://github.com/sgl-project/sglang/releases to review and publish${NC}" >&2 + else + echo -e "${GREEN}Release published successfully!${NC}" >&2 + fi +fi