#!/usr/bin/env bash VERSION=$1 if [ -z "$VERSION" ]; then echo "Usage: $0 " echo "Example: $0 1.2.0" exit 1 fi VERSION="v$VERSION" # Check if this page already exists in docs/changelogs/ if [ -f "docs/changelogs/$VERSION.mdx" ]; then echo "✅ Changelog for $VERSION already exists" exit 0 fi # Source changelog utilities source "$(dirname "$0")/changelog-utils.sh" # Get current date CURRENT_DATE=$(date +"%Y-%m-%d") # Preparing changelog file CHANGELOG_BODY="--- title: \"$VERSION\" description: \"$VERSION changelog - $CURRENT_DATE\" --- \`\`\`bash npx -y @maximhq/bifrost --transport-version $VERSION \`\`\` \`\`\`bash docker pull maximhq/bifrost:$VERSION docker run -p 8080:8080 maximhq/bifrost:$VERSION \`\`\` " # Array to track cleaned changelog files CLEANED_CHANGELOG_FILES=() # Helper to append a section if changelog file exists and is non-empty append_section () { label=$1 path=$2 if [ -f "$path" ]; then # Get changelog content content=$(get_file_content "$path") # If changelog is empty, skip if [ -z "$content" ]; then echo "❌ Changelog is empty" return fi # Remove /changelog.md from the path and add /version version_file_path="${path%/changelog.md}/version" # Get version content version_body=$(get_file_content "$version_file_path") # Build the changelog section CHANGELOG_BODY+=$'\n'""$'\n'"$content"$'\n\n'"" # Clear the changelog file after processing printf '' > "$path" # Track this file for git commit CLEANED_CHANGELOG_FILES+=("$path") fi } # HTTP changelog append_section "Bifrost(HTTP)" transports/changelog.md # Core changelog append_section "Core" core/changelog.md # Framework changelog append_section "Framework" framework/changelog.md # Plugins changelogs for plugin in plugins/*; do name=$(basename "$plugin") append_section "$name" "$plugin/changelog.md" done # Write to file mkdir -p docs/changelogs echo "$CHANGELOG_BODY" > docs/changelogs/$VERSION.mdx # Update docs.json to include this new changelog route in the Changelogs tab pages array # Uses month-based grouping: current month at top level, older months in groups # Automatically reorganizes when month changes route="changelogs/$VERSION" if ! grep -q "\"$route\"" docs/docs.json; then node -e " const fs = require('fs'); const docs = JSON.parse(fs.readFileSync('docs/docs.json', 'utf8')); // Semantic version comparison function // Extracts version from route/filename and compares in descending order (newest first) function compareVersionsDesc(a, b) { // Extract route string from string or object const routeA = typeof a === 'string' ? a : ''; const routeB = typeof b === 'string' ? b : ''; // Extract version from route (e.g., 'changelogs/v1.3.34' -> 'v1.3.34') const versionA = routeA.split('/').pop() || ''; const versionB = routeB.split('/').pop() || ''; // Remove 'v' prefix and split into parts const partsA = versionA.replace(/^v/, '').split(/[.-]/).map(p => { const num = parseInt(p, 10); return isNaN(num) ? p : num; }); const partsB = versionB.replace(/^v/, '').split(/[.-]/).map(p => { const num = parseInt(p, 10); return isNaN(num) ? p : num; }); // Compare each part (major, minor, patch, pre-release, etc.) const maxLength = Math.max(partsA.length, partsB.length); for (let i = 0; i < maxLength; i++) { // Release vs prerelease: release is newer (no suffix > has suffix) if (partsA[i] === undefined && partsB[i] !== undefined) { return -1; // A (release) comes first in descending order } if (partsB[i] === undefined && partsA[i] !== undefined) { return 1; // B (release) comes first in descending order } const partA = partsA[i]; const partB = partsB[i]; // If both are numbers, compare numerically if (typeof partA === 'number' && typeof partB === 'number') { if (partA !== partB) { return partB - partA; // Descending order } } else { // Handle prerelease strings with numeric suffixes (e.g., 'prerelease10') const strA = String(partA); const strB = String(partB); const matchA = strA.match(/^([a-zA-Z]+)(\\d+)$/); const matchB = strB.match(/^([a-zA-Z]+)(\\d+)$/); if (matchA && matchB && matchA[1] === matchB[1]) { // Same prefix, compare numbers numerically const numA = parseInt(matchA[2], 10); const numB = parseInt(matchB[2], 10); if (numA !== numB) { return numB - numA; // Descending order } } else if (strA !== strB) { return strB.localeCompare(strA); // Descending order } } } return 0; // Equal } // Sort a pages array by semver (descending) function sortPagesBySemver(pages) { return pages.slice().sort(compareVersionsDesc); } // Get current month/year const releaseDate = new Date('$CURRENT_DATE'); const currentDate = new Date(); const releaseMonthYear = releaseDate.toLocaleDateString('en-US', { year: 'numeric', month: 'long' }); const currentMonthYear = currentDate.toLocaleDateString('en-US', { year: 'numeric', month: 'long' }); // Find the Changelogs tab const changelogsTab = docs.navigation.tabs.find(tab => tab.tab === 'Changelogs'); if (!changelogsTab) { console.error('Changelogs tab not found'); process.exit(1); } // Find the Open Source menu item const openSourceItem = changelogsTab.menu?.find(item => item.item === 'Open Source'); if (!openSourceItem) { console.error('Open Source menu item not found in Changelogs tab'); process.exit(1); } // Get all top-level entries and existing groups const topLevelEntries = openSourceItem.pages.filter(p => typeof p === 'string'); const existingGroups = openSourceItem.pages.filter(p => typeof p === 'object'); // Check if we need to group existing top-level entries if (topLevelEntries.length > 0) { // Get the month of the first top-level entry (they should all be from same month) const firstEntryPath = topLevelEntries[0].replace('changelogs/', '') + '.mdx'; const firstEntryFile = 'docs/changelogs/' + firstEntryPath; let topLevelMonth = null; try { const content = fs.readFileSync(firstEntryFile, 'utf8'); const descMatch = content.match(/description:\\s*\"[^\"]*?(\\d{4}-\\d{2}-\\d{2})[^\"]*\"/); if (descMatch) { const entryDate = new Date(descMatch[1]); topLevelMonth = entryDate.toLocaleDateString('en-US', { year: 'numeric', month: 'long' }); } } catch (e) { console.log(\`Warning: Could not read entry file \${firstEntryFile}: \${e.message}\`); } // Only group if the month has changed if (topLevelMonth && topLevelMonth !== releaseMonthYear) { console.log(\`📦 Month changed from \${topLevelMonth} to \${releaseMonthYear}\`); console.log(\`📦 Grouping \${topLevelEntries.length} top-level entries into \${topLevelMonth} group...\`); // Create a group for all existing top-level entries const previousMonthGroup = { group: topLevelMonth, pages: sortPagesBySemver(topLevelEntries) }; // Add this group at the top of existing groups existingGroups.unshift(previousMonthGroup); console.log(\`✅ Created \${topLevelMonth} group with \${topLevelEntries.length} entries (sorted)\`); // Clear top-level entries (they're now in the group) openSourceItem.pages = existingGroups; } else { console.log(\`📋 Same month (\${releaseMonthYear}), keeping existing top-level entries\`); // Keep existing structure (top-level entries + groups) openSourceItem.pages = [...topLevelEntries, ...existingGroups]; } } const newRoute = '$route'; // Add the new changelog at the top level openSourceItem.pages.unshift(newRoute); console.log(\`✅ Added \${newRoute} to top level\`); // Sort the top-level pages array by semver const topLevelPages = openSourceItem.pages.filter(p => typeof p === 'string'); const groupPages = openSourceItem.pages.filter(p => typeof p === 'object'); if (topLevelPages.length > 0) { const sortedTopLevel = sortPagesBySemver(topLevelPages); openSourceItem.pages = [...sortedTopLevel, ...groupPages]; console.log(\`✅ Sorted \${topLevelPages.length} top-level pages by semver\`); } // Sort each group's pages by semver for (const group of groupPages) { if (group.pages && Array.isArray(group.pages)) { group.pages = sortPagesBySemver(group.pages); } } fs.writeFileSync('docs/docs.json', JSON.stringify(docs, null, 2) + '\n'); console.log('✅ Updated docs.json'); " fi # Pulling again before committing CURRENT_BRANCH="$(git rev-parse --abbrev-ref HEAD)" if [ "$CURRENT_BRANCH" = "HEAD" ]; then # In detached HEAD state (common in CI), use GITHUB_REF_NAME or default to main CURRENT_BRANCH="${GITHUB_REF_NAME:-main}" fi echo "Pulling latest changes from origin/$CURRENT_BRANCH..." if ! git pull origin "$CURRENT_BRANCH"; then echo "❌ Error: git pull origin $CURRENT_BRANCH failed" exit 1 fi # Commit and push changes git add docs/changelogs/$VERSION.mdx git add docs/docs.json # Add all cleaned changelog files for file in "${CLEANED_CHANGELOG_FILES[@]}"; do git add "$file" done git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git commit -m "Adds changelog for $VERSION --skip-ci" git push origin "$CURRENT_BRANCH"