Skip to content

Commit

Permalink
Support releases with multiple changelogs (#39)
Browse files Browse the repository at this point in the history
  • Loading branch information
dobladov authored Nov 30, 2020
1 parent b322cd9 commit 8a1438c
Show file tree
Hide file tree
Showing 8 changed files with 1,427 additions and 636 deletions.
6 changes: 5 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ module.exports = {
es2020: true,
node: true,
},
extends: ['@zattoo'],
extends: [
'@zattoo',
'@zattoo/eslint-config/rules/jest',
'@zattoo/eslint-config/rules/jsdoc',
],
parserOptions: {
ecmaVersion: 11,
},
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](http://keepachangelog.com/)

## Unreleased

Added multiple changelogs support

## [1.5.0] - 07.10.2020

### Added
Expand Down
1,771 changes: 1,191 additions & 580 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@
"glob": "7.1.6"
},
"devDependencies": {
"@zattoo/eslint-config": "^9.0.0",
"@zattoo/eslint-config": "11.0.4",
"@zeit/ncc": "0.22.3",
"eslint": "7.7.0",
"eslint": "7.14.0",
"eslint-config-airbnb-base": "14.2.0",
"eslint-plugin-import": "2.22.0",
"jest": "26.4.2"
"jest": "26.6.3"
}
}
63 changes: 56 additions & 7 deletions src/files.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const globPromise = util.promisify(glob);

/**
* List all folders specified in sources
*
* @param {string} [sources]
* @returns {Promise<string[]>}
*/
Expand All @@ -16,25 +17,29 @@ const getFolders = async (sources) => {

const folders = [];

for await (const source of sources.split(/, */g)) {
await Promise.all(sources.split(/, */g).map(async (source) => {
if (glob.hasMagic(source)) {
folders.push(...await globPromise(source.endsWith('/') ? source : `${source}/`));
} else {
folders.push(source);
}
}
}));

return folders;
};

