#!/usr/bin/env perl # Licensed to Elasticsearch under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on # an 'AS IS' BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, # either express or implied. See the License for the specific # language governing permissions and limitations under the License. use strict; use warnings; use HTTP::Tiny 0.070; use IO::Socket::SSL 1.52; use utf8; my $Github_Key = load_github_key(); my $Base_URL = "https://${Github_Key}api.github.com/repos/"; my $User_Repo = 'elastic/elasticsearch/'; my $Issue_URL = "http://github.com/${User_Repo}issues/"; my @Groups = ( ">breaking", ">breaking-java", ">deprecation", ">feature", ">enhancement", ">bug", ">regression", ">upgrade" ); my %Ignore = map { $_ => 1 } ( ">non-issue", ">refactoring", ">docs", ">test", ">test-failure", ">test-mute", ":Core/Infra/Build", "backport" ); my %Group_Labels = ( '>breaking' => 'Breaking changes', '>breaking-java' => 'Breaking Java changes', '>deprecation' => 'Deprecations', '>feature' => 'New features', '>enhancement' => 'Enhancements', '>bug' => 'Bug fixes', '>regression' => 'Regressions', '>upgrade' => 'Upgrades', 'other' => 'NOT CLASSIFIED', ); my %Area_Overrides = ( ':ml' => 'Machine Learning', ':Beats' => 'Beats Plugin', ':Docs' => 'Docs Infrastructure' ); use JSON(); use Encode qw(encode_utf8); my $json = JSON->new->utf8(1); my %All_Labels = fetch_labels(); my $version = shift @ARGV or dump_labels(); dump_labels("Unknown version '$version'") unless $All_Labels{$version}; my $issues = fetch_issues($version); dump_issues( $version, $issues ); #=================================== sub dump_issues { #=================================== my $version = shift; my $issues = shift; $version =~ s/v//; my $branch = $version; $branch =~ s/\.\d+$//; my ( $day, $month, $year ) = (gmtime)[ 3 .. 5 ]; $month++; $year += 1900; print <<"ASCIIDOC"; :issue: https://github.com/${User_Repo}issues/ :pull: https://github.com/${User_Repo}pull/ [[release-notes-$version]] == {es} version $version coming[$version] Also see <<breaking-changes-$branch,Breaking changes in $branch>>. ASCIIDOC for my $group ( @Groups, 'other' ) { my $group_issues = $issues->{$group} or next; my $group_id = $group; $group_id =~ s/^>//; print "[[$group_id-$version]]\n" . "[float]\n" . "=== $Group_Labels{$group}\n\n"; for my $header ( sort keys %$group_issues ) { my $header_issues = $group_issues->{$header}; print( $header || 'HEADER MISSING', "::\n" ); for my $issue (@$header_issues) { my $title = $issue->{title}; if ( $issue->{state} eq 'open' ) { $title .= " [OPEN]"; } unless ( $issue->{pull_request} ) { $title .= " [ISSUE]"; } my $number = $issue->{number}; print encode_utf8("* $title {pull}${number}[#${number}]"); if ( my $related = $issue->{related_issues} ) { my %uniq = map { $_ => 1 } @$related; print keys %uniq > 1 ? " (issues: " : " (issue: "; print join ", ", map {"{issue}${_}[#${_}]"} sort keys %uniq; print ")"; } print "\n"; } print "\n"; } print "\n\n"; } } #=================================== sub fetch_issues { #=================================== my $version = shift; my @issues; my %seen; for my $state ( 'open', 'closed' ) { my $page = 1; while (1) { my $tranche = fetch( $User_Repo . 'issues?labels=' . $version . '&pagesize=100&state=' . $state . '&page=' . $page ) or die "Couldn't fetch issues for version '$version'"; push @issues, @$tranche; for my $issue (@$tranche) { next unless $issue->{pull_request}; for ( $issue->{body} =~ m{(?:#|${User_Repo}issues/)(\d+)}g ) { $seen{$_}++; push @{ $issue->{related_issues} }, $_; } } $page++; last unless @$tranche; } } my %group; ISSUE: for my $issue (@issues) { next if $seen{ $issue->{number} } && !$issue->{pull_request}; for ( @{ $issue->{labels} } ) { next ISSUE if $Ignore{ $_->{name} }; } # uncomment for including/excluding PRs already issued in other versions # next if grep {$_->{name}=~/^v2/} @{$issue->{labels}}; my %labels = map { $_->{name} => 1 } @{ $issue->{labels} }; my @area_labels = grep {/^:/} sort keys %labels; my ($header) = map { m{:[^/]+/(.+)} && $1 } @area_labels; if (scalar @area_labels > 1) { $header = "MULTIPLE AREA LABELS"; } if (scalar @area_labels == 1 && exists $Area_Overrides{$area_labels[0]}) { $header = $Area_Overrides{$area_labels[0]}; } $header ||= 'NOT CLASSIFIED'; for (@Groups) { if ( $labels{$_} ) { push @{ $group{$_}{$header} }, $issue; next ISSUE; } } push @{ $group{other}{$header} }, $issue; } return \%group; } #=================================== sub fetch_labels { #=================================== my %all; my $page = 1; while (1) { my $labels = fetch( $User_Repo . 'labels?page=' . $page++ ) or die "Couldn't retrieve version labels"; last unless @$labels; for (@$labels) { my $name = $_->{name}; next unless $name =~ /^v/; $all{$name} = 1; } } return %all; } #=================================== sub fetch { #=================================== my $url = $Base_URL . shift(); my $response = HTTP::Tiny->new->get($url); die "$response->{status} $response->{reason}\n" unless $response->{success}; # print $response->{content}; return $json->decode( $response->{content} ); } #=================================== sub load_github_key { #=================================== my ($file) = glob("~/.github_auth"); unless ( -e $file ) { warn "File ~/.github_auth doesn't exist - using anonymous API. " . "Generate a Personal Access Token at https://github.com/settings/applications\n"; return ''; } open my $fh, $file or die "Couldn't open $file: $!"; my ($key) = <$fh> || die "Couldn't read $file: $!"; $key =~ s/^\s+//; $key =~ s/\s+$//; die "Invalid GitHub key: $key" unless $key =~ /^[0-9a-f]{40}$/; return "$key:x-oauth-basic@"; } #=================================== sub dump_labels { #=================================== my $error = shift || ''; if ($error) { $error = "\nERROR: $error\n"; } my $labels = join( "\n - ", '', ( sort keys %All_Labels ) ); die <<USAGE $error USAGE: $0 version > outfile Known versions:$labels USAGE }