DEV Community

Hansel Wei
Hansel Wei

Posted on • Edited on

Automate Node.js Version Bumps with GitHub Actions (No Manual PRs Needed)

Manually bumping a package version is one of those low-value chores that's easy to forget, easy to mess up, and just annoying enough to interrupt your flow. You change package.json, regenerate the lockfile, commit it, open a PR — every time, for every release.

There's a better way: let a GitHub Actions workflow handle the entire thing on demand.

Here's an example project with the workflow pattern I set up for one of my open source dev tool line-commenter-tool, which you can adapt for any Node.js project.


What It Does

When you're ready to cut a new version, you trigger a workflow dispatch from the GitHub Actions UI (or via the API/CLI). The workflow:

  1. Checks out your target branch
  2. Bumps the version in package.json
  3. Runs npm ci to regenerate package-lock.json
  4. Opens a pull request with a clean commit and auto-generated description

The resulting PR looks similar to this:

chore: bump version to 2.1.0

  • Updated package.json version to 2.1.0
  • Regenerated package-lock.json
  • Verified install with npm ci

Triggered via workflow dispatch by @hansel

No manual work. No forgotten lockfile updates. Just review and merge.


The Workflow

Create .github/workflows/bump-version.yml:

on:
  workflow_dispatch:
    inputs:
      version:
        description: 'New semver version (e.g. 2.2.0)'
        required: true
        type: string
      target_branch:
        description: "Branch to bump version on"
        required: true
        default: "main"

jobs:
  bump-version:
    runs-on: ubuntu-latest
    permissions:
      contents: write
      pull-requests: write

    steps:
      - uses: actions/checkout@v6
        with:
          fetch-depth: 0

      - uses: actions/setup-node@v6
        with:
          node-version: 20

      - name: Validate version input
        id: validate
        run: |
          VERSION="${{ github.event.inputs.version }}"

          # Validate semver format (major.minor.patch with optional pre-release/build metadata)
          if ! echo "$VERSION" | grep -Eq '^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*))*))?(\+([0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*))?$'; then
            echo "ERROR: '$VERSION' is not a valid semver version."
            exit 1
          fi

          CURRENT_VERSION=$(node -p "require('./package.json').version")
          echo "current=$CURRENT_VERSION" >> "$GITHUB_OUTPUT"
          echo "Current version: $CURRENT_VERSION"
          echo "Requested version: $VERSION"

          # Validate new version is strictly greater than current using Node
          node -e "
            const parseBase = (v) => v.replace(/-.*/, '').split('.').map(Number);
            const curr = parseBase('$CURRENT_VERSION');
            const next = parseBase('$VERSION');
            for (let i = 0; i < 3; i++) {
              if (next[i] > curr[i]) { console.log('Validation passed: $VERSION > $CURRENT_VERSION'); process.exit(0); }
              if (next[i] < curr[i]) {
                console.error('ERROR: $VERSION is not greater than current version $CURRENT_VERSION');
                process.exit(1);
              }
            }
            console.error('ERROR: $VERSION must be strictly greater than current version $CURRENT_VERSION (equal versions are not allowed)');
            process.exit(1);
          "

      - name: Configure Git
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"

      - name: Create release branch
        run: git checkout -b "chore/bump-version-${{ github.event.inputs.version }}"

      - name: Bump version in package.json
        run: npm version ${{ github.event.inputs.version }} --no-git-tag-version

      - name: Regenerate package-lock.json
        run: npm install --package-lock-only

      - name: Verify with npm ci
        run: npm ci

      - name: Commit changes
        run: |
          git add package.json package-lock.json
          git commit -m "chore: bump version to ${{ github.event.inputs.version }}"

      - name: Push branch
        run: git push origin "chore/bump-version-${{ github.event.inputs.version }}"

      - name: Create Pull Request
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          BODY=$(cat <<'PREOF'
          ## Version Bump

          Bumps the package version from `CURRENT_PLACEHOLDER` to `VERSION_PLACEHOLDER`.

          ### Changes
          - Updated `package.json` version to `VERSION_PLACEHOLDER`
          - Regenerated `package-lock.json`
          - Verified install with `npm ci`

          ---
          _Triggered via workflow dispatch by @ACTOR_PLACEHOLDER_
          PREOF
          )

          BODY="${BODY//CURRENT_PLACEHOLDER/${{ steps.validate.outputs.current }}}"
          BODY="${BODY//VERSION_PLACEHOLDER/${{ github.event.inputs.version }}}"
          BODY="${BODY//ACTOR_PLACEHOLDER/${{ github.actor }}}"

          gh pr create \
            --title "chore: bump version to ${{ github.event.inputs.version }}" \
            --body "$BODY" \
            --base ${{ github.event.inputs.target_branch }} \
            --head "chore/bump-version-${{ github.event.inputs.version }}"
Enter fullscreen mode Exit fullscreen mode

Aside: Alternatively, you can try setting default input target_branch to ${{ github.ref_name }} if you prefer to bump versions into feature branches rather than main and dynamically reference the current branch you are targeting.


How to Use It

From the GitHub UI:

  1. Go to your repo → Actions tab
  2. Select Bump Version from the left sidebar
  3. Click Run workflow
  4. Enter the new version number and target branch
  5. Hit Run workflow

Within seconds, a PR appears targeting your branch. Review the diff (should just be package.json and package-lock.json), then merge.

From the CLI with gh:

gh workflow run bump-version.yml \
  -f version=2.1.0 \
  -f target_branch=my-feature-branch
Enter fullscreen mode Exit fullscreen mode

Why This Pattern Works Well

It fits into your existing review process. The version bump goes through a PR like any other change. You get to review it, run your CI checks, and merge intentionally.

The bot commit is honest. The commit is attributed to github-actions[bot], so your git history clearly shows which bumps were automated versus manual. No noise in your contributor graph.

It handles the lockfile correctly. Running npm ci after the version bump ensures the lockfile is consistent. This catches any edge cases where a stale lockfile might cause issues downstream.

It targets feature branches, not just main. Because target_branch is an input, you can bump the version on a release branch or a feature branch mid-development — useful when you're preparing multiple releases in parallel.


Variations Worth Considering

Auto-detect the current version and increment it: Instead of specifying the full version, accept a bump type (patch, minor, major) and use npm version patch --no-git-tag-version to let npm calculate it.

- name: Bump version
  run: npm version ${{ github.event.inputs.bump_type }} --no-git-tag-version
Enter fullscreen mode Exit fullscreen mode

Auto-merge on approval: Add a pull_request_review trigger or use a merge queue to automatically merge version bump PRs after CI passes, removing the need to manually merge them.

Tag the release after merge: Chain a second workflow that listens for merged PRs with the chore/bump-version-* naming pattern and creates a git tag automatically.


Wrapping Up

This is a small workflow but it removes a surprisingly persistent source of friction. Once it's in place, you stop thinking about version bumps entirely — you just trigger the workflow, review the PR, and merge.

The full example is live in the line-commenter-tool repo if you want to see what the generated PR looks like in practice.


Have a variation of this pattern that works well for your project? Drop it in the comments.

Top comments (1)

Collapse
 
klement_gunndu profile image
klement Gunndu

Worth noting: the regex semver validation can miss pre-release tags like 1.2.3-rc.1 if the pattern is too strict. We added a dry-run step that compares against npm's own semver parser before committing — caught a few edge cases the regex missed.