/**
* Returns the modified files in the PR
* @param {function} octokit
* @param {string} repo
* @param {string} owner
* @param {number} pullNumber
*
* @param {PullRequest} param
* @returns {Promise<string[]>}
*/
const getModifiedFiles = async (octokit, repo, owner, pullNumber) => {
const getModifiedFiles = async ({
octokit,
repo,
owner,
pullNumber,
}) => {
const files = await octokit.pulls.listFiles({
owner,
repo,
Expand All @@ -45,7 +50,51 @@ const getModifiedFiles = async (octokit, repo, owner, pullNumber) => {
return files.data.map((file) => file.filename);
};

/**
* Returns the content of a file
*
* @param {PullRequest} param
* @returns {Promise<string>}
*/
const getFileContent = async ({
octokit,
repo,
owner,
path,
ref,
}) => {
try {
const content = await octokit.repos.getContent({
owner,
repo,
path,
ref,
});

return content.data?.content && Buffer.from(content.data.content, 'base64').toString();
} catch (error) {
/**
* Cases where file does not exists
* should not stop execution
*/
console.log(error.message);
return null;
}
};

module.exports = {
getFolders,
getModifiedFiles,
getFileContent,
};

/**
* @typedef {Object} PullRequest
* @param {GithubObject} octokit
* @param {string} repo
* @param {string} owner
* @param {string} path
* @param {string} [ref]
*/

/** @typedef {import('@actions/github/lib/utils').GitHub} GithubObject */
86 changes: 41 additions & 45 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,20 @@ const core = require('@actions/core');
const util = require('util');

const readFile = util.promisify(fs.readFile);
const stat = util.promisify(fs.stat);

const {
context,
getOctokit,
} = require('@actions/github');

const {
compareChangelog,
validateRelease,
} = require('./release');
const {validateChangelog} = require('./validate');
const {
getModifiedFiles,
getFolders,
getModifiedFiles,
} = require('./files');

const run = async () => {
Expand All @@ -29,6 +32,7 @@ const run = async () => {
const pullNumber = context.payload.pull_request.number;
const labels = context.payload.pull_request.labels.map((label) => label.name);
const branch = context.payload.pull_request.head.ref;
const base = context.payload.pull_request.base.ref;

try {
// Ignore the action if -changelog label (or custom name) exists
Expand All @@ -37,10 +41,15 @@ const run = async () => {
process.exit(0);
}

const modifiedFiles = await getModifiedFiles(octokit, repo, owner, pullNumber);
const modifiedFiles = await getModifiedFiles({
octokit,
repo,
owner,
pullNumber,
});
const folders = await getFolders(sources);

for await (const path of folders) {
await Promise.all(folders.map(async (path) => {
const isRoot = path === '';
const folder = (!path.endsWith('/') && !isRoot) ? `${path}/` : path;

Expand All @@ -53,53 +62,40 @@ const run = async () => {
}

const changelogContent = await readFile(`${folder}CHANGELOG.md`, {encoding: 'utf-8'});
const {
isUnreleased,
version,
date,
skeleton,
} = validateChangelog(changelogContent);

// Checks if the branch is release or once of release_branches input.
if (releaseBranches.find((releaseBranch) => branch.startsWith(releaseBranch))) {
if (isUnreleased) {
throw new Error(`"${branch}" branch can't be unreleased`);
}

if (!version || version === 'Unreleased') {
throw new Error(`"${branch}" branch should have a version`);
}

if (!date) {
throw new Error(`"${branch}" branch should have a date`);
}
validateChangelog(changelogContent);
}));

const {version: packageVersion} = JSON.parse(await readFile(`${folder}package.json`, {encoding: 'utf-8'}));
if (packageVersion !== version) {
throw new Error(`The package version "${packageVersion}" does not match the newest version "${version}"`);
}
// Checks if the branch is release or once of release_branches input.
if (releaseBranches.find((releaseBranch) => branch.startsWith(releaseBranch))) {
const changelogs = modifiedFiles.filter((file) => file.endsWith('CHANGELOG.md'));

const packageLockStats = await stat(`${folder}package-lock.json`);
if (changelogs.length) {
/** If branch name contains project ex: release/account */
const project = branch.includes('/') && branch.split('/').slice(-1)[0];

if (packageLockStats) {
const {version: packageLockVersion} = JSON.parse(await readFile(`${folder}package-lock.json`, {encoding: 'utf-8'}));
if (packageLockVersion !== version) {
throw new Error(`The package-lock version "${packageVersion}" does not match the newest version "${version}"`);
}
}
if (project) {
const projectChangelog = changelogs.find((file) => file.includes(`${project}/CHANGELOG.md`));

// Validate if branch contains breaking changes
// and version has the same major version as previous.
if (branch.startsWith('release')) {
const text = skeleton.versionText[version].map((v) => v.value).join();
const previousVersion = skeleton.versions[1];
if (
text.includes('breaking change')
&& (previousVersion.value.split('.')[0] === version.split('.')[0])
) {
throw new Error('This release includes breaking changes, major version should be increased.');
if (projectChangelog) {
validateRelease(projectChangelog);
} else {
throw new Error(`The changelog for project "${project}" must be modified for this release`);
}
} else {
/** For each changelog determine if last version is different than production and validate it */
await Promise.all(changelogs.map(async (path) => {
await compareChangelog({
octokit,
repo,
owner,
path,
base,
branch,
});
}));
}
} else {
throw new Error('At least one changelog should be modified for a release');
}
}
} catch (error) {
Expand Down
122 changes: 122 additions & 0 deletions src/release.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
const fs = require('fs');
const util = require('util');

const readFile = util.promisify(fs.readFile);
const exists = util.promisify(fs.exists);

const {validateChangelog} = require('./validate');
const {getFileContent} = require('./files');

/**
* Given the path of a changelog it will
* validate if it's a correct release
*
* @param {string} changelog
*/
const validateRelease = async (changelog) => {
const folder = changelog.replace('CHANGELOG.md', '');

const changelogContent = await readFile(changelog, {encoding: 'utf-8'});

const {
isUnreleased,
version,
date,
skeleton,
} = validateChangelog(changelogContent);

if (isUnreleased) {
throw new Error(`A release branch can't have "Unreleased" version for changelog: ${changelog}`);
}

if (!version || version === 'Unreleased') {
throw new Error(`A release branch should have a version for changelog ${changelog}`);
}

if (!date) {
throw new Error(`A release branch should have a date for changelog: ${changelog}`);
}

const {version: packageVersion} = JSON.parse(await readFile(`${folder}package.json`, {encoding: 'utf-8'}));
if (packageVersion !== version) {
throw new Error(`The package version "${packageVersion}" does not match the newest version "${version}" of changelog: ${changelog}`);
}

if (await exists(`${folder}package-lock.json`)) {
const {version: packageLockVersion} = JSON.parse(await readFile(`${folder}package-lock.json`, {encoding: 'utf-8'}));
if (packageLockVersion !== version) {
throw new Error(`The package-lock version "${packageVersion}" does not match the newest version "${version}" of changelog: ${changelog}`);
}
}

// Validate if branch contains breaking changes
// and version has the same major version as previous.
const text = skeleton.versionText[version].map((v) => v.value).join();
const previousVersion = skeleton.versions[1];
if (
text.includes('breaking change')
&& (previousVersion.value.split('.')[0] === version.split('.')[0])
) {
throw new Error(`This release includes breaking changes, major version should be increased for changelog: ${changelog}`);
}
};

/**
* Compares the current version of the given changelog
* with a previous version and validates in case is different.
*
* @param {Compare} param
*/
const compareChangelog = async ({
octokit,
repo,
owner,
path,
base,
branch,
}) => {
const previousText = await getFileContent({
octokit,
repo,
owner,
path,
ref: base,
});
const currentText = await getFileContent({
octokit,
repo,
owner,
path,
ref: branch,
});

if (previousText && currentText) {
const previousContent = validateChangelog(previousText);
const currentContent = validateChangelog(currentText);

if (
!previousContent.isUnreleased &&
!currentContent.isUnreleased &&
previousContent.version !== currentContent.version
) {
validateRelease(path);
}
}
};

module.exports = {
compareChangelog,
validateRelease,
};

/**
* @typedef {Object} Compare
* @prop {GithubObject} octokit
* @prop {string} repo
* @prop {string} owner
* @prop {string} path
* @prop {string} branch
* @prop {string} base
*/

/** @typedef {import('@actions/github/lib/utils').GitHub} GithubObject */
Loading

0 comments on commit 8a1438c

Please sign in to comment.