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
111 changes: 50 additions & 61 deletions .github/actions/version-bump/README.md
Original file line number Diff line number Diff line change
@@ -1,104 +1,93 @@
# version-bump

Composite GitHub Action that bumps `package.json#version` on the PR branch to `main + 1`.
Composite GitHub Action that bumps `package.json#version` to a caller-supplied target version, commits the change, and pushes it. Optionally tags the bump commit.

## Why
## Why this is publish-only

The version in `package.json` is bumped on every PR via a bot commit on the PR branch. When the PR merges to `main`, `main` already has the new version. Other open PRs rebase onto `main` to pick up the new version (or get auto-rebased by Dependabot).
This action used to support two modes:

- **pr-bump** (deprecated, removed): auto-increment `package.json#version` from `main + 1` on the PR branch. This is now handled by [`n3ary/release-bot`](https://github.com/n3ary/release-bot), which runs org-level on PR merge to main and opens a `release/calver-*` PR.
- **version-input** (this action): bump to an explicit target version. Used by the `package/publish` and `adapters/publish` composite actions when the publish workflow needs to force a specific version (e.g. a manual `workflow_dispatch` with `version-override`).

Calling this action with empty `version-input` now fails fast. There is no fallback to auto-increment; if you need auto-bumps on PR merge, use `n3ary/release-bot`.

## Features

- **Pre-release safe**: handles `0.2.0-m1` → `0.2.1-m1`, `1.0.0-alpha.1` → `1.0.1-alpha.1`, `0.2.0-rc.1+build.5` → `0.2.1-rc.1+build.5`.
- **Metadata-aware**: skips the bump when the PR touches only metadata (paths configurable).
- **Idempotent**: no-op if the PR branch's version already matches `main + 1`.
- **Self-contained**: pushes the bot commit directly to the PR branch.
- **Semver validation**: rejects malformed `version-input` (`1.0.0`, `0.2.0-rc.1`, `1.0.0-alpha.1`, etc.).
- **Pre-release safe**: any pre-release tag (`-rc.1`, `-m1`, `-alpha.1`) is preserved; only `major.minor.patch` is written.
- **Idempotent**: if `package.json#version` is already at the target, the action no-ops.
- **Tag-on-bump**: optional `tag-name` (with `{version}` placeholder) creates + pushes a git tag for downstream consumers.
- **Race-safe push**: re-fetches and rebases on the remote before pushing, so concurrent admin merges don't leave the bot commit behind.

## Inputs

| Input | Description | Default | Required |
|---|---|---|---|
| `bump-skip-paths` | Comma-separated paths that, when changed alone, skip the bump. | `.github/,docs/,.gitignore,LICENSE` | no |
| `commit-message` | Commit message. `{version}` is replaced with the new version. | `chore(release): auto-bump to v{version}` | no |
| `base-ref` | Base ref to compare against. Defaults to the workflow's `GITHUB_BASE_REF`. | (empty) | no |
| `version-input` | Target version. **Required.** Action fails if empty. | (empty) | **yes** |
| `commit-message` | Commit message. `{version}` is replaced with the new version. | `chore(release): v{version}` | no |
| `commit-message-override` | Full commit message override. Useful for publish flows that want a custom message. | (empty) | no |
| `package-dir` | Directory containing the `package.json` to bump. For monorepo sub-packages (e.g. `packages/spec`). | `.` (repo root) | no |
| `tag-name` | If set, creates a git tag with this name on the bump commit and pushes it. Supports `{version}` placeholder (e.g. `packages/spec/v{version}`). | (empty, no tag) | no |
| `skip-commit` | Skip the commit + push (only compute + write to `package.json`). Callers that want to compose the commit themselves set this. | `false` | no |

## Outputs

| Output | Description |
|---|---|
| `previous-version` | The version on `origin/<base-ref>` before the bump. |
| `previous-version` | The version in `package.json` before the bump (or the current version when no bump was needed). |
| `next-version` | The version after the bump (matches the new `package.json#version`). |
| `bumped` | `"true"` if the version was bumped; `"false"` if skipped (metadata-only diff or already correct). |
| `bumped` | `"true"` if the version was bumped; `"false"` if skipped (already at target). |
| `tag` | The tag name pushed (empty if `tag-name` input was unset or skipped). |

## Usage

This action is intended to be called from the `package/publish` or `adapters/publish` composite actions. Direct usage is rare; if you do call it directly:

```yaml
name: PR Validation
on:
pull_request:
branches: [main]

permissions:
contents: write # required for the bot commit + push

jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
ref: ${{ github.event.pull_request.head.ref }}
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}

- uses: actions/setup-node@v6
with:
node-version: 24
cache: npm

- run: npm ci

- name: Auto-bump version
uses: n3ary/actions/.github/actions/version-bump@v1
with:
bump-skip-paths: '.github/,docs/,.gitignore,LICENSE'

# Your validation steps here — `npm test`, `npm run lint`,
# `npm run check`, etc. Whatever the repo needs.

- run: npm test
- name: Bump to a specific version
uses: n3ary/actions/.github/actions/version-bump@v23
with:
version-input: 0.3.7
package-dir: adapters/cluj-napoca
tag-name: 'adapters/cluj-napoca/v{version}'
commit-message: 'chore(release): cluj-napoca v{version}'
github-token: ${{ secrets.GITHUB_TOKEN }}
```

### Required repo settings

For the version-sequencing story to work end-to-end:

1. **Branch protection on `main`**: "Require status checks to pass before merging" must include the workflow that runs this action (e.g. the `validate` job name).
2. **"Require branches to be up to date"** must be enabled (the `strict: true` setting on `required_status_checks`). Without it, a stale PR with the old `main` version can merge without the bump.
3. **PR required** (no direct pushes to `main`).
The publish flow (parent workflow) needs:

### Why these settings?

- Without status checks: a PR with a broken `npm test` could merge.
- Without "branches up to date": the version bump assumes `main` is the latest. A stale PR with the old version would bump to an unexpected number.
- `permissions: contents: write` for the commit + push.
- `permissions: packages: write` for npm publish (handled by the parent action).
- A clean main branch (or auto-rebase enabled) so the `force-with-lease` push succeeds.

## Pinning

Reference the action by tag (`@v1`) or by SHA (`@<full-sha>`). Tag pinning is the standard for active maintenance; SHA pinning is for security-critical cases.
Reference the action by tag (`@v23`) or by SHA (`@<full-sha>`). Tag pinning is the standard for active maintenance; SHA pinning is for security-critical cases.

```yaml
# Tag pinning (recommended for active use)
uses: n3ary/actions/.github/actions/version-bump@v1
uses: n3ary/actions/.github/actions/version-bump@v23

# SHA pinning (security-critical; updates require manual edit)
uses: n3ary/actions/.github/actions/version-bump@<full-sha>
```

## Migration from pr-bump mode

If your workflow previously called this action with no `version-input` (relying on auto-increment), migrate to `n3ary/release-bot`:

- Remove the call to `version-bump` from your `pr-validation.yml` / `pr-check.yml`.
- Remove the `auto-bump` job.
- The release-bot will pick up PR merges automatically (assuming the bot App is installed on your org/repo).
- For manual bumps, use `workflow_dispatch` with `version-override` on the publish workflow, which forwards to `version-input` here.

## Consumers

- [`n3ary/app`](https://github.com/n3ary/app) — the consumer PWA
- [`n3ary/gtfs`](https://github.com/n3ary/gtfs) — the producer pipeline
- `n3ary/gtfs-adapters/tree/main/adapters/cluj-napoca` -- the Cluj adapter (inside the `gtfs-adapters` monorepo; the legacy standalone repo is archived as `n3ary/archived-adapter`)
- [`n3ary/gtfs-publisher`](https://github.com/n3ary/gtfs-publisher) - `release-gtfs-spec.yml` (publishes `libs/spec` with explicit version)
- [`n3ary/gtfs-adapters`](https://github.com/n3ary/gtfs-adapters) - `release-gtfs-adapter.yml` (publishes per-adapter with explicit version)

## License

MIT.
MIT.
113 changes: 31 additions & 82 deletions .github/actions/version-bump/action.yml
Original file line number Diff line number Diff line change
@@ -1,28 +1,20 @@
name: 'Auto-bump version on PR'
description: 'Bumps package.json#version on the PR branch, either auto-incrementing from main+1 (default) or to an explicit override (version-input). Preserves pre-release tags and commits pnpm-lock.yaml if dirty. Returns the bumped version + whether anything actually changed.'
name: 'Bump version to a specific target (publish flow)'
description: 'Bumps package.json#version to a caller-supplied target (version-input), commits, pushes, and optionally tags. Used by the package/publish + adapters/publish composite actions. The pr-bump auto-increment mode is REMOVED - use n3ary/release-bot for auto-bumping on PR merge.'
author: 'n3ary'

inputs:
bump-skip-paths:
description: 'Comma-separated paths that, when changed alone, should skip the bump. Ignored when version-input is set.'
required: false
default: '.github/,docs/,.gitignore,LICENSE'
version-input:
description: 'REQUIRED. Explicit target version (e.g. "0.3.4"). Bumps to this value. The action fails if this is empty - auto-bump on PR is handled by n3ary/release-bot, not this action.'
required: true
default: ''
commit-message:
description: 'Commit message. {version} is replaced with the new version. Ignored when commit-message-override is set.'
required: false
default: 'chore(release): auto-bump to v{version}'
default: 'chore(release): v{version}'
commit-message-override:
description: 'Full commit message override. Ignored when unset. Useful for publish flows that want a custom message.'
required: false
default: ''
base-ref:
description: 'Base ref to compare against. Defaults to the workflow base ref.'
required: false
default: ''
version-input:
description: 'Explicit target version (e.g. "0.3.4"). When set, skips auto-increment and uses this value. Used by publish flows (publish-spec, adapters/publish) that need a precise target rather than main+1. Ignores bump-skip-paths.'
required: false
default: ''
package-dir:
description: 'Directory containing the package.json to bump. Defaults to repo root. Used by publish flows for monorepo sub-packages (e.g. "packages/spec").'
required: false
Expand All @@ -31,9 +23,9 @@ inputs:
description: >-
If set, creates a git tag with this name on the bump commit and
pushes it to origin. Supports the {version} placeholder (e.g.
'packages/spec/v{version}'). When unset, no tag is created -
this is the auto-bump-on-PR default. Used by publish flows that
need a tag to trigger a downstream publish job.
'packages/spec/v{version}'). When unset, no tag is created.
Used by publish flows that need a tag to trigger a downstream
publish job.
required: false
default: ''
skip-commit:
Expand All @@ -43,83 +35,54 @@ inputs:

outputs:
previous-version:
description: 'The version on origin/<base> before the bump (or current version when version-input is set and matches).'
description: 'The version in package.json before the bump (or current version when version-input matches current).'
next-version:
description: 'The version after the bump (matches the new package.json#version).'
bumped:
description: '"true" if the version was bumped; "false" if skipped (metadata-only diff, already correct, or version-input matched current).'
skipped:
description: '"true" if the action skipped (metadata-only diff OR version-input matched current).'
description: '"true" if the version was bumped; "false" if skipped (already at target).'
tag:
description: 'The tag name pushed (empty if tag-name input was unset).'

runs:
using: composite
steps:
- name: Skip if metadata-only diff
id: skip-check
if: inputs.version-input == ''
- name: Validate inputs
id: validate
shell: bash
env:
BUMP_SKIP_PATHS: ${{ inputs.bump-skip-paths }}
VERSION_INPUT: ${{ inputs.version-input }}
run: |
BASE="${BASE_REF:-${GITHUB_BASE_REF:-main}}"
CHANGED=$(git diff --name-only "origin/${BASE}"...HEAD)
SKIP_RE=$(printf '%s' "$BUMP_SKIP_PATHS" | tr ',' '\n' | sed 's:.*:^&:; s:/$::' | paste -sd'|' -)
NON_META=$(echo "$CHANGED" | grep -vE "$SKIP_RE" || true)
if [ -z "$NON_META" ]; then
echo "Only metadata changes — skipping version bump."
echo "skip=true" >> "$GITHUB_OUTPUT"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
if [ -z "$VERSION_INPUT" ]; then
cat <<'EOF' >&2
::error::version-input is required. The pr-bump auto-increment mode has been removed from this action - n3ary/release-bot handles auto-bumps on PR merge. This action is now publish-only: callers must pass an explicit target version. If you are wiring this into a workflow, pass `version-input: <semver-string>`.
EOF
exit 1
fi

- name: Compute target version
id: compute
if: steps.skip-check.outputs.skip != 'true'
shell: bash
env:
VERSION_INPUT: ${{ inputs.version-input }}
PACKAGE_DIR: ${{ inputs.package-dir }}
run: |
PKG_JSON="${PACKAGE_DIR:-.}/package.json"
if [ -n "$VERSION_INPUT" ]; then
# Validate semver-ish format. Matches the check in publish-spec
# + adapters/publish so the contract is consistent.
if ! echo "$VERSION_INPUT" | grep -qE \
'^[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.-]+)?$'; then
echo "::error::Invalid version-input format: $VERSION_INPUT"
echo "Expected: e.g. 0.1.0, 0.2.0-rc.1, 1.0.0-alpha.1"
exit 1
fi
TARGET="$VERSION_INPUT"
CURRENT=$(node -p "require('./${PKG_JSON}').version")
echo "version-input: $TARGET (current: $CURRENT, pkg: $PKG_JSON)"
else
BASE="${BASE_REF:-${GITHUB_BASE_REF:-main}}"
MAIN_VERSION=$(git show "origin/${BASE}:${PKG_JSON}" | \
node -p "JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')).version")
# Increment patch; preserve pre-release suffix.
# Handles: 1.5.68 -> 1.5.69, 0.2.0-m1 -> 0.2.1-m1,
# 1.0.0-alpha.1 -> 1.0.1-alpha.1,
# 0.2.0-rc.1+build.5 -> 0.2.1-rc.1+build.5.
TARGET=$(node -e '
const v = process.argv[1].match(/^(\d+)\.(\d+)\.(\d+)(-.+)?$/);
if (!v) {
console.error("Unparseable version:", process.argv[1]);
process.exit(1);
}
const [, major, minor, patchStr, prerelease = ""] = v;
console.log(`${major}.${minor}.${Number(patchStr) + 1}${prerelease}`);
' "$MAIN_VERSION")
CURRENT=$(node -p "require('./${PKG_JSON}').version")
echo "Auto-bump: $MAIN_VERSION -> $TARGET (PR branch: $CURRENT, pkg: $PKG_JSON)"
if ! echo "$VERSION_INPUT" | grep -qE \
'^[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.-]+)?$'; then
echo "::error::Invalid version-input format: $VERSION_INPUT"
echo "Expected: e.g. 0.1.0, 0.2.0-rc.1, 1.0.0-alpha.1"
exit 1
fi
TARGET="$VERSION_INPUT"
CURRENT=$(node -p "require('./${PKG_JSON}').version")
echo "version-input: $TARGET (current: $CURRENT, pkg: $PKG_JSON)"
echo "previous=$CURRENT" >> "$GITHUB_OUTPUT"
echo "next=$TARGET" >> "$GITHUB_OUTPUT"
echo "package_dir=$PKG_JSON" >> "$GITHUB_OUTPUT"

- name: Apply bump (commit + push)
id: apply
if: steps.skip-check.outputs.skip != 'true' && inputs.skip-commit != 'true'
if: inputs.skip-commit != 'true'
shell: bash
env:
COMMIT_MSG_OVERRIDE: ${{ inputs.commit-message-override }}
Expand Down Expand Up @@ -200,17 +163,3 @@ runs:
fi

echo "bumped=true" >> "$GITHUB_OUTPUT"
echo "skipped=false" >> "$GITHUB_OUTPUT"

- name: Mark as skipped
if: steps.skip-check.outputs.skip == 'true'
shell: bash
env:
PACKAGE_DIR: ${{ inputs.package-dir }}
run: |
PKG_JSON="${PACKAGE_DIR:-.}/package.json"
CURRENT=$(node -p "require('./${PKG_JSON}').version" 2>/dev/null || echo "")
echo "previous=${CURRENT}" >> "$GITHUB_OUTPUT"
echo "next=${CURRENT}" >> "$GITHUB_OUTPUT"
echo "bumped=false" >> "$GITHUB_OUTPUT"
echo "skipped=true" >> "$GITHUB_OUTPUT"
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Composite GitHub Actions and reusable workflows for the [n3ary org](https://gith

| Action | Description |
|---|---|
| [version-bump](.github/actions/version-bump) | Bumps `package.json#version` on the PR branch to `main + 1`. Handles pre-release tags. |
| [version-bump](.github/actions/version-bump) | Publish-only. Bumps `package.json#version` to a caller-supplied target, commits, pushes, optionally tags. **Not** for auto-bumping on PR — use [n3ary/release-bot](https://github.com/n3ary/release-bot) for that. |
| [ascii-commits](.github/actions/ascii-commits) | Fails the build if any commit message (subject + body) on the PR branch, or the PR title, contains non-ASCII characters. |

## Reusable workflows
Expand Down