rbs 4.1.1.pre.1-java → 4.1.2-java

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e68e5f0ead2c31e8a4ba1e8d597c4a9f6f93880edbddbf77c7a330a8a55d196f
4
- data.tar.gz: 64b5a5eb90cbfc1bf70489401d2d30e9998b1e579bcbc81f1459871c8c339023
3
+ metadata.gz: 7f6e4569588dd9eedcb1ba5870467a07929b78d4835efd094a61c0961f4452f7
4
+ data.tar.gz: e276b9379a7745f1886551f618ad059e5d75fa876729815311d40c18d8779709
5
5
  SHA512:
6
- metadata.gz: b5536aaba1814d05dfd3c6f4e3466f2c54e57f9a8544e95fb16fdd4e06d0f2867d412a17dd3679090253a3b9513f185af971acd1daf8bbf46541990f0a2ef6a8
7
- data.tar.gz: cc1f700e6b09443afe850e8c65a48f54000ce5ccc6cbf68072c837a193b9f7c63f6556c9db03752464daf7fc9376ff7053ef1c312707bfdf4da300dabbf6f34e
6
+ metadata.gz: 34148f72eaa920f876182110b854974b4554b5bc20589148254569b07201d083b3705330dee501c459b2241e5516a4166bd03013996647642c316f9a88506c0a
7
+ data.tar.gz: 2f19729a93364aeda81c447b1f232a7fbc3ad37ebd74e0a23f3d3176bbaddcf2c303c235f355d92b58be545620583b540a6ef666f8f63d5b8fa6ba40ffe4d393
@@ -64,12 +64,7 @@ jobs:
64
64
  ruby-version: jruby
65
65
  bundler: none
66
66
  - name: Install runtime and test gems
67
- # rdoc 8.0.0 added a runtime dependency on rbs, which pulls the released
68
- # C-extension rbs gem and fails to build on JRuby. Pin rdoc below 8 until
69
- # a -java rbs gem is published.
70
- run: |
71
- gem install prism rake rake-compiler test-unit rspec minitest json-schema pry --no-document
72
- gem install rdoc -v "< 8" --no-document
67
+ run: gem install prism rake rake-compiler test-unit rdoc rspec minitest json-schema pry --no-document
73
68
  # jar-dependencies resolves through the JVM, so download the Chicory/ASM
74
69
  # jars into ~/.m2 here on JRuby (jar-dependencies is not available in the
75
70
  # CRuby step above).
@@ -1,12 +1,17 @@
1
1
  name: Release gems
2
2
 
3
- # Builds, publishes, and announces a release. Dispatch it against the `vX.Y.Z` tag
4
- # of the release: the tag is created first so that everything published afterwards
5
- # traces back to an immutable ref, and so the reversible step comes before the
6
- # irreversible one.
3
+ # Builds, publishes, and announces a release. Dispatch it with the commit being
4
+ # released and the version that commit declares.
7
5
  #
8
- # Dispatching against a branch builds and verifies the gems and stops there, which
9
- # is how the build is exercised without releasing anything.
6
+ # Everything is built from `commit`, not from wherever the default branch happens to
7
+ # be when the run starts, so the release describes a state that is already immutable.
8
+ # `version` is the same fact stated a second time -- the run stops before anything is
9
+ # built unless it matches the `RBS::VERSION` of that commit, so dispatching the wrong
10
+ # commit, or the right one under the wrong name, is a failed run rather than a gem
11
+ # that has to be yanked.
12
+ #
13
+ # `dry_run` builds and checks both gems and stops before the tag, which is how the
14
+ # build is exercised without releasing anything.
10
15
  #
11
16
  # | Gem | Platform | Parser |
12
17
  # | -------------------- | ------------- | -------------------------------- |
@@ -19,11 +24,26 @@ name: Release gems
19
24
  #
20
25
  # One job on purpose. Tagging, pushing the gems, and opening the GitHub release
21
26
  # all belong to a single release, and keeping them in one place keeps their order
22
- # readable -- the tag is created before anything is published, so the reversible
23
- # step comes before the irreversible one.
27
+ # readable -- the tag is created once both gems are known to build and run, and
28
+ # before anything is published, so the reversible step comes before the irreversible
29
+ # one.
30
+ #
31
+ # The file name is what the RubyGems trusted publisher for `rbs` is registered
32
+ # against, so it cannot be renamed without registering the new name first.
24
33
 
25
34
  on:
26
35
  workflow_dispatch:
36
+ inputs:
37
+ commit:
38
+ description: "Commit to release, as a full 40-character SHA"
39
+ required: true
40
+ version:
41
+ description: "Version to release, without the leading `v` (e.g. `4.1.2`)"
42
+ required: true
43
+ dry_run:
44
+ description: "Build and check the gems without tagging or publishing anything"
45
+ type: boolean
46
+ default: false
27
47
 
28
48
  permissions:
29
49
  contents: read
@@ -38,12 +58,60 @@ jobs:
38
58
  name: release
39
59
  runs-on: ubuntu-latest
40
60
  permissions:
41
- contents: write # publish the GitHub release
61
+ contents: write # push the tag, publish the GitHub release
42
62
  id-token: write # trusted publishing to RubyGems
63
+ env:
64
+ # The inputs are read through the environment rather than interpolated into
65
+ # the scripts below.
66
+ COMMIT: ${{ inputs.commit }}
67
+ VERSION: ${{ inputs.version }}
68
+ TAG: v${{ inputs.version }}
43
69
  steps:
44
70
  # The gemspec takes its file list from `git ls-files`, so both gems are built
