Skip to content

Commit 60d920f

Browse files
Add version-bump reusable workflow
Automates version bump pull requests by detecting unreleased changes in CHANGELOG.md, determining the next semantic version, updating the changelog with the release date, and creating a PR from a fork to the upstream repository. Supports configurable release dates, custom git identity, and optional update scripts for projects that maintain version numbers in multiple files. Updates release-version-extract action to expose unreleased_changes output, making changelog entries available for use in commit messages and PR bodies. Co-Authored-By: roachdev-claude <roachdev-claude-bot@cockroachlabs.com>
1 parent f4da5a0 commit 60d920f

4 files changed

Lines changed: 343 additions & 6 deletions

File tree

.github/workflows/version-bump.yml

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
name: Version Bump
2+
3+
on:
4+
workflow_call:
5+
inputs:
6+
fork_owner:
7+
description: GitHub username or org that owns the fork
8+
required: true
9+
type: string
10+
fork_repo:
11+
description: Repository name of the fork
12+
required: true
13+
type: string
14+
pr_base_branch:
15+
description: Base branch for the PR. Defaults to repository default branch
16+
required: false
17+
type: string
18+
default: ""
19+
additional_update_script:
20+
description: Optional path to a bash script file to execute during version bump. The VERSION environment variable will be available.
21+
required: false
22+
type: string
23+
default: ""
24+
release_date:
25+
description: Release date in YYYY-MM-DD format. Defaults to current date
26+
required: false
27+
type: string
28+
default: ""
29+
git_user_name:
30+
description: Git user name for commits. Defaults to github-actions[bot]
31+
required: false
32+
type: string
33+
default: "github-actions[bot]"
34+
git_user_email:
35+
description: Git user email for commits. Defaults to github-actions[bot]@users.noreply.github.com
36+
required: false
37+
type: string
38+
default: "github-actions[bot]@users.noreply.github.com"
39+
secrets:
40+
fork_push_token:
41+
description: PAT with push access to the fork
42+
required: true
43+
pr_create_token:
44+
description: PAT with permission to create PRs on the upstream repo
45+
required: true
46+
47+
jobs:
48+
version-bump:
49+
runs-on: ubuntu-latest
50+
steps:
51+
- name: Checkout repository
52+
uses: actions/checkout@v6
53+
with:
54+
fetch-depth: 0
55+
56+
- name: Check for CHANGELOG file
57+
id: check-changelog
58+
run: |
59+
source actions_helpers.sh
60+
61+
if [ ! -f CHANGELOG.md ]; then
62+
log_error "CHANGELOG.md not found in repository root"
63+
exit 1
64+
fi
65+
log_notice "CHANGELOG.md found"
66+
67+
- name: Extract release version
68+
id: extract-version
69+
uses: cockroachdb/actions/release-version-extract@v0
70+
71+
- name: Set up git and auto-fork remote
72+
if: steps.extract-version.outputs.has_unreleased == 'true'
73+
env:
74+
FORK_PUSH_TOKEN: ${{ secrets.fork_push_token }}
75+
run: |
76+
# Configure git
77+
git config user.name "${{ inputs.git_user_name }}"
78+
git config user.email "${{ inputs.git_user_email }}"
79+
git config --local credential.helper "!f() { echo \"username=${{ inputs.fork_owner }}\"; echo \"password=\${FORK_PUSH_TOKEN}\"; }; f"
80+
81+
# Set up auto-fork remote
82+
fork_url="https://github.com/${{ inputs.fork_owner }}/${{ inputs.fork_repo }}.git"
83+
84+
if git remote | grep --quiet --fixed-strings --line-regexp "auto-fork"; then
85+
git remote set-url auto-fork "$fork_url"
86+
else
87+
git remote add auto-fork "$fork_url"
88+
fi
89+
90+
git remote -v
91+
92+
- name: Create and checkout version bump branch
93+
if: steps.extract-version.outputs.has_unreleased == 'true'
94+
id: create-branch
95+
run: |
96+
source actions_helpers.sh
97+
98+
branch_name="update-version-${{ steps.extract-version.outputs.next_version }}"
99+
git checkout -b "$branch_name"
100+
set_output "branch_name" "$branch_name"
101+
102+
- name: Update CHANGELOG with new version
103+
if: steps.extract-version.outputs.has_unreleased == 'true'
104+
run: |
105+
source actions_helpers.sh
106+
107+
version="${{ steps.extract-version.outputs.next_version }}"
108+
109+
if [ -n "${{ inputs.release_date }}" ]; then
110+
release_date="${{ inputs.release_date }}"
111+
else
112+
release_date=$(date +%Y-%m-%d)
113+
fi
114+
115+
# Insert new version header under [Unreleased] header
116+
awk -v version="$version" -v date="$release_date" '
117+
/^## \[Unreleased\]/ {
118+
print
119+
print ""
120+
print "## [" version "] - " date
121+
next
122+
}
123+
{ print }
124+
' CHANGELOG.md > CHANGELOG.md.tmp && mv CHANGELOG.md.tmp CHANGELOG.md
125+
126+
log_notice "Updated CHANGELOG.md with version $version dated $release_date"
127+
128+
- name: Run additional update script
129+
if: steps.extract-version.outputs.has_unreleased == 'true' && inputs.additional_update_script != ''
130+
env:
131+
VERSION: ${{ steps.extract-version.outputs.next_version }}
132+
run: |
133+
source actions_helpers.sh
134+
135+
log_notice "Running additional update script: ${{ inputs.additional_update_script }}"
136+
bash -e "${{ inputs.additional_update_script }}"
137+
138+
- name: Commit version bump changes
139+
if: steps.extract-version.outputs.has_unreleased == 'true'
140+
id: commit-changes
141+
run: |
142+
source actions_helpers.sh
143+
144+
version="${{ steps.extract-version.outputs.next_version }}"
145+
146+
# Get all changed files (unstaged, staged, and untracked)
147+
changed_files=$(git diff --name-only && git diff --name-only --cached && git ls-files --others --exclude-standard | sort --unique)
148+
149+
if [ -z "$changed_files" ]; then
150+
log_error "No changes to commit after version bump"
151+
exit 1
152+
fi
153+
154+
# Stage each changed file
155+
echo "$changed_files" | while read -r file; do
156+
if [ -n "$file" ]; then
157+
git add "$file"
158+
fi
159+
done
160+
161+
git commit -m "Prepare release v${version}" \
162+
-m "${{ steps.extract-version.outputs.unreleased_changes }}"
163+
164+
log_notice "Changes committed successfully"
165+
166+
- name: Push branch to auto-fork
167+
if: steps.extract-version.outputs.has_unreleased == 'true'
168+
run: |
169+
source actions_helpers.sh
170+
171+
branch_name="${{ steps.create-branch.outputs.branch_name }}"
172+
git push auto-fork "$branch_name"
173+
log_notice "Pushed branch $branch_name to auto-fork"
174+
175+
- name: Determine base branch for PR
176+
id: determine-base
177+
if: steps.extract-version.outputs.has_unreleased == 'true'
178+
env:
179+
GH_TOKEN: ${{ secrets.pr_create_token }}
180+
run: |
181+
source actions_helpers.sh
182+
183+
if [ -n "${{ inputs.pr_base_branch }}" ]; then
184+
base_branch="${{ inputs.pr_base_branch }}"
185+
log_notice "Using provided base branch: $base_branch"
186+
else
187+
# Get repository default branch
188+
base_branch=$(gh repo view --json defaultBranchRef --jq '.defaultBranchRef.name')
189+
if [ -z "$base_branch" ]; then
190+
base_branch="main"
191+
log_warning "Could not determine default branch, falling back to 'main'"
192+
else
193+
log_notice "Using repository default branch: $base_branch"
194+
fi
195+
fi
196+
set_output "base_branch" "$base_branch"
197+
198+
- name: Create version bump pull request
199+
if: steps.extract-version.outputs.has_unreleased == 'true'
200+
env:
201+
GH_TOKEN: ${{ secrets.pr_create_token }}
202+
run: |
203+
source actions_helpers.sh
204+
205+
version="${{ steps.extract-version.outputs.next_version }}"
206+
branch_name="${{ steps.create-branch.outputs.branch_name }}"
207+
base_branch="${{ steps.determine-base.outputs.base_branch }}"
208+
209+
pr_body="## Summary
210+
211+
This PR updates the version to \`${version}\` in preparation for release.
212+
213+
### Changes
214+
- Updated CHANGELOG.md with release version and date
215+
${{ inputs.additional_update_script != '' && '- Applied additional version bump updates' || '' }}
216+
217+
### Changelog Entries
218+
219+
${{ steps.extract-version.outputs.unreleased_changes }}"
220+
221+
gh pr create \
222+
--repo "${{ github.repository }}" \
223+
--title "Release v${version}" \
224+
--body "$pr_body" \
225+
--base "$base_branch" \
226+
--head "${{ inputs.fork_owner }}:$branch_name"
227+
228+
log_notice "Pull request created successfully"

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ Breaking changes are prefixed with "Breaking Change: ".
1717

1818
### Added
1919

20+
- `version-bump` reusable workflow: automates version bump PRs by checking for
21+
unreleased changes in CHANGELOG, extracting the next version, updating
22+
the CHANGELOG with new version and release date, optionally running custom update
23+
scripts, and creating a PR from a fork to the upstream repository.
2024
- `autotag-from-changelog` now exposes `tag_created` and `tag` outputs so
2125
callers can react to whether a new tag was pushed.
2226
- `expect_step_output` test helper for asserting GitHub Actions step outputs.

README.md

Lines changed: 108 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -92,12 +92,13 @@ determine whether a major, minor, or patch version bump is needed.
9292

9393
**Outputs:**
9494

95-
| Name | Description |
96-
| ----------------- | ---------------------------------------------------------------- |
97-
| `current_version` | Current latest released version (empty if no releases) |
98-
| `next_version` | Suggested next version based on unreleased changes |
99-
| `bump_type` | Type of version bump (`major`/`minor`/`patch`/`initial`, or empty if no changes) |
100-
| `has_unreleased` | Whether there are unreleased changes (`true`/`false`) |
95+
| Name | Description |
96+
| -------------------- | ---------------------------------------------------------------- |
97+
| `current_version` | Current latest released version (empty if no releases) |
98+
| `next_version` | Suggested next version based on unreleased changes |
99+
| `bump_type` | Type of version bump (`major`/`minor`/`patch`/`initial`, or empty if no changes) |
100+
| `has_unreleased` | Whether there are unreleased changes (`true`/`false`) |
101+
| `unreleased_changes` | Text content of unreleased changelog entries |
101102

102103
**Features:**
103104

@@ -107,6 +108,107 @@ determine whether a major, minor, or patch version bump is needed.
107108
- Returns empty `bump_type` when there are no unreleased changes
108109
- Follows semantic versioning principles
109110

111+
### get-workflow-ref
112+
113+
Resolves the git ref that a caller used to invoke a reusable workflow by parsing
114+
the caller's workflow file. Useful for reusable workflows that need to reference
115+
other resources (actions, scripts, etc.) at the same version they were invoked with.
116+
117+
**Usage:**
118+
119+
```yaml
120+
jobs:
121+
my-job:
122+
runs-on: ubuntu-latest
123+
steps:
124+
- uses: cockroachdb/actions/get-workflow-ref@v1
125+
id: ref
126+
- run: echo "Workflow was called with ref ${{ steps.ref.outputs.ref }}"
127+
```
128+
129+
**Outputs:**
130+
131+
| Name | Description |
132+
| ----- | ---------------------------------------------------------------- |
133+
| `ref` | Git ref used to invoke this workflow (e.g., `v1`, `main`, commit SHA) |
134+
135+
**Features:**
136+
137+
- No API calls or extra permissions needed
138+
- Works by parsing the caller's workflow file from the event payload
139+
- Returns the exact ref specified in the workflow call (tag, branch, or SHA)
140+
141+
## Workflows
142+
143+
### version-bump
144+
145+
Reusable workflow that automates version bump pull requests. Checks for unreleased
146+
changes in CHANGELOG.md, extracts the next version, updates the changelog with the
147+
release date, optionally runs custom update scripts, and creates a PR from a fork
148+
to the upstream repository.
149+
150+
**Usage:**
151+
152+
```yaml
153+
name: Create Version Bump PR
154+
155+
on:
156+
workflow_dispatch:
157+
158+
jobs:
159+
version-bump:
160+
uses: cockroachdb/actions/.github/workflows/version-bump.yml@v1
161+
with:
162+
fork_owner: my-release-bot
163+
fork_repo: my-repo-fork
164+
pr_base_branch: main
165+
release_date: 2026-03-30
166+
git_user_name: my-release-bot
167+
git_user_email: my-release-bot@users.noreply.github.com
168+
additional_update_script: .github/scripts/update-version.sh # optional
169+
secrets:
170+
fork_push_token: ${{ secrets.FORK_PAT }}
171+
pr_create_token: ${{ secrets.PR_PAT }}
172+
```
173+
174+
**Inputs:**
175+
176+
| Name | Required | Default | Description |
177+
| --------------------------- | -------- | ------- | ------------------------------------------------ |
178+
| `fork_owner` | Yes | | GitHub username or org that owns the fork |
179+
| `fork_repo` | Yes | | Repository name of the fork |
180+
| `pr_base_branch` | No | `""` | Base branch for the PR (defaults to repository default branch) |
181+
| `additional_update_script` | No | `""` | Optional path to a bash script file to execute during version bump. The `VERSION` environment variable will be available. |
182+
| `release_date` | No | `""` | Release date in YYYY-MM-DD format (defaults to current date) |
183+
| `git_user_name` | No | `github-actions[bot]` | Git user name for commits |
184+
| `git_user_email` | No | `github-actions[bot]@users.noreply.github.com` | Git user email for commits |
185+
186+
**Secrets:**
187+
188+
| Name | Required | Description |
189+
| ------------------ | -------- | ------------------------------------------------ |
190+
| `fork_push_token` | Yes | PAT with push access to the fork |
191+
| `pr_create_token` | Yes | PAT with permission to create PRs on the upstream repo |
192+
193+
**Features:**
194+
195+
- Automatically detects unreleased changes in CHANGELOG.md
196+
- Determines next version using semver principles
197+
- Updates CHANGELOG.md with new version and customizable release date (defaults to current date)
198+
- Supports custom bash scripts for additional version updates (via `additional_update_script` file path)
199+
- Creates PR from fork to upstream repository
200+
- Exits gracefully when no unreleased changes exist
201+
202+
**Required fork setup:**
203+
204+
The workflow pushes to a fork rather than creating branches directly on the upstream
205+
repository.
206+
207+
1. Create a fork of your repository under a service account or bot account
208+
2. Create a PAT for the fork owner with `repo` scope (for `fork_push_token`)
209+
3. Create a PAT with permission to create PRs on the upstream repo (for `pr_create_token`)
210+
4. Store both tokens as secrets in your repository
211+
110212
## Development
111213

112214
Run all tests locally:

release-version-extract/action.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ outputs:
1919
has_unreleased:
2020
description: Whether there are unreleased changes (true/false)
2121
value: ${{ steps.check-unreleased.outputs.has_changes }}
22+
unreleased_changes:
23+
description: Unreleased changelog entries text
24+
value: ${{ steps.unreleased.outputs.changes }}
2225

2326
runs:
2427
using: composite

0 commit comments

Comments
 (0)