|
/** |
|
* Common methods to apply semver process |
|
*/ |
|
|
|
// Commits analysis |
|
const semanticTagPattern = /^(v?)(\d+)\.(\d+)\.(\d+)$/ |
|
const releaseSeverityOrder = ['major', 'minor', 'patch'] |
|
/** |
|
build: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm) |
|
ci: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs) |
|
docs: Documentation only changes |
|
feat: A new feature |
|
fix: A bug fix |
|
perf: A code change that improves performance |
|
refactor: A code change that neither fixes a bug nor adds a feature |
|
style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) |
|
test: Adding missing tests or correcting existing tests |
|
*/ |
|
const semanticRules = [{ |
|
group: 'Fixes & improvements', |
|
|
|
releaseType: 'patch', |
|
prefixes: ['build', 'test', 'ci', 'fix', 'perf', 'refactor', 'style', 'docs', 'patch'], |
|
symbol: "###" |
|
}, |
|
{ |
|
group: 'Features', |
|
releaseType: 'minor', |
|
prefixes: ['feat', 'minor'], |
|
symbol: "##" |
|
}, |
|
{ |
|
group: 'BREAKING CHANGES', |
|
releaseType: 'major', |
|
keywords: ['BREAKING CHANGE', 'BREAKING CHANGES', 'major'], |
|
symbol: "#" |
|
} |
|
] |
|
|
|
/** |
|
* Select last semantic tag |
|
*/ |
|
export async function getLastTag() { |
|
const tags = (await $ `git describe --always --tags \`git rev-list --tags --ignore-missing --max-count=5\``) |
|
.toString() |
|
.split('\n') |
|
.map(tag => tag.trim()); |
|
return tags |
|
.find(tag => semanticTagPattern.test(tag)) |
|
} |
|
|
|
|
|
export async function getNewCommits(lastTag) { |
|
const commitsRange = lastTag ? `${(await $`git rev-list -1 ${lastTag}`).toString().trim()}..HEAD` : 'HEAD' |
|
return (await $.noquote `git log --format=+++%s__%b__%h__%H ${commitsRange}`) |
|
.toString() |
|
.split('+++') |
|
.filter(Boolean) |
|
.map(msg => { |
|
const [subj, body, short, hash] = msg.split('__').map(raw => raw.trim()) |
|
return { |
|
subj, |
|
body, |
|
short, |
|
hash |
|
} |
|
}) |
|
|
|
} |
|
|
|
|
|
export function getSemanticChanges(newCommits) { |
|
const semanticChanges = newCommits.reduce((acc, { |
|
subj, |
|
body, |
|
short, |
|
hash |
|
}) => { |
|
semanticRules.forEach(({ |
|
group, |
|
releaseType, |
|
prefixes, |
|
keywords |
|
}) => { |
|
const prefixMatcher = prefixes && new RegExp(`^(${prefixes.join('|')})(\\(\\w+\\))?:\\s.+$`) |
|
const keywordsMatcher = keywords && new RegExp(`(${keywords.join('|')}):\\s(.+)`) |
|
const change = subj.match(prefixMatcher)?.[0] || body.match(keywordsMatcher)?.[2] |
|
|
|
if (change) { |
|
acc.push({ |
|
group, |
|
releaseType, |
|
change, |
|
subj, |
|
body, |
|
short, |
|
hash |
|
}) |
|
} |
|
}) |
|
return acc |
|
}, []) |
|
console.log('semanticChanges=', semanticChanges) |
|
return semanticChanges |
|
} |
|
|
|
|
|
export function getNextReleaseType(semanticChanges) { |
|
const nextReleaseType = releaseSeverityOrder.find(type => semanticChanges.find(({ |
|
releaseType |
|
}) => type === releaseType)) |
|
if (!nextReleaseType) { |
|
console.log('No semantic changes - no semantic release.') |
|
return |
|
} |
|
return nextReleaseType |
|
} |
|
|
|
export function getNextVersion(lastTag, nextReleaseType) { |
|
const nextVersion = ((lastTag, releaseType) => { |
|
if (!releaseType) { |
|
return |
|
} |
|
if (!lastTag) { |
|
return '1.0.0' |
|
} |
|
|
|
const [, , c1, c2, c3] = semanticTagPattern.exec(lastTag) |
|
if (releaseType === 'major') { |
|
return `${-~c1}.0.0` |
|
} |
|
if (releaseType === 'minor') { |
|
return `${c1}.${-~c2}.0` |
|
} |
|
if (releaseType === 'patch') { |
|
return `${c1}.${c2}.${-~c3}` |
|
} |
|
})(lastTag, nextReleaseType) |
|
|
|
if (!nextVersion) { |
|
throw new Error(`Tag is probably mission. nextVersion has not been computed.`) |
|
} |
|
|
|
return nextVersion |
|
} |
|
|
|
export function getReleaseNote(nextReleaseType, semanticChanges, nextVersion) { |
|
let rule = semanticRules.find(s => s.releaseType === nextReleaseType) |
|
const nextSymbol = rule ? rule.symbol : "###" |
|
const releaseDetails = Object.values(semanticChanges |
|
.reduce((acc, { |
|
group, |
|
change, |
|
short, |
|
hash |
|
}) => { |
|
const { |
|
commits |
|
} = acc[group] || (acc[group] = { |
|
commits: [], |
|
group |
|
}) |
|
let pos = change.indexOf(':') |
|
let first = `[${change.substring(0,pos).toUpperCase().trim()}]` |
|
let second = `${change.substring(pos+1,change.length).trim()}` |
|
const commitRef = `* ${first} ${second} ([${short}])` |
|
|
|
commits.push(commitRef) |
|
|
|
return acc |
|
}, {})) |
|
.map(({ |
|
group, |
|
commits |
|
}) => `${commits.join('\n')}`).join('\n') |
|
|
|
const releaseNotes = `${nextSymbol} ${nextVersion}\n${releaseDetails}` |
|
return releaseNotes |
|
} |
|
|
|
export async function toPush(branch) { |
|
let result = await question(`Do you want to push ${branch.toUpperCase()} branch ? (y/n) `); |
|
if (result.toLowerCase() === 'y') await $ `git push --follow-tags origin HEAD:refs/heads/${branch}`; |
|
} |