Actually you don’t need 'semantic-release' for semantic release
source link: https://dev.to/antongolub/you-don-t-need-semantic-release-sometimes-3k6k
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Actually you don’t need 'semantic-release' for semantic release
May 23
・6 min read
I’m a big fan of semantic-release since it appeared. I followed its development, studied its inners. I did in-house reports, held workshops and finally brought semrel to our build infrastructure. I wrote plugins, plugin-factories and testing-tools for it. For several years now, I've been trying to combine semantic releases and monorepositories in many OSS projects:
Etc, etc, so on. I’m just trying to say, that semrel had a significant impact on my professional life.
Semrel goal
The main purpose of semantic-release is to transform semantic (conventional) commits into build artifacts and deployments. With version bumping, changelogs, tagging, pkg publishing. “Fully-automated release” — is the true. There are also dozens on plugins, so you’ll most likely find a solution for any standard case. It really saves times.
But sometimes
You may need a minor tweak up. For example, push some pkg to both public and internal registries. Ooops. "...publishing to two different registry is not a good idea". In this case you can not rely on stable, convenient and tested in millions runs semrel/npm plugin, and you have to just write a pair of commands by hand with semantic-release/exec instead:
echo "//npm-registry.domain.com/:_authToken=${TOKEN}” >> .npmrc
echo "\`jq '.name="@scope/pkg-name”’ package.json\`" > package.json
npm config set registry https://npm-registry.domain.com
npm publish --no-git-tag-version
Another instance — disabling git notes fetching. "Afraid that won't be possible".
Of course, you may fork semrel and remove the mentioned line. Or create a plugin/hook, that will override loaded execa module with patched one version, than just skips git notes
invocation (this is really frustrating, I did smth similar). Or… {{ another crazy workaround goes here }}
.
This is a watershed moment. Once you start to fight against the tool, it's time to just pick another one. The new dilemma:
- Spend days and days for searching, tuning and testing analogs.
- Write your own semantic-release.
My opinionated suggestion: if your case is very simple or, conversely, very complex, the second option will be optimal. Release script — is not a rocket science!
140 lines alternative
Let's take a look at what exactly each release consists of, if we discard the high-level tool contracts. I use zx in the examples, but it could be execa or native child_process.exec too.
1. Git configuration
To make a commit you need a committer: just name and email that will be associated with author. Also PAT or SSH token is required to push the commit.
const {GIT_COMMITTER_NAME, GIT_COMMITTER_EMAIL, GITHUB_TOKEN} = process.env
if (!GITHUB_TOKEN || !GIT_COMMITTER_NAME || !GIT_COMMITTER_EMAIL) {
throw new Error('env.GITHUB_TOKEN, env.GIT_COMMITTER_NAME & env.GIT_COMMITTER_EMAIL must be set')
}
const gitAuth = `${GIT_COMMITTER_NAME}:${GITHUB_TOKEN}`
const originUrl = (await $`git config --get remote.origin.url`).toString().trim()
const [,,repoHost, repoName] = originUrl.replace(':', '/').replace(/\.git/, '').match(/.+(@|\/\/)([^/]+)\/(.+)$/)
const repoPublicUrl = `https://${repoHost}/${repoName}`
const repoAuthedUrl = `https://${gitAuth}@${repoHost}/${repoName}`
await $`git config user.name ${GIT_COMMITTER_NAME}`
await $`git config user.email ${GIT_COMMITTER_EMAIL}`
await $`git remote set-url origin ${repoAuthedUrl}`
2. Commit analysis
Conventional commits are just a prefixed strings in git log. We should define some rules on how to associate messages substrings with corresponding release types:
const semanticTagPattern = /^(v?)(\d+)\.(\d+)\.(\d+)$/
const releaseSeverityOrder = ['major', 'minor', 'patch']
const semanticRules = [
{group: 'Features', releaseType: 'minor', prefixes: ['feat']},
{group: 'Fixes & improvements', releaseType: 'patch', prefixes: ['fix', 'perf', 'refactor', 'docs']},
{group: 'BREAKING CHANGES', releaseType: 'major', keywords: ['BREAKING CHANGE', 'BREAKING CHANGES']},
]
Then we search for the prev release tag, that satisfies semver pattern:
const tags = (await $`git tag -l --sort=-v:refname`).toString().split('\n').map(tag => tag.trim())
const lastTag = tags.find(tag => semanticTagPattern.test(tag))
And make commits cut from the found ref:
const newCommits = (lastTag
? await $`git log --format=+++%s__%b__%h__%H ${await $`git rev-list -1 ${lastTag}`}..HEAD`
: await $`git log --format=+++%s__%b__%h__%H HEAD`)
.toString()
.split('+++')
.filter(Boolean)
.map(msg => {
const [subj, body, short, hash] = msg.split('__').map(raw => raw.trim())
return {subj, body, short, hash}
})
Now we just need to parse them:
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
}, [])
Ta-da. Semantic changes:
semanticChanges= [
{
group: 'Fixes & improvements',
releaseType: 'patch',
change: 'perf: use git for tags sorting',
subj: 'perf: use git for tags sorting',
body: '',
short: 'a1abdae',
hash: 'a1abdaea801824d0392e69f9182daf4d5f4b97db'
},
{
group: 'Fixes & improvements',
releaseType: 'patch',
change: 'refactor: minor simplifications',
subj: 'refactor: minor simplifications',
body: '',
short: 'be847a2',
hash: 'be847a26e2b0583e889403ec00db45f9f9555e30'
},
{
group: 'Fixes & improvements',
releaseType: 'patch',
change: 'fix: fix commit url template',
subj: 'fix: fix commit url template',
body: '',
short: '3669edd',
hash: '3669edd7eb440e29dc0fcf493c76fbfc04271023'
}
]
3. Resolve next version:
const nextReleaseType = releaseSeverityOrder.find(type => semanticChanges.find(({releaseType}) => type === releaseType))
if (!nextReleaseType) {
console.log('No semantic changes - no semantic release.')
return
}
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)
const nextTag = 'v' + nextVersion
4. Generate release notes
const releaseDiffRef = `## [${nextVersion}](${repoPublicUrl}/compare/${lastTag}...${nextTag}) (${new Date().toISOString().slice(0, 10)})`
const releaseDetails = Object.values(semanticChanges
.reduce((acc, {group, change, short, hash}) => {
const {commits} = acc[group] || (acc[group] = {commits: [], group})
const commitRef = `* ${change} ([${short}](${repoPublicUrl}/commit/${hash}))`
commits.push(commitRef)
return acc
}, {}))
.map(({group, commits}) => `
### ${group}
${commits.join('\n')}`).join('\n')
const releaseNotes = releaseDiffRef + '\n' + releaseDetails + '\n'
5. Update CHANGELOG.md
Attach releaseNotes
to file. Just one string.
await $`echo ${releaseNotes}"\n$(cat ./CHANGELOG.md)" > ./CHANGELOG.md`
6. Update package version
await $`npm --no-git-tag-version version ${nextVersion}`
7. Git release.
Create commit. Create tag. Push them.
const releaseMessage = `chore(release): ${nextVersion} [skip ci]`
await $`git add -A .`
await $`git commit -am ${releaseMessage}`
await $`git tag -a ${nextTag} HEAD -m ${releaseMessage}`
await $`git push --follow-tags origin HEAD:refs/heads/master`
8. GitHub release
Just one curl POST to gh rest api.
const releaseData = JSON.stringify({
name: nextTag,
tag_name: nextTag,
body: releaseNotes
})
await $`curl -u ${GIT_COMMITTER_NAME}:${GITHUB_TOKEN} -H "Accept: application/vnd.github.v3+json" https://api.github.com/repos/${repoName}/releases -d ${releaseData}`
9. Npm publish
await $`npm publish --no-git-tag-version`
Need several registries? NP.
await $`npm config set registry https://registry.npmjs.org`
await $`npm publish --no-git-tag-version`
await $`echo "\`jq '.name="@${repoName}"' package.json\`" > package.json`
await $`npm config set registry https://npm.pkg.github.com`
await $`npm publish --no-git-tag-version`
That’s all. Of course, this solution has significant limitations of usage. Ultimately, you don't care if other tools have 99.99999% applicability if they don't solve just one case — yours. But now you have complete direct control over your release flow. You can improve and modify this snippet as you like and whenever you like.
release.mjs
gh release.yaml
release log
antongolub / zx-semrel
`zx`-based release script as `semantic-release` alternative (PoC)
zx-semrel
zx -based release script as semantic-release alternative (PoC)
Sometimes bloody enterprise enforces you not to use any third-party solutions for sensitive operations (like release, deploy, so on) Old good script copy-paste hurries to the rescue!
Requirements
- mac / linux
- Node.js >= 14
- git >= 2.0
- zx >= 1.6.0
Key features
- Zero dependencies
- Zero configuration
- Pretty fast
- Tiny, less than 140 lines with comments
- Reliability, safety, simplicity and maintainability (sarcasm)
Functionality
- Poor conventional commits analysis
-
CHANGELOG.md
generation -
package.json
version bumping - Git release commit creation
- GitHub Release
- Package publishing to both npmjs and gh registries
Usage
- Tweak up, inject tokens, etc
curl https://raw.githubusercontent.com/antongolub/zx-semrel/master/release.mjs > ./release.mjs zx ./release.mjs
or just this like if zx
is not installed:
# Just replace GIT* env values with your own GIT_COMMITTER_NAME=antongolub [email protected] GITHUB_TOKEN=token npx zx ./release.mjs
or run as is without any edits though npx:
# Cross your fingers for luck GIT_COMMITTER_NAME=antongolub [email protected]…
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK