Repository navigation
Expand file tree
/
Copy pathrelease
More file actions
executable file
·243 lines (203 loc) · 7.14 KB
/
Copy pathrelease
File metadata and controls
executable file
·243 lines (203 loc) · 7.14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
#!/usr/bin/env bash
set -Eeuo pipefail
# bundler-cache: true in setup-ruby enables frozen mode, which blocks
# updating Gemfile.lock after the gemspec version bump. Disable it for
# every bundler invocation in this script so the lockfile can be
# regenerated to match the new version.
export BUNDLE_FROZEN=false
export BUNDLE_DEPLOYMENT=false
release_branch=""
header() {
echo "==============================="
echo "$1"
echo "==============================="
}
fail() {
echo "Error: $1" >&2
exit 1
}
on_exit() {
local status=$?
[ "$status" == "0" ] && return
echo "" >&2
echo "Release failed (exit $status)." >&2
if [ -n "$release_branch" ]; then
echo "To reset and try again:" >&2
echo " git reset --hard && git clean -fd" >&2
echo " git switch main && git branch -D $release_branch" >&2
fi
}
trap on_exit EXIT
# In-place file edits via perl, since `sed -i` is incompatible between
# GNU (Linux/CI) and BSD (macOS) sed.
inplace() {
local expression=$1
shift
perl -pi -e "$expression" "$@"
}
branch_name() {
git symbolic-ref --short HEAD
}
working_tree_is_clean() {
[ -z "$(git status --porcelain)" ]
}
commits_behind_remote() {
git rev-list --count --left-only @{u}...HEAD
}
commits_ahead_of_remote() {
git rev-list --count --right-only @{u}...HEAD
}
tag_exists() {
git rev-parse --quiet --verify "refs/tags/v$1" > /dev/null ||
[ -n "$(git ls-remote --tags origin "refs/tags/v$1")" ]
}
prerequisite_checks() {
header "Prerequisite Checks"
working_tree_is_clean || fail "unclean working tree"
[ "$(branch_name)" == "main" ] || fail "can only make a release on the main branch"
git fetch --all --tags --quiet
[ "$(commits_ahead_of_remote)" == "0" ] ||
fail "changes exist that haven't been pushed to origin"
if [ "$(commits_behind_remote)" != "0" ]; then
echo "Pulling latest changes from origin..."
git pull --ff-only --quiet
fi
}
prompt_for_version() {
echo "Current version is $major.$minor.$patch."
echo "Type the number of an option to bump, or pick Manual to enter a version number"
select bump in Major Minor Patch Manual; do
case "$bump" in
Major) major=$((major + 1)); minor=0; patch=0 ;;
Minor) minor=$((minor + 1)); patch=0 ;;
Patch) patch=$((patch + 1)) ;;
*)
read -r -p "What version? (Currently $major.$minor.$patch): " new_version
[[ "$new_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || fail "version must look like X.Y.Z"
IFS=. read -r major minor patch <<< "$new_version"
;;
esac
break
done
}
update_changelog() {
VERSION="$1" perl -0777 -pi -e 's/^## main\n/## main\n\n## $ENV{VERSION}\n/m' docs/CHANGELOG.md
grep -q "^## $1\$" docs/CHANGELOG.md || fail "failed to add $1 heading to docs/CHANGELOG.md"
}
update_ruby_version() {
local pre_value=nil
[ -n "$4" ] && pre_value="\"$4\""
inplace "s/MAJOR = \\d+/MAJOR = $1/; s/MINOR = \\d+/MINOR = $2/; s/PATCH = \\d+/PATCH = $3/; s/PRE = (nil|\"[^\"]*\")/PRE = $pre_value/" \
lib/view_component/version.rb
inplace "s/DEPRECATION_HORIZON = \"\\d+\\.\\d+\\.\\d+\"/DEPRECATION_HORIZON = \"$(($1 + 1)).0.0\"/" \
lib/view_component/deprecation.rb
grep -q "MAJOR = $1\$" lib/view_component/version.rb &&
grep -q "MINOR = $2\$" lib/view_component/version.rb &&
grep -q "PATCH = $3\$" lib/view_component/version.rb &&
grep -q "PRE = $pre_value\$" lib/view_component/version.rb ||
fail "failed to update lib/view_component/version.rb"
}
update_lockfiles() {
# The only lockfile change during a release is the view_component
# version pinned by the path gemspec. Running `bundle install`/`bundle
# exec` locally can add platform-specific entries (e.g. on a dev Ruby
# without precompiled gems), and `bundle lock` from a mismatched Ruby
# produces lockfiles CI cannot consume in frozen mode. So restore the
# committed lockfiles and edit just the version in place.
local lockfiles=(Gemfile.lock gemfiles/*.gemfile.lock)
git restore --source=HEAD -- "${lockfiles[@]}"
VERSION="$1" perl -pi -e 's/^( | )view_component \([0-9][0-9A-Za-z.-]*\)/$1view_component ($ENV{VERSION})/' "${lockfiles[@]}"
local lockfile
for lockfile in "${lockfiles[@]}"; do
grep -q "view_component ($1)" "$lockfile" || fail "failed to update $lockfile"
done
}
build_docs() {
echo "version: $1" > docs/_data/library.yml
ruby script/sync_contributors.rb
# Install after the version bump, which changes the resolved gemspec.
bundle install --quiet
bundle exec rake docs:build
}
commit() {
git add \
docs/api.md \
docs/CHANGELOG.md \
docs/_data/contributors.yml \
docs/_data/library.yml \
Gemfile.lock \
gemfiles/*.gemfile.lock \
lib/view_component/deprecation.rb \
lib/view_component/version.rb
git commit --quiet -m "release $1"
}
push() {
local version=$1
if [ -n "${RELEASE_DRY_RUN:-}" ]; then
echo "RELEASE_DRY_RUN set; skipping push and PR creation."
return
fi
# Force-push: the release-X-Y-Z branch is auto-generated and is safe
# to overwrite. A previous failed run may have left the branch on the
# remote, which would otherwise reject a non-fast-forward push.
git push --force origin "$release_branch"
# Open PR (or reuse an existing one from a previous failed run).
pr_url=$(gh pr view "$release_branch" \
--repo ViewComponent/view_component \
--json url --jq .url 2>/dev/null || true)
if [ -z "$pr_url" ]; then
pr_url=$(gh pr create \
--title "Release $version" \
--body "Automated release PR for version $version." \
--base main \
--head "$release_branch" \
--repo ViewComponent/view_component)
else
echo "Reusing existing PR: $pr_url"
fi
echo "PR opened: $pr_url"
}
main() {
prerequisite_checks
major=$(grep "MAJOR = " lib/view_component/version.rb | grep -o "[0-9]\+")
minor=$(grep "MINOR = " lib/view_component/version.rb | grep -o "[0-9]\+")
patch=$(grep "PATCH = " lib/view_component/version.rb | grep -o "[0-9]\+")
pre=${4:-}
# Arguments are used by the release workflow: MAJOR MINOR PATCH [PRE]
if [ $# -gt 0 ]; then
[ $# -ge 3 ] || fail "usage: script/release [MAJOR MINOR PATCH [PRE]]"
major=$1
minor=$2
patch=$3
else
prompt_for_version
fi
for number in "$major" "$minor" "$patch"; do
[[ "$number" =~ ^(0|[1-9][0-9]*)$ ]] ||
fail "major, minor, and patch must be non-negative integers without leading zeros"
done
version="$major.$minor.$patch"
if [ -n "$pre" ]; then
version=$(ruby -e '
begin
version = Gem::Version.new(ARGV.fetch(0))
abort "pre-release version must contain a letter" unless version.prerelease?
puts version
rescue ArgumentError => error
abort error.message
end
' "$version.$pre") || fail "invalid pre-release version"
pre=${version#"$major.$minor.$patch."}
fi
tag_exists "$version" && fail "tag v$version already exists"
release_branch="release-${version//./-}"
header "Creating release for $version"
git switch --quiet -C "$release_branch"
update_changelog "$version"
update_ruby_version "$major" "$minor" "$patch" "$pre"
build_docs "$version"
update_lockfiles "$version"
commit "$version"
push "$version"
}
main "$@"