name: Update ABP Studio Docs on: repository_dispatch: types: [update_studio_docs] workflow_dispatch: inputs: version: description: 'Studio version (e.g., 2.1.10)' required: true name: description: 'Release name' required: true notes: description: 'Raw release notes' required: true url: description: 'Release URL' required: true target_branch: description: 'Target branch (default: dev)' required: false default: 'dev' jobs: update-docs: runs-on: ubuntu-latest permissions: contents: write pull-requests: write models: read steps: # ------------------------------------------------- # Extract payload (repository_dispatch or workflow_dispatch) # ------------------------------------------------- - name: Extract payload id: payload run: | if [ "${{ github.event_name }}" = "repository_dispatch" ]; then echo "version=${{ github.event.client_payload.version }}" >> $GITHUB_OUTPUT echo "name=${{ github.event.client_payload.name }}" >> $GITHUB_OUTPUT echo "url=${{ github.event.client_payload.url }}" >> $GITHUB_OUTPUT echo "target_branch=${{ github.event.client_payload.target_branch || 'dev' }}" >> $GITHUB_OUTPUT # Save notes to environment variable (multiline) { echo "RAW_NOTES<> $GITHUB_ENV else echo "version=${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT echo "name=${{ github.event.inputs.name }}" >> $GITHUB_OUTPUT echo "url=${{ github.event.inputs.url }}" >> $GITHUB_OUTPUT echo "target_branch=${{ github.event.inputs.target_branch || 'dev' }}" >> $GITHUB_OUTPUT # Save notes to environment variable (multiline) { echo "RAW_NOTES<> $GITHUB_ENV fi - name: Validate payload env: VERSION: ${{ steps.payload.outputs.version }} NAME: ${{ steps.payload.outputs.name }} URL: ${{ steps.payload.outputs.url }} TARGET_BRANCH: ${{ steps.payload.outputs.target_branch }} run: | if [ -z "$VERSION" ] || [ "$VERSION" = "null" ]; then echo "❌ Missing: version" exit 1 fi if [ -z "$NAME" ] || [ "$NAME" = "null" ]; then echo "❌ Missing: name" exit 1 fi if [ -z "$URL" ] || [ "$URL" = "null" ]; then echo "❌ Missing: url" exit 1 fi if [ -z "$RAW_NOTES" ]; then echo "❌ Missing: release notes" exit 1 fi echo "✅ Payload validated" echo " Version: $VERSION" echo " Name: $NAME" echo " Target Branch: $TARGET_BRANCH" # ------------------------------------------------- # Checkout target branch # ------------------------------------------------- - name: Checkout uses: actions/checkout@v4 with: ref: ${{ steps.payload.outputs.target_branch }} fetch-depth: 0 - name: Configure git run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" # ------------------------------------------------- # Create working branch # ------------------------------------------------- - name: Create branch env: VERSION: ${{ steps.payload.outputs.version }} run: | BRANCH="docs/studio-${VERSION}" # Delete remote branch if exists (idempotent) git push origin --delete "$BRANCH" 2>/dev/null || true git checkout -B "$BRANCH" echo "BRANCH=$BRANCH" >> $GITHUB_ENV # ------------------------------------------------- # Analyze existing release notes format # ------------------------------------------------- - name: Analyze existing format id: analyze run: | FILE="docs/en/studio/release-notes.md" if [ -f "$FILE" ] && [ -s "$FILE" ]; then { echo "EXISTING_FORMAT<> $GITHUB_OUTPUT else { echo "EXISTING_FORMAT<> $GITHUB_OUTPUT fi # ------------------------------------------------- # Try AI formatting (OPTIONAL - never fails workflow) # ------------------------------------------------- - name: Format release notes with AI id: ai continue-on-error: true uses: actions/ai-inference@v1 with: model: openai/gpt-4.1 prompt: | You are a technical writer for ABP Studio release notes. Existing release notes format: ${{ steps.analyze.outputs.EXISTING_FORMAT }} New release: Version: ${{ steps.payload.outputs.version }} Name: ${{ steps.payload.outputs.name }} Raw notes: ${{ env.RAW_NOTES }} CRITICAL RULES: 1. Extract ONLY essential, user-facing changes 2. Format as bullet points starting with "- " 3. Keep it concise and professional 4. Match the style of existing release notes 5. Skip internal/technical details unless critical 6. Return ONLY the bullet points (no version header, no date) 7. One change per line Output example: - Fixed books sample for blazor-webapp tiered solution - Enhanced Module Installation UI - Added AI Management option to Startup Templates Return ONLY the formatted bullet points. # ------------------------------------------------- # Fallback: Use raw notes if AI unavailable # ------------------------------------------------- - name: Prepare final release notes run: | mkdir -p .tmp AI_RESPONSE="${{ steps.ai.outputs.response }}" if [ -n "$AI_RESPONSE" ] && [ "$AI_RESPONSE" != "null" ]; then echo "✅ Using AI-formatted release notes" echo "$AI_RESPONSE" > .tmp/final-notes.txt else echo "⚠️ AI unavailable - using aggressive cleaning on raw release notes" # Clean and format raw notes with aggressive filtering echo "$RAW_NOTES" | while IFS= read -r line; do # Skip empty lines [ -z "$line" ] && continue # Skip section headers [[ "$line" =~ ^#+.*What.*Changed ]] && continue [[ "$line" =~ ^##[[:space:]] ]] && continue # Skip full changelog links [[ "$line" =~ ^\*\*Full\ Changelog ]] && continue [[ "$line" =~ ^Full\ Changelog ]] && continue # Remove leading bullet/asterisk line=$(echo "$line" | sed 's/^[[:space:]]*[*-][[:space:]]*//') # Aggressive cleaning: remove entire " by @user in https://..." suffix line=$(echo "$line" | sed 's/[[:space:]]*by @[a-zA-Z0-9_-]*[[:space:]]*in https:\/\/github\.com\/[^[:space:]]*//g') # Remove remaining "by @username" or "by username" line=$(echo "$line" | sed 's/[[:space:]]*by @[a-zA-Z0-9_-]*[[:space:]]*$//g') line=$(echo "$line" | sed 's/[[:space:]]*by [a-zA-Z0-9_-]*[[:space:]]*$//g') # Remove standalone @mentions line=$(echo "$line" | sed 's/@[a-zA-Z0-9_-]*//g') # Clean trailing periods if orphaned line=$(echo "$line" | sed 's/\.[[:space:]]*$//') # Trim all whitespace line=$(echo "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') # Skip if line is empty or too short [ -z "$line" ] && continue [ ${#line} -lt 5 ] && continue # Capitalize first letter if lowercase line="$(echo ${line:0:1} | tr '[:lower:]' '[:upper:]')${line:1}" # Add clean bullet and output echo "- $line" done > .tmp/final-notes.txt fi # Safety check: verify we have content if [ ! -s .tmp/final-notes.txt ]; then echo "⚠️ No valid release notes extracted, using minimal fallback" echo "- Release ${{ steps.payload.outputs.version }}" > .tmp/final-notes.txt fi echo "=== Final release notes ===" cat .tmp/final-notes.txt echo "===========================" # ------------------------------------------------- # Update release-notes.md (move "Latest" tag correctly) # ------------------------------------------------- - name: Update release-notes.md env: VERSION: ${{ steps.payload.outputs.version }} NAME: ${{ steps.payload.outputs.name }} URL: ${{ steps.payload.outputs.url }} run: | FILE="docs/en/studio/release-notes.md" DATE="$(date +%Y-%m-%d)" mkdir -p docs/en/studio # Check if version already exists (idempotent) if [ -f "$FILE" ] && grep -q "^## $VERSION " "$FILE"; then echo "⚠️ Version $VERSION already exists in release notes - skipping update" echo "VERSION_UPDATED=false" >> $GITHUB_ENV exit 0 fi # Read final notes NOTES_CONTENT="$(cat .tmp/final-notes.txt)" # Create new entry NEW_ENTRY="## $VERSION ($DATE) Latest $NOTES_CONTENT " # Process file if [ ! -f "$FILE" ]; then # Create new file cat > "$FILE" < "$FILE.new" mv "$FILE.new" "$FILE" fi echo "VERSION_UPDATED=true" >> $GITHUB_ENV echo "=== Updated release-notes.md preview ===" head -30 "$FILE" echo "========================================" # ------------------------------------------------- # Fetch latest stable ABP version (no preview/rc/beta) # ------------------------------------------------- - name: Fetch latest stable ABP version id: abp run: | # Fetch all releases RELEASES=$(curl -fsS \ -H "Accept: application/vnd.github+json" \ -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ "https://api.github.com/repos/abpframework/abp/releases?per_page=20") # Filter stable releases (exclude preview, rc, beta, dev) ABP_VERSION=$(echo "$RELEASES" | jq -r ' [.[] | select( (.prerelease == false) and (.tag_name | test("preview|rc|beta|dev"; "i") | not) )] | first | .tag_name ') if [ -z "$ABP_VERSION" ] || [ "$ABP_VERSION" = "null" ]; then echo "❌ Could not determine latest stable ABP version" exit 1 fi echo "✅ Latest stable ABP version: $ABP_VERSION" echo "ABP_VERSION=$ABP_VERSION" >> $GITHUB_ENV # ------------------------------------------------- # Update version-mapping.md (smart range expansion) # ------------------------------------------------- - name: Update version-mapping.md env: STUDIO_VERSION: ${{ steps.payload.outputs.version }} run: | FILE="docs/en/studio/version-mapping.md" ABP_VERSION="${{ env.ABP_VERSION }}" mkdir -p docs/en/studio # Create file if doesn't exist if [ ! -f "$FILE" ]; then cat > "$FILE" <> $GITHUB_ENV exit 0 fi # Use Python for smart version range handling python3 <<'PYTHON_EOF' import os import re from packaging.version import Version, InvalidVersion studio_ver = os.environ["STUDIO_VERSION"] abp_ver = os.environ["ABP_VERSION"] file_path = "docs/en/studio/version-mapping.md" try: studio = Version(studio_ver) except InvalidVersion: print(f"❌ Invalid Studio version: {studio_ver}") exit(1) with open(file_path, 'r') as f: lines = f.readlines() # Find table start (skip SEO and headers) table_start = 0 table_end = 0 for i, line in enumerate(lines): if line.strip().startswith('|') and '**ABP Studio Version**' in line: table_start = i elif table_start > 0 and line.strip() and not line.strip().startswith('|'): table_end = i break if table_start == 0: print("❌ Could not find version mapping table") exit(1) # If no end found, table goes to end of file if table_end == 0: table_end = len(lines) # Extract sections before_table = lines[:table_start] # Everything before table table_header = lines[table_start:table_start+2] # Header + separator data_rows = [l for l in lines[table_start+2:table_end] if l.strip().startswith('|')] # Data rows after_table = lines[table_end:] # Everything after table new_rows = [] handled = False def parse_version_range(version_str): """Parse '2.1.5 - 2.1.9' or '2.1.5' into (start, end)""" version_str = version_str.strip() if '–' in version_str or '-' in version_str: # Handle both em-dash and hyphen parts = re.split(r'\s*[–-]\s*', version_str) if len(parts) == 2: try: return Version(parts[0].strip()), Version(parts[1].strip()) except InvalidVersion: return None, None try: v = Version(version_str) return v, v except InvalidVersion: return None, None def format_row(studio_range, abp_version): """Format a table row with proper spacing""" return f"| {studio_range:<22} | {abp_version:<27} |\n" # Process existing rows for row in data_rows: match = re.match(r'\|\s*(.+?)\s*\|\s*(.+?)\s*\|', row) if not match: continue existing_studio_range = match.group(1).strip() existing_abp = match.group(2).strip() # Only consider rows with matching ABP version if existing_abp != abp_ver: new_rows.append(row) continue start_ver, end_ver = parse_version_range(existing_studio_range) if start_ver is None or end_ver is None: new_rows.append(row) continue # Check if current studio version is in this range if start_ver <= studio <= end_ver: print(f"✅ Studio version {studio_ver} already covered in range {existing_studio_range}") handled = True new_rows.append(row) # Check if we should extend the range elif end_ver < studio: # Calculate if studio is the next logical version # For patch versions: 2.1.9 -> 2.1.10 # For minor versions: 2.1.9 -> 2.2.0 # Simple heuristic: if major.minor match and patch increments, extend range if (start_ver.major == studio.major and start_ver.minor == studio.minor and studio.micro <= end_ver.micro + 5): # Allow small gaps new_range = f"{start_ver} - {studio}" new_rows.append(format_row(new_range, abp_ver)) print(f"✅ Extended range: {new_range}") handled = True else: new_rows.append(row) else: new_rows.append(row) # If not handled, add new row at top of data if not handled: new_row = format_row(str(studio), abp_ver) new_rows.insert(0, new_row) print(f"✅ Added new mapping: {studio_ver} -> {abp_ver}") # Write updated file - preserve ALL content with open(file_path, 'w') as f: f.writelines(before_table) # SEO, title, intro text f.writelines(table_header) # Table header f.writelines(new_rows) # Updated data rows f.writelines(after_table) # Content after table (preview section, etc.) print("MAPPING_UPDATED=true") PYTHON_EOF echo "MAPPING_UPDATED=true" >> $GITHUB_ENV echo "=== Updated version-mapping.md preview ===" head -35 "$FILE" echo "==========================================" # ------------------------------------------------- # Check for changes # ------------------------------------------------- - name: Check for changes id: changes run: | git add docs/en/studio/ if git diff --cached --quiet; then echo "has_changes=false" >> $GITHUB_OUTPUT echo "⚠️ No changes detected" else echo "has_changes=true" >> $GITHUB_OUTPUT echo "✅ Changes detected:" git diff --cached --stat fi # ------------------------------------------------- # Commit & push # ------------------------------------------------- - name: Commit and push if: steps.changes.outputs.has_changes == 'true' env: VERSION: ${{ steps.payload.outputs.version }} NAME: ${{ steps.payload.outputs.name }} run: | git commit -m "docs(studio): update documentation for release $VERSION - Updated release notes for $VERSION - Updated version mapping with ABP ${{ env.ABP_VERSION }} Release: $NAME" git push -f origin "$BRANCH" # ------------------------------------------------- # Create or update PR # ------------------------------------------------- - name: Create or update PR if: steps.changes.outputs.has_changes == 'true' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} VERSION: ${{ steps.payload.outputs.version }} NAME: ${{ steps.payload.outputs.name }} URL: ${{ steps.payload.outputs.url }} TARGET_BRANCH: ${{ steps.payload.outputs.target_branch }} run: | # Check for existing PR EXISTING_PR=$(gh pr list \ --head "$BRANCH" \ --base "$TARGET_BRANCH" \ --json number \ --jq '.[0].number' 2>/dev/null || echo "") PR_BODY="Automated documentation update for ABP Studio release **$VERSION**. ## Release Information - **Version**: $VERSION - **Name**: $NAME - **Release**: [View on GitHub]($URL) - **ABP Framework Version**: ${{ env.ABP_VERSION }} ## Changes - ✅ Updated [release-notes.md](docs/en/studio/release-notes.md) - ✅ Updated [version-mapping.md](docs/en/studio/version-mapping.md) --- *This PR was automatically generated by the [update-studio-docs workflow](.github/workflows/update-studio-docs.yml)*" if [ -n "$EXISTING_PR" ]; then echo "🔄 Updating existing PR #$EXISTING_PR" gh pr edit "$EXISTING_PR" \ --title "docs(studio): release $VERSION - $NAME" \ --body "$PR_BODY" echo "PR_NUMBER=$EXISTING_PR" >> $GITHUB_ENV else echo "📝 Creating new PR" sleep 2 # Wait for GitHub to sync PR_URL=$(gh pr create \ --title "docs(studio): release $VERSION - $NAME" \ --body "$PR_BODY" \ --base "$TARGET_BRANCH" \ --head "$BRANCH") PR_NUMBER=$(echo "$PR_URL" | grep -oE '[0-9]+$') echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_ENV echo "✅ Created PR #$PR_NUMBER: $PR_URL" fi # ------------------------------------------------- # Enable auto-merge (safe with branch protection) # ------------------------------------------------- - name: Enable auto-merge if: steps.changes.outputs.has_changes == 'true' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} continue-on-error: true run: | echo "🔄 Attempting to enable auto-merge for PR #$PR_NUMBER" gh pr merge "$PR_NUMBER" \ --auto \ --squash \ --delete-branch || { echo "⚠️ Auto-merge not available (branch protection or permissions)" echo " PR #$PR_NUMBER is ready for manual review" } # ------------------------------------------------- # Summary # ------------------------------------------------- - name: Workflow summary if: always() env: VERSION: ${{ steps.payload.outputs.version }} run: | echo "## 📚 ABP Studio Docs Update Summary" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "**Version**: $VERSION" >> $GITHUB_STEP_SUMMARY echo "**Release**: ${{ steps.payload.outputs.name }}" >> $GITHUB_STEP_SUMMARY echo "**Target Branch**: ${{ steps.payload.outputs.target_branch }}" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY if [ "${{ steps.changes.outputs.has_changes }}" = "true" ]; then echo "### ✅ Changes Applied" >> $GITHUB_STEP_SUMMARY echo "- Release notes updated: ${{ env.VERSION_UPDATED }}" >> $GITHUB_STEP_SUMMARY echo "- Version mapping updated: ${{ env.MAPPING_UPDATED }}" >> $GITHUB_STEP_SUMMARY echo "- ABP Framework version: ${{ env.ABP_VERSION }}" >> $GITHUB_STEP_SUMMARY echo "- PR: #${{ env.PR_NUMBER }}" >> $GITHUB_STEP_SUMMARY else echo "### ⚠️ No Changes" >> $GITHUB_STEP_SUMMARY echo "Version $VERSION already exists in documentation." >> $GITHUB_STEP_SUMMARY fi