Skip to content

Commit 18a7fdc

Browse files
create-release-pr: Add create-release-pr reusable workflow
Automates creation of release 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 optional build 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 10e23be commit 18a7fdc

11 files changed

Lines changed: 928 additions & 11 deletions
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# Commits release changes including CHANGELOG.md and specified files.
5+
#
6+
# Usage: commit-release-changes.sh
7+
#
8+
# Environment variables:
9+
# VERSION - Version being released (e.g., "1.2.3")
10+
# FILES_TO_COMMIT - Comma-separated list of files to commit (in addition to CHANGELOG.md)
11+
# UNRELEASED_CHANGES - Unreleased changelog entries (used in commit body)
12+
13+
# Save current directory (where files to commit are expected)
14+
WORK_DIR="$(pwd)"
15+
16+
# Change to script directory for relative sourcing
17+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
18+
cd "$SCRIPT_DIR"
19+
20+
source ../../actions_helpers.sh
21+
22+
# Go back to work directory where files are
23+
cd "$WORK_DIR"
24+
25+
version="$VERSION"
26+
files_to_commit="${FILES_TO_COMMIT:-}"
27+
28+
# Stage CHANGELOG.md (always updated by the workflow)
29+
git add CHANGELOG.md
30+
31+
# Stage additional files from comma-separated list
32+
if [ -n "$files_to_commit" ]; then
33+
IFS=',' read -ra files <<< "$files_to_commit"
34+
for file in "${files[@]}"; do
35+
# Trim whitespace using bash parameter expansion (avoids command substitution
36+
# with user input, safer than sed/xargs for potentially malicious filenames)
37+
file="${file#"${file%%[![:space:]]*}"}"
38+
file="${file%"${file##*[![:space:]]}"}"
39+
if [ -n "$file" ]; then
40+
if [ ! -f "$file" ]; then
41+
log_warning "File not found, skipping: $file"
42+
continue
43+
fi
44+
git add "$file"
45+
fi
46+
done
47+
fi
48+
49+
git commit -m "Prepare release v${version}" \
50+
-m "$UNRELEASED_CHANGES"
51+
52+
log_notice "Changes committed successfully"
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
#!/usr/bin/env bash
2+
# Tests for commit-release-changes.sh
3+
set -euo pipefail
4+
5+
cd "$(dirname "${BASH_SOURCE[0]}")"
6+
source ../../test_helpers.sh
7+
8+
SCRIPT_DIR="$(pwd)"
9+
TMPDIR_TEST=$(mktemp -d)
10+
trap 'rm -rf "$TMPDIR_TEST"' EXIT
11+
12+
# Helper: set up a git repo with modified files
13+
setup_test_repo() {
14+
local repo="$1"
15+
mkdir -p "$repo"
16+
cd "$repo"
17+
18+
git init
19+
git config user.name "Test"
20+
git config user.email "test@test.com"
21+
22+
# Create initial commit
23+
echo "# Changelog" > CHANGELOG.md
24+
git add CHANGELOG.md
25+
git commit -m "Initial commit"
26+
27+
# Modify multiple files
28+
echo "## [Unreleased]" >> CHANGELOG.md
29+
echo "content" > file1.txt
30+
echo "content" > file2.txt
31+
echo "secret" > credentials.json
32+
}
33+
34+
# Helper: check if a file is in the last commit
35+
file_in_commit() {
36+
local file="$1"
37+
git diff --name-only HEAD~1 HEAD | grep --quiet --fixed-strings --line-regexp "$file"
38+
}
39+
40+
test_commits_only_specified_files() {
41+
local repo="$TMPDIR_TEST/test1"
42+
setup_test_repo "$repo"
43+
44+
export FILES_TO_COMMIT="file1.txt"
45+
export UNRELEASED_CHANGES="- Added new feature"
46+
export VERSION="1.0.0"
47+
bash "$SCRIPT_DIR/commit_release_changes.sh"
48+
49+
# Should commit CHANGELOG.md and file1.txt
50+
file_in_commit "CHANGELOG.md" && file_in_commit "file1.txt"
51+
52+
# Should NOT commit file2.txt or credentials.json
53+
! file_in_commit "file2.txt" && ! file_in_commit "credentials.json"
54+
}
55+
expect_success "commits only specified files plus CHANGELOG.md" test_commits_only_specified_files
56+
57+
test_multiple_files_comma_separated() {
58+
local repo="$TMPDIR_TEST/test2"
59+
setup_test_repo "$repo"
60+
61+
export FILES_TO_COMMIT="file1.txt,file2.txt"
62+
export UNRELEASED_CHANGES="- Added feature"
63+
export VERSION="1.0.0"
64+
bash "$SCRIPT_DIR/commit_release_changes.sh"
65+
66+
# Should commit all three files
67+
file_in_commit "CHANGELOG.md" && file_in_commit "file1.txt" && file_in_commit "file2.txt"
68+
69+
# Should NOT commit credentials.json
70+
! file_in_commit "credentials.json"
71+
}
72+
expect_success "commits multiple comma-separated files" test_multiple_files_comma_separated
73+
74+
test_handles_whitespace_in_list() {
75+
local repo="$TMPDIR_TEST/test3"
76+
setup_test_repo "$repo"
77+
78+
export FILES_TO_COMMIT="file1.txt , file2.txt"
79+
export UNRELEASED_CHANGES="- Fixed bug"
80+
export VERSION="1.0.0"
81+
bash "$SCRIPT_DIR/commit_release_changes.sh"
82+
83+
# Should handle spaces and commit both files
84+
file_in_commit "file1.txt" && file_in_commit "file2.txt"
85+
}
86+
expect_success "handles whitespace in comma-separated list" test_handles_whitespace_in_list
87+
88+
test_empty_files_list_commits_only_changelog() {
89+
local repo="$TMPDIR_TEST/test4"
90+
setup_test_repo "$repo"
91+
92+
export FILES_TO_COMMIT=""
93+
export UNRELEASED_CHANGES="- Updated docs"
94+
export VERSION="1.0.0"
95+
bash "$SCRIPT_DIR/commit_release_changes.sh"
96+
97+
# Should commit only CHANGELOG.md
98+
file_in_commit "CHANGELOG.md"
99+
100+
# Should NOT commit any other files
101+
! file_in_commit "file1.txt" && ! file_in_commit "file2.txt" && ! file_in_commit "credentials.json"
102+
}
103+
expect_success "empty files list commits only CHANGELOG.md" test_empty_files_list_commits_only_changelog
104+
105+
test_commit_message_includes_version() {
106+
local repo="$TMPDIR_TEST/test5"
107+
setup_test_repo "$repo"
108+
109+
export FILES_TO_COMMIT="file1.txt"
110+
export UNRELEASED_CHANGES="- New feature"
111+
export VERSION="2.5.3"
112+
bash "$SCRIPT_DIR/commit_release_changes.sh"
113+
114+
git log -1 --pretty=%B | check_contains "Prepare release v2.5.3"
115+
}
116+
expect_success "commit message includes version" test_commit_message_includes_version
117+
118+
test_commit_body_includes_unreleased_changes() {
119+
local repo="$TMPDIR_TEST/test6"
120+
setup_test_repo "$repo"
121+
122+
export FILES_TO_COMMIT=""
123+
export UNRELEASED_CHANGES="- Added feature A
124+
- Fixed bug B"
125+
export VERSION="1.0.0"
126+
bash "$SCRIPT_DIR/commit_release_changes.sh"
127+
128+
git log -1 --pretty=%B | check_contains "Added feature A"
129+
git log -1 --pretty=%B | check_contains "Fixed bug B"
130+
}
131+
expect_success "commit body includes unreleased changes" test_commit_body_includes_unreleased_changes
132+
133+
test_does_not_commit_untracked_files_not_in_list() {
134+
local repo="$TMPDIR_TEST/test7"
135+
setup_test_repo "$repo"
136+
137+
# Create untracked file that's not in the list
138+
echo "secret-key-123" > .env
139+
140+
export FILES_TO_COMMIT="file1.txt"
141+
export UNRELEASED_CHANGES="- Security update"
142+
export VERSION="1.0.0"
143+
bash "$SCRIPT_DIR/commit_release_changes.sh"
144+
145+
# Should NOT commit .env file
146+
! file_in_commit ".env"
147+
}
148+
expect_success "does not commit untracked files not in list (security test)" test_does_not_commit_untracked_files_not_in_list
149+
150+
test_warns_and_skips_missing_files() {
151+
local repo="$TMPDIR_TEST/test8"
152+
setup_test_repo "$repo"
153+
154+
# Modify file1.txt but don't create missing_file.txt
155+
echo "content" > file1.txt
156+
157+
export FILES_TO_COMMIT="file1.txt,missing_file.txt"
158+
export UNRELEASED_CHANGES="- New feature"
159+
160+
# Capture output to check for warning
161+
local output
162+
export VERSION="1.0.0"
163+
output=$(bash "$SCRIPT_DIR/commit_release_changes.sh" 2>&1)
164+
165+
# Should commit file1.txt but not missing_file.txt
166+
file_in_commit "file1.txt"
167+
! file_in_commit "missing_file.txt"
168+
169+
# Should log warning about missing file
170+
echo "$output" | check_contains "File not found"
171+
echo "$output" | check_contains "missing_file.txt"
172+
}
173+
expect_success "warns and skips missing files" test_warns_and_skips_missing_files
174+
175+
print_results
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# Updates CHANGELOG.md by inserting a new version header under [Unreleased].
5+
#
6+
# Usage: update-changelog.sh
7+
#
8+
# Environment variables:
9+
# VERSION - Version being released (e.g., "1.2.3")
10+
# RELEASE_DATE - Release date in YYYY-MM-DD format (defaults to current date)
11+
12+
# Save current directory (where CHANGELOG.md is expected)
13+
WORK_DIR="$(pwd)"
14+
15+
# Change to script directory for relative sourcing
16+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
17+
cd "$SCRIPT_DIR"
18+
19+
source ../../actions_helpers.sh
20+
21+
version="$VERSION"
22+
23+
# Validate semver format
24+
if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
25+
log_error "Version must be in semver format (e.g., 1.2.3), got: $version"
26+
exit 1
27+
fi
28+
29+
release_date="${RELEASE_DATE:-$(date +%Y-%m-%d)}"
30+
31+
# Validate date format if provided
32+
if [ -n "${RELEASE_DATE:-}" ] && ! [[ "$release_date" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
33+
log_error "RELEASE_DATE must be in YYYY-MM-DD format, got: $release_date"
34+
exit 1
35+
fi
36+
37+
# Go back to work directory where CHANGELOG.md is
38+
cd "$WORK_DIR"
39+
40+
# Insert new version header under [Unreleased] header
41+
awk -v version="$version" -v date="$release_date" '
42+
/^## \[Unreleased\]/ {
43+
print
44+
print ""
45+
print "## [" version "] - " date
46+
next
47+
}
48+
{ print }
49+
' CHANGELOG.md > CHANGELOG.md.tmp
50+
51+
# Validate the transformation succeeded
52+
if [ ! -s CHANGELOG.md.tmp ]; then
53+
log_error "Failed to update CHANGELOG.md - transformation produced empty output"
54+
rm --force CHANGELOG.md.tmp
55+
exit 1
56+
fi
57+
58+
# Verify the version header was actually inserted
59+
if ! grep --quiet --fixed-strings "## [$version]" CHANGELOG.md.tmp; then
60+
log_error "Version header '## [$version]' not found in transformed changelog"
61+
rm --force CHANGELOG.md.tmp
62+
exit 1
63+
fi
64+
65+
mv CHANGELOG.md.tmp CHANGELOG.md
66+
67+
log_notice "Updated CHANGELOG.md with version $version dated $release_date"

0 commit comments

Comments
 (0)