Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## [Unreleased]

### Added

- The `bump-package-json` option (default: `false`) sets the `version` field of the root `package.json` to the release version.

## [1.1.0] - 2026-08-09

### Added
Expand Down
7 changes: 4 additions & 3 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,9 @@ jobs:
|===
| Input | Default | Description

| `version-file` | `''` | Path to a file to overwrite with the version. Leave empty to skip.
| `keep-v-prefix` | `false` | When `true`, writes the tag verbatim. Otherwise strips the leading `v`.
| `version-file` | `''` | Path to a file to overwrite with the version. Leave empty to skip. Not for JSON manifests: the file is replaced wholesale, so use `bump-package-json` for those.
| `keep-v-prefix` | `false` | When `true`, writes the tag verbatim. Otherwise strips the leading `v`. Does not apply to `bump-package-json`.
| `bump-package-json` | `false` | When `true`, sets the `version` field of the root `package.json`, leaving the rest of the manifest byte-for-byte intact. The leading `v` is always stripped, as npm requires plain semver.
|===

=== Changelog
Expand Down Expand Up @@ -156,7 +157,7 @@ jobs:
|===
| Input | Default | Description

| `commit` | `false` | Commit the modified `version-file` and `changelog-file` back to a branch.
| `commit` | `false` | Commit the modified `version-file`, `changelog-file` and `package.json` back to a branch.
| `commit-message` | `+chore: release {tag}+` | `+{tag}+` is substituted with the tag.
| `commit-user-name` | `github-actions` | Name used for the auto-commit author.
| `commit-user-email` | `+github-actions[bot]@users.noreply.github.com+` | Email used for the auto-commit author.
Expand Down
32 changes: 28 additions & 4 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,20 @@ inputs:
keep-v-prefix:
description: >
When `true`, the tag is written to `version-file` verbatim. When `false`
(default), the leading `v` is stripped.
(default), the leading `v` is stripped. Does not affect
`bump-package-json`, whose version must remain plain semver.
required: false
default: 'false'

bump-package-json:
description: >
When `true`, sets the `version` field of `package.json` in the repository
root to the release version, leaving the rest of the manifest untouched.
Use this instead of `version-file` for npm-shaped projects: `version-file`
overwrites its target with the bare version string, which would destroy a
JSON manifest. The leading `v` is always stripped, since npm requires
plain semver. The manifest is included in the commit when `commit` is
`true`.
required: false
default: 'false'

Expand Down Expand Up @@ -101,8 +114,8 @@ inputs:
# Commit updates back
commit:
description: >
When `true`, commits changes to `version-file` and `changelog-file` back
to the repository.
When `true`, commits changes to `version-file`, `changelog-file` and, when
`bump-package-json` is enabled, `package.json` back to the repository.
required: false
default: 'false'

Expand Down Expand Up @@ -314,6 +327,15 @@ runs:
args+=("$VERSION_FILE" "$TAG")
"${GITHUB_ACTION_PATH}/scripts/bump-version.sh" "${args[@]}"

- name: Bump package.json
if: inputs.bump-package-json == 'true'
shell: bash
env:
TAG: ${{ steps.compute.outputs.tag }}
run: |
set -euo pipefail
"${GITHUB_ACTION_PATH}/scripts/bump-package-json.sh" package.json "$TAG"

- name: Update changelog
if: inputs.changelog-file != '' && inputs.update-changelog != 'false'
uses: thomaseizinger/keep-a-changelog-new-release@v2
Expand All @@ -336,7 +358,9 @@ runs:
commit_user_name: ${{ inputs.commit-user-name }}
commit_user_email: ${{ inputs.commit-user-email }}
file_pattern: >-
${{ inputs.version-file }} ${{ inputs.changelog-file }}
${{ inputs.version-file }}
${{ inputs.bump-package-json == 'true' && 'package.json' || '' }}
${{ inputs.changelog-file }}
branch: ${{ inputs.commit-branch }}

- name: Move devbuild tag
Expand Down
70 changes: 70 additions & 0 deletions scripts/bump-package-json.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#!/usr/bin/env bash
# Sets the `version` field of a package.json-style JSON manifest.
#
# Usage: bump-package-json.sh <manifest-file> <tag>
#
# The leading `v` is always stripped, unlike `bump-version.sh`: npm requires the
# version field to be plain semver, so a `v`-prefixed value would be invalid.
#
# Only the version is rewritten. A manifest formatted the usual way keeps every
# other byte, so the resulting commit touches a single line. A manifest whose
# version cannot be located that way (unusual formatting, or no version field at
# all) is re-serialized instead, which may reformat it.

set -euo pipefail

file="${1:-}"
tag="${2:-}"

if [ -z "$file" ]; then
echo "bump-package-json.sh: missing manifest path" >&2
exit 2
fi

if [ -z "$tag" ]; then
echo "bump-package-json.sh: missing tag" >&2
exit 2
fi

if [ ! -f "$file" ]; then
echo "bump-package-json.sh: manifest '$file' does not exist" >&2
exit 2
fi

if ! command -v node >/dev/null 2>&1; then
echo "bump-package-json.sh: node is required to edit '$file'" >&2
exit 2
fi

node -e '
const fs = require("fs");
const [file, tag] = process.argv.slice(1);
const version = tag.replace(/^v/, "");
const source = fs.readFileSync(file, "utf8");

try {
JSON.parse(source);
} catch (error) {
console.error(`bump-package-json.sh: ${file} is not valid JSON: ${error.message}`);
process.exit(2);
}

