Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .github/workflows/release-tag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@

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

Check failure

Code scanning / zizmor

code injection via template expansion: may expand into attacker-controllable code Error

code injection via template expansion: may expand into attacker-controllable code

Check notice

Code scanning / zizmor

code injection via template expansion: may expand into attacker-controllable code Note

code injection via template expansion: may expand into attacker-controllable code

- name: Create Release for Tag
# only run if tag is not alpha
if: steps.tag.outputs.pkgName
Expand All @@ -48,5 +53,4 @@
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
131 changes: 131 additions & 0 deletions scripts/extractReleaseNotes.js
Original file line number Diff line number Diff line change
@@ -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 <pkgName> <tagName> <outputPath>',
)
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(`^## (?:<small>)?\\[${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(
`^## (?:<small>)?\\[?${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 (/^## (?:<small>)?(?:\[|\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
}
Loading