Skip to content

Commit d580d1e

Browse files
joelhawksleyCopilot
andcommitted
Streamline script/release
- Replace `sed -i` (incompatible between BSD and GNU sed) with perl. - Fail fast with `set -euo pipefail` and verify each edit. - Run prerequisite checks once, fast-forward pull automatically. - Install gems after the version bump so docs:build doesn't fail. - Restore lockfiles before bumping the version so local bundler platform changes aren't committed. - Fix existing-tag check (tags are prefixed with `v`). - Commit deprecation.rb changes on major releases. - Print reset instructions on failure; add RELEASE_DRY_RUN. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 702f3f08-0ed8-471b-ad37-cdabab86a94f
1 parent c6356a6 commit d580d1e

2 files changed

Lines changed: 142 additions & 159 deletions

File tree

‎docs/CHANGELOG.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ nav_order: 6
1212

1313
## 4.16.0
1414

15+
* Fix `script/release` on macOS, stop it on the first error, and streamline its prerequisite checks.
16+
17+
*Joel Hawksley*
18+
1519
* Fix subclasses defined after their parent renders a collection using the parent's default collection parameter name instead of their own. Fix recompiling a component not refreshing its collection parameter, counter, and iteration detection. Compute that detection when a component compiles, and store the results frozen so they're shareable across Ractors.
1620

1721
*Ben Sheldon*

‎script/release‎

Lines changed: 138 additions & 159 deletions
Original file line numberDiff line numberDiff line change
@@ -1,121 +1,183 @@
11
#!/usr/bin/env bash
22

3+
set -Eeuo pipefail
4+
35
# bundler-cache: true in setup-ruby enables frozen mode, which blocks
46
# updating Gemfile.lock after the gemspec version bump. Disable it for
57
# every bundler invocation in this script so the lockfile can be
68
# regenerated to match the new version.
79
export BUNDLE_FROZEN=false
810
export BUNDLE_DEPLOYMENT=false
911