// Indentation of the first nested key, reused so the manifest keeps its shape.
const indent = (source.match(/\n([ \t]+)"/) || [])[1] || " ";

// Anchoring to that indentation keeps the match at the top level, so a nested
// "version" (in an engines block, say) is never mistaken for the manifest own.
const topLevelVersion = new RegExp(`^${indent}"version"(\\s*:\\s*)"[^"]*"`, "m");

let updated;
if (topLevelVersion.test(source)) {
updated = source.replace(topLevelVersion, `${indent}"version"$1${JSON.stringify(version)}`);
} else {
const manifest = JSON.parse(source);
manifest.version = version;
const trailingNewline = source.endsWith("\n") ? "\n" : "";
updated = JSON.stringify(manifest, null, indent) + trailingNewline;
}

fs.writeFileSync(file, updated);
' "$file" "$tag"
121 changes: 121 additions & 0 deletions tests/bump-package-json.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
#!/usr/bin/env bats

load 'test_helper'

setup() {
setup_scripts_dir
setup_tmp_workspace
MANIFEST="$TMP_WORKSPACE/package.json"
}

teardown() {
teardown_tmp_workspace
}

# Writes a conventional two-space manifest to $MANIFEST.
write_manifest() {
cat > "$MANIFEST" << 'JSON'
{
"name": "example",
"version": "0.1.0",
"scripts": {
"build": "tsc"
}
}
JSON
}

@test "strips leading v and sets the version" {
write_manifest
run "$SCRIPTS_DIR/bump-package-json.sh" "$MANIFEST" "v1.2.3"
[ "$status" -eq 0 ]
[ "$(node -p "require('$MANIFEST').version")" = "1.2.3" ]
}

@test "leaves a tag without v prefix untouched" {
write_manifest
run "$SCRIPTS_DIR/bump-package-json.sh" "$MANIFEST" "2.0.0"
[ "$status" -eq 0 ]
[ "$(node -p "require('$MANIFEST').version")" = "2.0.0" ]
}

@test "handles semver with pre-release suffix" {
write_manifest
run "$SCRIPTS_DIR/bump-package-json.sh" "$MANIFEST" "v1.0.0-rc.1"
[ "$status" -eq 0 ]
[ "$(node -p "require('$MANIFEST').version")" = "1.0.0-rc.1" ]
}

@test "changes only the version line" {
write_manifest
cp "$MANIFEST" "$TMP_WORKSPACE/before.json"

run "$SCRIPTS_DIR/bump-package-json.sh" "$MANIFEST" "v1.2.3"
[ "$status" -eq 0 ]

run diff "$TMP_WORKSPACE/before.json" "$MANIFEST"
# One changed line: a single `<` and a single `>` in the diff output.
[ "$(printf '%s\n' "$output" | grep -c '^<')" -eq 1 ]
[ "$(printf '%s\n' "$output" | grep -c '^>')" -eq 1 ]
printf '%s\n' "$output" | grep -q '"version": "1.2.3"'
}

@test "preserves other fields and key order" {
write_manifest
run "$SCRIPTS_DIR/bump-package-json.sh" "$MANIFEST" "v1.2.3"
[ "$status" -eq 0 ]
[ "$(node -p "Object.keys(require('$MANIFEST')).join(',')")" = "name,version,scripts" ]
[ "$(node -p "require('$MANIFEST').scripts.build")" = "tsc" ]
}

@test "ignores a nested version field" {
cat > "$MANIFEST" << 'JSON'
{
"engines": {
"version": "18"
},
"version": "0.1.0"
}
JSON
run "$SCRIPTS_DIR/bump-package-json.sh" "$MANIFEST" "v1.2.3"
[ "$status" -eq 0 ]
[ "$(node -p "require('$MANIFEST').engines.version")" = "18" ]
[ "$(node -p "require('$MANIFEST').version")" = "1.2.3" ]
}

@test "adds a version field when the manifest has none" {
printf '{\n "name": "example"\n}\n' > "$MANIFEST"
run "$SCRIPTS_DIR/bump-package-json.sh" "$MANIFEST" "v1.2.3"
[ "$status" -eq 0 ]
[ "$(node -p "require('$MANIFEST').version")" = "1.2.3" ]
[ "$(node -p "require('$MANIFEST').name")" = "example" ]
}

@test "preserves tab indentation" {
printf '{\n\t"name": "example",\n\t"version": "0.1.0"\n}\n' > "$MANIFEST"
run "$SCRIPTS_DIR/bump-package-json.sh" "$MANIFEST" "v1.2.3"
[ "$status" -eq 0 ]
grep -q "$(printf '\t"version": "1.2.3"')" "$MANIFEST"
}

@test "fails when manifest path is empty" {
run "$SCRIPTS_DIR/bump-package-json.sh" "" "v1.0.0"
[ "$status" -ne 0 ]
}

@test "fails when tag is empty" {
write_manifest
run "$SCRIPTS_DIR/bump-package-json.sh" "$MANIFEST" ""
[ "$status" -ne 0 ]
}

@test "fails when the manifest does not exist" {
run "$SCRIPTS_DIR/bump-package-json.sh" "$TMP_WORKSPACE/missing.json" "v1.0.0"
[ "$status" -ne 0 ]
}

@test "fails when the manifest is not valid JSON" {
printf 'not json' > "$MANIFEST"
run "$SCRIPTS_DIR/bump-package-json.sh" "$MANIFEST" "v1.0.0"
[ "$status" -ne 0 ]
}
Loading