45
- # from the committed state.
71
+ # from the committed state -- of the dispatched commit, since that is what is
72
+ # checked out. The full history is needed to tell which branches contain it.
46
73
  - uses: actions/checkout@v7
74
+ with:
75
+ ref: ${{ inputs.commit }}
76
+ fetch-depth: 0
77
+
78
+ # Before anything is installed or built: these are the two things the release
79
+ # is named after and built from, and a mistake in either is cheapest to catch
80
+ # here.
81
+ - name: Check the inputs
82
+ run: |
83
+ if [[ ! "$COMMIT" =~ ^[0-9a-f]{40}$ ]]; then
84
+ echo "::error::\`$COMMIT\` is not a full 40-character SHA. A release names one exact commit."
85
+ exit 1
86
+ fi
87
+ if [[ ! "$VERSION" =~ ^[0-9][0-9a-zA-Z.]*$ ]]; then
88
+ echo "::error::\`$VERSION\` is not a version number. Pass it without the leading \`v\`."
89
+ exit 1
90
+ fi
91
+
92
+ # A release proper is cut from the default branch, while a patch release can
93
+ # be cut from a release branch, so which branch the commit is on is not this
94
+ # workflow's business. That it is on one is: a commit no branch contains is
95
+ # one that nothing in the repository leads to any more.
96
+ git fetch --no-tags origin "+refs/heads/*:refs/remotes/origin/*"
97
+ branches=$(git branch --remotes --contains "$COMMIT" --format "%(refname:lstrip=3)")
98
+ if [ -z "$branches" ]; then
99
+ echo "::error::$COMMIT is not on any branch."
100
+ exit 1
101
+ fi
102
+ echo "Branches containing $COMMIT:"
103
+ printf '%s\n' "$branches"
104
+
105
+ # A tag that already exists is a version that has already been released, and
106
+ # pushing it would fail after the build rather than before it.
107
+ - name: Check that the tag does not exist
108
+ if: ${{ !inputs.dry_run }}
109
+ run: |
110
+ if git ls-remote --exit-code --tags origin "refs/tags/$TAG" > /dev/null; then
111
+ echo "::error::$TAG already exists, so $VERSION has been released."
112
+ exit 1
113
+ fi
114
+
47
115
  - name: Set up Ruby
48
116
  uses: ruby/setup-ruby@v1
49
117
  with:
@@ -56,25 +124,16 @@ jobs:
56
124
  bundle config set --local without libs:profilers
57
125
  bundle install --jobs 4 --retry 3
58
126
 
59
- - name: Read the version
60
- id: version
61
- run: echo "version=$(ruby -e 'load "lib/rbs/version.rb"; print RBS::VERSION')" >> "$GITHUB_OUTPUT"
62
-
63
- # Fail before spending a minute on the build, and before anything is pushed:
64
- # the tag is what the release is named after, so it has to be the version the
65
- # tagged commit actually declares.
66
- - name: Check the tag against RBS::VERSION
67
- if: github.ref_type == 'tag'
68
- run: |
69
- if [ "${{ github.ref_name }}" != "v${{ steps.version.outputs.version }}" ]; then
70
- echo "::error::tag ${{ github.ref_name }} does not match RBS::VERSION ${{ steps.version.outputs.version }}"
71
- exit 1
72
- fi
127
+ # Fails before a minute is spent on the build, and before anything is pushed:
128
+ # the version has to be the one the released commit declares, and -- unless
129
+ # this is a `.dev.N` release -- the one CHANGELOG.md is written up for.
130
+ - name: Check the version and the changelog
131
+ run: bundle exec rake "gem:check_release[$VERSION]"
73
132
 
74
133
  - name: Build the ruby gem
75
134
  run: |
76
135
  mkdir -p pkg
77
- gem build rbs.gemspec -o "pkg/rbs-${{ steps.version.outputs.version }}.gem"
136
+ gem build rbs.gemspec -o "pkg/rbs-$VERSION.gem"
78
137
 
79
138
  # `rake wasm:jruby_setup` compiles src/**/*.c to WebAssembly and copies the
80
139
  # result to lib/rbs/wasm/, where the gemspec picks it up. clang runs as a
@@ -91,7 +150,7 @@ jobs:
91
150
  - name: Build the java gem
92
151
  env:
93
152
  RBS_PLATFORM: java
94
- run: gem build rbs.gemspec -o "pkg/rbs-${{ steps.version.outputs.version }}-java.gem"
153
+ run: gem build rbs.gemspec -o "pkg/rbs-$VERSION-java.gem"
95
154
 
96
155
  # `git ls-files` vouches for everything else, but rbs_parser.wasm is a build
97
156
  # artifact, so the java gem is the one that can come out quietly wrong.
@@ -108,8 +167,7 @@ jobs:
108
167
  raise "the java gem must not declare an extension" unless java_gem.extensions.empty?
109
168
 
110
169
  [ruby_gem, java_gem].each { puts "#{_1.full_name}: #{_1.files.size} files" }
111
- ' "pkg/rbs-${{ steps.version.outputs.version }}.gem" \
112
- "pkg/rbs-${{ steps.version.outputs.version }}-java.gem"
170
+ ' "pkg/rbs-$VERSION.gem" "pkg/rbs-$VERSION-java.gem"
113
171
 
114
172
  # The checks above cannot tell whether rbs_parser.wasm actually runs. Install
115
173
  # the gem the way a user would -- jar-dependencies fetches Chicory and ASM
@@ -122,7 +180,7 @@ jobs:
122
180
  bundler: none
