-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Automatically detects release dates, commits that were logged, etc. Also, prints warnings if something is not quite right. Technically, βchangelogableβ would be more precise (given that the bot does not make releases itself), but βreleasableβ is easier to remember. The data is taken from the rakudo repo directly (docs/ChangeLog, docs/release_guide.pod, VERSION). This means that no special actions should be taken to keep the bot up-to-date. Anyone can edit the changelog and the bot will pick up these changes automatically. Commits that should not be logged must be mentioned in one of the commits to the changelog file. This is possibly LTA, but we will see how it goes. Generally, this is an experiment in making the release process more awesome. Whether it is going to work better or not, I don't know. In any case, this is an independent bot that can be used in addition to anything else. There is a dependency on R6 to check for release blockers. As of today, there are no plans to change that (mainly because the whitelist approach of R6 is pretty much flawless). One of the cool features of releasable is that you can give it a link to the modified changelog file, and it will give you the information about it as if it was in rakudo repo (e.g. commits that are not logged and which ones are logged incorrectly). By the way, there's room for implementing other checks. Which abbreviated hash length the bot should enforce is debatable. Currently, it suggests the sha length of 8, and I was thinking about increasing it to 12. In the end I decided not to, but here was my logic: * From 2016.11 to 2017.04.2, seven characters were used. * 2017.04.3 had ten character shas in the changelog. * From 2017.05 to 2017.07, shas had eight characters. * If you ask git to give you an abbreviated hash, it gives nine characters. * It makes little sense to use abbreviated hashes that are smaller than what git produces. * Therefore, let's bump it to 9 charactersβ¦ * However, git project repo already has collisions with 8 characters and linux repo has collisions with 9, so in a long run even 9 is not that good of an ideaβ¦ * So let's use 12. * But even 12 may at some point may stop being sufficient (it's never enough). * Instead, let's keep it at 8. Once we start approaching birthday collisions for some length, we can bump it in the whole changelog. And there you go! Yet another blog post in a commit message π
- Loading branch information
1 parent
a1aa0e5
commit 5d7445b
Showing
3 changed files
with
348 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,187 @@ | ||
#!/usr/bin/env perl6 | ||
# Copyright Β© 2017 | ||
# Aleks-Daniel Jakimenko-Aleksejev <alex.jakimenko@gmail.com> | ||
# | ||
# This program is free software: you can redistribute it and/or modify | ||
# it under the terms of the GNU Affero General Public License as published by | ||
# the Free Software Foundation, either version 3 of the License, or | ||
# (at your option) any later version. | ||
# | ||
# This program is distributed in the hope that it will be useful, | ||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
# GNU Affero General Public License for more details. | ||
# | ||
# You should have received a copy of the GNU Affero General Public License | ||
# along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
|
||
use Whateverable; | ||
use Misc; | ||
|
||
use IRC::Client; | ||
|
||
unit class Releasable does Whateverable; | ||
|
||
# β Git itself suggests 9 characters, and 12 characters may be a better | ||
# β choice for the hundred-year languageβ¦ but let's increase it only | ||
# β when needed | ||
my $SHA-LENGTH = 8; | ||
my $RELEASE-HOUR = 19; # GMT+0 | ||
my $BLOCKERS-URL = βhttps://perl6.fail/release/blockers.jsonβ; | ||
my $TICKET-URL = βhttps://rt.perl.org/rt3/Public/Bug/Display.html?id=β; | ||
|
||
method help($msg) { | ||
βstatus | status linkβ | ||
} | ||
|
||
sub ignored-commits() { | ||
my $last-release = to-full-commit chomp slurp β$RAKUDO/VERSIONβ; | ||
die βCannot resolve the tag for the last releaseβ unless $last-release; | ||
my $result = run :out, :cwd($RAKUDO), βgitβ, βlogβ, β--pretty=%bβ, | ||
β-zβ, β$last-release..HEADβ, β--β, βdocs/ChangeLogβ; | ||
die βCannot git log the changelogβ unless $result; | ||
|
||
return gather for $result.out.split(0.chr, :skip-empty) { | ||
next unless /βnot loggedβ\N*β:β \s* [ @<shas>=[<.xdigit>**4..* ] ]+ % \s+/; | ||
{ take ~$_ if .chars == $SHA-LENGTH } for @<shas> | ||
} | ||
} | ||
|
||
sub time-left($then) { | ||
my $time-left = $then.Instant - now; | ||
my ($seconds, $minutes, $hours, $days) = $time-left.polymod: 60, 60, 24; | ||
return βis just a few moments awayβ if not $days and not $hours; | ||
my $answer = βin β; | ||
$answer ~= β$days day{$days β 1 ?? βsβ !! ββ} and β if $days; | ||
$answer ~= ββ$hours hour{$hours β 1 ?? βsβ !! ββ}β; | ||
$answer | ||
} | ||
|
||
sub time-to-release($msg) { | ||
my $guide = slurp β$RAKUDO/docs/release_guide.podβ; | ||
die βUnable to parse the release guideβ unless $guide ~~ / | ||
^^ β=head2 Planned future releasesβ $$ | ||
.*? | ||
(^^β β(\d\d\d\dβ-β\d\dβ-β\d\d)\s+ βRakudo #β(\d+) [\s+β(β (<-[)]>+) β)β]? \n)+ | ||
/; | ||
my @dates = $0.map: { %(date => Date.new(~.[0]), id => +.[1], manager => (.Str with .[2])) }; | ||
my $important-date; | ||
my $annoying-warning = False; | ||
for @dates { | ||
my $release = .<date>.yyyy-mm-dd.split(β-β)[0,1].join: β.β; | ||
if not to-full-commit $release { | ||
$important-date = $_; | ||
if not .<manager> and not $annoying-warning { | ||
$msg.reply: βRelease manager is not specified yet.β | ||
} | ||
last | ||
} | ||
if not $annoying-warning { | ||
$annoying-warning = True; | ||
$msg.reply: βRelease date for Rakudo $release is listed inβ | ||
~ β βPlanned future releasesβ, but it was already released.β; | ||
} | ||
} | ||
die βRelease date not foundβ without $important-date; | ||
my $time-left = time-left DateTime.new(date => $important-date<date>, | ||
hour => $RELEASE-HOUR); | ||
βNext release $time-leftβ | ||
} | ||
|
||
sub changelog-to-stats($changelog) { | ||
if not $changelog.match: /^ βNew in β (.*?) β:β (.*?) ^^ βNew in β (.*?) β:β/ { | ||
return { summary => βUnknown changelog formatβ } | ||
} | ||
my ($version, $changes, $version-old) = ~$0, ~$1, ~$2; | ||
|
||
my $actual-commit = to-full-commit $version; | ||
with $actual-commit { | ||
return { summary => βChangelog for this release was not started yetβ } | ||
} | ||
my $actual-commit-old = to-full-commit $version-old; | ||
die βCannot resolve the tag for the previous releaseβ without $actual-commit-old; | ||
|
||
my @shas = $changes.match(:g, / [β[β (<.xdigit>**4..*) β]β \s*]+ $$/)Β»[0].flatΒ».Str; | ||
my $result = run :out, :cwd($RAKUDO), βgitβ, βlogβ, β-zβ, β--pretty=%Hβ, | ||
β--reverseβ, β$actual-commit-old..HEADβ; | ||
die βFailed to query rakudo git logβ unless $result; | ||
my @git-commits = $result.out.slurp-rest.split(0.chr, :skip-empty) | ||
.map: *.substr: 0, $SHA-LENGTH; | ||
my @warnings; | ||
my $commit-mentioned = set gather for @shas { | ||
when .chars β $SHA-LENGTH { | ||
@warnings.push: β$_ should be $SHA-LENGTH characters in lengthβ | ||
} | ||
when @git-commits.none { | ||
@warnings.push: β$_ was referenced but there is no commit with this idβ | ||
} | ||
default { take $_ } | ||
} | ||
my $ignored = set ignored-commits; | ||
my @unlogged = @git-commits.grep: * !β ($commit-mentioned βͺ $ignored); # ordered | ||
my $summary = β{@git-commits - @unlogged} out of {+@git-commits} commits loggedβ; | ||
{ :$summary, :@unlogged, :@warnings } | ||
} | ||
|
||
sub blockers() { | ||
use HTTP::UserAgent; | ||
my $ua = HTTP::UserAgent.new; | ||
my $response = try { $ua.get: $BLOCKERS-URL }; | ||
return { summary => βR6 is downβ } without $response; | ||
return { summary => βR6 is downβ } unless $response.is-success; | ||
if $response.content-type ne βapplication/json;charset=UTF-8β { | ||
return { summary => βR6 is weirdβ } | ||
} | ||
my %data = from-json $response.decoded-content; | ||
return { summary => βR6 is weirdβ } unless %data<tickets>:exists; | ||
my @tickets = %data<tickets>.list; | ||
return { summary => βNo blockersβ } unless @tickets; | ||
my $summary = β{+@tickets} blocker{@tickets β 1 ?? βsβ !! ββ}β; | ||
{:$summary, :@tickets} | ||
} | ||
|
||
multi method irc-to-me($msg where /^ \s* [changelog|status|release|when]β?β? | ||
[\s+ $<url>=[βhttpβ.*]]? $/) { | ||
my $changelog = process-url ~$_, $msg with $<url>; | ||
$changelog //= slurp β$RAKUDO/docs/ChangeLogβ; | ||
my $answer = time-to-release($msg) ~ β. β without $<url>; | ||
my %stats = changelog-to-stats $changelog; | ||
my %blockers = blockers without $<url>; | ||
|
||
# β All code here just to make the message pretty β | ||
$answer ~= β$_. β with %blockers<summary>; | ||
$answer ~= %stats<summary>; | ||
$answer ~= β (β {+%stats<warnings>} warnings)β if %stats<warnings>; | ||
$msg.reply: $answer; | ||
return if none %blockers<tickets>, %stats<unlogged>, %stats<warnings>; | ||
|
||
# β And here just to make a pretty gist β | ||
my &escape-html = { .trans: (β&β, β<β, β>β) => (β&β, β<β, β>β) }; | ||
my %files; | ||
|
||
my $blockers = join β\nβ, (%blockers<tickets> // ()).map: { β<a href="β | ||
~ $TICKET-URL ~ .<ticket_id> ~ β">RT #β | ||
~ .<ticket_id> ~ β</a> β ~ escape-html .<subject> }; | ||
%files<!blockers!.md> = β<pre>β ~ $blockers ~ β</pre>β if %blockers<tickets>; | ||
|
||
my $warnings = .join(β\nβ) with %stats<warnings>; | ||
%files<!warnings!> = $warnings if $warnings; | ||
|
||
with %stats<unlogged> { | ||
my $descs = run :out, :cwd($RAKUDO), βgitβ, βshowβ, | ||
β--format=%sβ, | ||
β--abbrev=$SHA-LENGTHβ, β--quietβ, |$_; | ||
my $links = run :out, :cwd($RAKUDO), βgitβ, βshowβ, | ||
β--format=[<a href="β ~ $RAKUDO-REPO ~ β/commit/%H">%h</a>]β, | ||
β--abbrev=$SHA-LENGTHβ, β--quietβ, |$_; | ||
my $unreviewed = join β\nβ, ($descs.out.lines Z $links.out.lines).map: | ||
{β + β ~ escape-html(.[0]) ~ β β ~ .[1]}; | ||
%files<unreviewed.md> = β<pre>β ~ $unreviewed ~ β</pre>β if $unreviewed; | ||
} | ||
(ββ but FileStore(%files)) but PrettyLink({βDetails: $_β}) | ||
} | ||
|
||
Releasable.new.selfrun: βreleasable6β, [ / release6? <before β:β> /, | ||
fuzzy-nick(βreleasable6β, 2) ] | ||
|
||
# vim: expandtab shiftwidth=4 ft=perl6 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
#!/usr/bin/env perl6 | ||
BEGIN %*ENV<PERL6_TEST_DIE_ON_FAIL> = 1; | ||
%*ENV<TESTABLE> = βrakudo-mockβ; | ||
|
||
use lib βt/libβ; | ||
use File::Directory::Tree; | ||
use IRC::Client; | ||
use Test; | ||
use Testable; | ||
|
||
my $t = Testable.new: bot => βReleasableβ; | ||
|
||
$t.common-tests: help => βstatus | status linkβ; | ||
|
||
$t.shortcut-tests: <release: release6:>, | ||
<release release, release6 release6,>; | ||
|
||
# The idea is to generate a pseudorealistic repo that | ||
# is good enough for testing purposes. | ||
|
||
my $mock = βt/data/rakudoβ.IO; | ||
rmtree $mock if $mock.e; | ||
mkdir $mock; | ||
run :cwd($mock), :out(Nil), βgitβ, βinitβ; | ||
mkdir $mock.add: βdocsβ; | ||
spurt $mock.add(βdocs/ChangeLogβ), βNew in 2090.07:\n + Additions:\n\nβ; | ||
|
||
my @releases = ( | ||
β2090-08-19 Rakudo #990 (AlexDaniel)β, | ||
β2090-09-16 Rakudo #991β, | ||
β2090-10-21 Rakudo #992β, | ||
β2090-11-18 Rakudo #993β, | ||
); | ||
|
||
sub changelog(Block $do) { | ||
my $path = $mock.add: βdocs/ChangeLogβ; | ||
spurt $path, $do(slurp $path); | ||
} | ||
|
||
sub tag-last($tag-name, $new-section = βNew in {$tag-name + 0.01}β #β hahaha | ||
~ β:\n + Additions:\n\nβ ) { | ||
my $sha = run(:cwd($mock), :out, βgitβ, βrev-parseβ, βHEADβ) | ||
.out.slurp-rest.chomp; | ||
run :cwd($mock), βgitβ, βtagβ, β--annotateβ, | ||
β--message=Blah $tag-nameβ, $tag-name; | ||
spurt $mock.add(βVERSIONβ), $tag-name; | ||
changelog { $new-section ~ $_ } if defined $new-section | ||
} | ||
|
||
sub commit($message, :$log = True) { | ||
my $foo = ^9βΉ .pick; | ||
spurt $mock.add($foo), $foo; | ||
run :cwd($mock), βgitβ, βaddβ, β--β, $foo; | ||
run :cwd($mock), :out(Nil), βgitβ, βcommitβ, β--message=$messageβ; | ||
|
||
my $release-guide = β=head2 Planned future releases\n\nβ¦ β¦ β¦\n\nβ | ||
~ @releases.map({ β $_\nβ }).join; | ||
spurt $mock.add(βdocs/release_guide.podβ), $release-guide; | ||
|
||
my $sha = run(:cwd($mock), :out, βgitβ, βrev-parseβ, βHEADβ) | ||
.out.slurp-rest.chomp.substr: 0, 8; | ||
my $log-entry = $log ~~ Bool ?? β + $message [$sha]\nβ !! $log; | ||
changelog -> $file is copy { | ||
die without $file ~~ s/<after \n><before \n>/$log-entry/; | ||
$file | ||
} if $log; | ||
$sha | ||
} | ||
|
||
# TODO the number of blockers and the time left is not controllable | ||
|
||
# Basics | ||
|
||
commit β$!.pending (RT #68320)β; | ||
|
||
$t.test(βunknown formatβ, | ||
β{$t.bot-nick}: when?β, | ||
/^ <me($t)>β, Next release in β\d+β dayβs?β and ββ\d+β hourβs?β. β | ||
[ \d+β blockersβ | βNo blockersβ ]β. β | ||
βUnknown changelog formatβ $/, | ||
β{$t.our-nick}, Details: https://whatever.able/fakeuploadβ); | ||
|
||
|
||
tag-last β2090.07β; | ||
commit β.hyper and .race finally re-addedβ; | ||
tag-last β2090.08β, Nil; | ||
|
||
$t.test(βnot started yetβ, | ||
β{$t.bot-nick}: statusβ, | ||
β{$t.our-nick}, Release date for Rakudo 2090.08 is listed inβ | ||
~ β βPlanned future releasesβ, but it was already released.β, | ||
/^ <me($t)>β, Next release in β\d+β dayβs?β and ββ\d+β hourβs?β. β | ||
[ \d+β blockersβ | βNo blockersβ ]β. β | ||
βChangelog for this release was not started yetβ $/, | ||
β{$t.our-nick}, Details: https://whatever.able/fakeuploadβ); | ||
|
||
|
||
@releases.shift; | ||
|
||
my $to-be-logged = commit βA change that should be loggedβ, :!log; | ||
my $to-be-logged-not = commit βA change that should not be loggedβ, :!log; | ||
my @real = βFurious whitespace changesβ xx 4; | ||
@real.push: βFix nothingβ; | ||
@real .= map: { commit $_, :!log }; | ||
my $log-entries = qq:to/END/; | ||
New in 2090.09: | ||
+ Deprecations: | ||
+ Deprecate everything [de1e7ea1] | ||
+ Fixes: | ||
+ Fix nothing [@real[*-1]] | ||
+ Furious whitespace changes [@real[0]] [@real[1]] [@real[2]] | ||
[@real[3]] [abcabcabcabc] | ||
+ No really, this change is very important [@real[1]] | ||
END | ||
changelog { $log-entries ~ $_ }; | ||
|
||
run :cwd($mock), βgitβ, βaddβ, β--β, βdocs/ChangeLogβ; | ||
commit βChangelog\n\nIntentionally not logged: $to-be-logged-notβ, :!log; | ||
|
||
$t.test(βrealistic outputβ, | ||
β{$t.bot-nick}: releaseβ, | ||
β{$t.our-nick}, Release manager is not specified yet.β, | ||
/^ <me($t)>β, Next release in β\d+β dayβs?β and ββ\d+β hourβs?β. β | ||
[ \d+β blockersβ | βNo blockersβ ]β. β | ||
β6 out of 8 commits logged (β 2 warnings)β $/, # TODO ideally should be 7 out of 8 | ||
β{$t.our-nick}, Details: https://whatever.able/fakeuploadβ); | ||
|
||
$t.test-gist(βgisted files look alrightβ, | ||
%(β!warnings!β => | ||
βde1e7ea1 was referenced but there is no commit with this idβ | ||
~ β\nβ ~ βabcabcabcabc should be 8 characters in lengthβ, | ||
βunreviewed.mdβ => | ||
/ο½’<pre> + A change that should be loggedο½£ | ||
ο½’ [<a href="https://github.com/rakudo/rakudo/commit/ο½£ | ||
$to-be-logged <.xdigit>+ ο½’">ο½£ $to-be-logged ο½’</a>]ο½£.*ο½’</pre>ο½£ # TODO .* | ||
/, | ||
) | ||
); | ||
|
||
|
||
$t.test(βuncommitted changed from a linkβ, | ||
β{$t.bot-nick}: changelog https://gist.github.com/AlexDaniel/45b98a8bd5935a53a3ed4762ea5f5d43/raw/β, | ||
β{$t.our-nick}, Successfully fetched the code from the provided URL.β, | ||
β{$t.our-nick}, 1 out of 8 commits loggedβ, | ||
β{$t.our-nick}, Details: https://whatever.able/fakeuploadβ); | ||
|
||
# $t.last-test; # Deliberately no $t.last-test! (we can't repeat the first test) | ||
$t.test(βNo uncaught messagesβ, | ||
β{$t.bot-nick}: helpβ, | ||
/^ <me($t)>β, status | status linkβ /); | ||
|
||
done-testing; | ||
END $t.end; | ||
|
||
# vim: expandtab shiftwidth=4 ft=perl6 |