diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml index fa4b720085b812..07762985a93dd8 100644 --- a/.github/workflows/release-tag.yml +++ b/.github/workflows/release-tag.yml @@ -39,6 +39,11 @@ jobs: echo "pkgName=$pkgName" >> $GITHUB_OUTPUT + - name: Extract Release Notes + if: steps.tag.outputs.pkgName + run: | + node scripts/extractReleaseNotes.js ${{ steps.tag.outputs.pkgName }} ${{ github.ref_name }} .github/release-notes.md + - name: Create Release for Tag # only run if tag is not alpha if: steps.tag.outputs.pkgName @@ -48,5 +53,4 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tag_name: ${{ github.ref }} - body: | - Please refer to [CHANGELOG.md](https://github.com/vitejs/vite/blob/${{ github.ref_name }}/packages/${{ steps.tag.outputs.pkgName }}/CHANGELOG.md) for details. + body_path: .github/release-notes.md diff --git a/scripts/extractReleaseNotes.js b/scripts/extractReleaseNotes.js new file mode 100644 index 00000000000000..13a392af2ab1f6 --- /dev/null +++ b/scripts/extractReleaseNotes.js @@ -0,0 +1,131 @@ +import fs from 'node:fs' +import path from 'node:path' + +const pkgName = process.argv[2] +const tagName = process.argv[3] +const outputPath = process.argv[4] + +if (!pkgName || !tagName || !outputPath) { + console.error( + 'Usage: node extractReleaseNotes.js ', + ) + process.exit(1) +} + +function escapeRegex(str) { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +} + +function resolveRelativePath(pkgName, relativePath) { + const parts = `packages/${pkgName}/${relativePath}`.split('/') + const stack = [] + for (const part of parts) { + if (part === '.' || part === '') continue + if (part === '..') { + if (stack.length > 0) stack.pop() + } else { + stack.push(part) + } + } + return stack.join('/') +} + +try { + let version = tagName + if (pkgName === 'vite') { + if (version.startsWith('v')) { + version = version.slice(1) + } + } else { + if (version.includes('@')) { + version = version.split('@').pop() + } + } + + const changelogPath = path.resolve(`packages/${pkgName}/CHANGELOG.md`) + if (!fs.existsSync(changelogPath)) { + throw new Error( + `CHANGELOG.md not found for package ${pkgName} at ${changelogPath}`, + ) + } + + const content = fs.readFileSync(changelogPath, 'utf-8') + const lines = content.split('\n') + + const re = new RegExp(`^## (?:)?\\[${escapeRegex(version)}\\]`) + let idx = lines.findIndex((l) => re.test(l)) + if (idx === -1) { + // Try fallback check (e.g. without brackets, or matching any heading starting with version) + const fallbackRe = new RegExp( + `^## (?:)?\\[?${escapeRegex(version)}\\]?`, + ) + idx = lines.findIndex((l) => fallbackRe.test(l)) + } + + let releaseNotes = '' + const changelogUrl = `https://github.com/vitejs/vite/blob/${tagName}/packages/${pkgName}/CHANGELOG.md` + + if (idx !== -1) { + // Find where this version's section ends (the next version header) + let endIdx = lines.length + for (let i = idx + 1; i < lines.length; i++) { + if (/^## (?:)?(?:\[|\d)/.test(lines[i])) { + endIdx = i + break + } + } + + const sectionLines = lines.slice(idx + 1, endIdx) + // Trim empty lines at start and end + while (sectionLines.length > 0 && sectionLines[0].trim() === '') { + sectionLines.shift() + } + while ( + sectionLines.length > 0 && + sectionLines[sectionLines.length - 1].trim() === '' + ) { + sectionLines.pop() + } + + let rawBody = sectionLines.join('\n') + + // Rewrite relative markdown links/images to absolute links + const relativeRegex = /(!?)\[([^\]]+)\]\(([^)]+)\)/g + rawBody = rawBody.replace(relativeRegex, (match, isImage, text, url) => { + if ( + url.startsWith('http://') || + url.startsWith('https://') || + url.startsWith('mailto:') || + url.startsWith('#') + ) { + return match + } + const resolvedPath = resolveRelativePath(pkgName, url) + const absoluteUrl = `https://github.com/vitejs/vite/blob/${tagName}/${resolvedPath}` + return `${isImage}[${text}](${absoluteUrl})` + }) + + releaseNotes = + rawBody + + `\n\n---\n\nPlease refer to [CHANGELOG.md](${changelogUrl}) for details.` + } else { + // Fallback if version not found in CHANGELOG.md + releaseNotes = `Please refer to [CHANGELOG.md](${changelogUrl}) for details.` + } + + // Ensure output directory exists + const outputDir = path.dirname(outputPath) + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }) + } + + fs.writeFileSync(outputPath, releaseNotes, 'utf-8') + console.log(`Successfully extracted release notes to ${outputPath}`) +} catch (err) { + console.error('Error extracting release notes:', err) + // Fallback to minimal file so release tag step still completes + const fallbackUrl = `https://github.com/vitejs/vite/blob/${tagName}/packages/${pkgName}/CHANGELOG.md` + const fallbackContent = `Please refer to [CHANGELOG.md](${fallbackUrl}) for details.` + fs.writeFileSync(outputPath, fallbackContent, 'utf-8') + process.exit(0) // exit 0 to not break CI +}