123
181
  - name: Check the java gem on JRuby
124
182
  run: |
125
- gem install "pkg/rbs-${{ steps.version.outputs.version }}-java.gem"
183
+ gem install "pkg/rbs-$VERSION-java.gem"
126
184
  ruby -e '
127
185
  require "rbs"
128
186
  _, _, decls = RBS::Parser.parse_signature("class Foo end")
@@ -138,27 +196,40 @@ jobs:
138
196
  bundler: none
139
197
 
140
198
  # Uploaded before publishing, so a failed push still leaves the gems behind.
199
+ # This is also where a dry run ends.
141
200
  - uses: actions/upload-artifact@v7
142
201
  with:
143
202
  name: gems
144
203
  path: pkg/*.gem
145
204
  if-no-files-found: error
146
205
 
147
- # Everything below runs only for a release tag.
206
+ # Everything below runs only for a real release.
207
+
208
+ # The tag comes after the gems are known to build and run, and before anything
209
+ # is published: a tag can be deleted, while a version pushed to RubyGems can
210
+ # only be yanked. What it names was decided by the checkout rather than by the
211
+ # tagging, so nothing rests on it being created first.
212
+ - name: Tag the release
213
+ if: ${{ !inputs.dry_run }}
214
+ run: |
215
+ git config user.name "github-actions[bot]"
216
+ git config user.email "github-actions[bot]@users.noreply.github.com"
217
+ bundle exec rake gem:tag
218
+
148
219
  - name: Configure RubyGems credentials
149
- if: github.ref_type == 'tag'
220
+ if: ${{ !inputs.dry_run }}
150
221
  # No floating major tag on this action, so the exact release is pinned.
151
222
  uses: rubygems/configure-rubygems-credentials@v2.1.0
152
223
 
153
224
  - name: Push the gems
154
- if: github.ref_type == 'tag'
225
+ if: ${{ !inputs.dry_run }}
155
226
  run: |
156
- gem push "pkg/rbs-${{ steps.version.outputs.version }}.gem"
157
- gem push "pkg/rbs-${{ steps.version.outputs.version }}-java.gem"
227
+ gem push "pkg/rbs-$VERSION.gem"
228
+ gem push "pkg/rbs-$VERSION-java.gem"
158
229
 
159
230
  # Last, so that a failed push never announces a release that has no gems.
160
231
  - name: Publish the GitHub release
161
- if: github.ref_type == 'tag'
232
+ if: ${{ !inputs.dry_run }}
162
233
  env:
163
234
  GH_TOKEN: ${{ github.token }}
164
235
  run: bundle exec rake gem:gh_release
data/CHANGELOG.md CHANGED
@@ -1,6 +1,13 @@
1
1
  # CHANGELOG
2
2
 
3
- ## 4.1.1.pre.1 (2026-07-30)
3
+ ## 4.1.2 (2026-08-03)
4
+
5
+ ### Miscellaneous
6
+
7
+ * Keep GC disabled during GC_test#test_stress_and_stress= ([#3061](https://github.com/ruby/rbs/pull/3061))
8
+ * Fix GC_test#test_enable leaving GC disabled for the rest of the suite ([#3059](https://github.com/ruby/rbs/pull/3059))
9
+
10
+ ## 4.1.1 (2026-07-30)
4
11
 
5
12
  ### Library changes
6
13
 
@@ -99,6 +106,14 @@ This release also introduces `RBS::Rewriter`, an API to edit RBS source text whi
99
106
  * ci: skip Gemfile.lock BUNDLED WITH on ruby-head ([#2952](https://github.com/ruby/rbs/pull/2952))
100
107
  * Remove `logger` from sig dependencies ([#2904](https://github.com/ruby/rbs/pull/2904))
101
108
 
109
+ ## 4.0.3 (2026-06-18)
110
+
111
+ ### Miscellaneous
112
+
113
+ * Fix Ruby CI failure with compressed `Zlib::GzipReader` test fixtures. ([#3005](https://github.com/ruby/rbs/pull/3005))
114
+ * Fix flaky `DirSingletonTest#test_fchdir` and `DirSingletonTest#test_for_fd` under aggressive GC. ([#3005](https://github.com/ruby/rbs/pull/3005))
115
+ * Fix Ruby head CI failure caused by the lockfile-pinned Bundler version. ([#3005](https://github.com/ruby/rbs/pull/3005))
116
+
102
117
  ## 4.0.2 (2026-03-25)
103
118
 
104
119
  ### Library changes
data/README.md CHANGED
@@ -196,7 +196,7 @@ Here is a list of some places you can talk with active maintainers.
196
196
 
197
197
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
198
198
 
199
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
199
+ To install this gem onto your local machine, run `bundle exec rake install`. Releases are cut by the `Release gems` workflow rather than from a working copy -- see [docs/release.md](docs/release.md).
200
200
 
201
201
  ### C Code Formatting
202
202
 
data/Rakefile CHANGED
@@ -438,73 +438,6 @@ task :test_generate_stdlib do
438
438
  sh "ruby -c /tmp/Thread_Mutex_test.rb"
439
439
  end
440
440
 
441
- Rake::Task[:release].enhance do
442
- Rake::Task[:"release:note"].invoke
443
- end
444
-
445
- namespace :release do
446
- desc "Explain the post-release steps automatically"
447
- task :note do
448
- version = Gem::Version.new(RBS::VERSION)
449
- major, minor, patch, *_ = RBS::VERSION.split(".")
450
- major = major.to_i
451
- minor = minor.to_i
452
- patch = patch.to_i
453
-
454
- puts "🎉🎉🎉🎉 Congratulations for **#{version}** release! 🎉🎉🎉🎉"
455
- puts
456
- puts "There are a few things left to complete the release. 💪"
457
- puts
458
-
459
- if patch == 0 || version.prerelease?
460
- puts "* [ ] Update release note: https://github.com/ruby/rbs/wiki/Release-Note-#{major}.#{minor}"
461
- end
462
-
463
- if patch == 0 && !version.prerelease?
464
- puts "* [ ] Delete `RBS XYZ is the latest version of...` from release note: https://github.com/ruby/rbs/wiki/Release-Note-#{major}.#{minor}"
465
- end
466
-
467
- puts "* [ ] Publish a release at GitHub"
468
- puts "* [ ] Make some announcements on Twitter/Mustdon/Slack/???"
469
-
470
- puts
471
- puts
472
-
473
- puts "✏️ Making a draft release on GitHub..."
474
-
475
- content = File.read(File.join(__dir__, "CHANGELOG.md"))
476
- changelog = content.scan(/^## \d.*?(?=^## \d)/m)[0]
477
- changelog = changelog.sub(/^.*\n^.*\n/, "").rstrip
478
-
479
- notes = <<NOTES
480
- [Release note](https://github.com/ruby/rbs/wiki/Release-Note-#{major}.#{minor})
481
-
482
- #{changelog}
483
- NOTES
484
-
485
- command = [
486
- "gh",
487
- "release",
488
- "create",
489
- "--draft",
490
- "v#{RBS::VERSION}",
491
- "--title=#{RBS::VERSION}",
492
- "--notes=#{notes}"
493
- ]
494
-
495
- if version.prerelease?
496
- command << "--prerelease"
497
- end
498
-
499
- require "open3"
500
- output, status = Open3.capture2(*command)
501
- if status.success?
502
- puts " >> Done! Open #{output.chomp} and publish the release!"
503
- end
504
- end
505
- end
506
-
507
-
508
441
  # Pull requests with one of these labels are omitted from the changelog.
509
442
  CHANGELOG_SKIP_LABELS = ["skip-changelog"]
510
443
 
@@ -594,20 +527,54 @@ def changelog_commits(from, paths: [])
594
527
  output.lines.map(&:chomp).reject(&:empty?)
595
528
  end
596
529
 
597
- # Finds the pull requests the commits came from, keeping the order of `commits`.
530
+ # What `git cherry-pick -x` appends to the message of the commit it creates.
531
+ CHERRY_PICK_ORIGIN = /^\(cherry picked from commit ([0-9a-f]{40})\)$/
532
+
533
+ # Maps the commits that record where they were cherry-picked from to that commit.
598
534
  #
599
- # Returns the pull requests for the changelog and the ones omitted by `skip_labels`.
535
+ # A backport is a cherry-pick, so on a release branch it is the recorded origin, not the commit
536
+ # itself, that leads to the pull request the change was written and reviewed in. Without this a
537
+ # backported change is attributed to the pull request that carried the backport, which says
538
+ # nothing about the change and is the same for every commit it brought over.
600
539
  #
601
- def changelog_pull_requests(commits, skip_labels: CHANGELOG_SKIP_LABELS)
602
- pull_requests = {}
603
- skipped = {}
540
+ # A commit backported twice -- the development line, then a release branch -- carries one line
541
+ # per hop, appended in order, so the first one is where the change started.
542
+ #
543
+ def changelog_origins(commits)
544
+ return {} if commits.empty?
545
+
546
+ require "open3"
547
+
548
+ # `--no-walk` prints these commits and nothing else. NUL delimiters keep a commit message --
549
+ # which can contain anything, including what this format looks like -- from being read as the
550
+ # format itself.
551
+ output, status = Open3.capture2("git", "log", "--no-walk", "--format=%H%x00%B%x00", *commits, binmode: true)
552
+ raise status.inspect unless status.success?
553
+
554
+ # Commit messages are UTF-8, while the default external encoding follows the locale. Without
555
+ # this, splitting a message that is not ASCII fails under `LANG=C`, as in GitHub Actions.
556
+ output.force_encoding(Encoding::UTF_8)
604
557
 
605
- commits.each_slice(50) do |slice|
606
- # Ask GitHub which pull requests the commits came from, so that any merge strategy -- merge
607
- # commit, squash, or rebase -- is handled without parsing commit messages.
608
- aliases = slice.map.with_index do |commit, index|
558
+ output.split("\0").each_slice(2).each_with_object({}) do |(commit, message), origins|
559
+ commit = commit.to_s.strip
560
+ next if commit.empty?
561
+
562
+ origin = message.to_s[CHERRY_PICK_ORIGIN, 1] or next
563
+ origins[commit] = origin
564
+ end
565
+ end
566
+
567
+ # Asks GitHub which pull requests each commit came from, so that any merge strategy -- merge
568
+ # commit, squash, or rebase -- is handled without parsing commit messages.
569
+ #
570
+ # Returns `{ oid => [pull request, ...] }`, with an empty array for the commits GitHub has no
571
+ # merged pull request for, including the ones it does not know at all.
572
+ #
573
+ def changelog_associated_pull_requests(oids)
574
+ oids.uniq.each_slice(50).each_with_object({}) do |slice, found|
575
+ aliases = slice.map.with_index do |oid, index|
609
576
  <<~GRAPHQL
610
- c#{index}: object(oid: "#{commit}") {
577
+ c#{index}: object(oid: "#{oid}") {
611
578
  ... on Commit {
612
579
  associatedPullRequests(first: 10) {
613
580
  nodes {
@@ -620,18 +587,44 @@ def changelog_pull_requests(commits, skip_labels: CHANGELOG_SKIP_LABELS)
620
587
  GRAPHQL
621
588
  end
622
589
 
623
- changelog_graphql(aliases.join("\n")).each_value do |commit|
624
- next unless commit
590
+ response = changelog_graphql(aliases.join("\n"))
625
591
 
626
- commit.dig(:associatedPullRequests, :nodes).each do |pr|
627
- next unless pr[:merged]
592
+ slice.each_with_index do |oid, index|
593
+ nodes = response.dig(:"c#{index}", :associatedPullRequests, :nodes) || []
628
594
 
629
- pr = { number: pr[:number], title: pr[:title], url: pr[:url], labels: pr.dig(:labels, :nodes).map { |label| label[:name] } }
630
- if (pr[:labels] & skip_labels).empty?
631
- pull_requests[pr[:number]] ||= pr
632
- else
633
- skipped[pr[:number]] ||= pr
634
- end
595
+ found[oid] = nodes.select { |pr| pr[:merged] }.map do |pr|
596
+ { number: pr[:number], title: pr[:title], url: pr[:url], labels: pr.dig(:labels, :nodes).map { |label| label[:name] } }
597
+ end
598
+ end
599
+ end
600
+ end
601
+
602
+ # Finds the pull requests the commits came from, keeping the order of `commits`.
603
+ #
604
+ # Returns the pull requests for the changelog and the ones omitted by `skip_labels`.
605
+ #
606
+ def changelog_pull_requests(commits, skip_labels: CHANGELOG_SKIP_LABELS)
607
+ origins = changelog_origins(commits)
608
+ found = changelog_associated_pull_requests(commits.map { |commit| origins[commit] || commit })
609
+
610
+ # An origin that leads nowhere -- a commit cherry-picked from a fork, or one that went to the
611
+ # default branch without a pull request -- falls back to the commit in this history, which is
612
+ # at least the backport that brought it here.
613
+ fallbacks = commits.select { |commit| origins[commit] && found.fetch(origins[commit], []).empty? }
614
+ found.update(changelog_associated_pull_requests(fallbacks)) unless fallbacks.empty?
615
+
616
+ pull_requests = {}
617
+ skipped = {}
618
+
619
+ commits.each do |commit|
620
+ prs = found.fetch(origins[commit] || commit, [])
621
+ prs = found.fetch(commit, []) if prs.empty?
622
+
623
+ prs.each do |pr|
624
+ if (pr[:labels] & skip_labels).empty?
625
+ pull_requests[pr[:number]] ||= pr
626
+ else
627
+ skipped[pr[:number]] ||= pr
635
628
  end
636
629
  end
637
630
  end
@@ -779,6 +772,60 @@ namespace :gem do
779
772
  end
780
773
  end
781
774
 
775
+ # There are three kinds of release: `X.Y.Z`, `X.Y.Z.pre.N`, and `X.Y.Z.dev.N`. The
776
+ # `.dev.N` ones are cut from the development line for people who need a specific
777
+ # change early; they are not written up in the changelog, so there are no notes to
778
+ # publish and nothing worth announcing.
779
+ def dev_release?(version)
780
+ Gem::Version.new(version).segments.include?("dev")
781
+ end
782
+
783
+ # The body of the topmost section of CHANGELOG.md, which is the release being
784
+ # prepared, minus its own heading.
785
+ #
786
+ # The encoding is explicit because the default external encoding follows the
787
+ # locale, and the changelog is not ASCII.
788
+ #
789
+ def changelog_section(version)
790
+ content = File.read(File.join(__dir__, "CHANGELOG.md"), encoding: Encoding::UTF_8)
791
+ section = content.scan(/^## \d.*?(?=^## \d)/m)[0] or raise "🚨 Cannot find a release section in CHANGELOG.md"
792
+ heading, _, body = section.partition("\n")
793
+ heading.include?(version) or raise "🚨 CHANGELOG.md starts with `#{heading.strip}`, which is not #{version}"
794
+ body.strip
795
+ end
796
+
797
+ desc "Check that the working tree is ready to be released as the given version"
798
+ task :check_release, [:version] do |_task, args|
799
+ version = args[:version] or raise "🚨 Pass the version being released: `rake 'gem:check_release[4.1.2]'`"
800
+ Gem::Version.correct?(version) or raise "🚨 `#{version}` is not a version number."
801
+
802
+ # The version being released and the version the commit declares are stated
803
+ # separately -- one by whoever starts the release, one by the commit itself --
804
+ # so that releasing the wrong commit, or releasing the right one under the wrong
805
+ # name, fails here rather than on RubyGems.
806
+ version == RBS::VERSION or
807
+ raise "🚨 Releasing #{version}, but this commit declares `RBS::VERSION = #{RBS::VERSION.inspect}`."
808
+
809
+ if dev_release?(version)
810
+ puts "✅ #{version} is the version of this commit. It is a dev release, so CHANGELOG.md is not checked."
811
+ else
812
+ changelog_section(version)
813
+ puts "✅ #{version} is the version of this commit, and CHANGELOG.md documents it."
814
+ end
815
+ end
816
+
817
+ desc "Create and push the `vX.Y.Z` tag for RBS::VERSION"
818
+ task :tag do
819
+ tag = "v#{RBS::VERSION}"
820
+
821
+ # Annotated, so that the tag carries its own author and date rather than
822
+ # borrowing the tagged commit's.
823
+ sh "git", "tag", "--annotate", "--message", "RBS #{RBS::VERSION}", tag
824
+ sh "git", "push", "origin", tag
825
+
826
+ puts "🏷️ Pushed #{tag}."
827
+ end
828
+
782
829
  desc "Publish the GitHub release for RBS::VERSION, unless it is a `.dev.` version"
783
830
  task :gh_release do
784
831
  require "open3"
@@ -787,11 +834,7 @@ namespace :gem do
787
834
  major, minor, *_ = RBS::VERSION.split(".")
788
835
  tag = "v#{RBS::VERSION}"
789
836
 
790
- # There are three kinds of release: `X.Y.Z`, `X.Y.Z.pre.N`, and `X.Y.Z.dev.N`.
791
- # The `.dev.N` ones are cut from the development line for people who need a
792
- # specific change early; they are not written up in the changelog, so there are
793
- # no notes to publish and nothing worth announcing.
794
- if version.segments.include?("dev")
837
+ if dev_release?(RBS::VERSION)
795
838
  puts "⏭️ #{RBS::VERSION} is a dev release, so there is no GitHub release to publish."
796
839
  next
797
840
  end
@@ -801,18 +844,10 @@ namespace :gem do
801
844
  _, status = Open3.capture2("git", "rev-parse", "--verify", "--quiet", "#{tag}^{commit}")
802
845
  raise "🚨 No such tag: `#{tag}`. Tag the release before creating the GitHub release." unless status.success?
803
846
 
804
- # The topmost section of the changelog is this release, minus its own heading.
805
- # The encoding is explicit because the default external encoding follows the
806
- # locale, and the changelog is not ASCII.
807
- content = File.read(File.join(__dir__, "CHANGELOG.md"), encoding: Encoding::UTF_8)
808
- section = content.scan(/^## \d.*?(?=^## \d)/m)[0] or raise "🚨 Cannot find a release section in CHANGELOG.md"
809
- heading, _, body = section.partition("\n")
810
- heading.include?(RBS::VERSION) or raise "🚨 CHANGELOG.md starts with `#{heading.strip}`, which is not #{RBS::VERSION}"
811
-
812
847
  notes = <<~NOTES
813
848
  [Release note](https://github.com/ruby/rbs/wiki/Release-Note-#{major}.#{minor})
814
849
 
815
- #{body.strip}
850
+ #{changelog_section(RBS::VERSION)}
816
851
  NOTES
817
852
 
818
853
  # Published rather than drafted: the notes are the changelog section that was
data/docs/release.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Releasing RBS
2
2
 
3
- A release is a pull request, a tag, and one workflow run. Everything that leaves
4
- the repository — both gems and the GitHub release — is produced by the `Release
5
- gems` workflow, so nothing has to be built or pushed from a laptop.
3
+ A release is a pull request and one workflow run. Everything that leaves the
4
+ repository — the tag, both gems, and the GitHub release — is produced by the
5
+ `Release gems` workflow, so nothing has to be built or pushed from a laptop.
6
6
 
7
7
  Each release ships **two gems**:
8
8
 
@@ -36,6 +36,14 @@ is what lets you dispatch the workflow.
36
36
 
37
37
  ## Steps
38
38
 
39
+ The release pull request in step 1 is merged by a person who has reviewed it. Its merge commit is
40
+ what step 2 dispatches, tags, and pushes to RubyGems, and none of that can be taken back — so
41
+ prepare that pull request and stop there, rather than merging it and carrying on to step 2.
42
+
43
+ The bump that starts a new minor is the only other pull request that sets `RBS::VERSION`. It
44
+ publishes nothing and another bump undoes it, so one opened on an explicit request can go through
45
+ on its own.
46
+
39
47
  ### 1. Prepare the release
40
48
 
41
49
  Open a pull request that carries everything the release needs:
@@ -97,26 +105,29 @@ on a small release. Two things scale with the size of the release:
97
105
  The date is the day the gem is released, matching the `vX.Y.Z` tag — not the day this pull request
98
106
  is opened. Fix it up before step 2 if the pull request sat for a few days.
99
107
 
100
- ### 2. Tag the release
108
+ ### 2. Run the `Release gems` workflow
101
109
 
102
- Once the pull request is merged, tag the merge commit and push the tag:
110
+ Once the pull request is merged, dispatch
111
+ [`release-gems.yml`](../.github/workflows/release-gems.yml) from the Actions tab with two inputs:
103
112
 
104
- ```console
105
- $ git switch master && git pull
106
- $ git tag "v$(ruby -e 'load "lib/rbs/version.rb"; print RBS::VERSION')"
107
- $ git push origin --tags
108
- ```
113
+ | Input | Value |
114
+ | --- | --- |
115
+ | `commit` | The full 40-character SHA of the merge commit, taken from the merged pull request |
116
+ | `version` | `X.Y.Z`, without the leading `v` |
109
117
 
110
- The tag comes before anything is published, so that the gems and the release notes describe a
111
- commit that is already immutable and because a tag can be deleted, while a version pushed to
112
- RubyGems can only be yanked.
118
+ The ref selector picks which copy of the workflow file runs, not what gets released leave it on
119
+ `master`. Everything is built from `commit`, so the run is unaffected by whatever lands on `master`
120
+ in the meantime, and a patch release cut from a release branch is dispatched the same way as any
121
+ other: the workflow does not care which branch the commit is on.
113
122
 
114
- ### 3. Run the `Release gems` workflow against the tag
123
+ The two inputs say the same thing twice, once as a commit and once as a name, and the run stops
124
+ before anything is built unless they agree with each other and with the repository:
115
125
 
116
- Dispatch [`release-gems.yml`](../.github/workflows/release-gems.yml) from the Actions tab, picking
117
- the `vX.Y.Z` tag **not** a branch — in the ref selector. The trusted publisher has no branch
118
- condition, so the ref you pick is what decides what gets published; the workflow refuses to run
119
- unless the tag matches `RBS::VERSION`.
126
+ - `commit` has to be a full SHA that some branch contains,
127
+ - `version` has to be the `RBS::VERSION` that commit declares,
128
+ - CHANGELOG.md has to start with a section for `version` (skipped for `.dev.N`, which is not
129
+ written up),
130
+ - `vX.Y.Z` must not exist yet.
120
131
 
121
132
  It then:
122
133
 
@@ -127,25 +138,110 @@ It then:
127
138
  - installs the `java` gem on JRuby and parses with it, so the WebAssembly runtime is exercised
128
139
  before anything is published,
129
140
  - uploads both gems as an artifact,
130
- - pushes both to RubyGems through trusted publishing,
141
+ - tags `commit` as `vX.Y.Z` and pushes the tag,
142
+ - pushes both gems to RubyGems through trusted publishing,
131
143
  - publishes the GitHub release with the notes from CHANGELOG.md, skipping this last step for
132
144
  `.dev.N` versions.
133
145
 
134
- Dispatching against a branch runs everything up to the artifact and stops, which is how the build
135
- is exercised without releasing.
146
+ The tag is created once both gems are known to build and run, and before anything is published: a
147
+ tag can be deleted, while a version pushed to RubyGems can only be yanked.
148
+
149
+ Checking the `dry_run` box runs everything up to the artifact and stops — no tag, no gems pushed,
150
+ no release — which is how the build is exercised without releasing. `version` still has to match
151
+ the commit, so a dry run is also how a release is rehearsed before it is cut.
152
+
153
+ ## The version on `master`
154
+
155
+ `RBS::VERSION` on `master` is read one of two ways, told apart by how the version ends:
156
+
157
+ | On `master` | Means |
158
+ | --- | --- |
159
+ | `X.Y.0.dev` — a bare `.dev` | `X.Y.0` is being developed |
160
+ | A complete version — `X.Y.Z`, `X.Y.Z.pre.N`, `X.Y.Z.dev.N` | The version *after* the one named is being developed |
161
+
162
+ So `4.1.1` on `master` is not a claim that `master` is 4.1.1. It says 4.1.1 has shipped and what
163
+ comes after it is being worked on. `4.1.2.dev.1` says the same thing about itself: that release is
164
+ out, and the line continues towards 4.1.2.
165
+
166
+ Both become true the moment the release is tagged, so **nothing has to be done to `master` after a
167
+ release**. `4.0.1` was followed by `4.0.2` with no version change in between, and `4.1.2.dev.1` is
168
+ what `master` carries today.
169
+
170
+ The bare `X.Y.0.dev` is the exception because it is the one version that names a target rather than
171
+ a predecessor: a new minor is developed towards `X.Y.0` for a long time, before it is known whether
172
+ the next thing to ship is `X.Y.0.pre.1` or `X.Y.0` itself. Setting it is the only version change
173
+ that has to be made deliberately.
174
+
175
+ `rake gem:changelog` reads `RBS::VERSION` too, to decide where the next changelog starts — but the
176
+ version is set to the one being released before the changelog is generated, so it sees that rather
177
+ than whatever `master` was carrying.
136
178
 
137
- ### 4. Start the next development cycle
179
+ ## Starting a new minor
180
+
181
+ `master` is the development line of one minor at a time. Moving it from `X.Y` to `X.(Y+1)` is not
182
+ part of any one release — it is the decision that the `X.Y` line is done, taken whenever that
183
+ becomes true — and it is the one moment the version on `master` is changed by hand. Two changes, in
184
+ opposite places:
185
+
186
+ 1. **Branch the line being left behind**, from the last `master` commit that belongs to it:
187
+
188
+ ```console
189
+ $ git switch --create aaa-X.Y.x <that commit>
190
+ $ git push -u origin aaa-X.Y.x
191
+ ```
192
+
193
+ Branch from the commit *before* the bump below, so the branch keeps the version its line was
194
+ released under. Patch releases of `X.Y` are cut from here from now on, with their changes
195
+ cherry-picked from `master` — see [Backports](#backports). The `aaa-` prefix carries no meaning
196
+ beyond sorting the release branches to the top of the branch list.
197
+
198
+ 2. **Bump `master`** to `X.(Y+1).0.dev`, in a pull request with `Gemfile.lock` regenerated and
199
+ labeled `skip-changelog` like the release pull request itself. `4.1` was started exactly this
200
+ way: `aaa-4.0.x` was branched at the commit before `Start 4.1 development`, which set
201
+ `RBS::VERSION` to `4.1.0.dev`.
202
+
203
+ Two loose ends that are easy to forget:
204
+
205
+ - **The release note of the new line.** `rake gem:gh_release` links every published release to
206
+ `https://github.com/ruby/rbs/wiki/Release-Note-X.Y`, built from the version number without
207
+ checking that the page is there. Nothing has to be written when the line starts — the page comes
208
+ together as the first release proper of the line comes into view — but it does have to exist by
209
+ the time that release is published, or its notes link to an empty page.
210
+ - **Release branches that are done.** A branch is worth keeping only while its line might still
211
+ get a patch. The ones that exist do not cover every line that ever had one — `3.8.1` shipped and
212
+ there is no `aaa-3.8.x` — so this is housekeeping rather than a rule, but starting a new minor is
213
+ the natural moment to look at the bottom of the branch list and delete what has been superseded.
214
+
215
+ ## Backports
216
+
217
+ A patch release is cut from a release branch (`aaa-X.Y.x`), and what it carries beyond the previous
218
+ release is cherry-picked from the development line. Cherry-pick with `-x`:
219
+
220
+ ```console
221
+ $ git cherry-pick -x <commit>
222
+ ```
138
223
 
139
- Open another pull request setting `RBS::VERSION` to the next prerelease (`4.1.1` `4.1.2.pre`),
140
- with `Gemfile.lock` regenerated, labeled `skip-changelog` like the release pull request itself.
141
- Without it the version on `master` keeps claiming to be the released version for the whole
142
- development period, and `rake gem:changelog` reads that version to decide where the next changelog
143
- starts.
224
+ `-x` records the commit the change was copied from, and that recorded line is what `rake
225
+ gem:changelog` follows to reach the pull request the change was written and reviewed in. Without
226
+ it, the only pull request a backported commit is associated with is the one that carried the
227
+ backport, which says nothing about the change and is the same for every commit it brought over
228
+ that is why the 4.0.3 changelog credits its three entries to the same pull request.
144
229
 
145
230
  ## Notes
146
231
 
147
232
  - Prereleases (`X.Y.Z.pre.N`) are only installed with `gem install rbs --pre`;
148
233
  a plain `gem install rbs` is unaffected. On JRuby, `gem install rbs [--pre]`
149
234
  resolves to the `-java` gem automatically.
150
- - `Dockerfile.jruby` pins the WASI SDK / Chicory / ASM versions to match the
151
- `wasm`, `jruby`, and `release-gems` workflows. Keep them in sync when bumping.
235
+ - The WASI SDK version is pinned in `wasm.yml`, `jruby.yml`, `release-gems.yml`, and
236
+ `Dockerfile.jruby`, each carrying its own copy. Keep them in sync when bumping. The
237
+ Chicory/ASM versions are not duplicated: they are the `jar` requirements in
238
+ `rbs.gemspec`, which is where the workflow, `Dockerfile.jruby` and `gem install` all
239
+ read them from.
240
+ - `rake 'gem:check_release[X.Y.Z]'` and `rake gem:tag` are what the workflow runs to
241
+ check the release and to create the tag. Both work locally, which is the fallback
242
+ if the tag ever has to be created by hand.
243
+ - Those two tasks and `rake gem:gh_release` come from the Rakefile of the commit
244
+ being released, not from the branch the workflow was dispatched from. Releasing
245
+ from a release branch (`aaa-X.Y.x`) therefore needs the release tooling on that
246
+ branch as well; without it the run fails on the missing task, before publishing
247
+ anything.
data/lib/rbs/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RBS
4
- VERSION = "4.1.1.pre.1"
4
+ VERSION = "4.1.2"
5
5
  end
data/wasm/README.md CHANGED
@@ -27,6 +27,39 @@ $ rake wasm:install_jars # download the Chicory/ASM jars into ~/.m2 (run on JRub
27
27
 
28
28
  The compiled `rbs_parser.wasm` is a build artifact and is not checked in.
29
29
 
30
+ The WASI SDK is needed for the *build*, not for running the result — the host clang already
31
+ knows the `wasm32` target, but there is no wasm32 libc on a normal machine, so it picks up the
32
+ host headers and fails on the first `#include`. That is what the SDK supplies, along with the
33
+ builtins the link step needs.
34
+
35
+ ## Running the suite on JRuby
36
+
37
+ [`Dockerfile.jruby`](../Dockerfile.jruby) builds an image that has everything this needs, so no
38
+ JRuby, JDK or WASI SDK has to be installed to work on the JRuby side:
39
+
40
+ ```console
41
+ $ docker build -f Dockerfile.jruby -t rbs-jruby .
42
+ $ docker run --rm rbs-jruby # run the test suite
43
+ $ docker run --rm -e RBS_PLATFORM=java rbs-jruby \
44
+ gem build rbs.gemspec # build the -java gem
45
+ ```
46
+
47
+ Two things in it are not obvious:
48
+
49
+ - `build-essential` is for prism, which builds `libprism.so` and loads it through FFI on JRuby
50
+ rather than as an MRI C extension. It needs `cc` and `make`.
51
+ - Bundler is skipped. The development `Gemfile` pulls in CRuby-only C extensions (bigdecimal,
52
+ stackprof, …) that cannot build on JRuby, so the few gems the suite needs are installed
53
+ directly, in the same set as [`jruby.yml`](../.github/workflows/jruby.yml).
54
+
55
+ The image compiles `rbs_parser.wasm` itself, which is why it carries the WASI SDK. That is not
56
+ the only arrangement: the build needs the SDK but not JRuby, and running the suite needs JRuby
57
+ but not the SDK, so `jruby.yml` splits them instead — it compiles the module on CRuby and then
58
+ switches engines to test against the result.
59
+
60
+ `rake wasm:install_jars` is the step that has to be on JRuby either way: it resolves the `jar`
61
+ requirements from `rbs.gemspec` through the JVM.
62
+
30
63
  ## Exported functions
31
64
 
32
65
  The module is built as a "reactor": it has no `main`, and the host calls
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rbs
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.1.1.pre.1
4
+ version: 4.1.2
5
5
  platform: java
6
6
  authors:
7
7
  - Soutaro Matsumoto