Skip to content

Commit 5d7445b

Browse files
committed
New bot: Releasable (with tests)
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 😉
1 parent a1aa0e5 commit 5d7445b

File tree

3 files changed

+348
-1
lines changed

3 files changed

+348
-1
lines changed

bin/Releasable.p6

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
#!/usr/bin/env perl6
2+
# Copyright © 2017
3+
# Aleks-Daniel Jakimenko-Aleksejev <alex.jakimenko@gmail.com>
4+
#
5+
# This program is free software: you can redistribute it and/or modify
6+
# it under the terms of the GNU Affero General Public License as published by
7+
# the Free Software Foundation, either version 3 of the License, or
8+
# (at your option) any later version.
9+
#
10+
# This program is distributed in the hope that it will be useful,
11+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
# GNU Affero General Public License for more details.
14+
#
15+
# You should have received a copy of the GNU Affero General Public License
16+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
18+
use Whateverable;
19+
use Misc;
20+
21+
use IRC::Client;
22+
23+
unit class Releasable does Whateverable;
24+
25+
# ↓ Git itself suggests 9 characters, and 12 characters may be a better
26+
# ↓ choice for the hundred-year language… but let's increase it only
27+
# ↓ when needed
28+
my $SHA-LENGTH = 8;
29+
my $RELEASE-HOUR = 19; # GMT+0
30+
my $BLOCKERS-URL = https://perl6.fail/release/blockers.json;
31+
my $TICKET-URL = https://rt.perl.org/rt3/Public/Bug/Display.html?id=;
32+
33+
method help($msg) {
34+
status | status link
35+
}
36+
37+
sub ignored-commits() {
38+
my $last-release = to-full-commit chomp slurp $RAKUDO/VERSION;
39+
die Cannot resolve the tag for the last release unless $last-release;
40+
my $result = run :out, :cwd($RAKUDO), git, log, --pretty=%b,
41+
-z, $last-release..HEAD, --, docs/ChangeLog;
42+
die Cannot git log the changelog unless $result;
43+
44+
return gather for $result.out.split(0.chr, :skip-empty) {
45+
next unless /not logged\N*: \s* [ @<shas>=[<.xdigit>**4..* ] ]+ % \s+/;
46+
{ take ~$_ if .chars == $SHA-LENGTH } for @<shas>
47+
}
48+
}
49+
50+
sub time-left($then) {
51+
my $time-left = $then.Instant - now;
52+
my ($seconds, $minutes, $hours, $days) = $time-left.polymod: 60, 60, 24;
53+
return is just a few moments away if not $days and not $hours;
54+
my $answer = in ;
55+
$answer ~= $days day{$days1 ?? s !! } and if $days;
56+
$answer ~= $hours hour{$hours1 ?? s !! };
57+
$answer
58+
}
59+
60+
sub time-to-release($msg) {
61+
my $guide = slurp $RAKUDO/docs/release_guide.pod;
62+
die Unable to parse the release guide unless $guide ~~ /
63+
^^ ‘=head2 Planned future releases’ $$
64+
.*?
65+
(^^‘ ’(\d\d\d\d‘-’\d\d‘-’\d\d)\s+ ‘Rakudo #’(\d+) [\s+‘(’ (<-[)]>+) ‘)’]? \n)+
66+
/;
67+
my @dates = $0.map: { %(date => Date.new(~.[0]), id => +.[1], manager => (.Str with .[2])) };
68+
my $important-date;
69+
my $annoying-warning = False;
70+
for @dates {
71+
my $release = .<date>.yyyy-mm-dd.split(-)[0,1].join: .;
72+
if not to-full-commit $release {
73+
$important-date = $_;
74+
if not .<manager> and not $annoying-warning {
75+
$msg.reply: Release manager is not specified yet.
76+
}
77+
last
78+
}
79+
if not $annoying-warning {
80+
$annoying-warning = True;
81+
$msg.reply: Release date for Rakudo $release is listed in
82+
~ “Planned future releases”, but it was already released.;
83+
}
84+
}
85+
die Release date not found without $important-date;
86+
my $time-left = time-left DateTime.new(date => $important-date<date>,
87+
hour => $RELEASE-HOUR);
88+
Next release $time-left
89+
}
90+
91+
sub changelog-to-stats($changelog) {
92+
if not $changelog.match: /^ ‘New in ’ (.*?) ‘:’ (.*?) ^^ ‘New in ’ (.*?) ‘:’/ {
93+
return { summary => Unknown changelog format }
94+
}
95+
my ($version, $changes, $version-old) = ~$0, ~$1, ~$2;
96+
97+
my $actual-commit = to-full-commit $version;
98+
with $actual-commit {
99+
return { summary => Changelog for this release was not started yet }
100+
}
101+
my $actual-commit-old = to-full-commit $version-old;
102+
die Cannot resolve the tag for the previous release without $actual-commit-old;
103+
104+
my @shas = $changes.match(:g, / [‘[’ (<.xdigit>**4..*) ‘]’ \s*]+ $$/)»[0].flat».Str;
105+
my $result = run :out, :cwd($RAKUDO), git, log, -z, --pretty=%H,
106+
--reverse, $actual-commit-old..HEAD;
107+
die Failed to query rakudo git log unless $result;
108+
my @git-commits = $result.out.slurp-rest.split(0.chr, :skip-empty)
109+
.map: *.substr: 0, $SHA-LENGTH;
110+
my @warnings;
111+
my $commit-mentioned = set gather for @shas {
112+
when .chars$SHA-LENGTH {
113+
@warnings.push: $_ should be $SHA-LENGTH characters in length
114+
}
115+
when @git-commits.none {
116+
@warnings.push: $_ was referenced but there is no commit with this id
117+
}
118+
default { take $_ }
119+
}
120+
my $ignored = set ignored-commits;
121+
my @unlogged = @git-commits.grep: * ! ($commit-mentioned $ignored); # ordered
122+
my $summary = {@git-commits - @unlogged} out of {+@git-commits} commits logged;
123+
{ :$summary, :@unlogged, :@warnings }
124+
}
125+
126+
sub blockers() {
127+
use HTTP::UserAgent;
128+
my $ua = HTTP::UserAgent.new;
129+
my $response = try { $ua.get: $BLOCKERS-URL };
130+
return { summary => R6 is down } without $response;
131+
return { summary => R6 is down } unless $response.is-success;
132+
if $response.content-type ne application/json;charset=UTF-8 {
133+
return { summary => R6 is weird }
134+
}
135+
my %data = from-json $response.decoded-content;
136+
return { summary => R6 is weird } unless %data<tickets>:exists;
137+
my @tickets = %data<tickets>.list;
138+
return { summary => No blockers } unless @tickets;
139+
my $summary = {+@tickets} blocker{@tickets1 ?? s !! };
140+
{:$summary, :@tickets}
141+
}
142+
143+
multi method irc-to-me($msg where /^ \s* [changelog|status|release|when]??
144+
[\s+ $<url>=[http.*]]? $/) {
145+
my $changelog = process-url ~$_, $msg with $<url>;
146+
$changelog //= slurp $RAKUDO/docs/ChangeLog;
147+
my $answer = time-to-release($msg) ~ . without $<url>;
148+
my %stats = changelog-to-stats $changelog;
149+
my %blockers = blockers without $<url>;
150+
151+
# ↓ All code here just to make the message pretty ↓
152+
$answer ~= $_. with %blockers<summary>;
153+
$answer ~= %stats<summary>;
154+
$answer ~= (⚠ {+%stats<warnings>} warnings) if %stats<warnings>;
155+
$msg.reply: $answer;
156+
return if none %blockers<tickets>, %stats<unlogged>, %stats<warnings>;
157+
158+
# ↓ And here just to make a pretty gist ↓
159+
my &escape-html = { .trans: (&, <, >) => (&amp;, &lt;, &gt;) };
160+
my %files;
161+
162+
my $blockers = join \n, (%blockers<tickets> // ()).map: { <a href="
163+
~ $TICKET-URL ~ .<ticket_id> ~ ">RT #
164+
~ .<ticket_id> ~ </a> ~ escape-html .<subject> };
165+
%files<!blockers!.md> = <pre> ~ $blockers ~ </pre> if %blockers<tickets>;
166+
167+
my $warnings = .join(\n) with %stats<warnings>;
168+
%files<!warnings!> = $warnings if $warnings;
169+
170+
with %stats<unlogged> {
171+
my $descs = run :out, :cwd($RAKUDO), git, show,
172+
--format=%s,
173+
--abbrev=$SHA-LENGTH, --quiet, |$_;
174+
my $links = run :out, :cwd($RAKUDO), git, show,
175+
--format=[<a href=" ~ $RAKUDO-REPO ~ /commit/%H">%h</a>],
176+
--abbrev=$SHA-LENGTH, --quiet, |$_;
177+
my $unreviewed = join \n, ($descs.out.lines Z $links.out.lines).map:
178+
{ + ~ escape-html(.[0]) ~ ~ .[1]};
179+
%files<unreviewed.md> = <pre> ~ $unreviewed ~ </pre> if $unreviewed;
180+
}
181+
( but FileStore(%files)) but PrettyLink({Details: $_})
182+
}
183+
184+
Releasable.new.selfrun: releasable6, [ / release6? <before ‘:’> /,
185+
fuzzy-nick(releasable6, 2) ]
186+
187+
# vim: expandtab shiftwidth=4 ft=perl6

lib/Whateverable.pm6

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ use Text::Diff::Sift4;
3030

3131
use Misc;
3232

33-
our $RAKUDO = ./data/rakudo-moar.IO.absolute;
33+
our $RAKUDO = (%*ENV<TESTABLE> // ).contains(rakudo-mock)
34+
?? ./t/data/rakudo.IO.absolute
35+
!! ./data/rakudo-moar.IO.absolute;
3436
constant MOARVM = ./data/moarvm.IO.absolute;
3537
constant CONFIG = ./config.json.IO.absolute;
3638
constant SOURCE = https://github.com/perl6/whateverable;
@@ -43,6 +45,8 @@ constant MESSAGE-LIMIT is export = 260;
4345
constant COMMITS-LIMIT = 500;
4446
constant PARENTS = AlexDaniel, MasterDuke;
4547

48+
our $RAKUDO-REPO = https://github.com/rakudo/rakudo;
49+
4650
constant Message = IRC::Client::Message;
4751

4852
unit role Whateverable[:$default-timeout = 10] does IRC::Client::Plugin does Helpful;

t/releasable.t

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
#!/usr/bin/env perl6
2+
BEGIN %*ENV<PERL6_TEST_DIE_ON_FAIL> = 1;
3+
%*ENV<TESTABLE> = rakudo-mock;
4+
5+
use lib t/lib;
6+
use File::Directory::Tree;
7+
use IRC::Client;
8+
use Test;
9+
use Testable;
10+
11+
my $t = Testable.new: bot => Releasable;
12+
13+
$t.common-tests: help => status | status link;
14+
15+
$t.shortcut-tests: <release: release6:>,
16+
<release release, release6 release6,>;
17+
18+
# The idea is to generate a pseudorealistic repo that
19+
# is good enough for testing purposes.
20+
21+
my $mock = t/data/rakudo.IO;
22+
rmtree $mock if $mock.e;
23+
mkdir $mock;
24+
run :cwd($mock), :out(Nil), git, init;
25+
mkdir $mock.add: docs;
26+
spurt $mock.add(docs/ChangeLog), New in 2090.07:\n + Additions:\n\n;
27+
28+
my @releases = (
29+
2090-08-19 Rakudo #990 (AlexDaniel),
30+
2090-09-16 Rakudo #991,
31+
2090-10-21 Rakudo #992,
32+
2090-11-18 Rakudo #993,
33+
);
34+
35+
sub changelog(Block $do) {
36+
my $path = $mock.add: docs/ChangeLog;
37+
spurt $path, $do(slurp $path);
38+
}
39+
40+
sub tag-last($tag-name, $new-section = New in {$tag-name + 0.01} #← hahaha
41+
~ :\n + Additions:\n\n ) {
42+
my $sha = run(:cwd($mock), :out, git, rev-parse, HEAD)
43+
.out.slurp-rest.chomp;
44+
run :cwd($mock), git, tag, --annotate,
45+
--message=Blah $tag-name, $tag-name;
46+
spurt $mock.add(VERSION), $tag-name;
47+
changelog { $new-section ~ $_ } if defined $new-section
48+
}
49+
50+
sub commit($message, :$log = True) {
51+
my $foo = ^9.pick;
52+
spurt $mock.add($foo), $foo;
53+
run :cwd($mock), git, add, --, $foo;
54+
run :cwd($mock), :out(Nil), git, commit, --message=$message;
55+
56+
my $release-guide = =head2 Planned future releases\n\n… … …\n\n
57+
~ @releases.map({ $_\n }).join;
58+
spurt $mock.add(docs/release_guide.pod), $release-guide;
59+
60+
my $sha = run(:cwd($mock), :out, git, rev-parse, HEAD)
61+
.out.slurp-rest.chomp.substr: 0, 8;
62+
my $log-entry = $log ~~ Bool ?? + $message [$sha]\n !! $log;
63+
changelog -> $file is copy {
64+
die without $file ~~ s/<after \n><before \n>/$log-entry/;
65+
$file
66+
} if $log;
67+
$sha
68+
}
69+
70+
# TODO the number of blockers and the time left is not controllable
71+
72+
# Basics
73+
74+
commit $!.pending (RT #68320);
75+
76+
$t.test(unknown format,
77+
{$t.bot-nick}: when?,
78+
/^ <me($t)>, Next release in \d+ days? and ≈\d+ hours?.
79+
[ \d+ blockers | No blockers ].
80+
Unknown changelog format $/,
81+
{$t.our-nick}, Details: https://whatever.able/fakeupload);
82+
83+
84+
tag-last 2090.07;
85+
commit .hyper and .race finally re-added;
86+
tag-last 2090.08, Nil;
87+
88+
$t.test(not started yet,
89+
{$t.bot-nick}: status,
90+
{$t.our-nick}, Release date for Rakudo 2090.08 is listed in
91+
~ “Planned future releases”, but it was already released.,
92+
/^ <me($t)>, Next release in \d+ days? and ≈\d+ hours?.
93+
[ \d+ blockers | No blockers ].
94+
Changelog for this release was not started yet $/,
95+
{$t.our-nick}, Details: https://whatever.able/fakeupload);
96+
97+
98+
@releases.shift;
99+
100+
my $to-be-logged = commit A change that should be logged, :!log;
101+
my $to-be-logged-not = commit A change that should not be logged, :!log;
102+
my @real = Furious whitespace changes xx 4;
103+
@real.push: Fix nothing;
104+
@real .= map: { commit $_, :!log };
105+
my $log-entries = qq:to/END/;
106+
New in 2090.09:
107+
+ Deprecations:
108+
+ Deprecate everything [de1e7ea1]
109+
+ Fixes:
110+
+ Fix nothing [@real[*-1]]
111+
+ Furious whitespace changes [@real[0]] [@real[1]] [@real[2]]
112+
[@real[3]] [abcabcabcabc]
113+
+ No really, this change is very important [@real[1]]
114+
115+
END
116+
changelog { $log-entries ~ $_ };
117+
118+
run :cwd($mock), git, add, --, docs/ChangeLog;
119+
commit Changelog\n\nIntentionally not logged: $to-be-logged-not, :!log;
120+
121+
$t.test(realistic output,
122+
{$t.bot-nick}: release,
123+
{$t.our-nick}, Release manager is not specified yet.,
124+
/^ <me($t)>, Next release in \d+ days? and ≈\d+ hours?.
125+
[ \d+ blockers | No blockers ].
126+
6 out of 8 commits logged (⚠ 2 warnings) $/, # TODO ideally should be 7 out of 8
127+
{$t.our-nick}, Details: https://whatever.able/fakeupload);
128+
129+
$t.test-gist(gisted files look alright,
130+
%(!warnings! =>
131+
de1e7ea1 was referenced but there is no commit with this id
132+
~ \n ~ abcabcabcabc should be 8 characters in length,
133+
unreviewed.md =>
134+
/<pre> + A change that should be logged
135+
[<a href="https://github.com/rakudo/rakudo/commit/
136+
$to-be-logged <.xdigit>+ "> $to-be-logged </a>].*</pre> # TODO .*
137+
/,
138+
)
139+
);
140+
141+
142+
$t.test(uncommitted changed from a link,
143+
{$t.bot-nick}: changelog https://gist.github.com/AlexDaniel/45b98a8bd5935a53a3ed4762ea5f5d43/raw/,
144+
{$t.our-nick}, Successfully fetched the code from the provided URL.,
145+
{$t.our-nick}, 1 out of 8 commits logged,
146+
{$t.our-nick}, Details: https://whatever.able/fakeupload);
147+
148+
# $t.last-test; # Deliberately no $t.last-test! (we can't repeat the first test)
149+
$t.test(No uncaught messages,
150+
{$t.bot-nick}: help,
151+
/^ <me($t)>‘, status | status link’ /);
152+
153+
done-testing;
154+
END $t.end;
155+
156+
# vim: expandtab shiftwidth=4 ft=perl6

0 commit comments

Comments
 (0)