Skip to content

Commit 3963334

Browse files
committed
Building for action
1 parent 46afc0a commit 3963334

File tree

5 files changed

+253
-14
lines changed

5 files changed

+253
-14
lines changed

action.yml

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,7 @@ inputs:
1818
outputs:
1919
result:
2020
description: 'JSON array of parsed changelog entries'
21-
value: ${{ steps.changelog-extractor.outputs.result }}
22-
23-
# runs:
24-
# using: 'node24'
25-
# main: 'src/index.js'
2621

2722
runs:
28-
using: 'composite'
29-
steps:
30-
- name: Get WordPress Versions
31-
id: changelog-extractor
32-
uses: actions/github-script@v7
33-
with:
34-
script: |
35-
const run = require('${{github.action_path}}/src/index.js');
36-
await run(core);
23+
using: 'node20'
24+
main: 'dist/index.js'

build.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
const fs = require('fs');
2+
const path = require('path');
3+
4+
// Create dist directory if it doesn't exist
5+
const distDir = path.join(__dirname, 'dist');
6+
if (!fs.existsSync(distDir)) {
7+
fs.mkdirSync(distDir, { recursive: true });
8+
}
9+
10+
// Read all source files
11+
const indexContent = fs.readFileSync(path.join(__dirname, 'src/index.js'), 'utf8');
12+
const parserContent = fs.readFileSync(path.join(__dirname, 'src/parser.js'), 'utf8');
13+
14+
// Create a bundled file
15+
const bundled = `
16+
${parserContent.replace('module.exports = { parseChangelog };', '')}
17+
18+
${indexContent.replace("const { parseChangelog } = require('./parser');", '')}
19+
`.trim();
20+
21+
fs.writeFileSync(path.join(distDir, 'index.js'), bundled);
22+
23+
console.log('✓ Built action to dist/index.js');

