Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support releases with multiple changelogs #39

Merged
merged 17 commits into from
Nov 30, 2020
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
58 changes: 51 additions & 7 deletions src/files.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,32 @@ 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 {Object} params
* @param {function} params.octokit
* @param {string} params.repo
* @param {string} params.owner
* @param {number} params.pullNumber
* @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 +52,44 @@ const getModifiedFiles = async (octokit, repo, owner, pullNumber) => {
return files.data.map((file) => file.filename);
};

/**
* Returns the content of a file
* @param {Object} params
* @param {function} params.octokit
* @param {string} params.repo
* @param {string} params.owner
* @param {string} params.path
* @param {string} [params.ref] - Default: the repository’s default
gotbahn marked this conversation as resolved.
Show resolved Hide resolved
* @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,
};
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
118 changes: 118 additions & 0 deletions src/release.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
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 {Function} octokit
* @prop {string} repo
* @prop {string} owner
* @prop {string} path
* @prop {string} branch
* @prop {string} base
*/