snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
This commit is contained in:
258
internal/scripts/extract-draft-changelog.tsx
Normal file
258
internal/scripts/extract-draft-changelog.tsx
Normal file
@@ -0,0 +1,258 @@
|
||||
import { join } from 'path'
|
||||
import { exec } from './lib/exec'
|
||||
import { REPO_ROOT, writeStringFile } from './lib/file'
|
||||
import { nicelog } from './lib/nicelog'
|
||||
|
||||
function convertPRLinks(text: string): string {
|
||||
// Convert (#1234) to ([#1234](https://github.com/tldraw/tldraw/issues/1234))
|
||||
return text.replace(/\(#(\d+)\)/g, '([#$1](https://github.com/tldraw/tldraw/issues/$1))')
|
||||
}
|
||||
|
||||
function extractRelevantSections(message: string): string {
|
||||
// Extract Release Notes and API Changes sections (case-insensitive, flexible markdown heading levels)
|
||||
const lines = message.split('\n')
|
||||
const relevantSections: string[] = []
|
||||
let inRelevantSection = false
|
||||
let currentSection: string[] = []
|
||||
|
||||
for (const line of lines) {
|
||||
// Check if this is a heading line (one or more #s followed by text)
|
||||
const headingMatch = line.match(/^(#{1,6})\s+(.+)$/i)
|
||||
if (headingMatch) {
|
||||
const headingText = headingMatch[2].trim().toLowerCase()
|
||||
|
||||
// If we were in a relevant section, save it
|
||||
if (inRelevantSection && currentSection.length > 0) {
|
||||
relevantSections.push(currentSection.join('\n'))
|
||||
currentSection = []
|
||||
}
|
||||
|
||||
// Check if this is a Release Notes or API Changes heading
|
||||
if (headingText.includes('release note') || headingText.includes('api change')) {
|
||||
inRelevantSection = true
|
||||
// Include the heading itself in the section
|
||||
currentSection.push(line)
|
||||
} else {
|
||||
inRelevantSection = false
|
||||
}
|
||||
} else if (inRelevantSection) {
|
||||
// We're in a relevant section, collect the line
|
||||
currentSection.push(line)
|
||||
}
|
||||
}
|
||||
|
||||
// Don't forget the last section
|
||||
if (inRelevantSection && currentSection.length > 0) {
|
||||
relevantSections.push(currentSection.join('\n'))
|
||||
}
|
||||
|
||||
// If we found relevant sections, return them joined together
|
||||
if (relevantSections.length > 0) {
|
||||
// Remove the section headings themselves (first line of each section)
|
||||
const sectionsWithoutHeadings = relevantSections
|
||||
.map((section) => {
|
||||
const sectionLines = section.split('\n')
|
||||
// Remove the first line (the heading) and keep the rest
|
||||
return sectionLines.slice(1).join('\n').trim()
|
||||
})
|
||||
.filter((section) => section.length > 0)
|
||||
|
||||
return sectionsWithoutHeadings.join('\n\n').trim()
|
||||
}
|
||||
|
||||
// Otherwise, return empty string (no content to include)
|
||||
return ''
|
||||
}
|
||||
|
||||
async function getLatestMinorRelease() {
|
||||
// otherwise get the latest minor release
|
||||
const tags = (await exec('git', ['tag', '--list', `v*.*.0`, '--sort=-version:refname']))
|
||||
.toString()
|
||||
.trim()
|
||||
return tags.split('\n')[0]
|
||||
}
|
||||
|
||||
export async function extractChangelog(startRef: string, endRef: string): Promise<string> {
|
||||
nicelog(`Extracting changelog from ${startRef} to ${endRef}`)
|
||||
|
||||
const commitHashes = (await exec('git', ['cherry', startRef, endRef]))
|
||||
.trim()
|
||||
.split('\n')
|
||||
.filter((line) => !line.startsWith('-'))
|
||||
.map((line) => line.split(' ')[1])
|
||||
|
||||
nicelog(`Found ${commitHashes.length} commits`)
|
||||
|
||||
// Extract full commit messages for each commit
|
||||
const entries: Array<{
|
||||
firstLine: string
|
||||
restOfMessage: string
|
||||
changeType: string
|
||||
changedFolders: string[]
|
||||
}> = []
|
||||
|
||||
for (const hash of commitHashes) {
|
||||
const fullMessage = await exec('git', ['log', '-1', '--pretty=format:%B', hash], {
|
||||
processStdoutLine: () => {}, // Suppress output
|
||||
processStderrLine: () => {},
|
||||
})
|
||||
|
||||
const lines = fullMessage.trim().split('\n')
|
||||
const firstLine = lines[0] || hash
|
||||
|
||||
// Skip commits with [skip ci] in the title
|
||||
if (firstLine.includes('[skip ci]')) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get changed files for this commit
|
||||
const changedFiles = await exec('git', ['show', '--name-only', '--pretty=format:', hash], {
|
||||
processStdoutLine: () => {}, // Suppress output
|
||||
processStderrLine: () => {},
|
||||
})
|
||||
|
||||
// Extract unique folders (max 2 levels deep) from changed files
|
||||
const folders = new Set<string>()
|
||||
changedFiles
|
||||
.split('\n')
|
||||
.filter((file) => file.trim())
|
||||
.forEach((file) => {
|
||||
const parts = file.split('/')
|
||||
if (parts.length >= 2) {
|
||||
folders.add(`${parts[0]}/${parts[1]}`)
|
||||
} else if (parts.length === 1) {
|
||||
folders.add(parts[0])
|
||||
}
|
||||
})
|
||||
|
||||
let restOfMessage = lines.slice(1).join('\n').trim()
|
||||
|
||||
// Extract change type from checkboxes
|
||||
let changeType = 'other'
|
||||
const changeTypes = ['bugfix', 'improvement', 'feature', 'api', 'other']
|
||||
|
||||
for (const type of changeTypes) {
|
||||
if (restOfMessage.includes(`- [x] \`${type}\``)) {
|
||||
changeType = type
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the Change type section and all checkboxes
|
||||
restOfMessage = restOfMessage
|
||||
// remove cursor summary
|
||||
.replace(/<!-- CURSOR_SUMMARY -->(.|\s)*/gm, '')
|
||||
// remove Change type section
|
||||
.replace(/### Change type[\s\n]*/g, '')
|
||||
// remove checkboxes
|
||||
.replace(/- \[[ x]\] `(bugfix|improvement|feature|api|other)`[\s\n]*/g, '')
|
||||
// remove co-authored-by lines
|
||||
.replace(/^co-authored-by: .*$/gim, '')
|
||||
// Clean up excessive newlines
|
||||
.replace(/\n{3,}/g, '\n\n')
|
||||
// remove trailing horizontal rule
|
||||
.trim()
|
||||
.replace(/-{3,}$/, '')
|
||||
.trim()
|
||||
|
||||
// Extract only Release Notes and API Changes sections
|
||||
const extractedContent = extractRelevantSections(restOfMessage)
|
||||
|
||||
// Convert PR links like (#1234) to full URLs
|
||||
const contentWithLinks = convertPRLinks(extractedContent)
|
||||
|
||||
// Only include entries that touch packages/ folder (but not internal packages)
|
||||
const excludedPackages = ['packages/dotcom-shared', 'packages/worker-shared']
|
||||
const touchesPackages = Array.from(folders).some(
|
||||
(folder) => folder.startsWith('packages/') && !excludedPackages.includes(folder)
|
||||
)
|
||||
if (touchesPackages) {
|
||||
entries.push({
|
||||
firstLine: convertPRLinks(firstLine),
|
||||
restOfMessage: contentWithLinks,
|
||||
changeType,
|
||||
changedFolders: Array.from(folders).sort(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Group entries by change type
|
||||
const groupedEntries: Record<string, typeof entries> = {
|
||||
bugfix: [],
|
||||
improvement: [],
|
||||
feature: [],
|
||||
api: [],
|
||||
other: [],
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
groupedEntries[entry.changeType].push(entry)
|
||||
}
|
||||
|
||||
// Generate output.md content
|
||||
let output = `Generated from commits between \`${startRef}\` and \`${endRef}\`\n\n`
|
||||
|
||||
// Output each change type section
|
||||
const changeTypeLabels = {
|
||||
bugfix: 'Bug Fixes',
|
||||
improvement: 'Improvements',
|
||||
feature: 'Features',
|
||||
api: 'API Changes',
|
||||
other: 'Other Changes',
|
||||
}
|
||||
|
||||
for (const [changeType, label] of Object.entries(changeTypeLabels)) {
|
||||
const entriesForType = groupedEntries[changeType]
|
||||
if (entriesForType.length === 0) continue
|
||||
|
||||
output += `## ${label}\n\n`
|
||||
|
||||
for (const entry of entriesForType) {
|
||||
output += `### ${entry.firstLine}\n\n`
|
||||
|
||||
if (entry.restOfMessage) {
|
||||
output += `${entry.restOfMessage}\n\n`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nicelog(`Extracted ${entries.length} changelog entries`)
|
||||
return output
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2)
|
||||
|
||||
await exec('git', ['fetch', 'origin'])
|
||||
await exec('git', ['fetch', 'origin', '--tags'])
|
||||
|
||||
if (args.includes('--help') || args.includes('-h')) {
|
||||
console.error('Usage: yarn extract-draft-changelog [start-ref] [end-ref]')
|
||||
console.error('Example: yarn extract-draft-changelog v2.0.0 HEAD')
|
||||
console.error('Example: yarn extract-draft-changelog v2.0.0 # uses current HEAD')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const startRef = args[0] || (await getLatestMinorRelease())
|
||||
const endRef = args[1] || 'origin/production'
|
||||
|
||||
try {
|
||||
const output = await extractChangelog(startRef, endRef)
|
||||
|
||||
// Write to changelog.md in the repo root
|
||||
const outputPath = join(REPO_ROOT, 'changelog.md')
|
||||
await writeStringFile(outputPath, output)
|
||||
|
||||
nicelog(`Draft changelog written to changelog.md`)
|
||||
} catch (error) {
|
||||
console.error('Error extracting changelog:', error)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main().catch((error) => {
|
||||
console.error('Unhandled error:', error)
|
||||
process.exit(1)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user