Skip to content

Commit a74cd2c

Browse files
version-bump: 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 a74cd2c

5 files changed

Lines changed: 349 additions & 7 deletions

File tree

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@ jobs:
77
test:
88
runs-on: ubuntu-latest
99
steps:
10-
- uses: actions/checkout@v5
10+
- uses: actions/checkout@v6
1111
- run: ./test.sh

.github/workflows/version-bump.yml

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
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+
outputs:
47+
pr_url:
48+
description: URL of the created pull request (empty if no unreleased changes)
49+
value: ${{ jobs.version-bump.outputs.pr_url }}
50+
51+
jobs:
52+
version-bump:
53+
runs-on: ubuntu-latest
54+
outputs:
55+
pr_url: ${{ steps.create-pr.outputs.pr_url }}
56+
steps:
57+
- name: Get workflow ref
58+
id: workflow-ref
59+
uses: cockroachdb/actions/get-workflow-ref@v0
60+
with:
61+
workflow_name: cockroachdb/actions/.github/workflows/version-bump.yml
62+
63+
- name: Checkout actions repo
64+
uses: actions/checkout@v6
65+
with:
66+
repository: cockroachdb/actions
67+
ref: ${{ steps.workflow-ref.outputs.ref }}
68+
path: actions-repo
69+
70+
- name: Checkout repository
71+
uses: actions/checkout@v6
72+
with:
73+
fetch-depth: 0
74+
75+
- name: Extract release version
76+
id: extract-version
77+
uses: ./actions-repo/release-version-extract
78+
79+
- name: Set up fork remote and create branch
80+
if: steps.extract-version.outputs.has_unreleased == 'true'
81+
id: create-branch
82+
run: |
83+
source actions-repo/actions_helpers.sh
84+
85+
# Set up auto-fork remote
86+
fork_url="https://github.com/${{ inputs.fork_owner }}/${{ inputs.fork_repo }}.git"
87+
88+
if git remote | grep --quiet --fixed-strings --line-regexp "auto-fork"; then
89+
git remote set-url auto-fork "$fork_url"
90+
else
91+
git remote add auto-fork "$fork_url"
92+
fi
93+
94+
# Create branch
95+
branch_name="update-version-${{ steps.extract-version.outputs.next_version }}"
96+
log_notice "Creating new branch $branch_name"
97+
git checkout -b "$branch_name"
98+
set_output "branch_name" "$branch_name"
99+
100+
- name: Update CHANGELOG with new version
101+
if: steps.extract-version.outputs.has_unreleased == 'true'
102+
run: |
103+
source actions-repo/actions_helpers.sh
104+
105+
version="${{ steps.extract-version.outputs.next_version }}"
106+
107+
if [ -n "${{ inputs.release_date }}" ]; then
108+
release_date="${{ inputs.release_date }}"
109+
else
110+
release_date=$(date +%Y-%m-%d)
111+
fi
112+
113+
# Insert new version header under [Unreleased] header
114+
awk -v version="$version" -v date="$release_date" '
115+
/^## \[Unreleased\]/ {
116+
print
117+
print ""
118+
print "## [" version "] - " date
119+
next
120+
}
121+
{ print }
122+
' CHANGELOG.md > CHANGELOG.md.tmp && mv CHANGELOG.md.tmp CHANGELOG.md
123+
124+
log_notice "Updated CHANGELOG.md with version $version dated $release_date"
125+
126+
- name: Run additional update script
127+
if: steps.extract-version.outputs.has_unreleased == 'true' && inputs.additional_update_script != ''
128+
env:
129+
VERSION: ${{ steps.extract-version.outputs.next_version }}
130+
run: |
131+
source actions-repo/actions_helpers.sh
132+
133+
log_notice "Running additional update script: ${{ inputs.additional_update_script }}"
134+
bash -e "${{ inputs.additional_update_script }}"
135+
136+
- name: Commit version bump changes
137+
if: steps.extract-version.outputs.has_unreleased == 'true'
138+
id: commit-changes
139+
run: |
140+
source actions-repo/actions_helpers.sh
141+
142+
# Configure git for commits
143+
git config user.name "${{ inputs.git_user_name }}"
144+
git config user.email "${{ inputs.git_user_email }}"
145+
146+
version="${{ steps.extract-version.outputs.next_version }}"
147+
148+
# Get all changed files (unstaged, staged, and untracked)
149+
changed_files=$({ git diff --name-only; git diff --name-only --cached; git ls-files --others --exclude-standard; } | sort --unique)
150+
151+
if [ -z "$changed_files" ]; then
152+
log_error "No changes to commit after version bump"
153+
exit 1
154+
fi
155+
156+
# Stage each changed file
157+
echo "$changed_files" | while read -r file; do
158+
if [ -n "$file" ]; then
159+
git add "$file"
160+
fi
161+
done
162+
163+
git commit -m "Prepare release v${version}" \
164+
-m "${{ steps.extract-version.outputs.unreleased_changes }}"
165+
166+
log_notice "Changes committed successfully"
167+
168+
- name: Push branch to auto-fork
169+
if: steps.extract-version.outputs.has_unreleased == 'true'
170+
env:
171+
FORK_PUSH_TOKEN: ${{ secrets.fork_push_token }}
172+
run: |
173+
source actions-repo/actions_helpers.sh
174+
175+
# Configure git credentials for push
176+
git config --local credential.helper "!f() { echo \"username=${{ inputs.fork_owner }}\"; echo \"password=\${FORK_PUSH_TOKEN}\"; }; f"
177+
178+
branch_name="${{ steps.create-branch.outputs.branch_name }}"
179+
git push auto-fork "$branch_name"
180+
log_notice "Pushed branch $branch_name to auto-fork"
181+
182+
- name: Create version bump pull request
183+
if: steps.extract-version.outputs.has_unreleased == 'true'
184+
id: create-pr
185+
env:
186+
GH_TOKEN: ${{ secrets.pr_create_token }}
187+
run: |
188+
source actions-repo/actions_helpers.sh
189+
190+
# Determine base branch
191+
if [ -n "${{ inputs.pr_base_branch }}" ]; then
192+
base_branch="${{ inputs.pr_base_branch }}"
193+
log_notice "Using provided base branch: $base_branch"
194+
else
195+
base_branch=$(gh repo view --json defaultBranchRef --jq '.defaultBranchRef.name')
196+
if [ -z "$base_branch" ]; then
197+
base_branch="main"
198+
log_warning "Could not determine default branch, falling back to 'main'"
199+
else
200+
log_notice "Using repository default branch: $base_branch"
201+
fi
202+
fi
203+
204+
# Build PR body
205+
version="${{ steps.extract-version.outputs.next_version }}"
206+
additional_changes=""
207+
if [ -n "${{ inputs.additional_update_script }}" ]; then
208+
additional_changes="
209+
- Applied additional version bump updates"
210+
fi
211+
212+
pr_body="## Summary
213+
214+
This PR updates the version to \`${version}\` in preparation for release.
215+
216+
### Changes
217+
- Updated CHANGELOG.md with release version and date${additional_changes}
218+
219+
### Changelog Entries
220+
221+
${{ steps.extract-version.outputs.unreleased_changes }}"
222+
223+
# Create pull request
224+
pr_url=$(gh pr create --repo "${{ github.repository }}" --title "Release v${version}" --body "$pr_body" --base "$base_branch" --head "${{ inputs.fork_owner }}:${{ steps.create-branch.outputs.branch_name }}")
225+
226+
set_output "pr_url" "$pr_url"
227+
log_notice "Pull request created successfully: $pr_url"

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: 114 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,113 @@ 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+
**Outputs:**
194+
195+
| Name | Description |
196+
| -------- | ---------------------------------------------------------------- |
197+
| `pr_url` | URL of the created pull request (empty if no unreleased changes) |
198+
199+
**Features:**
200+
201+
- Automatically detects unreleased changes in CHANGELOG.md
202+
- Determines next version using semver principles
203+
- Updates CHANGELOG.md with new version and customizable release date (defaults to current date)
204+
- Supports custom bash scripts for additional version updates (via `additional_update_script` file path)
205+
- Creates PR from fork to upstream repository
206+
- Exits gracefully when no unreleased changes exist
207+
208+
**Required fork setup:**
209+
210+
The workflow pushes to a fork rather than creating branches directly on the upstream
211+
repository.
212+
213+
1. Create a fork of your repository under a service account or bot account
214+
2. Create a PAT for the fork owner with `repo` scope (for `fork_push_token`)
215+
3. Create a PAT with permission to create PRs on the upstream repo (for `pr_create_token`)
216+
4. Store both tokens as secrets in your repository
217+
110218
## Development
111219

112220
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)