@alleyinteractive/changelog-extractor 0.0.1 ā 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -31
- package/action.yml +28 -0
- package/dist/index.js +27479 -0
- package/dist/index.js.map +1 -0
- package/package.json +26 -16
- package/src/cli.js +136 -0
- package/src/index.js +4 -44
- package/src/main.js +50 -0
- package/src/parser.js +73 -55
- package/cli.js +0 -112
package/package.json
CHANGED
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alleyinteractive/changelog-extractor",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
4
5
|
"description": "Parse and extract structured information from Keep a Changelog formatted changelogs",
|
|
5
6
|
"main": "src/parser.js",
|
|
6
7
|
"bin": {
|
|
7
|
-
"changelog-extractor": "./cli.js"
|
|
8
|
+
"changelog-extractor": "./src/cli.js"
|
|
8
9
|
},
|
|
9
10
|
"scripts": {
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
11
|
+
"build": "npm run prettier && npm run package",
|
|
12
|
+
"fix-and-build": "npm run prettier:fix && npm run package",
|
|
13
|
+
"package:watch": "npm run package -- --watch",
|
|
14
|
+
"package": "npx rollup --config rollup.config.js",
|
|
15
|
+
"prepublishOnly": "npm test",
|
|
16
|
+
"prettier:fix": "npx prettier --write .",
|
|
17
|
+
"prettier": "npx prettier --check .",
|
|
18
|
+
"release:major": "npm run version:major && npm publish --access public && git push --follow-tags",
|
|
19
|
+
"release:minor": "npm run version:minor && npm publish --access public && git push --follow-tags",
|
|
20
|
+
"release:patch": "npm run version:patch && npm publish --access public && git push --follow-tags",
|
|
21
|
+
"test": "NODE_OPTIONS=--experimental-vm-modules NODE_NO_WARNINGS=1 npx jest"
|
|
13
22
|
},
|
|
14
23
|
"keywords": [
|
|
15
24
|
"changelog",
|
|
@@ -31,26 +40,27 @@
|
|
|
31
40
|
"homepage": "https://github.com/alleyinteractive/action-changelog-extractor#readme",
|
|
32
41
|
"files": [
|
|
33
42
|
"src/",
|
|
43
|
+
"dist/",
|
|
34
44
|
"cli.js",
|
|
45
|
+
"action.yml",
|
|
35
46
|
"README.md",
|
|
36
47
|
"LICENSE"
|
|
37
48
|
],
|
|
49
|
+
"exports": {
|
|
50
|
+
".": "./dist/index.js"
|
|
51
|
+
},
|
|
38
52
|
"devDependencies": {
|
|
53
|
+
"@jest/globals": "^30.2.0",
|
|
54
|
+
"@rollup/plugin-commonjs": "^29.0.0",
|
|
55
|
+
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
39
56
|
"@types/jest": "^29.5.12",
|
|
40
57
|
"@types/node": "^20.11.24",
|
|
41
|
-
"jest": "^
|
|
58
|
+
"jest": "^30.2.0",
|
|
59
|
+
"prettier": "^3.6.2",
|
|
60
|
+
"prettier-eslint": "^16.4.2",
|
|
61
|
+
"rollup": "^4.53.3"
|
|
42
62
|
},
|
|
43
63
|
"dependencies": {
|
|
44
64
|
"@actions/core": "^1.10.1"
|
|
45
|
-
},
|
|
46
|
-
"jest": {
|
|
47
|
-
"testEnvironment": "node",
|
|
48
|
-
"coverageDirectory": "coverage",
|
|
49
|
-
"collectCoverageFrom": [
|
|
50
|
-
"src/**/*.js"
|
|
51
|
-
],
|
|
52
|
-
"testMatch": [
|
|
53
|
-
"**/__tests__/**/*.test.js"
|
|
54
|
-
]
|
|
55
65
|
}
|
|
56
66
|
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Changelog Extractor CLI
|
|
5
|
+
*
|
|
6
|
+
* Parse and extract structured information from Keep a Changelog formatted files.
|
|
7
|
+
* Can be run via npx or installed globally.
|
|
8
|
+
*
|
|
9
|
+
* @package @alleyinteractive/changelog-extractor
|
|
10
|
+
* @author Alley Interactive
|
|
11
|
+
* @license GPL-3.0
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import fs from 'fs'
|
|
15
|
+
import path from 'path'
|
|
16
|
+
import { parseChangelog } from './parser.js'
|
|
17
|
+
|
|
18
|
+
function printUsage() {
|
|
19
|
+
console.log(`
|
|
20
|
+
Usage: npx @alleyinteractive/changelog-extractor [options]
|
|
21
|
+
|
|
22
|
+
Options:
|
|
23
|
+
-f, --file <path> Path to changelog file (default: CHANGELOG.md)
|
|
24
|
+
-v, --version <ver> Specific version to extract (e.g., "1.14.0" or "v1.14.0")
|
|
25
|
+
Leave empty to return all versions
|
|
26
|
+
-l, --list List the count of versions found and exit
|
|
27
|
+
-h, --help Show this help message
|
|
28
|
+
|
|
29
|
+
Examples:
|
|
30
|
+
npx @alleyinteractive/changelog-extractor # Parse CHANGELOG.md, all versions
|
|
31
|
+
npx @alleyinteractive/changelog-extractor -l # Count versions in CHANGELOG.md
|
|
32
|
+
npx @alleyinteractive/changelog-extractor -v 1.14.0 # Extract specific version
|
|
33
|
+
npx @alleyinteractive/changelog-extractor -f /tmp/test.md # Parse specific file
|
|
34
|
+
npx @alleyinteractive/changelog-extractor -f CHANGELOG.md -v 1.14.0 # Both options
|
|
35
|
+
changelog-extractor -l # If installed globally
|
|
36
|
+
`)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Parse command line arguments
|
|
40
|
+
const args = process.argv.slice(2)
|
|
41
|
+
|
|
42
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
43
|
+
printUsage()
|
|
44
|
+
process.exit(0)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const listMode = args.includes('-l') || args.includes('--list')
|
|
48
|
+
|
|
49
|
+
// Parse file option
|
|
50
|
+
let changelogFile = 'CHANGELOG.md'
|
|
51
|
+
const fileIndex = args.findIndex((arg) => arg === '-f' || arg === '--file')
|
|
52
|
+
if (fileIndex !== -1 && args[fileIndex + 1]) {
|
|
53
|
+
changelogFile = args[fileIndex + 1]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Parse version option
|
|
57
|
+
let versionArg = null
|
|
58
|
+
const versionIndex = args.findIndex(
|
|
59
|
+
(arg) => arg === '-v' || arg === '--version'
|
|
60
|
+
)
|
|
61
|
+
if (versionIndex !== -1 && args[versionIndex + 1]) {
|
|
62
|
+
versionArg = args[versionIndex + 1]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const version = versionArg
|
|
66
|
+
|
|
67
|
+
// Read changelog file
|
|
68
|
+
const changelogPath = path.resolve(process.cwd(), changelogFile)
|
|
69
|
+
|
|
70
|
+
if (!fs.existsSync(changelogPath)) {
|
|
71
|
+
console.error(`ā Error: Changelog file not found: ${changelogPath}`)
|
|
72
|
+
console.error(
|
|
73
|
+
'\nTip: You can create a test changelog at /tmp/test-changelog.md'
|
|
74
|
+
)
|
|
75
|
+
process.exit(1)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const changelogContent = fs.readFileSync(changelogPath, 'utf8')
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const results = parseChangelog(changelogContent, version)
|
|
82
|
+
|
|
83
|
+
// List mode: just display count and exit
|
|
84
|
+
if (listMode) {
|
|
85
|
+
console.log(`${results.length}`)
|
|
86
|
+
process.exit(0)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Parse changelog
|
|
90
|
+
console.log(`š Parsing: ${changelogPath}`)
|
|
91
|
+
console.log(`šÆ Version: ${versionArg || 'all versions'}`)
|
|
92
|
+
console.log('')
|
|
93
|
+
|
|
94
|
+
if (results.length === 0) {
|
|
95
|
+
console.log('ā ļø No changelog entries found')
|
|
96
|
+
process.exit(1)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
console.log(`ā
Found ${results.length} version(s)\n`)
|
|
100
|
+
console.log('ā'.repeat(80))
|
|
101
|
+
|
|
102
|
+
// Pretty print results
|
|
103
|
+
results.forEach((entry, index) => {
|
|
104
|
+
console.log(`\nš¦ Version: ${entry.name}`)
|
|
105
|
+
console.log(
|
|
106
|
+
`š Sections: ${Object.keys(entry.sections).join(', ') || 'none'}`
|
|
107
|
+
)
|
|
108
|
+
console.log('')
|
|
109
|
+
|
|
110
|
+
// Print sections
|
|
111
|
+
for (const [sectionName, sectionContent] of Object.entries(
|
|
112
|
+
entry.sections
|
|
113
|
+
)) {
|
|
114
|
+
console.log(
|
|
115
|
+
` ### ${sectionName.charAt(0).toUpperCase() + sectionName.slice(1)}`
|
|
116
|
+
)
|
|
117
|
+
console.log('')
|
|
118
|
+
// Indent content
|
|
119
|
+
const lines = sectionContent.split('\n')
|
|
120
|
+
lines.forEach((line) => console.log(` ${line}`))
|
|
121
|
+
console.log('')
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (index < results.length - 1) {
|
|
125
|
+
console.log('ā'.repeat(80))
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
// Output JSON at the end
|
|
130
|
+
console.log('\n' + 'ā'.repeat(80))
|
|
131
|
+
console.log('\nš JSON Output:\n')
|
|
132
|
+
console.log(JSON.stringify(results, null, 2))
|
|
133
|
+
} catch (error) {
|
|
134
|
+
console.error('ā Error parsing changelog:', error.message)
|
|
135
|
+
process.exit(1)
|
|
136
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,47 +1,7 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const { parseChangelog } = require('./parser');
|
|
4
|
-
|
|
5
1
|
/**
|
|
6
|
-
*
|
|
2
|
+
* The entrypoint for the action. This file simply imports and runs the action's
|
|
3
|
+
* main logic.
|
|
7
4
|
*/
|
|
8
|
-
|
|
9
|
-
try {
|
|
10
|
-
// Get inputs
|
|
11
|
-
const changelogPath = core.getInput('changelog-path') || 'CHANGELOG.md';
|
|
12
|
-
const versionInput = core.getInput('version');
|
|
13
|
-
|
|
14
|
-
// Only pass version if explicitly specified, otherwise return all versions
|
|
15
|
-
const version = versionInput || null;
|
|
16
|
-
|
|
17
|
-
// Read changelog file
|
|
18
|
-
const fullPath = path.resolve(process.cwd(), changelogPath);
|
|
19
|
-
|
|
20
|
-
if (!fs.existsSync(fullPath)) {
|
|
21
|
-
throw new Error(`Changelog file not found: ${fullPath}`);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const changelogContent = fs.readFileSync(fullPath, 'utf8');
|
|
25
|
-
|
|
26
|
-
// Parse changelog
|
|
27
|
-
const results = parseChangelog(changelogContent, version);
|
|
28
|
-
|
|
29
|
-
if (results.length === 0) {
|
|
30
|
-
core.warning(`No changelog entries found${version ? ` for version ${version}` : ''}`);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Set output as JSON string
|
|
34
|
-
core.setOutput('result', JSON.stringify(results));
|
|
35
|
-
|
|
36
|
-
// Log summary
|
|
37
|
-
core.info(`Parsed ${results.length} changelog ${results.length === 1 ? 'entry' : 'entries'}`);
|
|
38
|
-
for (const entry of results) {
|
|
39
|
-
core.info(` - Version ${entry.name}: ${Object.keys(entry.sections).length} sections`);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
} catch (error) {
|
|
43
|
-
core.setFailed(error.message);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
5
|
+
import { run } from './main.js'
|
|
46
6
|
|
|
47
|
-
|
|
7
|
+
run()
|
package/src/main.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as core from '@actions/core'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import { parseChangelog } from './parser.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Main action entry point
|
|
8
|
+
*/
|
|
9
|
+
export async function run() {
|
|
10
|
+
try {
|
|
11
|
+
const changelogPath = core.getInput('changelog-path') || 'CHANGELOG.md'
|
|
12
|
+
const versionInput = core.getInput('version')
|
|
13
|
+
|
|
14
|
+
// Only pass version if explicitly specified, otherwise return all versions
|
|
15
|
+
const version = versionInput || null
|
|
16
|
+
|
|
17
|
+
// Read changelog file
|
|
18
|
+
const fullPath = path.resolve(process.cwd(), changelogPath)
|
|
19
|
+
|
|
20
|
+
if (!fs.existsSync(fullPath)) {
|
|
21
|
+
throw new Error(`Changelog file not found: ${fullPath}`)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const changelogContent = fs.readFileSync(fullPath, 'utf8')
|
|
25
|
+
|
|
26
|
+
// Parse changelog
|
|
27
|
+
const results = parseChangelog(changelogContent, version)
|
|
28
|
+
|
|
29
|
+
if (results.length === 0) {
|
|
30
|
+
core.warning(
|
|
31
|
+
`No changelog entries found${version ? ` for version ${version}` : ''}`
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Set output as JSON string
|
|
36
|
+
core.setOutput('result', JSON.stringify(results))
|
|
37
|
+
|
|
38
|
+
// Log summary
|
|
39
|
+
core.info(
|
|
40
|
+
`Parsed ${results.length} changelog ${results.length === 1 ? 'entry' : 'entries'}`
|
|
41
|
+
)
|
|
42
|
+
for (const entry of results) {
|
|
43
|
+
core.info(
|
|
44
|
+
` - Version ${entry.name}: ${Object.keys(entry.sections).length} sections`
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
} catch (error) {
|
|
48
|
+
core.setFailed(error.message)
|
|
49
|
+
}
|
|
50
|
+
}
|
package/src/parser.js
CHANGED
|
@@ -5,31 +5,31 @@
|
|
|
5
5
|
* @param {string|null} version - Specific version to extract (e.g., "v1.14.0" or "1.14.0"), or null/undefined for all versions
|
|
6
6
|
* @returns {Array<{name: string, sections: Object, contents: string}>} Array of version objects
|
|
7
7
|
*/
|
|
8
|
-
function parseChangelog(changelogContent, version = null) {
|
|
9
|
-
const versions = []
|
|
10
|
-
const lines = changelogContent.split('\n')
|
|
8
|
+
export function parseChangelog(changelogContent, version = null) {
|
|
9
|
+
const versions = []
|
|
10
|
+
const lines = changelogContent.split('\n')
|
|
11
11
|
|
|
12
|
-
let currentVersion = null
|
|
13
|
-
let currentSection = null
|
|
14
|
-
let currentSectionContent = []
|
|
15
|
-
let generalContent = []
|
|
12
|
+
let currentVersion = null
|
|
13
|
+
let currentSection = null
|
|
14
|
+
let currentSectionContent = []
|
|
15
|
+
let generalContent = [] // Content without section headers
|
|
16
16
|
|
|
17
17
|
// Regex to match version headers: ## v1.14.0 or ## v1.14.0 - 2024-04-29
|
|
18
|
-
const versionRegex = /^##\s+v?(\d+\.\d+\.\d+(?:-[^\s]+)?)\s*(?:-\s*(.*))
|
|
18
|
+
const versionRegex = /^##\s+v?(\d+\.\d+\.\d+(?:-[^\s]+)?)\s*(?:-\s*(.*))?$/
|
|
19
19
|
// Regex to match section headers: ### Added, ### Changed, etc.
|
|
20
|
-
const sectionRegex = /^###\s+(.+)
|
|
20
|
+
const sectionRegex = /^###\s+(.+)$/
|
|
21
21
|
|
|
22
22
|
for (let i = 0; i < lines.length; i++) {
|
|
23
|
-
const line = lines[i]
|
|
23
|
+
const line = lines[i]
|
|
24
24
|
|
|
25
25
|
// Check for version header
|
|
26
|
-
const versionMatch = line.match(versionRegex)
|
|
26
|
+
const versionMatch = line.match(versionRegex)
|
|
27
27
|
if (versionMatch) {
|
|
28
28
|
// Save previous version if exists
|
|
29
29
|
if (currentVersion) {
|
|
30
|
-
saveCurrentSection()
|
|
31
|
-
saveGeneralContent()
|
|
32
|
-
versions.push(currentVersion)
|
|
30
|
+
saveCurrentSection()
|
|
31
|
+
saveGeneralContent()
|
|
32
|
+
versions.push(currentVersion)
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
// Start new version
|
|
@@ -37,62 +37,68 @@ function parseChangelog(changelogContent, version = null) {
|
|
|
37
37
|
name: versionMatch[1], // Version without 'v' prefix
|
|
38
38
|
sections: {},
|
|
39
39
|
contents: ''
|
|
40
|
-
}
|
|
41
|
-
currentSection = null
|
|
42
|
-
currentSectionContent = []
|
|
43
|
-
generalContent = []
|
|
44
|
-
continue
|
|
40
|
+
}
|
|
41
|
+
currentSection = null
|
|
42
|
+
currentSectionContent = []
|
|
43
|
+
generalContent = []
|
|
44
|
+
continue
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
// Check for section header
|
|
48
|
-
const sectionMatch = line.match(sectionRegex)
|
|
48
|
+
const sectionMatch = line.match(sectionRegex)
|
|
49
49
|
if (sectionMatch && currentVersion) {
|
|
50
50
|
// If we have general content, save it before starting a new section
|
|
51
51
|
if (generalContent.length > 0) {
|
|
52
|
-
saveGeneralContent()
|
|
52
|
+
saveGeneralContent()
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
// Save previous section if exists
|
|
56
|
-
saveCurrentSection()
|
|
56
|
+
saveCurrentSection()
|
|
57
57
|
|
|
58
58
|
// Start new section
|
|
59
|
-
currentSection = sectionMatch[1].toLowerCase()
|
|
60
|
-
currentSectionContent = []
|
|
61
|
-
continue
|
|
59
|
+
currentSection = sectionMatch[1].toLowerCase()
|
|
60
|
+
currentSectionContent = []
|
|
61
|
+
continue
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
// Add content to current section or general content
|
|
65
65
|
if (currentVersion) {
|
|
66
66
|
if (currentSection) {
|
|
67
|
-
currentSectionContent.push(line)
|
|
67
|
+
currentSectionContent.push(line)
|
|
68
68
|
} else {
|
|
69
69
|
// Content without a section header goes to general
|
|
70
|
-
generalContent.push(line)
|
|
70
|
+
generalContent.push(line)
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
// Save final version and section
|
|
76
76
|
if (currentVersion) {
|
|
77
|
-
saveCurrentSection()
|
|
78
|
-
saveGeneralContent()
|
|
79
|
-
versions.push(currentVersion)
|
|
77
|
+
saveCurrentSection()
|
|
78
|
+
saveGeneralContent()
|
|
79
|
+
versions.push(currentVersion)
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
// Helper function to save the current section
|
|
83
83
|
function saveCurrentSection() {
|
|
84
84
|
if (currentVersion && currentSection && currentSectionContent.length > 0) {
|
|
85
85
|
// Trim empty lines from start and end
|
|
86
|
-
while (
|
|
87
|
-
currentSectionContent.
|
|
86
|
+
while (
|
|
87
|
+
currentSectionContent.length > 0 &&
|
|
88
|
+
currentSectionContent[0].trim() === ''
|
|
89
|
+
) {
|
|
90
|
+
currentSectionContent.shift()
|
|
88
91
|
}
|
|
89
|
-
while (
|
|
90
|
-
currentSectionContent.
|
|
92
|
+
while (
|
|
93
|
+
currentSectionContent.length > 0 &&
|
|
94
|
+
currentSectionContent[currentSectionContent.length - 1].trim() === ''
|
|
95
|
+
) {
|
|
96
|
+
currentSectionContent.pop()
|
|
91
97
|
}
|
|
92
98
|
|
|
93
|
-
const content = currentSectionContent.join('\n')
|
|
99
|
+
const content = currentSectionContent.join('\n')
|
|
94
100
|
if (content.trim()) {
|
|
95
|
-
currentVersion.sections[currentSection] = content
|
|
101
|
+
currentVersion.sections[currentSection] = content
|
|
96
102
|
}
|
|
97
103
|
}
|
|
98
104
|
}
|
|
@@ -101,59 +107,71 @@ function parseChangelog(changelogContent, version = null) {
|
|
|
101
107
|
function saveGeneralContent() {
|
|
102
108
|
if (currentVersion && generalContent.length > 0) {
|
|
103
109
|
// Trim empty lines from start and end
|
|
104
|
-
let trimmedContent = [...generalContent]
|
|
110
|
+
let trimmedContent = [...generalContent]
|
|
105
111
|
while (trimmedContent.length > 0 && trimmedContent[0].trim() === '') {
|
|
106
|
-
trimmedContent.shift()
|
|
112
|
+
trimmedContent.shift()
|
|
107
113
|
}
|
|
108
|
-
while (
|
|
109
|
-
trimmedContent.
|
|
114
|
+
while (
|
|
115
|
+
trimmedContent.length > 0 &&
|
|
116
|
+
trimmedContent[trimmedContent.length - 1].trim() === ''
|
|
117
|
+
) {
|
|
118
|
+
trimmedContent.pop()
|
|
110
119
|
}
|
|
111
120
|
|
|
112
|
-
const content = trimmedContent.join('\n')
|
|
121
|
+
const content = trimmedContent.join('\n')
|
|
113
122
|
if (content.trim()) {
|
|
114
|
-
currentVersion.sections['general'] = content
|
|
123
|
+
currentVersion.sections['general'] = content
|
|
115
124
|
}
|
|
116
|
-
generalContent = []
|
|
125
|
+
generalContent = []
|
|
117
126
|
}
|
|
118
127
|
}
|
|
119
128
|
|
|
120
129
|
// Build contents for each version (all sections combined)
|
|
121
130
|
for (const ver of versions) {
|
|
122
|
-
const contentParts = []
|
|
131
|
+
const contentParts = []
|
|
123
132
|
|
|
124
133
|
// Order sections in conventional order, with general at the end
|
|
125
|
-
const sectionOrder = [
|
|
134
|
+
const sectionOrder = [
|
|
135
|
+
'added',
|
|
136
|
+
'changed',
|
|
137
|
+
'deprecated',
|
|
138
|
+
'removed',
|
|
139
|
+
'fixed',
|
|
140
|
+
'security'
|
|
141
|
+
]
|
|
126
142
|
|
|
127
143
|
for (const sectionName of sectionOrder) {
|
|
128
144
|
if (ver.sections[sectionName]) {
|
|
129
|
-
contentParts.push(
|
|
145
|
+
contentParts.push(
|
|
146
|
+
`### ${sectionName.charAt(0).toUpperCase() + sectionName.slice(1)}\n\n${ver.sections[sectionName]}`
|
|
147
|
+
)
|
|
130
148
|
}
|
|
131
149
|
}
|
|
132
150
|
|
|
133
151
|
// Add any sections not in the standard order (except 'general')
|
|
134
152
|
for (const [sectionName, sectionContent] of Object.entries(ver.sections)) {
|
|
135
153
|
if (!sectionOrder.includes(sectionName) && sectionName !== 'general') {
|
|
136
|
-
contentParts.push(
|
|
154
|
+
contentParts.push(
|
|
155
|
+
`### ${sectionName.charAt(0).toUpperCase() + sectionName.slice(1)}\n\n${sectionContent}`
|
|
156
|
+
)
|
|
137
157
|
}
|
|
138
158
|
}
|
|
139
159
|
|
|
140
160
|
// Add general section last if it exists (without a header since it's raw content)
|
|
141
161
|
if (ver.sections['general']) {
|
|
142
|
-
contentParts.push(ver.sections['general'])
|
|
162
|
+
contentParts.push(ver.sections['general'])
|
|
143
163
|
}
|
|
144
164
|
|
|
145
|
-
ver.contents = contentParts.join('\n\n')
|
|
165
|
+
ver.contents = contentParts.join('\n\n')
|
|
146
166
|
}
|
|
147
167
|
|
|
148
168
|
// Filter by specific version if requested
|
|
149
169
|
if (version) {
|
|
150
|
-
const normalizedVersion = version.replace(/^v/, '')
|
|
151
|
-
const filtered = versions.filter(v => v.name === normalizedVersion)
|
|
152
|
-
return filtered
|
|
170
|
+
const normalizedVersion = version.replace(/^v/, '') // Remove 'v' prefix if present
|
|
171
|
+
const filtered = versions.filter((v) => v.name === normalizedVersion)
|
|
172
|
+
return filtered
|
|
153
173
|
}
|
|
154
174
|
|
|
155
175
|
// Return all versions by default
|
|
156
|
-
return versions
|
|
176
|
+
return versions
|
|
157
177
|
}
|
|
158
|
-
|
|
159
|
-
module.exports = { parseChangelog };
|
package/cli.js
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const { parseChangelog } = require('./src/parser');
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* CLI for testing the changelog parser
|
|
9
|
-
* Usage: node cli.js [changelog-file] [version]
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
function printUsage() {
|
|
13
|
-
console.log(`
|
|
14
|
-
Usage: node cli.js [options] [changelog-file] [version]
|
|
15
|
-
|
|
16
|
-
Options:
|
|
17
|
-
-l, --list List the count of versions found and exit
|
|
18
|
-
-h, --help Show this help message
|
|
19
|
-
|
|
20
|
-
Arguments:
|
|
21
|
-
changelog-file Path to changelog file (default: CHANGELOG.md)
|
|
22
|
-
version Specific version to extract (e.g., "1.14.0" or "v1.14.0")
|
|
23
|
-
Leave empty to return all versions (default: all versions)
|
|
24
|
-
|
|
25
|
-
Examples:
|
|
26
|
-
node cli.js # Parse CHANGELOG.md, extract all versions
|
|
27
|
-
node cli.js -l # Count versions in CHANGELOG.md
|
|
28
|
-
node cli.js CHANGELOG.md # Same as above
|
|
29
|
-
node cli.js CHANGELOG.md 1.14.0 # Extract specific version
|
|
30
|
-
node cli.js /tmp/test-changelog.md # Parse specific file, all versions
|
|
31
|
-
node cli.js -l /tmp/test-changelog.md # Count versions in specific file
|
|
32
|
-
`);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Parse command line arguments
|
|
36
|
-
const args = process.argv.slice(2);
|
|
37
|
-
|
|
38
|
-
if (args.includes('--help') || args.includes('-h')) {
|
|
39
|
-
printUsage();
|
|
40
|
-
process.exit(0);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const listMode = args.includes('-l') || args.includes('--list');
|
|
44
|
-
const filteredArgs = args.filter(arg => arg !== '-l' && arg !== '--list');
|
|
45
|
-
|
|
46
|
-
const changelogFile = filteredArgs[0] || 'CHANGELOG.md';
|
|
47
|
-
const versionArg = filteredArgs[1] || null;
|
|
48
|
-
const version = versionArg;
|
|
49
|
-
|
|
50
|
-
// Read changelog file
|
|
51
|
-
const changelogPath = path.resolve(process.cwd(), changelogFile);
|
|
52
|
-
|
|
53
|
-
if (!fs.existsSync(changelogPath)) {
|
|
54
|
-
console.error(`ā Error: Changelog file not found: ${changelogPath}`);
|
|
55
|
-
console.error('\nTip: You can create a test changelog at /tmp/test-changelog.md');
|
|
56
|
-
process.exit(1);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const changelogContent = fs.readFileSync(changelogPath, 'utf8');
|
|
60
|
-
|
|
61
|
-
try {
|
|
62
|
-
const results = parseChangelog(changelogContent, version);
|
|
63
|
-
|
|
64
|
-
// List mode: just display count and exit
|
|
65
|
-
if (listMode) {
|
|
66
|
-
console.log(`${results.length}`);
|
|
67
|
-
process.exit(0);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Parse changelog
|
|
71
|
-
console.log(`š Parsing: ${changelogPath}`);
|
|
72
|
-
console.log(`šÆ Version: ${versionArg || 'all versions'}`);
|
|
73
|
-
console.log('');
|
|
74
|
-
|
|
75
|
-
if (results.length === 0) {
|
|
76
|
-
console.log('ā ļø No changelog entries found');
|
|
77
|
-
process.exit(0);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
console.log(`ā
Found ${results.length} version(s)\n`);
|
|
81
|
-
console.log('ā'.repeat(80));
|
|
82
|
-
|
|
83
|
-
// Pretty print results
|
|
84
|
-
results.forEach((entry, index) => {
|
|
85
|
-
console.log(`\nš¦ Version: ${entry.name}`);
|
|
86
|
-
console.log(`š Sections: ${Object.keys(entry.sections).join(', ') || 'none'}`);
|
|
87
|
-
console.log('');
|
|
88
|
-
|
|
89
|
-
// Print sections
|
|
90
|
-
for (const [sectionName, sectionContent] of Object.entries(entry.sections)) {
|
|
91
|
-
console.log(` ### ${sectionName.charAt(0).toUpperCase() + sectionName.slice(1)}`);
|
|
92
|
-
console.log('');
|
|
93
|
-
// Indent content
|
|
94
|
-
const lines = sectionContent.split('\n');
|
|
95
|
-
lines.forEach(line => console.log(` ${line}`));
|
|
96
|
-
console.log('');
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (index < results.length - 1) {
|
|
100
|
-
console.log('ā'.repeat(80));
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
// Output JSON at the end
|
|
105
|
-
console.log('\n' + 'ā'.repeat(80));
|
|
106
|
-
console.log('\nš JSON Output:\n');
|
|
107
|
-
console.log(JSON.stringify(results, null, 2));
|
|
108
|
-
|
|
109
|
-
} catch (error) {
|
|
110
|
-
console.error('ā Error parsing changelog:', error.message);
|
|
111
|
-
process.exit(1);
|
|
112
|
-
}
|