Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ Here is a list of some places you can talk with active maintainers.

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.

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).
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).

### C Code Formatting

Expand Down
67 changes: 0 additions & 67 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -438,73 +438,6 @@ task :test_generate_stdlib do
sh "ruby -c /tmp/Thread_Mutex_test.rb"
end

Rake::Task[:release].enhance do
Rake::Task[:"release:note"].invoke
end

namespace :release do
desc "Explain the post-release steps automatically"
task :note do
version = Gem::Version.new(RBS::VERSION)
major, minor, patch, *_ = RBS::VERSION.split(".")
major = major.to_i
minor = minor.to_i
patch = patch.to_i

puts "🎉🎉🎉🎉 Congratulations for **#{version}** release! 🎉🎉🎉🎉"
puts
puts "There are a few things left to complete the release. 💪"
puts

if patch == 0 || version.prerelease?
puts "* [ ] Update release note: https://github.com/ruby/rbs/wiki/Release-Note-#{major}.#{minor}"
end

if patch == 0 && !version.prerelease?
puts "* [ ] Delete `RBS XYZ is the latest version of...` from release note: https://github.com/ruby/rbs/wiki/Release-Note-#{major}.#{minor}"
end

puts "* [ ] Publish a release at GitHub"
puts "* [ ] Make some announcements on Twitter/Mustdon/Slack/???"

puts
puts

puts "✏️ Making a draft release on GitHub..."

