From c6356a685cccaf798799b1dca96add6ecf6321b2 Mon Sep 17 00:00:00 2001 From: Joel Hawksley Date: Fri, 16 Oct 2026 13:34:58 -0600 Subject: [PATCH 1/3] release 4.16.0 --- Gemfile.lock | 4 ++-- docs/CHANGELOG.md | 2 ++ docs/_data/contributors.yml | 5 ++++- docs/_data/library.yml | 2 +- gemfiles/rails_7.1.gemfile.lock | 2 +- gemfiles/rails_7.2.gemfile.lock | 2 +- gemfiles/rails_8.0.gemfile.lock | 2 +- gemfiles/rails_8.1.gemfile.lock | 2 +- gemfiles/rails_main.gemfile.lock | 4 ++-- gemfiles/rails_main_head.gemfile.lock | 4 ++-- lib/view_component/version.rb | 2 +- 11 files changed, 18 insertions(+), 13 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 35773929a..73d35a8e3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - view_component (4.15.0) + view_component (4.16.0) actionview (>= 7.1.0) activesupport (>= 7.1.0) concurrent-ruby (~> 1) @@ -681,7 +681,7 @@ CHECKSUMS unicode-emoji (4.3.0) sha256=11c02fa73290378c066bb0562cd4c87d8e1b706fbbe0059ca12746a4244de8ce uri (1.1.2) sha256=89292e98eabbef6248b0a0ed5e7e398e152f83f17560023a6d94a7f492b169fb useragent (0.16.11) sha256=700e6413ad4bb954bb63547fa098dddf7b0ebe75b40cc6f93b8d54255b173844 - view_component (4.15.0) + view_component (4.16.0) warning (1.6.0) sha256=a49cdfae19fb77d19afff2efbe45f8ab759e9cd25b4e4ce2c79dbaf46bdb6c9e websocket (1.2.11) sha256=b7e7a74e2410b5e85c25858b26b3322f29161e300935f70a0e0d3c35e0462737 websocket-driver (0.8.2) sha256=97c556b019bf3410b4961002ac501621e9322d3f8a7bc02161a09301cc4c4146 diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index e3902c099..7cf29eb89 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -10,6 +10,8 @@ nav_order: 6 ## main +## 4.16.0 + * 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. *Ben Sheldon* diff --git a/docs/_data/contributors.yml b/docs/_data/contributors.yml index 9f8986841..1ac83c22f 100644 --- a/docs/_data/contributors.yml +++ b/docs/_data/contributors.yml @@ -29,7 +29,6 @@ usernames: - bigbigdoudou - biow0lf - blakewilliams -- boardfish - bobermod - bobmaerten - boof @@ -46,6 +45,7 @@ usernames: - ceritium - cheshire137 - chloe-meister +- chrislo - claudiob - cllns - compostbrain @@ -83,8 +83,10 @@ usernames: - eleetyson - elia - enriikke +- erikaxel - erinnachen - fbraure +- finalburn - foca - franco - franzliedke @@ -183,6 +185,7 @@ usernames: - mikz - milk1000cc - mixergtz +- mizor - mkanakana - mkdynamic - moeki0 diff --git a/docs/_data/library.yml b/docs/_data/library.yml index 3b75288bd..c5340f102 100644 --- a/docs/_data/library.yml +++ b/docs/_data/library.yml @@ -1 +1 @@ -version: 4.15.0 +version: 4.16.0 diff --git a/gemfiles/rails_7.1.gemfile.lock b/gemfiles/rails_7.1.gemfile.lock index d96408294..e93c53ec7 100644 --- a/gemfiles/rails_7.1.gemfile.lock +++ b/gemfiles/rails_7.1.gemfile.lock @@ -1,7 +1,7 @@ PATH remote: .. specs: - view_component (4.15.0) + view_component (4.16.0) actionview (>= 7.1.0) activesupport (>= 7.1.0) concurrent-ruby (~> 1) diff --git a/gemfiles/rails_7.2.gemfile.lock b/gemfiles/rails_7.2.gemfile.lock index cf616f9cf..4b5ec5f84 100644 --- a/gemfiles/rails_7.2.gemfile.lock +++ b/gemfiles/rails_7.2.gemfile.lock @@ -1,7 +1,7 @@ PATH remote: .. specs: - view_component (4.15.0) + view_component (4.16.0) actionview (>= 7.1.0) activesupport (>= 7.1.0) concurrent-ruby (~> 1) diff --git a/gemfiles/rails_8.0.gemfile.lock b/gemfiles/rails_8.0.gemfile.lock index 4f39bc6dd..f0c15d3fb 100644 --- a/gemfiles/rails_8.0.gemfile.lock +++ b/gemfiles/rails_8.0.gemfile.lock @@ -1,7 +1,7 @@ PATH remote: .. specs: - view_component (4.15.0) + view_component (4.16.0) actionview (>= 7.1.0) activesupport (>= 7.1.0) concurrent-ruby (~> 1) diff --git a/gemfiles/rails_8.1.gemfile.lock b/gemfiles/rails_8.1.gemfile.lock index e93558eb0..82467b4bc 100644 --- a/gemfiles/rails_8.1.gemfile.lock +++ b/gemfiles/rails_8.1.gemfile.lock @@ -1,7 +1,7 @@ PATH remote: .. specs: - view_component (4.15.0) + view_component (4.16.0) actionview (>= 7.1.0) activesupport (>= 7.1.0) concurrent-ruby (~> 1) diff --git a/gemfiles/rails_main.gemfile.lock b/gemfiles/rails_main.gemfile.lock index 779fc36fb..976847651 100644 --- a/gemfiles/rails_main.gemfile.lock +++ b/gemfiles/rails_main.gemfile.lock @@ -110,7 +110,7 @@ GIT PATH remote: .. specs: - view_component (4.15.0) + view_component (4.16.0) actionview (>= 7.1.0) activesupport (>= 7.1.0) concurrent-ruby (~> 1) @@ -641,7 +641,7 @@ CHECKSUMS unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f uri (1.1.1) sha256=379fa58d27ffb1387eaada68c749d1426738bd0f654d812fcc07e7568f5c57c6 useragent (0.16.11) sha256=700e6413ad4bb954bb63547fa098dddf7b0ebe75b40cc6f93b8d54255b173844 - view_component (4.15.0) + view_component (4.16.0) warning (1.6.0) sha256=a49cdfae19fb77d19afff2efbe45f8ab759e9cd25b4e4ce2c79dbaf46bdb6c9e webrick (1.9.2) sha256=beb4a15fc474defed24a3bda4ffd88a490d517c9e4e6118c3edce59e45864131 websocket (1.2.11) sha256=b7e7a74e2410b5e85c25858b26b3322f29161e300935f70a0e0d3c35e0462737 diff --git a/gemfiles/rails_main_head.gemfile.lock b/gemfiles/rails_main_head.gemfile.lock index d4179a841..410c817a5 100644 --- a/gemfiles/rails_main_head.gemfile.lock +++ b/gemfiles/rails_main_head.gemfile.lock @@ -110,7 +110,7 @@ GIT PATH remote: .. specs: - view_component (4.15.0) + view_component (4.16.0) actionview (>= 7.1.0) activesupport (>= 7.1.0) concurrent-ruby (~> 1) @@ -585,7 +585,7 @@ CHECKSUMS unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f uri (1.1.1) sha256=379fa58d27ffb1387eaada68c749d1426738bd0f654d812fcc07e7568f5c57c6 useragent (0.16.11) sha256=700e6413ad4bb954bb63547fa098dddf7b0ebe75b40cc6f93b8d54255b173844 - view_component (4.15.0) + view_component (4.16.0) warning (1.6.0) sha256=a49cdfae19fb77d19afff2efbe45f8ab759e9cd25b4e4ce2c79dbaf46bdb6c9e webrick (1.9.2) sha256=beb4a15fc474defed24a3bda4ffd88a490d517c9e4e6118c3edce59e45864131 websocket (1.2.11) sha256=b7e7a74e2410b5e85c25858b26b3322f29161e300935f70a0e0d3c35e0462737 diff --git a/lib/view_component/version.rb b/lib/view_component/version.rb index cee9e3d37..7030af26e 100644 --- a/lib/view_component/version.rb +++ b/lib/view_component/version.rb @@ -3,7 +3,7 @@ module ViewComponent module VERSION MAJOR = 4 - MINOR = 15 + MINOR = 16 PATCH = 0 PRE = nil From d580d1eb6ebacd63ee323ec87d353a82cab01f4d Mon Sep 17 00:00:00 2001 From: Joel Hawksley Date: Fri, 16 Oct 2026 13:35:15 -0600 Subject: [PATCH 2/3] 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 --- docs/CHANGELOG.md | 4 + script/release | 297 +++++++++++++++++++++------------------------- 2 files changed, 142 insertions(+), 159 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 7cf29eb89..452f9dc07 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -12,6 +12,10 @@ nav_order: 6 ## 4.16.0 +* Fix `script/release` on macOS, stop it on the first error, and streamline its prerequisite checks. + + *Joel Hawksley* + * 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. *Ben Sheldon* diff --git a/script/release b/script/release index 7def69e6b..693283174 100755 --- a/script/release +++ b/script/release @@ -1,5 +1,7 @@ #!/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 @@ -7,85 +9,140 @@ export BUNDLE_FROZEN=false export BUNDLE_DEPLOYMENT=false -fetch() { - git fetch --all +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 } -local_history_is_clean() { - history=$(git rev-list --count --right-only @{u}...HEAD) - [ "$history" == "0" ] +working_tree_is_clean() { + [ -z "$(git status --porcelain)" ] } -remote_history_is_clean() { - history=$(git rev-list --count --left-only @{u}...HEAD) - [ "$history" == "0" ] +commits_behind_remote() { + git rev-list --count --left-only @{u}...HEAD } -tag_exists_on_remote() { - git rev-parse --quiet --verify refs/tags/$1.$2.$3 > /dev/null +commits_ahead_of_remote() { + git rev-list --count --right-only @{u}...HEAD } -working_tree_is_clean() { - status=$(git status --porcelain) - [ "$status" == "" ] +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 } -create_release_branch() { - git switch -c release-$1-$2-$3 +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" + [ "$new_version" != "$major.$minor.$patch" ] || fail "can't be the same version" + IFS=. read -r major minor patch <<< "$new_version" + ;; + esac + break + done } -update_readme() { - sed -i "/## main/ {a\\ -\\ -## $1.$2.$3 - }" docs/CHANGELOG.md +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() { - # Update version file - sed -i \ - -e "s/MAJOR = [0-9]*/MAJOR = $1/g" \ - -e "s/MINOR = [0-9]*/MINOR = $2/g" \ - -e "s/PATCH = [0-9]*/PATCH = $3/g" \ - lib/view_component/version.rb - - # Update deprecation horizon version - major=$1 - sed -i \ - -e "s/DEPRECATION_HORIZON = \"[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\"/DEPRECATION_HORIZON = \"$((major + 1)).0.0\"/g" \ - lib/view_component/deprecation.rb -} + inplace "s/MAJOR = \\d+/MAJOR = $1/; s/MINOR = \\d+/MINOR = $2/; s/PATCH = \\d+/PATCH = $3/" \ + lib/view_component/version.rb + inplace "s/DEPRECATION_HORIZON = \"\\d+\\.\\d+\\.\\d+\"/DEPRECATION_HORIZON = \"$(($1 + 1)).0.0\"/" \ + lib/view_component/deprecation.rb -update_gemfiles() { - major=$1 - minor=$2 - patch=$3 - new_version="$major.$minor.$patch" + 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 || + 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. Update it in place across every - # lockfile. Running `bundle lock` from a mismatched Ruby (e.g. ruby-head - # appraisals from Ruby 4.0) produces lockfiles CI cannot consume in - # frozen mode, so we edit them directly instead. - for lockfile in Gemfile.lock gemfiles/*.gemfile.lock; do - [ -f "$lockfile" ] || continue - sed -i \ - -e "s/^\( view_component \)([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*)/\1($new_version)/" \ - -e "s/^\( view_component \)([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*)/\1($new_version)/" \ - "$lockfile" + # 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 \(\d+\.\d+\.\d+\)/$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() { - ruby script/sync_contributors.rb || { echo "Error: sync_contributors.rb failed"; exit 1; } - bundle exec rake docs:build || { echo "Error: rake docs:build failed"; exit 1; } + 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 } -add_changed_files() { +commit() { git add \ docs/api.md \ docs/CHANGELOG.md \ @@ -93,29 +150,34 @@ add_changed_files() { docs/_data/library.yml \ Gemfile.lock \ gemfiles/*.gemfile.lock \ + lib/view_component/deprecation.rb \ lib/view_component/version.rb -} - -commit() { - git commit -m "release $1.$2.$3" + 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-$1-$2-$3 + git push --force origin "$release_branch" # Open PR (or reuse an existing one from a previous failed run). - pr_url=$(gh pr view "release-$1-$2-$3" \ + 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 $1.$2.$3" \ - --body "Automated release PR for version $1.$2.$3." \ + --title "Release $version" \ + --body "Automated release PR for version $version." \ --base main \ - --head release-$1-$2-$3 \ + --head "$release_branch" \ --repo ViewComponent/view_component) else echo "Reusing existing PR: $pr_url" @@ -138,120 +200,37 @@ push() { } 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=$(grep "PRE = " lib/view_component/version.rb | grep -o "\"[^\"]*\"" | tr -d '"') - # If arguments provided, use them (for CI/workflow usage) + # 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 - [ $# -gt 3 ] && pre=$4 || pre="" else - # Interactive mode - echo "===================" - echo "Prerequisite Checks" - echo "===================" - - if ! working_tree_is_clean; then - echo "Error: unclean working tree" - exit 1 - fi - - if [ "$(branch_name)" != "main" ]; then - echo "Error: can only make a release on the main branch" - exit 1 - fi - - fetch - - if ! remote_history_is_clean; then - echo "Error: changes exist on origin not pulled into this branch. Please pull" - exit 1 - fi - - if ! local_history_is_clean; then - echo "Error: changes exist that haven't been pushed to origin. Please pull" - exit 1 - fi - - 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 - if [ "$bump" == "Major" ]; then - major=$((major + 1)) - minor=0 - patch=0 - elif [ "$bump" == "Minor" ]; then - minor=$((minor + 1)) - patch=0 - elif [ "$bump" == "Patch" ]; then - patch=$((patch + 1)) - else - read -p "What version? (Currently $major.$minor.$patch): " new_version - if [ "$new_version" == "$major.$minor.$patch" ]; then - echo "Error: Can't be the same version" - exit 1 - fi - - new_version=(${new_version//./ }) - - major=${new_version[0]} - minor=${new_version[1]} - patch=${new_version[2]} - fi - - break - done + prompt_for_version fi - echo "===================" - echo "Prerequisite Checks" - echo "===================" + version="$major.$minor.$patch" - if ! working_tree_is_clean; then - echo "Error: unclean working tree" - exit 1 - fi + tag_exists "$version" && fail "tag v$version already exists" - if [ "$(branch_name)" != "main" ]; then - echo "Error: can only make a release on the main branch" - exit 1 - fi - - fetch - - if ! remote_history_is_clean; then - echo "Error: changes exist on origin not pulled into this branch. Please pull" - exit 1 - fi - - if ! local_history_is_clean; then - echo "Error: changes exist that haven't been pushed to origin. Please pull" - exit 1 - fi - - if tag_exists_on_remote $major $minor $patch; then - echo "Error: tag exists on remote" - exit 1 - fi + release_branch="release-$major-$minor-$patch" - echo "===============================" - echo "Creating release for $major.$minor.$patch" - echo "===============================" + header "Creating release for $version" - create_release_branch $major $minor $patch - update_readme $major $minor $patch - update_ruby_version $major $minor $patch - update_gemfiles $major $minor $patch - echo "version: $major.$minor.$patch" > docs/_data/library.yml - build_docs - add_changed_files $major $minor $patch - commit $major $minor $patch - push $major $minor $patch - exit 0 + git switch --quiet -C "$release_branch" + update_changelog "$version" + update_ruby_version "$major" "$minor" "$patch" + build_docs "$version" + update_lockfiles "$version" + commit "$version" + push "$version" } main "$@" From 33da161848bb9109948d7069208bde0bfc128259 Mon Sep 17 00:00:00 2001 From: Joel Hawksley Date: Fri, 16 Oct 2026 13:39:18 -0600 Subject: [PATCH 3/3] Remove changelog entry for script/release changes --- docs/CHANGELOG.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 452f9dc07..7cf29eb89 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -12,10 +12,6 @@ nav_order: 6 ## 4.16.0 -* Fix `script/release` on macOS, stop it on the first error, and streamline its prerequisite checks. - - *Joel Hawksley* - * 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. *Ben Sheldon*