github action: automate version bump on merge to main

2021-09-12

 | 

~3 min read

 | 

529 words

I spent a few hours this weekend writing a Github action to accomplish a different way of managing the versions of my library.

I think the final answer is actually quite readable, which makes me quite happy!

First, the Github workflow, saved in .github/workflows/bumpVersionOnPushToMain.yml:

.github/workflows/bumpVersionOnPushToMain.yml
name: "Update the package version on pushes to main"
on:
  push:
    branches:
      - main
jobs:
  scripts:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - run: chmod +x ./scripts/incrementMinor.js
      - run: ./scripts/incrementMinor.js
      - uses: stefanzweifel/git-auto-commit-action@v4
        with:
          commit_message: "Automated version bump on merge to main"

One of the things I stumbled over as this was one of the first Github Action I’d written was how I could access the actual code. It turns out that’s exactly what actions/checkout@v2 is for - and by default it checks out the current repository.

Before I was using that action, the following lines which assume the presence of files failed.

Now, let’s step through the actual pieces.

The script of incrementMinor is just a few lines of code. In time I’d love to make this “smarter” so that it can receive arguments to dictate which version to increment.

scripts/incrementMinor.js
#!/usr/bin/env node
const { incrementVersion } = require("./incrementVersion")

function incMinorVersion(version) {
  const [major, minor, patch] = version.split(".")
  if (isNaN(Number(minor)))
    throw Error(`Version is not made up of numbers! "${version}"`)
  const newMinor = String(Number(minor) + 1)
  return [major, newMinor, patch].join(".")
}

incrementVersion(incMinorVersion)

The way this works is that this function is actually passed into a more generic incrementVersion function which receives an incrementer:

scripts/incrementVersion.js
const fs = require("fs")
const { getLatestVersion } = require("./getVersion")
const { saveVersion } = require("./saveVersion")

function incrementVersion(incrementer) {
  // read and parse package.json
  const rawPackage = fs.readFileSync("./package.json")
  const parsedPackage = JSON.parse(rawPackage)

  // get latest published version
  const latestVersion = getLatestVersion(parsedPackage)

  // update the version
  const nextVersion = incrementer(latestVersion)
  parsedPackage.version = nextVersion

  // save and commit the package.json
  saveVersion(parsedPackage)
}

module.exports = { incrementVersion }

This has two helper functions of its own:

  1. getLatestVersion and
  2. saveVersion

First the getLatestVersion:

scripts/getLatestVersion.js
const { spawnSync } = require("child_process")

function getLatestVersion(packageJson) {
  let latestVersion
  const latestPublishedVersion = spawnSync("npm", [
    "show",
    packageJson.name,
    "version",
  ])

  if (latestPublishedVersion.stderr.toString()) {
    // The package has never been published
    latestVersion = packageJson.version
  } else {
    latestVersion = latestPublishedVersion.stdout.toString().trim()
  }

  return latestVersion
}

module.exports = { getLatestVersion }
scripts/saveVersion.js
const fs = require("fs")
const { gitAdd, gitCommit } = require("./simpleGit")

function saveVersion(packageJson) {
  const { version } = packageJson

  fs.writeFile(
    "./package.json",
    Buffer.from(JSON.stringify(packageJson, null, 2)),
    { encoding: "utf8" },
    (err) => {
      if (err) {
        return console.log("Error!", err)
      }

      gitAdd()
      gitCommit(`Published version: ${version}`)

      return console.log(
        `Successfully updated and committed package.json to version ${version}`,
      )
    },
  )
}

module.exports = { saveVersion }

This one uses a few “simple” git commands:

scripts/simpleGit.js
const { spawnSync } = require("child_process")

const gitAdd = () => spawnSync("git", ["add", "package.json"])
const gitCommit = (message) => spawnSync("git", ["commit", `-m '${message}'`])

module.exports = { gitAdd, gitCommit }

All of these helper scripts rely on the spawnSync method on the child_process module.

That’s the whole thing. With this workflow in place, whenever code is merged to main, I run a workflow that will automatically bump the minor version of the project.


Related Posts
  • 2021 Daily Journal
  • Github Actions: Debugging 'No such file or directory'
  • Github Actions: Run On Merge Only


  • Hi there and thanks for reading! My name's Stephen. I live in Chicago with my wife, Kate, and dog, Finn. Want more? See about and get in touch!