Skip to content

Commit d852544

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 d852544

12 files changed

Lines changed: 1016 additions & 35 deletions
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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 - Newline-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 newline-separated list
32+
if [ -n "$files_to_commit" ]; then
33+
while IFS= read -r file; do
34+
# Trim whitespace using bash parameter expansion (avoids command substitution
35+
# with user input, safer than sed/xargs for potentially malicious filenames)
36+
file="${file#"${file%%[![:space:]]*}"}"
37+
file="${file%"${file##*[![:space:]]}"}"
38+
if [ -n "$file" ]; then
39+
if [ ! -f "$file" ]; then
40+
log_warning "File not found, skipping: $file"
41+
continue
42+
fi
43+
git add "$file"
44+
fi
45+
done <<< "$files_to_commit"
46+
fi
47+
48+
# Check for leftover unstaged or untracked files
49+
unstaged=$(git diff --name-only)
50+
untracked=$(git ls-files --others --exclude-standard)
51+
52+
# Combine unstaged and untracked files (add newline separator if both exist)
53+
leftover=""
54+
if [ -n "$unstaged" ] && [ -n "$untracked" ]; then
55+
leftover="${unstaged}"$'\n'"${untracked}"
56+
elif [ -n "$unstaged" ]; then
57+
leftover="$unstaged"
58+
elif [ -n "$untracked" ]; then
59+
leftover="$untracked"
60+
fi
61+
62+
if [ -n "$leftover" ]; then
63+
log_error "Leftover files detected. These files were modified or created but not included in files_to_commit:"
64+
echo "$leftover" >&2
65+
exit 1
66+
fi
67+
68+
git commit -m "Prepare release v${version}" \
69+
-m "$UNRELEASED_CHANGES"
70+
71+
log_notice "Changes committed successfully"
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
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 an initial commit and modified CHANGELOG.md
13+
# Creates optional additional untracked files for testing
14+
# Usage: setup_repo "$repo_path" [file1 file2 ...]
15+
setup_repo() {
16+
local repo="$1"
17+
shift # Remove repo from arguments, rest are files to create
18+
19+
mkdir -p "$repo"
20+
cd "$repo"
21+
22+
git init
23+
git config user.name "Test"
24+
git config user.email "test@test.com"
25+
26+
# Create initial commit
27+
echo "# Changelog" > CHANGELOG.md
28+
git add CHANGELOG.md
29+
git commit -m "Initial commit"
30+
31+
# Modify CHANGELOG
32+
echo "## [Unreleased]" >> CHANGELOG.md
33+
34+
# Create any additional files passed as arguments
35+
for file in "$@"; do
36+
echo "content" > "$file"
37+
done
38+
}
39+
40+
test_commits_specified_file() {
41+
local repo="$TMPDIR_TEST/test1"
42+
setup_repo "$repo" file1.txt
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+
expect_files_in_commit "CHANGELOG.md" "file1.txt"
50+
}
51+
expect_success "commits specified file plus CHANGELOG.md" test_commits_specified_file
52+
53+
test_multiple_files_newline_separated() {
54+
local repo="$TMPDIR_TEST/test2"
55+
setup_repo "$repo" file1.txt file2.txt
56+
57+
export FILES_TO_COMMIT="file1.txt
58+
file2.txt"
59+
export UNRELEASED_CHANGES="- Added feature"
60+
export VERSION="1.0.0"
61+
bash "$SCRIPT_DIR/commit_release_changes.sh"
62+
63+
expect_files_in_commit "CHANGELOG.md" "file1.txt" "file2.txt"
64+
}
65+
expect_success "commits multiple newline-separated files" test_multiple_files_newline_separated
66+
67+
test_handles_whitespace_in_list() {
68+
local repo="$TMPDIR_TEST/test3"
69+
setup_repo "$repo" file1.txt file2.txt credentials.json
70+
71+
export FILES_TO_COMMIT=" file1.txt
72+
file2.txt
73+
credentials.json "
74+
export UNRELEASED_CHANGES="- Fixed bug"
75+
export VERSION="1.0.0"
76+
bash "$SCRIPT_DIR/commit_release_changes.sh"
77+
78+
expect_files_in_commit "CHANGELOG.md" "file1.txt" "file2.txt" "credentials.json"
79+
}
80+
expect_success "handles whitespace in newline-separated list" test_handles_whitespace_in_list
81+
82+
test_empty_files_list_commits_only_changelog() {
83+
local repo="$TMPDIR_TEST/test4"
84+
setup_repo "$repo"
85+
86+
export FILES_TO_COMMIT=""
87+
export UNRELEASED_CHANGES="- Updated docs"
88+
export VERSION="1.0.0"
89+
bash "$SCRIPT_DIR/commit_release_changes.sh"
90+
91+
expect_files_in_commit "CHANGELOG.md"
92+
}
93+
expect_success "empty files list commits only CHANGELOG.md" test_empty_files_list_commits_only_changelog
94+
95+
test_commit_message_includes_version() {
96+
local repo="$TMPDIR_TEST/test5"
97+
setup_repo "$repo" file1.txt file2.txt credentials.json
98+
99+
export FILES_TO_COMMIT="file1.txt
100+
file2.txt
101+
credentials.json"
102+
export UNRELEASED_CHANGES="- New feature"
103+
export VERSION="2.5.3"
104+
bash "$SCRIPT_DIR/commit_release_changes.sh"
105+
106+
git log --max-count=1 --pretty=%B | check_contains "Prepare release v2.5.3"
107+
}
108+
expect_success "commit message includes version" test_commit_message_includes_version
109+
110+
test_commit_body_includes_unreleased_changes() {
111+
local repo="$TMPDIR_TEST/test6"
112+
setup_repo "$repo"
113+
114+
export FILES_TO_COMMIT=""
115+
export UNRELEASED_CHANGES="- Added feature A
116+
- Fixed bug B"
117+
export VERSION="1.0.0"
118+
bash "$SCRIPT_DIR/commit_release_changes.sh"
119+
120+
git log --max-count=1 --pretty=%B | check_contains "Added feature A"
121+
git log --max-count=1 --pretty=%B | check_contains "Fixed bug B"
122+
}
123+
expect_success "commit body includes unreleased changes" test_commit_body_includes_unreleased_changes
124+
125+
test_fails_when_leftover_untracked_files_exist() {
126+
local repo="$TMPDIR_TEST/test7"
127+
setup_repo "$repo" file1.txt .env
128+
129+
export FILES_TO_COMMIT="file1.txt"
130+
export UNRELEASED_CHANGES="- Security update"
131+
export VERSION="1.0.0"
132+
bash "$SCRIPT_DIR/commit_release_changes.sh" 2>&1
133+
}
134+
expect_failure_output "fails when leftover untracked files exist" "Leftover files detected" test_fails_when_leftover_untracked_files_exist
135+
136+
test_warns_and_skips_missing_files() {
137+
local repo="$TMPDIR_TEST/test8"
138+
setup_repo "$repo" file1.txt file2.txt credentials.json
139+
140+
export FILES_TO_COMMIT="file1.txt
141+
file2.txt
142+
credentials.json
143+
missing_file.txt"
144+
export UNRELEASED_CHANGES="- New feature"
145+
146+
# Capture output to check for warning
147+
local output
148+
export VERSION="1.0.0"
149+
output=$(bash "$SCRIPT_DIR/commit_release_changes.sh" 2>&1)
150+
151+
expect_files_in_commit "file1.txt" "file2.txt" "credentials.json"
152+
expect_files_not_in_commit "missing_file.txt"
153+
154+
# Should log warning about missing file
155+
echo "$output" | check_contains "File not found"
156+
echo "$output" | check_contains "missing_file.txt"
157+
}
158+
expect_success "warns and skips missing files" test_warns_and_skips_missing_files
159+
160+
test_fails_when_leftover_unstaged_changes_exist() {
161+
local repo="$TMPDIR_TEST/test9"
162+
setup_repo "$repo"
163+
164+
# Create and commit a file, then modify it without staging
165+
echo "original" > file1.txt
166+
git add file1.txt
167+
git commit -m "Add file1.txt"
168+
169+
# Modify CHANGELOG again for the release
170+
echo "## [Unreleased]" >> CHANGELOG.md
171+
172+
# Now modify file1.txt but don't include it in FILES_TO_COMMIT
173+
echo "modified" > file1.txt
174+
175+
export FILES_TO_COMMIT=""
176+
export UNRELEASED_CHANGES="- New release"
177+
export VERSION="1.0.0"
178+
bash "$SCRIPT_DIR/commit_release_changes.sh" 2>&1
179+
}
180+
expect_failure_output "fails when leftover unstaged changes exist" "Leftover files detected" test_fails_when_leftover_unstaged_changes_exist
181+
182+
# Return to original directory before cleanup
183+
cd "$SCRIPT_DIR"
184+
185+
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 -f CHANGELOG.md.tmp
55+
exit 1
56+
fi
57+
58+
# Verify the version header was actually inserted
59+
if ! check_contains "## [$version]" CHANGELOG.md.tmp; then
60+
log_error "Version header '## [$version]' not found in transformed changelog"
61+
rm -f 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)