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:
- Checks out your target branch
- Bumps the version in
package.json - Runs
npm cito regeneratepackage-lock.json - 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.jsonversion to2.1.0- Regenerated
package-lock.json- Verified install with
npm ciTriggered via workflow dispatch by @hansel
No manual work. No forgotten lockfile updates. Just review and merge.
The Workflow
Create .github/workflows/bump-version.yml:
name: Bump Version
on:
workflow_dispatch:
inputs:
version:
description: "New version (e.g. 2.1.0)"
required: true
target_branch:
description: "Branch to bump version on"
required: true
default: "main"
jobs:
bump:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.target_branch }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "lts/*"
- name: Bump version in package.json
run: |
npm version ${{ github.event.inputs.version }} --no-git-tag-version
- name: Verify install
run: npm ci
- name: Create PR branch and push
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git checkout -b chore/bump-version-${{ github.event.inputs.version }}
git add package.json package-lock.json
git commit -m "chore: bump version to ${{ github.event.inputs.version }}"
git push origin chore/bump-version-${{ github.event.inputs.version }}
- name: Open Pull Request
uses: actions/github-script@v7
with:
script: |
const { data: pr } = await github.rest.pulls.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: "chore: bump version to ${{ github.event.inputs.version }}",
head: "chore/bump-version-${{ github.event.inputs.version }}",
base: "${{ github.event.inputs.target_branch }}",
body: `## Version Bump\n\nBumps the package version from the current version to \`${{ github.event.inputs.version }}\`.\n\n### Changes\n\n- Updated \`package.json\` version to \`${{ github.event.inputs.version }}\`\n- Regenerated \`package-lock.json\`\n- Verified install with \`npm ci\`\n\n---\n\n*Triggered via workflow dispatch by @${{ github.actor }}*`
});
console.log(`PR created: ${pr.html_url}`);
How to Use It
From the GitHub UI:
- Go to your repo → Actions tab
- Select Bump Version from the left sidebar
- Click Run workflow
- Enter the new version number and target branch
- 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
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
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 (0)