Skip to content

Instantly share code, notes, and snippets.

@malys
Last active January 18, 2022 12:57
Show Gist options
  • Save malys/f295388ac10c8fc30b8912598b13ceb6 to your computer and use it in GitHub Desktop.
Save malys/f295388ac10c8fc30b8912598b13ceb6 to your computer and use it in GitHub Desktop.
[Semantic releasing automation] zx scripts for semver automation #zx #semver #maven #nodejs #javascript

Based on zx-semrel concept, followed scripts are examples to automate full releasing process with zx scripts.

Purpose

zx-semrel concept proposes an implementation of semantic release for node projects using zx.

In our case, we try to generalize this idea to:

  • provide a script agnostic to build technology (npm, maven ,gulp, ...): release.mjs
  • provide a script using maven for build engine: release_maven.mjs
  • provide modularity architecture to factorize code and to cover more use case: release_util_update.mjs
  • support semantic release and taks to apply after releasing process : see below for explanation

Built project structure

Every project to release will provide a release.mjs file to define semantic release to apply.

example:

// maven implementation will be applied
import {release} from 'file://D:/Developpement/archi/all-modules/common-stuffs/scripts/release/release_maven.mjs'

(async () => {
   release();
})()

and a post-release.mjs to define post release tasks to apply after releasing process like:

  • propagate new version number in a target project using this dependency (maven project, docker-compose file, kubernetes descriptor, ..) see release_util_postRelease.mjs
  • launch optionnaly releasing process project
  • ...

Example of post-release.mjs file:

// Update dependency version in pom.xml
import {postReleaseMaven} from 'file://D:/Developpement/archi/all-modules/common-stuffs/scripts/release/release_util.mjs'

(async () => {
    const nextVersion = process.argv[3]
    const nextReleaseType=process.argv[4]
    const project='example-api'
    const tag='example-deps.version'
    postReleaseMaven(nextVersion,nextReleaseType,project,tag)
})()

Warnings

Please rename file extensions from .js to .mjs bacause of with mjs extension, syntax color is not working.

