[{"data":1,"prerenderedAt":19},["ShallowReactive",2],{"$fqaWuuFmJB-IH9s8vi9KSJ8YuD8X4mw8VskwfjYr6Kzs":3},{"slug":4,"title":5,"date":6,"description":7,"tags":8,"cover":13,"source":14,"canonical":15,"link":13,"series":13,"draft":16,"readingTime":17,"html":18},"automate-nodejs-version-bumps-with-github-actions-no-manual-prs-needed-ol8","Automate Node.js Version Bumps with GitHub Actions (No Manual PRs Needed)","2026-02-24T01:10:00.000Z","Manually bumping a package version is one of those low-value chores that's easy to forget, easy to...",[9,10,11,12],"node","github","automation","devops",null,"dev.to","https:\u002F\u002Fdev.to\u002Fhansel\u002Fautomate-nodejs-version-bumps-with-github-actions-no-manual-prs-needed-ol8",false,4,"\u003Cp>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 \u003Ccode>package.json\u003C\u002Fcode>, regenerate the lockfile, commit it, open a PR — every time, for every release.\u003C\u002Fp>\n\u003Cp>There's a better way: let a GitHub Actions workflow handle the entire thing on demand.\u003C\u002Fp>\n\u003Cp>Here's an example project with the workflow pattern I set up for one of my open source dev tool \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fdarkmastermindz\u002Fline-commenter-tool\" target=\"_blank\" rel=\"noopener\">line-commenter-tool\u003C\u002Fa>, which you can adapt for any Node.js project.\u003C\u002Fp>\n\u003Chr \u002F>\n\u003Ch2>What It Does\u003C\u002Fh2>\n\u003Cp>When you're ready to cut a new version, you trigger a workflow dispatch from the GitHub Actions UI (or via the API\u002FCLI). The workflow:\u003C\u002Fp>\n\u003Col>\n\u003Cli>Checks out your target branch\u003C\u002Fli>\n\u003Cli>Bumps the version in \u003Ccode>package.json\u003C\u002Fcode>\u003C\u002Fli>\n\u003Cli>Runs \u003Ccode>npm ci\u003C\u002Fcode> to regenerate \u003Ccode>package-lock.json\u003C\u002Fcode>\u003C\u002Fli>\n\u003Cli>Opens a pull request with a clean commit and auto-generated description\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>The resulting PR looks similar to this:\u003C\u002Fp>\n\u003Cblockquote>\n\u003Cp>\u003Cstrong>chore: bump version to 2.1.0\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cul>\n\u003Cli>Updated \u003Ccode>package.json\u003C\u002Fcode> version to \u003Ccode>2.1.0\u003C\u002Fcode>\u003C\u002Fli>\n\u003Cli>Regenerated \u003Ccode>package-lock.json\u003C\u002Fcode>\u003C\u002Fli>\n\u003Cli>Verified install with \u003Ccode>npm ci\u003C\u002Fcode>\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>\u003Cem>Triggered via workflow dispatch by @hansel\u003C\u002Fem>\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Cp>No manual work. No forgotten lockfile updates. Just review and merge.\u003C\u002Fp>\n\u003Chr \u002F>\n\u003Ch2>The Workflow\u003C\u002Fh2>\n\u003Cp>Create \u003Ccode>.github\u002Fworkflows\u002Fbump-version.yml\u003C\u002Fcode>:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-yaml\">on:\n  workflow_dispatch:\n    inputs:\n      version:\n        description: 'New semver version (e.g. 2.2.0)'\n        required: true\n        type: string\n      target_branch:\n        description: \"Branch to bump version on\"\n        required: true\n        default: \"main\"\n\njobs:\n  bump-version:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n      pull-requests: write\n\n    steps:\n      - uses: actions\u002Fcheckout@v6\n        with:\n          fetch-depth: 0\n\n      - uses: actions\u002Fsetup-node@v6\n        with:\n          node-version: 20\n\n      - name: Validate version input\n        id: validate\n        run: |\n          VERSION=\"${{ github.event.inputs.version }}\"\n\n          # Validate semver format (major.minor.patch with optional pre-release\u002Fbuild metadata)\n          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\n            echo \"ERROR: '$VERSION' is not a valid semver version.\"\n            exit 1\n          fi\n\n          CURRENT_VERSION=$(node -p \"require('.\u002Fpackage.json').version\")\n          echo \"current=$CURRENT_VERSION\" &gt;&gt; \"$GITHUB_OUTPUT\"\n          echo \"Current version: $CURRENT_VERSION\"\n          echo \"Requested version: $VERSION\"\n\n          # Validate new version is strictly greater than current using Node\n          node -e \"\n            const parseBase = (v) =&gt; v.replace(\u002F-.*\u002F, '').split('.').map(Number);\n            const curr = parseBase('$CURRENT_VERSION');\n            const next = parseBase('$VERSION');\n            for (let i = 0; i &lt; 3; i++) {\n              if (next[i] &gt; curr[i]) { console.log('Validation passed: $VERSION &gt; $CURRENT_VERSION'); process.exit(0); }\n              if (next[i] &lt; curr[i]) {\n                console.error('ERROR: $VERSION is not greater than current version $CURRENT_VERSION');\n                process.exit(1);\n              }\n            }\n            console.error('ERROR: $VERSION must be strictly greater than current version $CURRENT_VERSION (equal versions are not allowed)');\n            process.exit(1);\n          \"\n\n      - name: Configure Git\n        run: |\n          git config user.name \"github-actions[bot]\"\n          git config user.email \"github-actions[bot]@users.noreply.github.com\"\n\n      - name: Create release branch\n        run: git checkout -b \"chore\u002Fbump-version-${{ github.event.inputs.version }}\"\n\n      - name: Bump version in package.json\n        run: npm version ${{ github.event.inputs.version }} --no-git-tag-version\n\n      - name: Regenerate package-lock.json\n        run: npm install --package-lock-only\n\n      - name: Verify with npm ci\n        run: npm ci\n\n      - name: Commit changes\n        run: |\n          git add package.json package-lock.json\n          git commit -m \"chore: bump version to ${{ github.event.inputs.version }}\"\n\n      - name: Push branch\n        run: git push origin \"chore\u002Fbump-version-${{ github.event.inputs.version }}\"\n\n      - name: Create Pull Request\n        env:\n          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        run: |\n          BODY=$(cat &lt;&lt;'PREOF'\n          ## Version Bump\n\n          Bumps the package version from `CURRENT_PLACEHOLDER` to `VERSION_PLACEHOLDER`.\n\n          ### Changes\n          - Updated `package.json` version to `VERSION_PLACEHOLDER`\n          - Regenerated `package-lock.json`\n          - Verified install with `npm ci`\n\n          ---\n          _Triggered via workflow dispatch by @ACTOR_PLACEHOLDER_\n          PREOF\n          )\n\n          BODY=\"${BODY\u002F\u002FCURRENT_PLACEHOLDER\u002F${{ steps.validate.outputs.current }}}\"\n          BODY=\"${BODY\u002F\u002FVERSION_PLACEHOLDER\u002F${{ github.event.inputs.version }}}\"\n          BODY=\"${BODY\u002F\u002FACTOR_PLACEHOLDER\u002F${{ github.actor }}}\"\n\n          gh pr create \\\n            --title \"chore: bump version to ${{ github.event.inputs.version }}\" \\\n            --body \"$BODY\" \\\n            --base ${{ github.event.inputs.target_branch }} \\\n            --head \"chore\u002Fbump-version-${{ github.event.inputs.version }}\"\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Aside: Alternatively, you can try setting default input \u003Ccode>target_branch\u003C\u002Fcode> to \u003Ccode>${{ github.ref_name }}\u003C\u002Fcode> if you prefer to bump versions into feature branches rather than main and dynamically reference the current branch you are targeting.\u003C\u002Fp>\n\u003Chr \u002F>\n\u003Ch2>How to Use It\u003C\u002Fh2>\n\u003Cp>\u003Cstrong>From the GitHub UI:\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Col>\n\u003Cli>Go to your repo → \u003Cstrong>Actions\u003C\u002Fstrong> tab\u003C\u002Fli>\n\u003Cli>Select \u003Cstrong>Bump Version\u003C\u002Fstrong> from the left sidebar\u003C\u002Fli>\n\u003Cli>Click \u003Cstrong>Run workflow\u003C\u002Fstrong>\u003C\u002Fli>\n\u003Cli>Enter the new version number and target branch\u003C\u002Fli>\n\u003Cli>Hit \u003Cstrong>Run workflow\u003C\u002Fstrong>\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>Within seconds, a PR appears targeting your branch. Review the diff (should just be \u003Ccode>package.json\u003C\u002Fcode> and \u003Ccode>package-lock.json\u003C\u002Fcode>), then merge.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>From the CLI with \u003Ccode>gh\u003C\u002Fcode>:\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-bash\">gh workflow run bump-version.yml \\\n  -f version=2.1.0 \\\n  -f target_branch=my-feature-branch\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Chr \u002F>\n\u003Ch2>Why This Pattern Works Well\u003C\u002Fh2>\n\u003Cp>\u003Cstrong>It fits into your existing review process.\u003C\u002Fstrong> The version bump goes through a PR like any other change. You get to review it, run your CI checks, and merge intentionally.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>The bot commit is honest.\u003C\u002Fstrong> The commit is attributed to \u003Ccode>github-actions[bot]\u003C\u002Fcode>, so your git history clearly shows which bumps were automated versus manual. No noise in your contributor graph.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>It handles the lockfile correctly.\u003C\u002Fstrong> Running \u003Ccode>npm ci\u003C\u002Fcode> after the version bump ensures the lockfile is consistent. This catches any edge cases where a stale lockfile might cause issues downstream.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>It targets feature branches, not just main.\u003C\u002Fstrong> Because \u003Ccode>target_branch\u003C\u002Fcode> 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.\u003C\u002Fp>\n\u003Chr \u002F>\n\u003Ch2>Variations Worth Considering\u003C\u002Fh2>\n\u003Cp>\u003Cstrong>Auto-detect the current version and increment it:\u003C\u002Fstrong> Instead of specifying the full version, accept a bump type (\u003Ccode>patch\u003C\u002Fcode>, \u003Ccode>minor\u003C\u002Fcode>, \u003Ccode>major\u003C\u002Fcode>) and use \u003Ccode>npm version patch --no-git-tag-version\u003C\u002Fcode> to let npm calculate it.\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-yaml\">- name: Bump version\n  run: npm version ${{ github.event.inputs.bump_type }} --no-git-tag-version\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Cstrong>Auto-merge on approval:\u003C\u002Fstrong> Add a \u003Ccode>pull_request_review\u003C\u002Fcode> trigger or use a merge queue to automatically merge version bump PRs after CI passes, removing the need to manually merge them.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>Tag the release after merge:\u003C\u002Fstrong> Chain a second workflow that listens for merged PRs with the \u003Ccode>chore\u002Fbump-version-*\u003C\u002Fcode> naming pattern and creates a git tag automatically.\u003C\u002Fp>\n\u003Chr \u002F>\n\u003Ch2>Wrapping Up\u003C\u002Fh2>\n\u003Cp>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. \u003C\u002Fp>\n\u003Cp>The full example is live in the \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fdarkmastermindz\u002Fline-commenter-tool\u002Fpull\u002F93\" target=\"_blank\" rel=\"noopener\">line-commenter-tool repo\u003C\u002Fa> if you want to see what the generated PR looks like in practice.\u003C\u002Fp>\n\u003Chr \u002F>\n\u003Cp>\u003Cem>Have a variation of this pattern that works well for your project? Drop it in the comments.\u003C\u002Fem>\u003C\u002Fp>\n",1792375889163]