10-
fetch() {
11-
git fetch --all
12+
release_branch=""
13+
14+
header() {
15+
echo "==============================="
16+
echo "$1"
17+
echo "==============================="
18+
}
19+
20+
fail() {
21+
echo "Error: $1" >&2
22+
exit 1
23+
}
24+
25+
on_exit() {
26+
local status=$?
27+
[ "$status" == "0" ] && return
28+
echo "" >&2
29+
echo "Release failed (exit $status)." >&2
30+
if [ -n "$release_branch" ]; then
31+
echo "To reset and try again:" >&2
32+
echo " git reset --hard && git clean -fd" >&2
33+
echo " git switch main && git branch -D $release_branch" >&2
34+
fi
35+
}
36+
trap on_exit EXIT
37+
38+
# In-place file edits via perl, since `sed -i` is incompatible between
39+
# GNU (Linux/CI) and BSD (macOS) sed.
40+
inplace() {
41+
local expression=$1
42+
shift
43+
perl -pi -e "$expression" "$@"
1244
}
1345

1446
branch_name() {
1547
git symbolic-ref --short HEAD
1648
}
1749

18-
local_history_is_clean() {
19-
history=$(git rev-list --count --right-only @{u}...HEAD)
20-
[ "$history" == "0" ]
50+
working_tree_is_clean() {
51+
[ -z "$(git status --porcelain)" ]
2152
}
2253

23-
remote_history_is_clean() {
24-
history=$(git rev-list --count --left-only @{u}...HEAD)
25-
[ "$history" == "0" ]
54+
commits_behind_remote() {
55+
git rev-list --count --left-only @{u}...HEAD
2656
}
2757

28-
tag_exists_on_remote() {
29-
git rev-parse --quiet --verify refs/tags/$1.$2.$3 > /dev/null
58+
commits_ahead_of_remote() {
59+
git rev-list --count --right-only @{u}...HEAD
3060
}
3161

32-
working_tree_is_clean() {
33-
status=$(git status --porcelain)
34-
[ "$status" == "" ]
62+
tag_exists() {
63+
git rev-parse --quiet --verify "refs/tags/v$1" > /dev/null ||
64+
[ -n "$(git ls-remote --tags origin "refs/tags/v$1")" ]
65+
}
66+
67+
prerequisite_checks() {
68+
header "Prerequisite Checks"
69+
70+
working_tree_is_clean || fail "unclean working tree"
71+
[ "$(branch_name)" == "main" ] || fail "can only make a release on the main branch"
72+
73+
git fetch --all --tags --quiet
74+
75+
[ "$(commits_ahead_of_remote)" == "0" ] ||
76+
fail "changes exist that haven't been pushed to origin"
77+
78+
if [ "$(commits_behind_remote)" != "0" ]; then
79+
echo "Pulling latest changes from origin..."
80+
git pull --ff-only --quiet
81+
fi
3582
}
3683

37-
create_release_branch() {
38-
git switch -c release-$1-$2-$3
84+
prompt_for_version() {
85+
echo "Current version is $major.$minor.$patch."
86+
echo "Type the number of an option to bump, or pick Manual to enter a version number"
87+
select bump in Major Minor Patch Manual; do
88+
case "$bump" in
89+
Major) major=$((major + 1)); minor=0; patch=0 ;;
90+
Minor) minor=$((minor + 1)); patch=0 ;;
91+
Patch) patch=$((patch + 1)) ;;
92+
*)
93+
read -r -p "What version? (Currently $major.$minor.$patch): " new_version
94+
[[ "$new_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || fail "version must look like X.Y.Z"
95+
[ "$new_version" != "$major.$minor.$patch" ] || fail "can't be the same version"
96+
IFS=. read -r major minor patch <<< "$new_version"
97+
;;
98+
esac
99+
break
100+
done
39101
}
40102

41-
update_readme() {
42-
sed -i "/## main/ {a\\
43-
\\
44-
## $1.$2.$3
45-
}" docs/CHANGELOG.md
103+
update_changelog() {
104+
VERSION="$1" perl -0777 -pi -e 's/^## main\n/## main\n\n## $ENV{VERSION}\n/m' docs/CHANGELOG.md
105+
grep -q "^## $1\$" docs/CHANGELOG.md || fail "failed to add $1 heading to docs/CHANGELOG.md"
46106
}
47107

48108
update_ruby_version() {
49-
# Update version file
50-
sed -i \
51-
-e "s/MAJOR = [0-9]*/MAJOR = $1/g" \
52-
-e "s/MINOR = [0-9]*/MINOR = $2/g" \
53-
-e "s/PATCH = [0-9]*/PATCH = $3/g" \
54-
lib/view_component/version.rb
55-
56-
# Update deprecation horizon version
57-
major=$1
58-
sed -i \
59-
-e "s/DEPRECATION_HORIZON = \"[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\"/DEPRECATION_HORIZON = \"$((major + 1)).0.0\"/g" \
60-
lib/view_component/deprecation.rb
61-
}
109+
inplace "s/MAJOR = \\d+/MAJOR = $1/; s/MINOR = \\d+/MINOR = $2/; s/PATCH = \\d+/PATCH = $3/" \
110+
lib/view_component/version.rb
111+
inplace "s/DEPRECATION_HORIZON = \"\\d+\\.\\d+\\.\\d+\"/DEPRECATION_HORIZON = \"$(($1 + 1)).0.0\"/" \
112+
lib/view_component/deprecation.rb
62113

63-
update_gemfiles() {
64-
major=$1
65-
minor=$2
66-
patch=$3
67-
new_version="$major.$minor.$patch"
114+
grep -q "MAJOR = $1\$" lib/view_component/version.rb &&
115+
grep -q "MINOR = $2\$" lib/view_component/version.rb &&
116+
grep -q "PATCH = $3\$" lib/view_component/version.rb ||
117+
fail "failed to update lib/view_component/version.rb"
118+
}
68119

120+
update_lockfiles() {
69121
# The only lockfile change during a release is the view_component
70-
# version pinned by the path gemspec. Update it in place across every
71-
# lockfile. Running `bundle lock` from a mismatched Ruby (e.g. ruby-head
72-
# appraisals from Ruby 4.0) produces lockfiles CI cannot consume in
73-
# frozen mode, so we edit them directly instead.
74-
for lockfile in Gemfile.lock gemfiles/*.gemfile.lock; do
75-
[ -f "$lockfile" ] || continue
76-
sed -i \
77-
-e "s/^\( view_component \)([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*)/\1($new_version)/" \
78-
-e "s/^\( view_component \)([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*)/\1($new_version)/" \
79-
"$lockfile"
122+
# version pinned by the path gemspec. Running `bundle install`/`bundle
123+
# exec` locally can add platform-specific entries (e.g. on a dev Ruby
124+
# without precompiled gems), and `bundle lock` from a mismatched Ruby
125+
# produces lockfiles CI cannot consume in frozen mode. So restore the
126+
# committed lockfiles and edit just the version in place.
127+
local lockfiles=(Gemfile.lock gemfiles/*.gemfile.lock)
128+
git restore --source=HEAD -- "${lockfiles[@]}"
129+
VERSION="$1" perl -pi -e 's/^( | )view_component \(\d+\.\d+\.\d+\)/$1view_component ($ENV{VERSION})/' "${lockfiles[@]}"
130+
131+
local lockfile
132+
for lockfile in "${lockfiles[@]}"; do
133+
grep -q "view_component ($1)" "$lockfile" || fail "failed to update $lockfile"
80134
done
81135
}
82136

83137
build_docs() {
84-
ruby script/sync_contributors.rb || { echo "Error: sync_contributors.rb failed"; exit 1; }
85-
bundle exec rake docs:build || { echo "Error: rake docs:build failed"; exit 1; }
138+
echo "version: $1" > docs/_data/library.yml
139+
ruby script/sync_contributors.rb
140+
# Install after the version bump, which changes the resolved gemspec.
141+
bundle install --quiet
142+
bundle exec rake docs:build
86143
}
87144

88-
add_changed_files() {
145+
commit() {
89146
git add \
90147
docs/api.md \
91148
docs/CHANGELOG.md \
92149
docs/_data/contributors.yml \
93150
docs/_data/library.yml \
94151
Gemfile.lock \
95152
gemfiles/*.gemfile.lock \
153+
lib/view_component/deprecation.rb \
96154
lib/view_component/version.rb
97-
}
98-
99-
commit() {
100-
git commit -m "release $1.$2.$3"
155+
git commit --quiet -m "release $1"
101156
}
102157

103158
push() {
159+
local version=$1
160+
161+
if [ -n "${RELEASE_DRY_RUN:-}" ]; then
162+
echo "RELEASE_DRY_RUN set; skipping push and PR creation."
163+
return
164+
fi
165+
104166
# Force-push: the release-X-Y-Z branch is auto-generated and is safe
105167
# to overwrite. A previous failed run may have left the branch on the
106168
# remote, which would otherwise reject a non-fast-forward push.
107-
git push --force origin release-$1-$2-$3
169+
git push --force origin "$release_branch"
108170

109171
# Open PR (or reuse an existing one from a previous failed run).
110-
pr_url=$(gh pr view "release-$1-$2-$3" \
172+
pr_url=$(gh pr view "$release_branch" \
111173
--repo ViewComponent/view_component \
112174
--json url --jq .url 2>/dev/null || true)
113175
if [ -z "$pr_url" ]; then
114176
pr_url=$(gh pr create \
115-
--title "Release $1.$2.$3" \
116-
--body "Automated release PR for version $1.$2.$3." \
177+
--title "Release $version" \
178+
--body "Automated release PR for version $version." \
117179
--base main \
118-
--head release-$1-$2-$3 \
180+
--head "$release_branch" \
119181
--repo ViewComponent/view_component)
120182
else
121183
echo "Reusing existing PR: $pr_url"
@@ -138,120 +200,37 @@ push() {
138200
}
139201

140202
main() {
203+
prerequisite_checks
204+
141205
major=$(grep "MAJOR = " lib/view_component/version.rb | grep -o "[0-9]\+")
142206
minor=$(grep "MINOR = " lib/view_component/version.rb | grep -o "[0-9]\+")
143207
patch=$(grep "PATCH = " lib/view_component/version.rb | grep -o "[0-9]\+")
144-
pre=$(grep "PRE = " lib/view_component/version.rb | grep -o "\"[^\"]*\"" | tr -d '"')
145208

146-
# If arguments provided, use them (for CI/workflow usage)
209+
# Arguments are used by the release workflow: MAJOR MINOR PATCH [PRE]
147210
if [ $# -gt 0 ]; then
211+
[ $# -ge 3 ] || fail "usage: script/release [MAJOR MINOR PATCH [PRE]]"
148212
major=$1
149213
minor=$2
150214
patch=$3
151-
[ $# -gt 3 ] && pre=$4 || pre=""
152215
else
153-
# Interactive mode
154-
echo "==================="
155-
echo "Prerequisite Checks"
156-
echo "==================="
157-
158-
if ! working_tree_is_clean; then
159-
echo "Error: unclean working tree"
160-
exit 1
161-
fi
162-
163-
if [ "$(branch_name)" != "main" ]; then
164-
echo "Error: can only make a release on the main branch"
165-
exit 1
166-
fi
167-
168-
fetch
169-
170-
if ! remote_history_is_clean; then
171-
echo "Error: changes exist on origin not pulled into this branch. Please pull"
172-
exit 1
173-
fi
174-
175-
if ! local_history_is_clean; then
176-
echo "Error: changes exist that haven't been pushed to origin. Please pull"
177-
exit 1
178-
fi
179-
180-
echo "Type the number of an option to bump, or pick Manual to enter a version number"
181-
select bump in Major Minor Patch Manual
182-
do
183-
if [ "$bump" == "Major" ]; then
184-
major=$((major + 1))
185-
minor=0
186-
patch=0
187-
elif [ "$bump" == "Minor" ]; then
188-
minor=$((minor + 1))
189-
patch=0
190-
elif [ "$bump" == "Patch" ]; then
191-
patch=$((patch + 1))
192-
else
193-
read -p "What version? (Currently $major.$minor.$patch): " new_version
194-
if [ "$new_version" == "$major.$minor.$patch" ]; then
195-
echo "Error: Can't be the same version"
196-
exit 1
197-
fi
198-
199-
new_version=(${new_version//./ })
200-
201-
major=${new_version[0]}
202-
minor=${new_version[1]}
203-
patch=${new_version[2]}
204-
fi
205-
206-
break
207-
done
216+
prompt_for_version
208217
fi
209218

210-
echo "==================="
211-
echo "Prerequisite Checks"
212-
echo "==================="
219+
version="$major.$minor.$patch"
213220

214-
if ! working_tree_is_clean; then
215-
echo "Error: unclean working tree"
216-
exit 1
217-
fi
221+
tag_exists "$version" && fail "tag v$version already exists"
218222

219-
if [ "$(branch_name)" != "main" ]; then
220-
echo "Error: can only make a release on the main branch"
221-
exit 1
222-
fi
223-
224-
fetch
225-
226-
if ! remote_history_is_clean; then
227-
echo "Error: changes exist on origin not pulled into this branch. Please pull"
228-
exit 1
229-
fi
230-
231-
if ! local_history_is_clean; then
232-
echo "Error: changes exist that haven't been pushed to origin. Please pull"
233-
exit 1
234-
fi
235-
236-
if tag_exists_on_remote $major $minor $patch; then
237-
echo "Error: tag exists on remote"
238-
exit 1
239-
fi
223+
release_branch="release-$major-$minor-$patch"
240224

241-
echo "==============================="
242-
echo "Creating release for $major.$minor.$patch"
243-
echo "==============================="
225+
header "Creating release for $version"
244226

245-
create_release_branch $major $minor $patch
246-
update_readme $major $minor $patch
247-
update_ruby_version $major $minor $patch
248-
update_gemfiles $major $minor $patch
249-
echo "version: $major.$minor.$patch" > docs/_data/library.yml
250-
build_docs
251-
add_changed_files $major $minor $patch
252-
commit $major $minor $patch
253-
push $major $minor $patch
254-
exit 0
227+
git switch --quiet -C "$release_branch"
228+
update_changelog "$version"
229+
update_ruby_version "$major" "$minor" "$patch"
230+
build_docs "$version"
231+
update_lockfiles "$version"
232+
commit "$version"
233+
push "$version"
255234
}
256235

257236
main "$@"

0 commit comments

Comments
 (0)