/***
* Git semver process agnotism to build technology
*/
import {
getReleaseNote,
getNextVersion,
getNextReleaseType,
getSemanticChanges,
getNewCommits,
getLastTag,
toPush
} from 'file://D:/Developpement/archi/all-modules/common-stuffs/scripts/release/release_util.mjs'
import {
updateChangelog,
updateReadme,
} from 'file://D:/Developpement/archi/all-modules/common-stuffs/scripts/release/release_util_update.mjs'
import {
toPostRelease
} from "file://D:/Developpement/archi/all-modules/common-stuffs/scripts/release/release_util_postRelease.mjs";
export async function release(masterHook, developHook) {
let nextReleaseType
let nextVersion
try {
$.verbose = !!process.env.VERBOSE
$.noquote = async (...args) => {
const q = $.quote;
$.quote = v => v;
const p = $(...args);
await p;
$.quote = q;
return p
}
// Switch on develop branch
try {
await $ `git merge --abort`;
} catch (error) {
//Nothing to do
}
await $ `git checkout develop`;
// stash current work if required
let now = new Date().toISOString()
await $ `git stash push -m Release-${now}`;
// update local workspace with remote one
await $ `git pull`
// retrieve required information
const lastTag = await getLastTag()
const newCommits = await getNewCommits(lastTag)
const semanticChanges = getSemanticChanges(newCommits)
nextReleaseType = getNextReleaseType(semanticChanges)
nextVersion = getNextVersion(lastTag, nextReleaseType)
const releaseNotes = getReleaseNote(nextReleaseType, semanticChanges, nextVersion)
console.info("nextVersion="+nextVersion)
// Switch on master branch
await $ `git checkout master`;
// update local workspace with remote one
await $ `git pull`;
// Merge develop branch
try {
await $ `git merge develop`;
} catch (e) {
//Merge failed
await $ `nyagos -c gitex.cmd browse %CD%`;
}
await updateChangelog(releaseNotes);
await updateReadme(lastTag, nextVersion);
if (masterHook) await masterHook(nextVersion);
// Prepare git commit and push
const releaseMessage = `Release version ${nextVersion}`;
await $ `git commit -am ${releaseMessage}`;
await $ `git tag -a ${nextVersion} HEAD -m ${releaseMessage}`;
// PUSH
await toPush('master');
} catch (e) {
console.error(chalk.bold.red(e))
try {
await $ `git tag -d ${nextVersion}`;
} finally {
await $ `git checkout -B master origin/master`;
process.exit(1)
}
}
try {
// Switch on develop branch
await $ `git checkout develop`;
// Merge develop branch
try {
await $ `git merge master`;
} catch (e) {
//Merge failed
await $ `nyagos -c gitex.cmd browse %CD%`;
}
if (developHook) await developHook(nextVersion);
try{
await $ `git commit -am "Bump version"`;
}catch(e){
console.warn(chalk.bold.cyan(e))
}
// PUSH
await toPush('develop');
await toPostRelease(nextVersion, nextReleaseType);
console.log(chalk.bold('Great success!'))
} catch (e) {
console.error(chalk.bold.red(e))
await $ `git checkout -B develop origin/develop`;
process.exit(1)
}
}
/***
* Git semver process for maven
*/
import {
release as releaseParent,
} from 'file://D:/Developpement/archi/all-modules/common-stuffs/scripts/release/release.mjs'
export async function release(masterHook, developHook) {
async function masterHookLocal(nextVersion) {
await $ `mvn versions:set -DgenerateBackupPoms=false -DnewVersion=${nextVersion}`;
if (masterHook) {
await masterHook(nextVersion);
}
}
async function developHookLocal(nextVersion) {
//SNAPSHOT`
let snapshot = `${nextVersion}-SNAPSHOT`
await $ `mvn versions:set -DgenerateBackupPoms=false -DnewVersion=${snapshot}`;
if (developHook) {
await developHook(nextVersion);
}
}
await releaseParent(masterHookLocal, developHookLocal);
}
/***
* Git semver process for maven and git modules
*/
import {
release as releaseParent,
} from 'file://D:/Developpement/archi/all-modules/common-stuffs/scripts/release/release_maven.mjs'
async function updateJSON(nextVersion){
let content = await fs.readJson('./file/jbossModules.json', {
throws: false
})
content.export.version = nextVersion
await fs.writeJson('./file/jbossModules.json', content, {
spaces: 2
})
}
export async function release(masterHook, developHook) {
async function masterHookLocal(nextVersion) {
// Update jbossModules.json version
await updateJSON(nextVersion);
if (masterHook) {
await masterHook(nextVersion);
}
}
async function developHookLocal(nextVersion) {
// Update jbossModules.json version
await updateJSON(`${nextVersion}-SNAPSHOT`);
if (developHook) {
await developHook(nextVersion);
}
}
await releaseParent(masterHookLocal, developHookLocal);
}
/***
* Git semver process for maven
*/
import {
release as releaseParent,
} from 'file://D:/Developpement/archi/all-modules/common-stuffs/scripts/release/release.mjs'
export async function release(masterHook, developHook) {
async function masterHookLocal(nextVersion) {
await $ `npm --no-git-tag-version version ${nextVersion}`;
if (masterHook) {
await masterHook(nextVersion);
}
}
async function developHookLocal(nextVersion) {
//SNAPSHOT`
let snapshot = `${nextVersion}-SNAPSHOT`
await $ `npm --no-git-tag-version version ${snapshot}`;
if (developHook) {
await developHook(nextVersion);
}
}
await releaseParent(masterHookLocal, developHookLocal);
}
/**
* 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}`;
}
/**
* Post release methods to propagate bumping version
*/
export async function postReleaseMaven(nextVersion, nextReleaseType, project, tag,origin) {
cd(`../${project}`)
await $`git checkout develop`;
const REGEXP = new RegExp(`(<${tag}>)(.*)(<\/${tag}>)`)
let pom = await fs.readFile(`../${project}/pom.xml`, 'utf8');
let toReplace = REGEXP.exec(pom)[0]
await fs.writeFile(`../${project}/pom.xml`, pom.replace(toReplace, `<${tag}>${nextVersion}</${tag}>`))
await $`git add -A .`;
await $`git commit -am "${nextReleaseType}: Use version ${nextVersion} of ${origin}"`;;
let result=await question(`Do you want to release ${project} ? (y/n) `);
if (result.toLowerCase() === 'y') await $`zx release.mjs`;
}
export async function postReleaseDocker(nextVersion, nextReleaseType, file,project, tag, origin) {
cd(`../${project}`)
const REGEXP = new RegExp(`${tag}=([\\d\\.]+)`,'gm')
let pom = await fs.readFile(file, 'utf8');
await fs.writeFile(file, pom.replace(REGEXP, `${tag}=${nextVersion}`));
await $`git add ${file}`;
await $`git commit -am "${nextReleaseType}: Use version ${nextVersion} of ${origin}"`;
}
export async function postReleaseOpenshift(nextVersion, nextReleaseType, file,project, tag, origin) {
cd(`../${project}`)
const REGEXP = new RegExp(`name: ${tag}(.+)value: ([\\d\\.]+)`,'gs')
let pom = await fs.readFile(file, 'utf8');
let arr=REGEXP.exec(pom)
let toReplace = arr[0]
let version = arr[2]
await fs.writeFile(file, pom.replace(toReplace, toReplace.replace(version,nextVersion)));
await $`git add ${file}`;
await $`git commit -am "${nextReleaseType}: Use version ${nextVersion} of ${origin}"`;
}
export async function toPostRelease(nextVersion,nextReleaseType) {
let result = await fs.exists('./post-release.mjs');
if (result) await $ `zx post-release.mjs ${nextVersion} ${nextReleaseType}`;
}
/**
* Common methods ro update local files
*
*/
export async function updateChangelog(releaseNotes) {
let changelogcontent = await fs.readFile('./CHANGELOG.md');
if(changelogcontent.indexOf('# Changelog') >= 0) {
await $ `sed -i "s/##[[:space:]]/# /g" CHANGELOG.md`;
await $ `sed -i "s/# Changelog//g" CHANGELOG.md`;
}
await $ `echo ${releaseNotes}"\n$(cat ./CHANGELOG.md)" > ./CHANGELOG.md`;
console.log('Changelog updated')
}
export async function updateReadme(lastTag, nextVersion) {
//Update Readme
await $ `sed -i 's/message=${lastTag.replace('v','')}/message=${nextVersion}/g' ReadMe.md`;
await $ `sed -i 's/tags\\/${lastTag.replace('v','')}/tags\\/${nextVersion}/g' ReadMe.md`;
console.log('ReadMe updated')
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment