es_release_notes.pl 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  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 0.070;
  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", ">test-failure", ":Core/Build", "backport" );
  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. my %Area_Overrides = (
  44. ':ml' => 'Machine Learning',
  45. ':beats' => 'Beats Plugin',
  46. ':Docs' => 'Docs Infrastructure'
  47. );
  48. use JSON();
  49. use Encode qw(encode_utf8);
  50. my $json = JSON->new->utf8(1);
  51. my %All_Labels = fetch_labels();
  52. my $version = shift @ARGV
  53. or dump_labels();
  54. dump_labels("Unknown version '$version'")
  55. unless $All_Labels{$version};
  56. my $issues = fetch_issues($version);
  57. dump_issues( $version, $issues );
  58. #===================================
  59. sub dump_issues {
  60. #===================================
  61. my $version = shift;
  62. my $issues = shift;
  63. $version =~ s/v//;
  64. my $branch = $version;
  65. $branch =~ s/\.\d+$//;
  66. my ( $day, $month, $year ) = (gmtime)[ 3 .. 5 ];
  67. $month++;
  68. $year += 1900;
  69. print <<"ASCIIDOC";
  70. :issue: https://github.com/${User_Repo}issues/
  71. :pull: https://github.com/${User_Repo}pull/
  72. [[release-notes-$version]]
  73. == $version Release Notes
  74. coming[$version]
  75. Also see <<breaking-changes-$branch>>.
  76. ASCIIDOC
  77. for my $group ( @Groups, 'other' ) {
  78. my $group_issues = $issues->{$group} or next;
  79. my $group_id = $group;
  80. $group_id =~ s/^>//;
  81. print "[[$group_id-$version]]\n"
  82. . "[float]\n"
  83. . "=== $Group_Labels{$group}\n\n";
  84. for my $header ( sort keys %$group_issues ) {
  85. my $header_issues = $group_issues->{$header};
  86. print( $header || 'HEADER MISSING', "::\n" );
  87. for my $issue (@$header_issues) {
  88. my $title = $issue->{title};
  89. if ( $issue->{state} eq 'open' ) {
  90. $title .= " [OPEN]";
  91. }
  92. unless ( $issue->{pull_request} ) {
  93. $title .= " [ISSUE]";
  94. }
  95. my $number = $issue->{number};
  96. print encode_utf8("* $title {pull}${number}[#${number}]");
  97. if ( my $related = $issue->{related_issues} ) {
  98. my %uniq = map { $_ => 1 } @$related;
  99. print keys %uniq > 1
  100. ? " (issues: "
  101. : " (issue: ";
  102. print join ", ", map {"{issue}${_}[#${_}]"}
  103. sort keys %uniq;
  104. print ")";
  105. }
  106. print "\n";
  107. }
  108. print "\n";
  109. }
  110. print "\n\n";
  111. }
  112. }
  113. #===================================
  114. sub fetch_issues {
  115. #===================================
  116. my $version = shift;
  117. my @issues;
  118. my %seen;
  119. for my $state ( 'open', 'closed' ) {
  120. my $page = 1;
  121. while (1) {
  122. my $tranche
  123. = fetch( $User_Repo
  124. . 'issues?labels='
  125. . $version
  126. . '&pagesize=100&state='
  127. . $state
  128. . '&page='
  129. . $page )
  130. or die "Couldn't fetch issues for version '$version'";
  131. push @issues, @$tranche;
  132. for my $issue (@$tranche) {
  133. next unless $issue->{pull_request};
  134. for ( $issue->{body} =~ m{(?:#|${User_Repo}issues/)(\d+)}g ) {
  135. $seen{$_}++;
  136. push @{ $issue->{related_issues} }, $_;
  137. }
  138. }
  139. $page++;
  140. last unless @$tranche;
  141. }
  142. }
  143. my %group;
  144. ISSUE:
  145. for my $issue (@issues) {
  146. next if $seen{ $issue->{number} } && !$issue->{pull_request};
  147. for ( @{ $issue->{labels} } ) {
  148. next ISSUE if $Ignore{ $_->{name} };
  149. }
  150. # uncomment for including/excluding PRs already issued in other versions
  151. # next if grep {$_->{name}=~/^v2/} @{$issue->{labels}};
  152. my %labels = map { $_->{name} => 1 } @{ $issue->{labels} };
  153. my @area_labels = grep {/^:/} sort keys %labels;
  154. my ($header) = map { m{:[^/]+/(.+)} && $1 } @area_labels;
  155. if (scalar @area_labels > 1) {
  156. $header = "MULTIPLE AREA LABELS";
  157. }
  158. if (scalar @area_labels == 1 && exists $Area_Overrides{$area_labels[0]}) {
  159. $header = $Area_Overrides{$area_labels[0]};
  160. }
  161. $header ||= 'NOT CLASSIFIED';
  162. for (@Groups) {
  163. if ( $labels{$_} ) {
  164. push @{ $group{$_}{$header} }, $issue;
  165. next ISSUE;
  166. }
  167. }
  168. push @{ $group{other}{$header} }, $issue;
  169. }
  170. return \%group;
  171. }
  172. #===================================
  173. sub fetch_labels {
  174. #===================================
  175. my %all;
  176. my $page = 1;
  177. while (1) {
  178. my $labels = fetch( $User_Repo . 'labels?page=' . $page++ )
  179. or die "Couldn't retrieve version labels";
  180. last unless @$labels;
  181. for (@$labels) {
  182. my $name = $_->{name};
  183. next unless $name =~ /^v/;
  184. $all{$name} = 1;
  185. }
  186. }
  187. return %all;
  188. }
  189. #===================================
  190. sub fetch {
  191. #===================================
  192. my $url = $Base_URL . shift();
  193. my $response = HTTP::Tiny->new->get($url);
  194. die "$response->{status} $response->{reason}\n"
  195. unless $response->{success};
  196. # print $response->{content};
  197. return $json->decode( $response->{content} );
  198. }
  199. #===================================
  200. sub load_github_key {
  201. #===================================
  202. my ($file) = glob("~/.github_auth");
  203. unless ( -e $file ) {
  204. warn "File ~/.github_auth doesn't exist - using anonymous API. "
  205. . "Generate a Personal Access Token at https://github.com/settings/applications\n";
  206. return '';
  207. }
  208. open my $fh, $file or die "Couldn't open $file: $!";
  209. my ($key) = <$fh> || die "Couldn't read $file: $!";
  210. $key =~ s/^\s+//;
  211. $key =~ s/\s+$//;
  212. die "Invalid GitHub key: $key"
  213. unless $key =~ /^[0-9a-f]{40}$/;
  214. return "$key:x-oauth-basic@";
  215. }
  216. #===================================
  217. sub dump_labels {
  218. #===================================
  219. my $error = shift || '';
  220. if ($error) {
  221. $error = "\nERROR: $error\n";
  222. }
  223. my $labels = join( "\n - ", '', ( sort keys %All_Labels ) );
  224. die <<USAGE
  225. $error
  226. USAGE: $0 version > outfile
  227. Known versions:$labels
  228. USAGE
  229. }