Fixed PR Changelog Generation Workflow (#40410)
This adds support for using double-quotes in the description of the PR.
This commit is contained in:
parent
67dadbb249
commit
b6674ef0c1
|
@ -2,10 +2,9 @@
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { Command } from '@commander-js/extra-typings';
|
import { Command } from '@commander-js/extra-typings';
|
||||||
import { execSync } from 'child_process';
|
|
||||||
import simpleGit from 'simple-git';
|
import simpleGit from 'simple-git';
|
||||||
import nodePath from 'path';
|
import nodePath from 'path';
|
||||||
import { existsSync } from 'fs';
|
import { existsSync, readFileSync, rmSync, writeFileSync } from 'fs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -118,60 +117,85 @@ const program = new Command( 'changefile' )
|
||||||
'Removing existing changelog files in case a change is reverted and the entry is no longer needed'
|
'Removing existing changelog files in case a change is reverted and the entry is no longer needed'
|
||||||
);
|
);
|
||||||
allProjectPaths.forEach( ( projectPath ) => {
|
allProjectPaths.forEach( ( projectPath ) => {
|
||||||
const path = nodePath.join(
|
const composerFilePath = nodePath.join(
|
||||||
tmpRepoPath,
|
tmpRepoPath,
|
||||||
projectPath,
|
projectPath,
|
||||||
'changelog',
|
'composer.json'
|
||||||
|
);
|
||||||
|
if ( ! existsSync( composerFilePath ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Figure out where the changelog files belong for this project.
|
||||||
|
const composerFile = JSON.parse(
|
||||||
|
readFileSync( composerFilePath, {
|
||||||
|
encoding: 'utf-8',
|
||||||
|
} )
|
||||||
|
);
|
||||||
|
const changelogFilePath = nodePath.join(
|
||||||
|
tmpRepoPath,
|
||||||
|
projectPath,
|
||||||
|
composerFile.extra?.changelogger[ 'changes-dir' ] ??
|
||||||
|
'changelog',
|
||||||
fileName
|
fileName
|
||||||
);
|
);
|
||||||
|
|
||||||
if ( existsSync( path ) ) {
|
if ( ! existsSync( changelogFilePath ) ) {
|
||||||
Logger.notice(
|
return;
|
||||||
`Remove existing changelog file ${ path }`
|
|
||||||
);
|
|
||||||
|
|
||||||
execSync( `rm ${ path }`, {
|
|
||||||
cwd: tmpRepoPath,
|
|
||||||
stdio: 'inherit',
|
|
||||||
} );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Logger.notice(
|
||||||
|
`Remove existing changelog file ${ changelogFilePath }`
|
||||||
|
);
|
||||||
|
|
||||||
|
rmSync( changelogFilePath );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
if ( touchedProjectsRequiringChangelog.length === 0 ) {
|
if ( ! touchedProjectsRequiringChangelog ) {
|
||||||
Logger.notice( 'No projects require a changelog' );
|
Logger.notice( 'No projects require a changelog' );
|
||||||
process.exit( 0 );
|
process.exit( 0 );
|
||||||
}
|
}
|
||||||
|
|
||||||
// When a devRepoPath is provided, assume that the dependencies are already installed.
|
for ( const project in touchedProjectsRequiringChangelog ) {
|
||||||
if ( ! devRepoPath ) {
|
const projectPath = nodePath.join(
|
||||||
Logger.notice(
|
tmpRepoPath,
|
||||||
`Installing dependencies in ${ tmpRepoPath }`
|
touchedProjectsRequiringChangelog[ project ]
|
||||||
);
|
);
|
||||||
execSync( 'pnpm install', {
|
|
||||||
cwd: tmpRepoPath,
|
|
||||||
stdio: 'inherit',
|
|
||||||
} );
|
|
||||||
}
|
|
||||||
|
|
||||||
touchedProjectsRequiringChangelog.forEach( ( project ) => {
|
|
||||||
Logger.notice(
|
Logger.notice(
|
||||||
`Running changelog command for ${ project }`
|
`Generating changefile for ${ project } (${ projectPath }))`
|
||||||
);
|
);
|
||||||
const messageExpression = message
|
|
||||||
? `-e "${ message }"`
|
// Figure out where the changelog file belongs for this project.
|
||||||
: '--entry=""';
|
const composerFile = JSON.parse(
|
||||||
const commentExpression = comment
|
readFileSync(
|
||||||
? `-c "${ comment }"`
|
nodePath.join( projectPath, 'composer.json' ),
|
||||||
: '';
|
{ encoding: 'utf-8' }
|
||||||
const cmd = `pnpm --filter=${ project } run changelog add -f ${ fileName } -s ${ significance } -t ${ type } ${ messageExpression } ${ commentExpression } -n`;
|
)
|
||||||
execSync( cmd, { cwd: tmpRepoPath, stdio: 'inherit' } );
|
);
|
||||||
} );
|
const changelogFilePath = nodePath.join(
|
||||||
|
projectPath,
|
||||||
|
composerFile.extra?.changelogger[ 'changes-dir' ] ??
|
||||||
|
'changelog',
|
||||||
|
fileName
|
||||||
|
);
|
||||||
|
|
||||||
|
// Write the changefile using the correct format.
|
||||||
|
let fileContent = `Significance: ${ significance }\n`;
|
||||||
|
fileContent += `Type: ${ type }\n`;
|
||||||
|
if ( comment ) {
|
||||||
|
fileContent += `Comment: ${ comment }\n`;
|
||||||
|
}
|
||||||
|
fileContent += `\n${ message }`;
|
||||||
|
writeFileSync( changelogFilePath, fileContent );
|
||||||
|
}
|
||||||
} catch ( e ) {
|
} catch ( e ) {
|
||||||
Logger.error( e );
|
Logger.error( e );
|
||||||
}
|
}
|
||||||
|
|
||||||
const touchedProjectsString =
|
const touchedProjectsString = Object.keys(
|
||||||
touchedProjectsRequiringChangelog.join( ', ' );
|
touchedProjectsRequiringChangelog
|
||||||
|
).join( ', ' );
|
||||||
|
|
||||||
Logger.notice(
|
Logger.notice(
|
||||||
`Changelogs created for ${ touchedProjectsString }`
|
`Changelogs created for ${ touchedProjectsString }`
|
||||||
|
|
|
@ -351,6 +351,53 @@ describe( 'getChangelogDetails', () => {
|
||||||
expect( details.comment ).toEqual( 'This is a very useful comment.' );
|
expect( details.comment ).toEqual( 'This is a very useful comment.' );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
it( 'should remove newlines from message and comment', () => {
|
||||||
|
const body =
|
||||||
|
'### Changelog entry\r\n' +
|
||||||
|
'\r\n' +
|
||||||
|
'<!-- You can optionally choose to enter a changelog entry by checking the box and supplying data. -->\r\n' +
|
||||||
|
'\r\n' +
|
||||||
|
'- [x] Automatically create a changelog entry from the details below.\r\n' +
|
||||||
|
'\r\n' +
|
||||||
|
'<details>\r\n' +
|
||||||
|
'\r\n' +
|
||||||
|
'#### Significance\r\n' +
|
||||||
|
'<!-- Choose only one -->\r\n' +
|
||||||
|
'- [x] Patch\r\n' +
|
||||||
|
'- [ ] Minor\r\n' +
|
||||||
|
'- [ ] Major\r\n' +
|
||||||
|
'\r\n' +
|
||||||
|
'#### Type\r\n' +
|
||||||
|
'<!-- Choose only one -->\r\n' +
|
||||||
|
'- [x] Fix - Fixes an existing bug\r\n' +
|
||||||
|
'- [ ] Add - Adds functionality\r\n' +
|
||||||
|
'- [ ] Update - Update existing functionality\r\n' +
|
||||||
|
'- [ ] Dev - Development related task\r\n' +
|
||||||
|
'- [ ] Tweak - A minor adjustment to the codebase\r\n' +
|
||||||
|
'- [ ] Performance - Address performance issues\r\n' +
|
||||||
|
'- [ ] Enhancement\r\n' +
|
||||||
|
'\r\n' +
|
||||||
|
'#### Message ' +
|
||||||
|
'<!-- Add a changelog message here -->\r\n' +
|
||||||
|
'This is a very useful fix.\r\n' +
|
||||||
|
'I promise!\r\n' +
|
||||||
|
'\r\n' +
|
||||||
|
'#### Comment ' +
|
||||||
|
`<!-- If the changes in this pull request don't warrant a changelog entry, you can alternatively supply a comment here. Note that comments are only accepted with a significance of "Patch" -->\r\n` +
|
||||||
|
'This is a very useful comment.\r\n' +
|
||||||
|
"I don't promise!\r\n" +
|
||||||
|
'\r\n' +
|
||||||
|
'</details>';
|
||||||
|
|
||||||
|
const details = getChangelogDetails( body );
|
||||||
|
expect( details.message ).toEqual(
|
||||||
|
'This is a very useful fix. I promise!'
|
||||||
|
);
|
||||||
|
expect( details.comment ).toEqual(
|
||||||
|
"This is a very useful comment. I don't promise!"
|
||||||
|
);
|
||||||
|
} );
|
||||||
|
|
||||||
it( 'should return a comment even when it is entered with a significance other than patch', () => {
|
it( 'should return a comment even when it is entered with a significance other than patch', () => {
|
||||||
const body =
|
const body =
|
||||||
'### Changelog entry\r\n' +
|
'### Changelog entry\r\n' +
|
||||||
|
|
|
@ -93,11 +93,11 @@ describe( 'Changelog project functions', () => {
|
||||||
changeLoggerProjects
|
changeLoggerProjects
|
||||||
);
|
);
|
||||||
|
|
||||||
expect( intersectedProjects ).toHaveLength( 2 );
|
expect( intersectedProjects ).toMatchObject( {
|
||||||
expect( intersectedProjects ).toContain(
|
'folder-with-lots-of-projects/project-b':
|
||||||
'folder-with-lots-of-projects/project-b'
|
'folder-with-lots-of-projects/project-b',
|
||||||
);
|
'projects/very-cool-project': 'projects/very-cool-project',
|
||||||
expect( intersectedProjects ).toContain( 'projects/very-cool-project' );
|
} );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
it( 'getTouchedChangeloggerProjectsPathsMappedToProjects should map plugins and js packages to the correct name', async () => {
|
it( 'getTouchedChangeloggerProjectsPathsMappedToProjects should map plugins and js packages to the correct name', async () => {
|
||||||
|
@ -119,11 +119,12 @@ describe( 'Changelog project functions', () => {
|
||||||
changeLoggerProjects
|
changeLoggerProjects
|
||||||
);
|
);
|
||||||
|
|
||||||
expect( intersectedProjects ).toHaveLength( 4 );
|
expect( intersectedProjects ).toMatchObject( {
|
||||||
expect( intersectedProjects ).toContain( 'woocommerce' );
|
woocommerce: 'plugins/woocommerce',
|
||||||
expect( intersectedProjects ).toContain( 'beta-tester' );
|
'beta-tester': 'plugins/beta-tester',
|
||||||
expect( intersectedProjects ).toContain( '@woocommerce/components' );
|
'@woocommerce/components': 'packages/js/components',
|
||||||
expect( intersectedProjects ).toContain( '@woocommerce/data' );
|
'@woocommerce/data': 'packages/js/data',
|
||||||
|
} );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
it( 'getTouchedChangeloggerProjectsPathsMappedToProjects should handle woocommerce-admin projects mapped to woocommerce core', async () => {
|
it( 'getTouchedChangeloggerProjectsPathsMappedToProjects should handle woocommerce-admin projects mapped to woocommerce core', async () => {
|
||||||
|
@ -138,7 +139,8 @@ describe( 'Changelog project functions', () => {
|
||||||
changeLoggerProjects
|
changeLoggerProjects
|
||||||
);
|
);
|
||||||
|
|
||||||
expect( intersectedProjects ).toHaveLength( 1 );
|
expect( intersectedProjects ).toMatchObject( {
|
||||||
expect( intersectedProjects ).toContain( 'woocommerce' );
|
woocommerce: 'plugins/woocommerce',
|
||||||
|
} );
|
||||||
} );
|
} );
|
||||||
} );
|
} );
|
||||||
|
|
|
@ -120,7 +120,12 @@ export const getChangelogMessage = ( body: string ) => {
|
||||||
Logger.error( 'No changelog message found' );
|
Logger.error( 'No changelog message found' );
|
||||||
}
|
}
|
||||||
|
|
||||||
return match[ 3 ].trim();
|
let message = match[ 3 ].trim();
|
||||||
|
|
||||||
|
// Newlines break the formatting of the changelog, so we replace them with spaces.
|
||||||
|
message = message.replace( /\r\n|\n/g, ' ' );
|
||||||
|
|
||||||
|
return message;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -133,7 +138,12 @@ export const getChangelogComment = ( body: string ) => {
|
||||||
const commentRegex = /#### Comment ?(<!--(.*)-->)?(.*)<\/details>/gms;
|
const commentRegex = /#### Comment ?(<!--(.*)-->)?(.*)<\/details>/gms;
|
||||||
const match = commentRegex.exec( body );
|
const match = commentRegex.exec( body );
|
||||||
|
|
||||||
return match ? match[ 3 ].trim() : '';
|
let comment = match ? match[ 3 ].trim() : '';
|
||||||
|
|
||||||
|
// Newlines break the formatting of the changelog, so we replace them with spaces.
|
||||||
|
comment = comment.replace( /\r\n|\n/g, ' ' );
|
||||||
|
|
||||||
|
return comment;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -120,7 +120,7 @@ export const getTouchedFilePaths = async (
|
||||||
*
|
*
|
||||||
* @param {Array<string>} touchedFiles List of files changed in a PR. touchedFiles
|
* @param {Array<string>} touchedFiles List of files changed in a PR. touchedFiles
|
||||||
* @param {Array<string>} changeloggerProjects List of projects that have Jetpack changelogger enabled.
|
* @param {Array<string>} changeloggerProjects List of projects that have Jetpack changelogger enabled.
|
||||||
* @return {Array<string>} List of projects that have Jetpack changelogger enabled and have files changed in a PR.
|
* @return {Object.<string, string>} Paths to projects that have files changed in a PR keyed by the project name.
|
||||||
*/
|
*/
|
||||||
export const getTouchedChangeloggerProjectsPathsMappedToProjects = (
|
export const getTouchedChangeloggerProjectsPathsMappedToProjects = (
|
||||||
touchedFiles: Array< string >,
|
touchedFiles: Array< string >,
|
||||||
|
@ -142,14 +142,19 @@ export const getTouchedChangeloggerProjectsPathsMappedToProjects = (
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return touchedProjectPathsRequiringChangelog.map( ( project ) => {
|
|
||||||
|
const projectPaths = {};
|
||||||
|
for ( const projectPath of touchedProjectPathsRequiringChangelog ) {
|
||||||
|
let project = projectPath;
|
||||||
if ( project.includes( 'plugins/' ) ) {
|
if ( project.includes( 'plugins/' ) ) {
|
||||||
return project.replace( 'plugins/', '' );
|
project = project.replace( 'plugins/', '' );
|
||||||
} else if ( project.includes( 'packages/js/' ) ) {
|
} else if ( project.includes( 'packages/js/' ) ) {
|
||||||
return project.replace( 'packages/js/', '@woocommerce/' );
|
project = project.replace( 'packages/js/', '@woocommerce/' );
|
||||||
}
|
}
|
||||||
return project;
|
|
||||||
} );
|
projectPaths[ project ] = projectPath;
|
||||||
|
}
|
||||||
|
return projectPaths;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -175,7 +180,7 @@ export const getAllProjectPaths = async ( tmpRepoPath: string ) => {
|
||||||
* @param {string} fileName changelog file name
|
* @param {string} fileName changelog file name
|
||||||
* @param {string} baseOwner PR base owner
|
* @param {string} baseOwner PR base owner
|
||||||
* @param {string} baseName PR base name
|
* @param {string} baseName PR base name
|
||||||
* @return {Array<string>} List of projects that have Jetpack changelogger enabled and have files changed in a PR.
|
* @return {Object.<string, string>} Paths to projects that have files changed in a PR keyed by the project name.
|
||||||
*/
|
*/
|
||||||
export const getTouchedProjectsRequiringChangelog = async (
|
export const getTouchedProjectsRequiringChangelog = async (
|
||||||
tmpRepoPath: string,
|
tmpRepoPath: string,
|
||||||
|
|
Loading…
Reference in New Issue