es_release_notes.pl 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. #!/usr/bin/env perl
  2. # Licensed to Elasticsearch under one or more contributor
  3. # license agreements. See the NOTICE file distributed with
  4. # this work for additional information regarding copyright
  5. # ownership. Elasticsearch licenses this file to you under
  6. # the Apache License, Version 2.0 (the "License"); you may
  7. # not use this file except in compliance with the License.
  8. # You may obtain a copy of the License at
  9. #
  10. # http://www.apache.org/licenses/LICENSE-2.0
  11. #
  12. # Unless required by applicable law or agreed to in writing,
  13. # software distributed under the License is distributed on
  14. # an 'AS IS' BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
  15. # either express or implied. See the License for the specific
  16. # language governing permissions and limitations under the License.
  17. use strict;
  18. use warnings;
  19. use HTTP::Tiny;
  20. use IO::Socket::SSL 1.52;
  21. use utf8;
  22. my $Github_Key = load_github_key();
  23. my $Base_URL = "https://${Github_Key}api.github.com/repos/";
  24. my $User_Repo = 'elastic/elasticsearch/';
  25. my $Issue_URL = "http://github.com/${User_Repo}issues/";
  26. my @Groups = (
  27. ">breaking", ">breaking-java", ">deprecation", ">feature",
  28. ">enhancement", ">bug", ">regression", ">upgrade"
  29. );
  30. my %Ignore = map { $_ => 1 }
  31. ( ">non-issue", ">refactoring", ">docs", ">test", ":Core/Build" );
  32. my %Group_Labels = (
  33. '>breaking' => 'Breaking changes',
  34. '>breaking-java' => 'Breaking Java changes',
  35. '>deprecation' => 'Deprecations',
  36. '>feature' => 'New features',
  37. '>enhancement' => 'Enhancements',
  38. '>bug' => 'Bug fixes',
  39. '>regression' => 'Regressions',
  40. '>upgrade' => 'Upgrades',
  41. 'other' => 'NOT CLASSIFIED',
  42. );
  43. use JSON();
  44. use Encode qw(encode_utf8);
  45. my $json = JSON->new->utf8(1);
  46. my %All_Labels = fetch_labels();
  47. my $version = shift @ARGV
  48. or dump_labels();
  49. dump_labels("Unknown version '$version'")
  50. unless $All_Labels{$version};
  51. my $issues = fetch_issues($version);
  52. dump_issues( $version, $issues );
  53. #===================================
  54. sub dump_issues {
  55. #===================================
  56. my $version = shift;
  57. my $issues = shift;
  58. $version =~ s/v//;
  59. my $branch = $version;
  60. $branch =~ s/\.\d+$//;
  61. my ( $day, $month, $year ) = (gmtime)[ 3 .. 5 ];
  62. $month++;
  63. $year += 1900;
  64. print <<"ASCIIDOC";
  65. :issue: https://github.com/${User_Repo}issues/
  66. :pull: https://github.com/${User_Repo}pull/
  67. [[release-notes-$version]]
  68. == $version Release Notes
  69. coming[$version]
  70. Also see <<breaking-changes-$branch>>.
  71. ASCIIDOC
  72. for my $group ( @Groups, 'other' ) {
  73. my $group_issues = $issues->{$group} or next;
  74. my $group_id = $group;
  75. $group_id =~ s/^>//;
  76. print "[[$group_id-$version]]\n"
  77. . "[float]\n"
  78. . "=== $Group_Labels{$group}\n\n";
  79. for my $header ( sort keys %$group_issues ) {
  80. my $header_issues = $group_issues->{$header};
  81. print( $header || 'HEADER MISSING', "::\n" );
  82. for my $issue (@$header_issues) {
  83. my $title = $issue->{title};
  84. if ( $issue->{state} eq 'open' ) {
  85. $title .= " [OPEN]";
  86. }
  87. unless ( $issue->{pull_request} ) {
  88. $title .= " [ISSUE]";
  89. }
  90. my $number = $issue->{number};
  91. print encode_utf8("* $title {pull}${number}[#${number}]");
  92. if ( my $related = $issue->{related_issues} ) {
  93. my %uniq = map { $_ => 1 } @$related;
  94. print keys %uniq > 1
  95. ? " (issues: "
  96. : " (issue: ";
  97. print join ", ", map {"{issue}${_}[#${_}]"}
  98. sort keys %uniq;
  99. print ")";
  100. }
  101. print "\n";
  102. }
  103. print "\n";
  104. }
  105. print "\n\n";
  106. }
  107. }
  108. #===================================
  109. sub fetch_issues {
  110. #===================================
  111. my $version = shift;
  112. my @issues;
  113. my %seen;
  114. for my $state ( 'open', 'closed' ) {
  115. my $page = 1;
  116. while (1) {
  117. my $tranche
  118. = fetch( $User_Repo
  119. . 'issues?labels='
  120. . $version
  121. . '&pagesize=100&state='
  122. . $state
  123. . '&page='
  124. . $page )
  125. or die "Couldn't fetch issues for version '$version'";
  126. push @issues, @$tranche;
  127. for my $issue (@$tranche) {
  128. next unless $issue->{pull_request};
  129. for ( $issue->{body} =~ m{(?:#|${User_Repo}issues/)(\d+)}g ) {
  130. $seen{$_}++;
  131. push @{ $issue->{related_issues} }, $_;
  132. }
  133. }
  134. $page++;
  135. last unless @$tranche;
  136. }
  137. }
  138. my %group;
  139. ISSUE:
  140. for my $issue (@issues) {
  141. next if $seen{ $issue->{number} } && !$issue->{pull_request};
  142. for ( @{ $issue->{labels} } ) {
  143. next ISSUE if $Ignore{ $_->{name} };
  144. }
  145. # uncomment for including/excluding PRs already issued in other versions
  146. # next if grep {$_->{name}=~/^v2/} @{$issue->{labels}};
  147. my %labels = map { $_->{name} => 1 } @{ $issue->{labels} };
  148. my ($header) = map { m{:[^/]+/(.+)} && $1 }
  149. grep {/^:/} sort keys %labels;
  150. $header ||= 'NOT CLASSIFIED';
  151. for (@Groups) {
  152. if ( $labels{$_} ) {
  153. push @{ $group{$_}{$header} }, $issue;
  154. next ISSUE;
  155. }
  156. }
  157. push @{ $group{other}{$header} }, $issue;
  158. }
  159. return \%group;
  160. }
  161. #===================================
  162. sub fetch_labels {
  163. #===================================
  164. my %all;
  165. my $page = 1;
  166. while (1) {
  167. my $labels = fetch( $User_Repo . 'labels?page=' . $page++ )
  168. or die "Couldn't retrieve version labels";
  169. last unless @$labels;
  170. for (@$labels) {
  171. my $name = $_->{name};
  172. next unless $name =~ /^v/;
  173. $all{$name} = 1;
  174. }
  175. }
  176. return %all;
  177. }
  178. #===================================
  179. sub fetch {
  180. #===================================
  181. my $url = $Base_URL . shift();
  182. my $response = HTTP::Tiny->new->get($url);
  183. die "$response->{status} $response->{reason}\n"
  184. unless $response->{success};
  185. # print $response->{content};
  186. return $json->decode( $response->{content} );
  187. }
  188. #===================================
  189. sub load_github_key {
  190. #===================================
  191. my ($file) = glob("~/.github_auth");
  192. unless ( -e $file ) {
  193. warn "File ~/.github_auth doesn't exist - using anonymous API. "
  194. . "Generate a Personal Access Token at https://github.com/settings/applications\n";
  195. return '';
  196. }
  197. open my $fh, $file or die "Couldn't open $file: $!";
  198. my ($key) = <$fh> || die "Couldn't read $file: $!";
  199. $key =~ s/^\s+//;
  200. $key =~ s/\s+$//;
  201. die "Invalid GitHub key: $key"
  202. unless $key =~ /^[0-9a-f]{40}$/;
  203. return "$key:x-oauth-basic@";
  204. }
  205. #===================================
  206. sub dump_labels {
  207. #===================================
  208. my $error = shift || '';
  209. if ($error) {
  210. $error = "\nERROR: $error\n";
  211. }
  212. my $labels = join( "\n - ", '', ( sort keys %All_Labels ) );
  213. die <<USAGE
  214. $error
  215. USAGE: $0 version > outfile
  216. Known versions:$labels
  217. USAGE
  218. }