rbs 4.1.0 → 4.1.2
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 +4 -4
- data/.github/dependabot.yml +1 -1
- data/.github/workflows/bundle-update.yml +1 -1
- data/.github/workflows/jruby.yml +1 -6
- data/.github/workflows/release-gems.yml +235 -0
- data/CHANGELOG.md +26 -0
- data/README.md +1 -1
- data/Rakefile +407 -79
- data/config.yml +2 -0
- data/docs/release.md +210 -32
- data/include/rbs/ast.h +4 -4
- data/lib/rbs/version.rb +1 -1
- data/src/ast.c +2 -2
- data/src/parser.c +6 -3
- data/wasm/README.md +33 -0
- metadata +3 -3
- data/.github/workflows/milestone.yml +0 -91
data/Rakefile
CHANGED
|
@@ -55,8 +55,15 @@ task :confirm_lexer => :lexer do
|
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
task :confirm_templates => :templates do
|
|
58
|
-
puts "Testing if generated code
|
|
59
|
-
|
|
58
|
+
puts "Testing if generated code is updated with respect to templates"
|
|
59
|
+
|
|
60
|
+
# Every template generates the file it is named after: `templates/<path>.erb` generates `<path>`.
|
|
61
|
+
generated = Dir.glob("templates/**/*.erb").sort.map { _1.delete_prefix("templates/").delete_suffix(".erb") }
|
|
62
|
+
|
|
63
|
+
missing = generated.reject { File.exist?(_1) }
|
|
64
|
+
raise "Templates without a generated file: #{missing.join(", ")}. Is the `templates` task missing an entry?" unless missing.empty?
|
|
65
|
+
|
|
66
|
+
sh "git diff --exit-code -- #{generated.join(" ")}"
|
|
60
67
|
end
|
|
61
68
|
|
|
62
69
|
# Task to format C code using clang-format
|
|
@@ -147,6 +154,9 @@ end
|
|
|
147
154
|
rule %r{^include/(.*)\.c} => 'templates/%X.c.erb' do |t|
|
|
148
155
|
puts "⚠️⚠️⚠️ #{t.name} is older than #{t.source}. You may need to run `rake templates` ⚠️⚠️⚠️"
|
|
149
156
|
end
|
|
157
|
+
rule %r{^ext/(.*)\.c} => 'templates/%X.c.erb' do |t|
|
|
158
|
+
puts "⚠️⚠️⚠️ #{t.name} is older than #{t.source}. You may need to run `rake templates` ⚠️⚠️⚠️"
|
|
159
|
+
end
|
|
150
160
|
|
|
151
161
|
task :annotate do
|
|
152
162
|
sh "bin/generate_docs.sh"
|
|
@@ -428,113 +438,431 @@ task :test_generate_stdlib do
|
|
|
428
438
|
sh "ruby -c /tmp/Thread_Mutex_test.rb"
|
|
429
439
|
end
|
|
430
440
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
end
|
|
441
|
+
# Pull requests with one of these labels are omitted from the changelog.
|
|
442
|
+
CHANGELOG_SKIP_LABELS = ["skip-changelog"]
|
|
434
443
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
patch = patch.to_i
|
|
444
|
+
# Resolves the commit-ish the changelog starts from.
|
|
445
|
+
#
|
|
446
|
+
# `version` is a version number, a tag, or any commit-ish. When it is omitted, the latest tag
|
|
447
|
+
# matching `tag_glob` is used, skipping the ones matching `exclude_globs`.
|
|
448
|
+
#
|
|
449
|
+
def resolve_changelog_base(version, tag_glob:, exclude_globs: [])
|
|
450
|
+
require "open3"
|
|
443
451
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
452
|
+
from =
|
|
453
|
+
if version
|
|
454
|
+
# `4.1.0` and `v4.1.0` both mean the tag `v4.1.0`, while `master` or a SHA is used as is.
|
|
455
|
+
version.match?(/\A\d/) ? "v#{version}" : version
|
|
456
|
+
else
|
|
457
|
+
command = ["git", "describe", "--tags", "--match", tag_glob, "--abbrev=0"]
|
|
458
|
+
exclude_globs.each { |glob| command.push("--exclude", glob) }
|
|
448
459
|
|
|
449
|
-
|
|
450
|
-
|
|
460
|
+
output, status = Open3.capture2(*command)
|
|
461
|
+
raise "🚨 Cannot detect the latest tag matching `#{tag_glob}`. Give the previous version explicitly." unless status.success?
|
|
462
|
+
output.chomp
|
|
451
463
|
end
|
|
452
464
|
|
|
453
|
-
|
|
454
|
-
|
|
465
|
+
_, status = Open3.capture2("git", "rev-parse", "--verify", "--quiet", "#{from}^{commit}")
|
|
466
|
+
raise "🚨 No such commit-ish: `#{from}`" unless status.success?
|
|
467
|
+
|
|
468
|
+
from
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
# Runs a GraphQL query against the repository of the working directory.
|
|
472
|
+
#
|
|
473
|
+
# `body` is the selection set inside `repository`, so a query can use the `$owner` and `$name`
|
|
474
|
+
# variables. Returns the contents of `data.repository`.
|
|
475
|
+
#
|
|
476
|
+
def changelog_graphql(body)
|
|
477
|
+
require "open3"
|
|
478
|
+
require "json"
|
|
479
|
+
|
|
480
|
+
@changelog_repository ||=
|
|
481
|
+
begin
|
|
482
|
+
output, status = Open3.capture2("gh", "repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner")
|
|
483
|
+
raise status.inspect unless status.success?
|
|
484
|
+
output.chomp.split("/", 2)
|
|
455
485
|
end
|
|
486
|
+
owner, name = @changelog_repository
|
|
456
487
|
|
|
457
|
-
|
|
458
|
-
|
|
488
|
+
query = <<~GRAPHQL
|
|
489
|
+
query($owner: String!, $name: String!) {
|
|
490
|
+
repository(owner: $owner, name: $name) {
|
|
491
|
+
#{body}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
GRAPHQL
|
|
495
|
+
|
|
496
|
+
output, status = Open3.capture2(
|
|
497
|
+
"gh", "api", "graphql",
|
|
498
|
+
"-f", "query=#{query}",
|
|
499
|
+
"-f", "owner=#{owner}",
|
|
500
|
+
"-f", "name=#{name}",
|
|
501
|
+
binmode: true
|
|
502
|
+
)
|
|
503
|
+
raise status.inspect unless status.success?
|
|
459
504
|
|
|
460
|
-
|
|
461
|
-
|
|
505
|
+
# GitHub always answers in UTF-8, while the default external encoding follows the locale. Without
|
|
506
|
+
# this, a pull request body with an emoji fails to parse under `LANG=C`, as in GitHub Actions.
|
|
507
|
+
JSON.parse(output.force_encoding(Encoding::UTF_8), symbolize_names: true).dig(:data, :repository)
|
|
508
|
+
end
|
|
462
509
|
|
|
463
|
-
|
|
510
|
+
# Lists the commits between `from` and `HEAD`, newest first.
|
|
511
|
+
#
|
|
512
|
+
# Giving `paths` limits the commits to the ones touching the paths.
|
|
513
|
+
#
|
|
514
|
+
def changelog_commits(from, paths: [])
|
|
515
|
+
require "open3"
|
|
464
516
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
517
|
+
command = ["git", "log", "--format=%H", "#{from}..HEAD"]
|
|
518
|
+
# `--simplify-merges` keeps the default history simplification from following only one parent of
|
|
519
|
+
# a merge commit, which can drop the other side. Note that `--full-history` alone is wrong here:
|
|
520
|
+
# it also lists merge commits that do not touch the paths, bringing back the excluded pull
|
|
521
|
+
# requests. The two flags produce the same commits as the default mode for this repository today.
|
|
522
|
+
command.push("--full-history", "--simplify-merges", "--", *paths) unless paths.empty?
|
|
468
523
|
|
|
469
|
-
|
|
470
|
-
|
|
524
|
+
output, status = Open3.capture2(*command)
|
|
525
|
+
raise status.inspect unless status.success?
|
|
471
526
|
|
|
472
|
-
|
|
473
|
-
|
|
527
|
+
output.lines.map(&:chomp).reject(&:empty?)
|
|
528
|
+
end
|
|
474
529
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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.
|
|
534
|
+
#
|
|
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.
|
|
539
|
+
#
|
|
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)
|
|
557
|
+
|
|
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
|
|
484
566
|
|
|
485
|
-
|
|
486
|
-
|
|
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|
|
|
576
|
+
<<~GRAPHQL
|
|
577
|
+
c#{index}: object(oid: "#{oid}") {
|
|
578
|
+
... on Commit {
|
|
579
|
+
associatedPullRequests(first: 10) {
|
|
580
|
+
nodes {
|
|
581
|
+
number title url merged
|
|
582
|
+
labels(first: 100) { nodes { name } }
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
GRAPHQL
|
|
487
588
|
end
|
|
488
589
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
590
|
+
response = changelog_graphql(aliases.join("\n"))
|
|
591
|
+
|
|
592
|
+
slice.each_with_index do |oid, index|
|
|
593
|
+
nodes = response.dig(:"c#{index}", :associatedPullRequests, :nodes) || []
|
|
594
|
+
|
|
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
|
|
628
|
+
end
|
|
493
629
|
end
|
|
494
630
|
end
|
|
631
|
+
|
|
632
|
+
[pull_requests.values, skipped.values]
|
|
495
633
|
end
|
|
496
634
|
|
|
635
|
+
# Fetches the details that help classifying the pull requests: the changed files and the body.
|
|
636
|
+
#
|
|
637
|
+
def changelog_pull_request_details(pull_requests)
|
|
638
|
+
pull_requests.each_slice(50).flat_map do |slice|
|
|
639
|
+
aliases = slice.map do |pr|
|
|
640
|
+
<<~GRAPHQL
|
|
641
|
+
p#{pr[:number]}: pullRequest(number: #{pr[:number]}) {
|
|
642
|
+
body
|
|
643
|
+
author { login }
|
|
644
|
+
files(first: 100) {
|
|
645
|
+
nodes { path }
|
|
646
|
+
pageInfo { hasNextPage }
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
GRAPHQL
|
|
650
|
+
end
|
|
497
651
|
|
|
498
|
-
|
|
499
|
-
task :changelog do
|
|
500
|
-
major, minor, patch, _pre = RBS::VERSION.split(".", 4)
|
|
501
|
-
major = major.to_i
|
|
502
|
-
minor = minor.to_i
|
|
503
|
-
patch = patch.to_i
|
|
652
|
+
details = changelog_graphql(aliases.join("\n"))
|
|
504
653
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
654
|
+
slice.map do |pr|
|
|
655
|
+
detail = details[:"p#{pr[:number]}"] or next pr
|
|
656
|
+
|
|
657
|
+
pr.merge(
|
|
658
|
+
author: detail.dig(:author, :login),
|
|
659
|
+
# The body is a hint for writing the changelog, not a copy source. Keep it short.
|
|
660
|
+
body: detail[:body].to_s.strip.slice(0, 1000),
|
|
661
|
+
files: detail.dig(:files, :nodes).map { |file| file[:path] },
|
|
662
|
+
files_truncated: detail.dig(:files, :pageInfo, :hasNextPage)
|
|
663
|
+
)
|
|
664
|
+
end
|
|
509
665
|
end
|
|
666
|
+
end
|
|
510
667
|
|
|
511
|
-
|
|
668
|
+
# Reports the pull requests omitted by their label, so that they do not disappear silently.
|
|
669
|
+
#
|
|
670
|
+
def warn_skipped_pull_requests(skipped, skip_labels)
|
|
671
|
+
return if skipped.empty?
|
|
512
672
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
"pr",
|
|
516
|
-
"list",
|
|
517
|
-
"--limit=10000",
|
|
518
|
-
"--json",
|
|
519
|
-
"url,title,number",
|
|
520
|
-
"--search" ,
|
|
521
|
-
"milestone:\"#{milestone}\" is:merged sort:updated-desc -label:Released"
|
|
522
|
-
]
|
|
673
|
+
numbers = skipped.map { |pr| "##{pr[:number]}" }
|
|
674
|
+
numbers = numbers.take(20).push("and #{numbers.size - 20} more") if numbers.size > 20
|
|
523
675
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
676
|
+
$stderr.puts
|
|
677
|
+
$stderr.puts " (⏭️ Skipped #{skipped.size} pull request(s) labeled #{skip_labels.map { |label| "`#{label}`" }.join(" or ")}: #{numbers.join(", ")})"
|
|
678
|
+
end
|
|
679
|
+
|
|
680
|
+
# Prints the changelog template listing the pull requests merged between `from` and `HEAD`.
|
|
681
|
+
#
|
|
682
|
+
# The changelog goes to STDOUT and everything else goes to STDERR, so that the output can be
|
|
683
|
+
# piped to another command: `rake gem:changelog | pbcopy`
|
|
684
|
+
#
|
|
685
|
+
def print_changelog(from, paths: [], skip_labels: CHANGELOG_SKIP_LABELS)
|
|
686
|
+
$stderr.puts "🔍 Finding pull requests merged between `#{from}` and `HEAD`..."
|
|
687
|
+
|
|
688
|
+
commits = changelog_commits(from, paths: paths)
|
|
689
|
+
if commits.empty?
|
|
690
|
+
$stderr.puts " (🤔 There is no commit after `#{from}`.)"
|
|
691
|
+
return
|
|
692
|
+
end
|
|
693
|
+
|
|
694
|
+
pull_requests, skipped = changelog_pull_requests(commits, skip_labels: skip_labels)
|
|
695
|
+
|
|
696
|
+
if pull_requests.empty?
|
|
697
|
+
$stderr.puts " (🤔 No pull request is associated to the commits after `#{from}`.)"
|
|
698
|
+
else
|
|
699
|
+
$stderr.puts
|
|
700
|
+
pull_requests.each do |pr|
|
|
701
|
+
puts "* #{pr[:title]} ([##{pr[:number]}](#{pr[:url]}))"
|
|
702
|
+
end
|
|
703
|
+
$stdout.flush
|
|
704
|
+
end
|
|
705
|
+
|
|
706
|
+
warn_skipped_pull_requests(skipped, skip_labels)
|
|
707
|
+
end
|
|
527
708
|
|
|
709
|
+
# Prints the same pull requests as `print_changelog` as JSON, with the details that help
|
|
710
|
+
# classifying them into the sections of CHANGELOG.md.
|
|
711
|
+
#
|
|
712
|
+
# This is the input for the release automation, so it always prints a valid JSON document.
|
|
713
|
+
#
|
|
714
|
+
def print_changelog_json(from, paths: [], skip_labels: CHANGELOG_SKIP_LABELS)
|
|
528
715
|
require "json"
|
|
529
|
-
json = JSON.parse(output, symbolize_names: true)
|
|
530
716
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
717
|
+
$stderr.puts "🔍 Finding pull requests merged between `#{from}` and `HEAD`..."
|
|
718
|
+
|
|
719
|
+
commits = changelog_commits(from, paths: paths)
|
|
720
|
+
pull_requests, skipped = changelog_pull_requests(commits, skip_labels: skip_labels)
|
|
721
|
+
pull_requests = changelog_pull_request_details(pull_requests)
|
|
722
|
+
|
|
723
|
+
$stderr.puts " (📋 #{pull_requests.size} pull request(s))"
|
|
724
|
+
|
|
725
|
+
puts JSON.pretty_generate(
|
|
726
|
+
{
|
|
727
|
+
from: from,
|
|
728
|
+
to: "HEAD",
|
|
729
|
+
pull_requests: pull_requests,
|
|
730
|
+
skipped: skipped
|
|
731
|
+
}
|
|
732
|
+
)
|
|
733
|
+
$stdout.flush
|
|
734
|
+
|
|
735
|
+
warn_skipped_pull_requests(skipped, skip_labels)
|
|
736
|
+
end
|
|
737
|
+
|
|
738
|
+
namespace :gem do
|
|
739
|
+
# The gem is developed in the whole repository except the Rust crate, which has its own release
|
|
740
|
+
# cycle. Note that this is an *exclusion*, not a list of the directories shipped in the gem:
|
|
741
|
+
# changes in `test/` or `.github/` are part of the gem's changelog too.
|
|
742
|
+
# A constant defined in a `namespace` block is a top-level constant, so it needs the prefix.
|
|
743
|
+
GEM_CHANGELOG_PATHS = [".", ":(exclude)rust"]
|
|
744
|
+
|
|
745
|
+
# The tags a release proper starts *after*, rather than at.
|
|
746
|
+
GEM_PRERELEASE_TAGS = ["v*.pre*", "v*.dev*"]
|
|
747
|
+
|
|
748
|
+
# Where the changelog of the release being prepared starts, derived from `RBS::VERSION`:
|
|
749
|
+
#
|
|
750
|
+
# * `X.Y.Z.pre.N` documents what changed since `X.Y.Z.pre.N-1`, so it starts from the latest tag.
|
|
751
|
+
# * `X.Y.Z` documents the whole cycle, the prereleases included, so it skips the prerelease tags
|
|
752
|
+
# in between and starts from the previous release proper.
|
|
753
|
+
#
|
|
754
|
+
# This is the step that is easy to get wrong by hand: on a release proper the latest tag is a
|
|
755
|
+
# prerelease, so the obvious default would produce only the tail of the cycle. Passing a version
|
|
756
|
+
# explicitly overrides all of it.
|
|
757
|
+
#
|
|
758
|
+
def changelog_base(version)
|
|
759
|
+
excluded = Gem::Version.new(RBS::VERSION).prerelease? ? [] : GEM_PRERELEASE_TAGS
|
|
760
|
+
resolve_changelog_base(version, tag_glob: "v*", exclude_globs: excluded)
|
|
761
|
+
end
|
|
762
|
+
|
|
763
|
+
desc "Generate changelog template from GH pull requests merged since the previous release"
|
|
764
|
+
task :changelog, [:version] do |_task, args|
|
|
765
|
+
print_changelog(changelog_base(args[:version]), paths: GEM_CHANGELOG_PATHS)
|
|
766
|
+
end
|
|
767
|
+
|
|
768
|
+
namespace :changelog do
|
|
769
|
+
desc "Print the pull requests of `gem:changelog` as JSON, with the changed files and body of each"
|
|
770
|
+
task :json, [:version] do |_task, args|
|
|
771
|
+
print_changelog_json(changelog_base(args[:version]), paths: GEM_CHANGELOG_PATHS)
|
|
535
772
|
end
|
|
536
|
-
|
|
537
|
-
|
|
773
|
+
end
|
|
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
|
+
|
|
829
|
+
desc "Publish the GitHub release for RBS::VERSION, unless it is a `.dev.` version"
|
|
830
|
+
task :gh_release do
|
|
831
|
+
require "open3"
|
|
832
|
+
|
|
833
|
+
version = Gem::Version.new(RBS::VERSION)
|
|
834
|
+
major, minor, *_ = RBS::VERSION.split(".")
|
|
835
|
+
tag = "v#{RBS::VERSION}"
|
|
836
|
+
|
|
837
|
+
if dev_release?(RBS::VERSION)
|
|
838
|
+
puts "⏭️ #{RBS::VERSION} is a dev release, so there is no GitHub release to publish."
|
|
839
|
+
next
|
|
840
|
+
end
|
|
841
|
+
|
|
842
|
+
# The release is created against an existing tag, so that the artifacts and the
|
|
843
|
+
# notes describe a commit that is already immutable.
|
|
844
|
+
_, status = Open3.capture2("git", "rev-parse", "--verify", "--quiet", "#{tag}^{commit}")
|
|
845
|
+
raise "🚨 No such tag: `#{tag}`. Tag the release before creating the GitHub release." unless status.success?
|
|
846
|
+
|
|
847
|
+
notes = <<~NOTES
|
|
848
|
+
[Release note](https://github.com/ruby/rbs/wiki/Release-Note-#{major}.#{minor})
|
|
849
|
+
|
|
850
|
+
#{changelog_section(RBS::VERSION)}
|
|
851
|
+
NOTES
|
|
852
|
+
|
|
853
|
+
# Published rather than drafted: the notes are the changelog section that was
|
|
854
|
+
# already reviewed in the release pull request, so there is nothing left to edit.
|
|
855
|
+
command = [
|
|
856
|
+
"gh", "release", "create", tag,
|
|
857
|
+
"--title=#{RBS::VERSION}",
|
|
858
|
+
"--notes=#{notes}"
|
|
859
|
+
]
|
|
860
|
+
command << "--prerelease" if version.prerelease?
|
|
861
|
+
|
|
862
|
+
output, status = Open3.capture2(*command)
|
|
863
|
+
raise "🚨 `gh release create` failed: #{status.inspect}" unless status.success?
|
|
864
|
+
|
|
865
|
+
puts "📝 Released #{tag}: #{output.chomp}"
|
|
538
866
|
end
|
|
539
867
|
end
|
|
540
868
|
|
data/config.yml
CHANGED
|
@@ -769,6 +769,7 @@ nodes:
|
|
|
769
769
|
c_type: rbs_location_range
|
|
770
770
|
- name: type_name
|
|
771
771
|
c_type: rbs_type_name
|
|
772
|
+
optional: true # NULL when the name is left out, to be inferred from the Ruby code
|
|
772
773
|
- name: type_name_location
|
|
773
774
|
c_type: rbs_location_range
|
|
774
775
|
optional: true
|
|
@@ -781,6 +782,7 @@ nodes:
|
|
|
781
782
|
c_type: rbs_location_range
|
|
782
783
|
- name: type_name
|
|
783
784
|
c_type: rbs_type_name
|
|
785
|
+
optional: true # NULL when the name is left out, to be inferred from the Ruby code
|
|
784
786
|
- name: type_name_location
|
|
785
787
|
c_type: rbs_location_range
|
|
786
788
|
optional: true
|