content = File.read(File.join(__dir__, "CHANGELOG.md"))
changelog = content.scan(/^## \d.*?(?=^## \d)/m)[0]
changelog = changelog.sub(/^.*\n^.*\n/, "").rstrip

notes = <<NOTES
[Release note](https://github.com/ruby/rbs/wiki/Release-Note-#{major}.#{minor})

#{changelog}
NOTES

command = [
"gh",
"release",
"create",
"--draft",
"v#{RBS::VERSION}",
"--title=#{RBS::VERSION}",
"--notes=#{notes}"
]

if version.prerelease?
command << "--prerelease"
end

require "open3"
output, status = Open3.capture2(*command)
if status.success?
puts " >> Done! Open #{output.chomp} and publish the release!"
end
end
end


# Pull requests with one of these labels are omitted from the changelog.
CHANGELOG_SKIP_LABELS = ["skip-changelog"]

Expand Down
81 changes: 73 additions & 8 deletions docs/release.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ is what lets you dispatch the workflow.

## Steps

The release pull request in step 1 is merged by a person who has reviewed it. Its merge commit is
what step 2 dispatches, tags, and pushes to RubyGems, and none of that can be taken back — so
prepare that pull request and stop there, rather than merging it and carrying on to step 2.

The bump that starts a new minor is the only other pull request that sets `RBS::VERSION`. It
publishes nothing and another bump undoes it, so one opened on an explicit request can go through
on its own.

### 1. Prepare the release

Open a pull request that carries everything the release needs:
Expand Down Expand Up @@ -142,13 +150,67 @@ Checking the `dry_run` box runs everything up to the artifact and stops — no t
no release — which is how the build is exercised without releasing. `version` still has to match
the commit, so a dry run is also how a release is rehearsed before it is cut.

### 3. Start the next development cycle
## The version on `master`

Open another pull request setting `RBS::VERSION` to the next prerelease (`4.1.1` → `4.1.2.pre`),
with `Gemfile.lock` regenerated, labeled `skip-changelog` like the release pull request itself.
Without it the version on `master` keeps claiming to be the released version for the whole
development period, and `rake gem:changelog` reads that version to decide where the next changelog
starts.
`RBS::VERSION` on `master` is read one of two ways, told apart by how the version ends:

| On `master` | Means |
| --- | --- |
| `X.Y.0.dev` — a bare `.dev` | `X.Y.0` is being developed |
| 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 |

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
comes after it is being worked on. `4.1.2.dev.1` says the same thing about itself: that release is
out, and the line continues towards 4.1.2.

Both become true the moment the release is tagged, so **nothing has to be done to `master` after a
release**. `4.0.1` was followed by `4.0.2` with no version change in between, and `4.1.2.dev.1` is
what `master` carries today.

The bare `X.Y.0.dev` is the exception because it is the one version that names a target rather than
a predecessor: a new minor is developed towards `X.Y.0` for a long time, before it is known whether
the next thing to ship is `X.Y.0.pre.1` or `X.Y.0` itself. Setting it is the only version change
that has to be made deliberately.

`rake gem:changelog` reads `RBS::VERSION` too, to decide where the next changelog starts — but the
version is set to the one being released before the changelog is generated, so it sees that rather
than whatever `master` was carrying.

## Starting a new minor

`master` is the development line of one minor at a time. Moving it from `X.Y` to `X.(Y+1)` is not
part of any one release — it is the decision that the `X.Y` line is done, taken whenever that
becomes true — and it is the one moment the version on `master` is changed by hand. Two changes, in
opposite places:

1. **Branch the line being left behind**, from the last `master` commit that belongs to it:

```console
$ git switch --create aaa-X.Y.x <that commit>
$ git push -u origin aaa-X.Y.x
```

Branch from the commit *before* the bump below, so the branch keeps the version its line was
released under. Patch releases of `X.Y` are cut from here from now on, with their changes
cherry-picked from `master` — see [Backports](#backports). The `aaa-` prefix carries no meaning
beyond sorting the release branches to the top of the branch list.

2. **Bump `master`** to `X.(Y+1).0.dev`, in a pull request with `Gemfile.lock` regenerated and
labeled `skip-changelog` like the release pull request itself. `4.1` was started exactly this
way: `aaa-4.0.x` was branched at the commit before `Start 4.1 development`, which set
`RBS::VERSION` to `4.1.0.dev`.

Two loose ends that are easy to forget:

- **The release note of the new line.** `rake gem:gh_release` links every published release to
`https://github.com/ruby/rbs/wiki/Release-Note-X.Y`, built from the version number without
checking that the page is there. Nothing has to be written when the line starts — the page comes
together as the first release proper of the line comes into view — but it does have to exist by
the time that release is published, or its notes link to an empty page.
- **Release branches that are done.** A branch is worth keeping only while its line might still
get a patch. The ones that exist do not cover every line that ever had one — `3.8.1` shipped and
there is no `aaa-3.8.x` — so this is housekeeping rather than a rule, but starting a new minor is
the natural moment to look at the bottom of the branch list and delete what has been superseded.

## Backports

Expand All @@ -170,8 +232,11 @@ that is why the 4.0.3 changelog credits its three entries to the same pull reque
- Prereleases (`X.Y.Z.pre.N`) are only installed with `gem install rbs --pre`;
a plain `gem install rbs` is unaffected. On JRuby, `gem install rbs [--pre]`
resolves to the `-java` gem automatically.
- `Dockerfile.jruby` pins the WASI SDK / Chicory / ASM versions to match the
`wasm`, `jruby`, and `release-gems` workflows. Keep them in sync when bumping.
- The WASI SDK version is pinned in `wasm.yml`, `jruby.yml`, `release-gems.yml`, and
`Dockerfile.jruby`, each carrying its own copy. Keep them in sync when bumping. The
Chicory/ASM versions are not duplicated: they are the `jar` requirements in
`rbs.gemspec`, which is where the workflow, `Dockerfile.jruby` and `gem install` all
read them from.
- `rake 'gem:check_release[X.Y.Z]'` and `rake gem:tag` are what the workflow runs to
check the release and to create the tag. Both work locally, which is the fallback
if the tag ever has to be created by hand.
Expand Down
33 changes: 33 additions & 0 deletions wasm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,39 @@ $ rake wasm:install_jars # download the Chicory/ASM jars into ~/.m2 (run on JRub

The compiled `rbs_parser.wasm` is a build artifact and is not checked in.

The WASI SDK is needed for the *build*, not for running the result — the host clang already
knows the `wasm32` target, but there is no wasm32 libc on a normal machine, so it picks up the
host headers and fails on the first `#include`. That is what the SDK supplies, along with the
builtins the link step needs.

## Running the suite on JRuby

[`Dockerfile.jruby`](../Dockerfile.jruby) builds an image that has everything this needs, so no
JRuby, JDK or WASI SDK has to be installed to work on the JRuby side:

```console
$ docker build -f Dockerfile.jruby -t rbs-jruby .
$ docker run --rm rbs-jruby # run the test suite
$ docker run --rm -e RBS_PLATFORM=java rbs-jruby \
gem build rbs.gemspec # build the -java gem
```

Two things in it are not obvious:

- `build-essential` is for prism, which builds `libprism.so` and loads it through FFI on JRuby
rather than as an MRI C extension. It needs `cc` and `make`.
- Bundler is skipped. The development `Gemfile` pulls in CRuby-only C extensions (bigdecimal,
stackprof, …) that cannot build on JRuby, so the few gems the suite needs are installed
directly, in the same set as [`jruby.yml`](../.github/workflows/jruby.yml).

The image compiles `rbs_parser.wasm` itself, which is why it carries the WASI SDK. That is not
the only arrangement: the build needs the SDK but not JRuby, and running the suite needs JRuby
but not the SDK, so `jruby.yml` splits them instead — it compiles the module on CRuby and then
switches engines to test against the result.

`rake wasm:install_jars` is the step that has to be on JRuby either way: it resolves the `jar`
requirements from `rbs.gemspec` through the JVM.

## Exported functions

The module is built as a "reactor": it has no `main`, and the host calls
Expand Down
Loading