dist/index.js

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
/**
2+
* Parse a Keep a Changelog formatted changelog file.
3+
*
4+
* @param {string} changelogContent - The raw markdown content of the changelog
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+
* @returns {Array<{name: string, sections: Object, contents: string}>} Array of version objects
7+
*/
8+
function parseChangelog(changelogContent, version = null) {
9+
const versions = [];
10+
const lines = changelogContent.split('\n');
11+
12+
let currentVersion = null;
13+
let currentSection = null;
14+
let currentSectionContent = [];
15+
let generalContent = []; // Content without section headers
16+
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*(.*))?$/;
19+
// Regex to match section headers: ### Added, ### Changed, etc.
20+
const sectionRegex = /^###\s+(.+)$/;
21+
22+
for (let i = 0; i < lines.length; i++) {
23+
const line = lines[i];
24+
25+
// Check for version header
26+
const versionMatch = line.match(versionRegex);
27+
if (versionMatch) {
28+
// Save previous version if exists
29+
if (currentVersion) {
30+
saveCurrentSection();
31+
saveGeneralContent();
32+
versions.push(currentVersion);
33+
}
34+
35+
// Start new version
36+
currentVersion = {
37+
name: versionMatch[1], // Version without 'v' prefix
38+
sections: {},
39+
contents: ''
40+
};
41+
currentSection = null;
42+
currentSectionContent = [];
43+
generalContent = [];
44+
continue;
45+
}
46+
47+
// Check for section header
48+
const sectionMatch = line.match(sectionRegex);
49+
if (sectionMatch && currentVersion) {
50+
// If we have general content, save it before starting a new section
51+
if (generalContent.length > 0) {
52+
saveGeneralContent();
53+
}
54+
55+
// Save previous section if exists
56+
saveCurrentSection();
57+
58+
// Start new section
59+
currentSection = sectionMatch[1].toLowerCase();
60+
currentSectionContent = [];
61+
continue;
62+
}
63+
64+
// Add content to current section or general content
65+
if (currentVersion) {
66+
if (currentSection) {
67+
currentSectionContent.push(line);
68+
} else {
69+
// Content without a section header goes to general
70+
generalContent.push(line);
71+
}
72+
}
73+
}
74+
75+
// Save final version and section
76+
if (currentVersion) {
77+
saveCurrentSection();
78+
saveGeneralContent();
79+
versions.push(currentVersion);
80+
}
81+
82+
// Helper function to save the current section
83+
function saveCurrentSection() {
84+
if (currentVersion && currentSection && currentSectionContent.length > 0) {
85+
// Trim empty lines from start and end
86+
while (currentSectionContent.length > 0 && currentSectionContent[0].trim() === '') {
87+
currentSectionContent.shift();
88+
}
89+
while (currentSectionContent.length > 0 && currentSectionContent[currentSectionContent.length - 1].trim() === '') {
90+
currentSectionContent.pop();
91+
}
92+
93+
const content = currentSectionContent.join('\n');
94+
if (content.trim()) {
95+
currentVersion.sections[currentSection] = content;
96+
}
97+
}
98+
}
99+
100+
// Helper function to save general content (content without section headers)
101+
function saveGeneralContent() {
102+
if (currentVersion && generalContent.length > 0) {
103+
// Trim empty lines from start and end
104+
let trimmedContent = [...generalContent];
105+
while (trimmedContent.length > 0 && trimmedContent[0].trim() === '') {
106+
trimmedContent.shift();
107+
}
108+
while (trimmedContent.length > 0 && trimmedContent[trimmedContent.length - 1].trim() === '') {
109+
trimmedContent.pop();
110+
}
111+
112+
const content = trimmedContent.join('\n');
113+
if (content.trim()) {
114+
currentVersion.sections['general'] = content;
115+
}
116+
generalContent = [];
117+
}
118+
}
119+
120+
// Build contents for each version (all sections combined)
121+
for (const ver of versions) {
122+
const contentParts = [];
123+
124+
// Order sections in conventional order, with general at the end
125+
const sectionOrder = ['added', 'changed', 'deprecated', 'removed', 'fixed', 'security'];
126+
127+
for (const sectionName of sectionOrder) {
128+
if (ver.sections[sectionName]) {
129+
contentParts.push(`### ${sectionName.charAt(0).toUpperCase() + sectionName.slice(1)}\n\n${ver.sections[sectionName]}`);
130+
}
131+
}
132+
133+
// Add any sections not in the standard order (except 'general')
134+
for (const [sectionName, sectionContent] of Object.entries(ver.sections)) {
135+
if (!sectionOrder.includes(sectionName) && sectionName !== 'general') {
136+
contentParts.push(`### ${sectionName.charAt(0).toUpperCase() + sectionName.slice(1)}\n\n${sectionContent}`);
137+
}
138+
}
139+
140+
// Add general section last if it exists (without a header since it's raw content)
141+
if (ver.sections['general']) {
142+
contentParts.push(ver.sections['general']);
143+
}
144+
145+
ver.contents = contentParts.join('\n\n');
146+
}
147+
148+
// Filter by specific version if requested
149+
if (version) {
150+
const normalizedVersion = version.replace(/^v/, ''); // Remove 'v' prefix if present
151+
const filtered = versions.filter(v => v.name === normalizedVersion);
152+
return filtered;
153+
}
154+
155+
// Return all versions by default
156+
return versions;
157+
}
158+
159+
160+
161+
162+
const fs = require('fs');
163+
const path = require('path');
164+
165+
166+
/**
167+
* Main action entry point
168+
*
169+
* @param {import('@actions/core')} core
170+
*/
171+
async function run(core) {
172+
try {
173+
// Get inputs
174+
const changelogPath = core.getInput('changelog-path') || 'CHANGELOG.md';
175+
const versionInput = core.getInput('version');
176+
177+
// Only pass version if explicitly specified, otherwise return all versions
178+
const version = versionInput || null;
179+
180+
// Read changelog file
181+
const fullPath = path.resolve(process.cwd(), changelogPath);
182+
183+
if (!fs.existsSync(fullPath)) {
184+
throw new Error(`Changelog file not found: ${fullPath}`);
185+
}
186+
187+
const changelogContent = fs.readFileSync(fullPath, 'utf8');
188+
189+
// Parse changelog
190+
const results = parseChangelog(changelogContent, version);
191+
192+
if (results.length === 0) {
193+
core.warning(`No changelog entries found${version ? ` for version ${version}` : ''}`);
194+
}
195+
196+
// Set output as JSON string
197+
core.setOutput('result', JSON.stringify(results));
198+
199+
// Log summary
200+
core.info(`Parsed ${results.length} changelog ${results.length === 1 ? 'entry' : 'entries'}`);
201+
for (const entry of results) {
202+
core.info(` - Version ${entry.name}: ${Object.keys(entry.sections).length} sections`);
203+
}
204+
205+
} catch (error) {
206+
core.setFailed(error.message);
207+
}
208+
}
209+
210+
// Only run if this is the main module (for GitHub Actions)
211+
if (require.main === module) {
212+
const core = require('@actions/core');
213+
run(core);
214+
}
215+
216+
module.exports = run;

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"test": "jest",
1111
"test:watch": "jest --watch",
1212
"test:coverage": "jest --coverage",
13+
"build": "node build.js",
1314
"version:patch": "npm version patch",
1415
"version:minor": "npm version minor",
1516
"version:major": "npm version major",
@@ -38,7 +39,9 @@
3839
"homepage": "https://github.com/alleyinteractive/action-changelog-extractor#readme",
3940
"files": [
4041
"src/",
42+
"dist/",
4143
"cli.js",
44+
"action.yml",
4245
"README.md",
4346
"LICENSE"
4447
],

src/index.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ const { parseChangelog } = require('./parser');
44

55
/**
66
* Main action entry point
7+
*
8+
* @param {import('@actions/core')} core
79
*/
810
async function run(core) {
911
try {
@@ -44,4 +46,11 @@ async function run(core) {
4446
}
4547
}
4648

49+
// Only run if this is the main module (for GitHub Actions)
50+
if (require.main === module) {
51+
const core = require('@actions/core');
52+
run(core);
53+
}
54+
4755
module.exports = run;
56+

0 commit comments

Comments
 (0)