2

Actually you don’t need 'semantic-release' for semantic release

 2 years ago
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
Enter fullscreen modeExit fullscreen mode

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:

  1. Spend days and days for searching, tuning and testing analogs.
  2. 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}`
Enter fullscreen modeExit fullscreen mode

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']},
]
Enter fullscreen modeExit fullscreen mode

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))
Enter fullscreen modeExit fullscreen mode

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}
  })
Enter fullscreen modeExit fullscreen mode

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
}, [])
Enter fullscreen modeExit fullscreen mode

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'
  }
]
Enter fullscreen modeExit fullscreen mode

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
Enter fullscreen modeExit fullscreen mode

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'
Enter fullscreen modeExit fullscreen mode

5. Update CHANGELOG.md

Attach releaseNotes to file. Just one string.

await $`echo ${releaseNotes}"\n$(cat ./CHANGELOG.md)" > ./CHANGELOG.md`
Enter fullscreen modeExit fullscreen mode

6. Update package version

await $`npm --no-git-tag-version version ${nextVersion}`
Enter fullscreen modeExit fullscreen mode

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`
Enter fullscreen modeExit fullscreen mode

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}`
Enter fullscreen modeExit fullscreen mode

9. Npm publish

await $`npm publish --no-git-tag-version`
Enter fullscreen modeExit fullscreen mode

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`
Enter fullscreen modeExit fullscreen mode

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

GitHub logo 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

Usage

  1. Tweak up, inject tokens, etc
curl https://raw.githubusercontent.com/antongolub/zx-semrel/master/release.mjs > ./release.mjs
zx ./release.mjs
Enter fullscreen modeExit fullscreen mode

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
Enter fullscreen modeExit fullscreen mode

or run as is without any edits though npx:

# Cross your fingers for luck
GIT_COMMITTER_NAME=antongolub [email protected]
Enter fullscreen modeExit fullscreen mode

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK