Update version when we are in RC - to uncomment
[release-tools.git] / release-tool.pl
1 #!/usr/bin/perl -w
2
3 # release-tool.pl - script to manage the Koha release process
4 #
5 # Copyright (C) 2012  C & P Bibliography Services
6 #
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version.
11 #
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU Affero General Public License for more details.
16 #
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
19
20
21 =head1 NAME
22
23 release-tool.pl
24
25 =head1 SYNOPSIS
26
27   release-tool.pl
28   release-tool.pl --version 3.06.05
29
30 =head1 DESCRIPTION
31
32 This script takes care of most of the Koha release process, as it is
33 done by Jared Camins-Esakov and C & P Bibliography Services. This may
34 not perfectly meet the needs of other Release Maintainers and/or
35 organizations.
36
37 =cut
38
39 use strict;
40 use warnings;
41 use Getopt::Long;
42 use Pod::Usage;
43 use File::Spec;
44 use File::Copy;
45 use Data::Dumper;
46 use File::Basename;
47 use File::Path qw/make_path remove_tree/;
48 use Term::ANSIColor;
49 use Time::HiRes qw/time/;
50 use POSIX qw/strftime ceil/;
51 use TAP::Harness;
52 use DBI;
53 use MIME::Lite;
54 use Template;
55 use Config::Simple;
56 use Net::OpenSSH;
57
58 $SIG{INT} = \&interrupt;
59
60 sub usage {
61     pod2usage( -verbose => 2 );
62     exit;
63 }
64
65 $|                          = 1;
66 $Term::ANSIColor::AUTORESET = 1;
67
68 my %defaults = (
69     alert                => '',
70     autoversion          => 0,
71     branch               => '',
72     'build-result'       => '',
73     clean                => 0,
74     deploy               => 0,
75     'email-file'         => '',
76     errorlog             => '',
77     kohaclone            => '',
78     'maintainer-email'   => '',
79     'maintainer-name'    => '',
80     package              => '',
81     'post-deploy-script' => [ '' ],
82     quiet                => 0,
83     rnotes               => '',
84     sign                 => 0,
85     since                => '',
86     'skip-deb'           => 0,
87     'skip-install'       => 0,
88     'skip-marc21'        => 0,
89     'skip-normarc'       => 0,
90     'skip-pbuilder'      => 0,
91     'skip-rnotes'        => 0,
92     'skip-stats'         => 0,
93     'skip-tests'         => 0,
94     'skip-tgz'           => 0,
95     'skip-unimarc'       => 0,
96     'skip-webinstall'    => 0,
97     stats                => '',
98     tag                  => 0,
99     tarball              => '',
100     'use-dist-rnotes'    => 0,
101     verbose              => 0,
102     version              => '',
103
104     # database settings
105     database => $ENV{KOHA_DATABASE} || 'koha',
106     user     => $ENV{KOHA_USER}     || 'kohaadmin',
107     password => $ENV{KOHA_PASS}     || 'katikoan',
108
109     # announcement settings
110     'email-template' => 'announcement.eml.tt',
111     'email-recipients' =>
112       'koha@lists.katipo.co.nz, koha-devel@lists.koha-community.org',
113     'email-subject'    => "New Koha version",
114     'website-template' => 'announcement.html.tt',
115 );
116
117 my $deployed        = 'no';
118 my $signed_tarball  = 'no';
119 my $signed_packages = 'no';
120 my $tagged          = 'no';
121 my $cleaned         = 'no';
122 my $pushed          = 'no';
123 my $skipped         = '';
124 my $finished_tests  = 'no';
125 my $built_tarball   = 'no';
126 my $built_packages  = 'no';
127 my $version_mismatch;
128 my $output;
129 my @tested_tarball_installs;
130 my @tested_package_installs;
131 my %cmdline;
132 my $config = new Config::Simple( syntax => 'http' );
133 my $log = '';
134 my $lxc_host = '';
135
136 =head2 General options
137
138 =over 8
139
140 =item B<--help>
141
142 Prints this help
143
144 =item B<--quiet, -q>
145
146 Don't display any status information while running. When specified
147 twice, also suppress the summary.
148
149 =item B<--verbose, -v>
150
151 Provide verbose diagnostic information
152
153 =item B<--config>
154
155 Read configuration settings from the specified file. Options set on the
156 command line will override options in the configuration file.
157
158 =back
159
160 =head2 Action control options
161
162 =over 8
163
164 =item B<--clean, -c>
165
166 Delete all the files created in the course of the test
167
168 =item B<--deploy, -d>
169
170 Deploy the package to the apt repository
171
172 =item B<--release>
173
174 Equivalent to I<--sign --deploy --tag --tarball=koha-${VERSION}.tar.gz>
175
176 =item B<--sign, -s>
177
178 Sign the tarball and package and tag (if created)
179
180 =item B<--tag, -g>
181
182 Tag the git repository
183
184 =item B<--push, -p>
185
186 Push the branch to the specified remote(s).
187
188 =item B<--skip-THING>
189
190 Most actions are performed automatically, unless the user requests that they
191 be skipped. Skip THING. Currently the following can be skipped:
192
193 =over 4
194
195 =item B<tests>
196 Unit tests
197
198 =item B<deb>
199 Debian package-related tasks
200
201 =item B<tgz>
202 Tarball-related tasks
203
204 =item B<install>
205 Installation-related tasks
206
207 =item B<marc21>
208 MARC21 instance installation
209
210 =item B<unimarc>
211 UNIMARC instance installation
212
213 =item B<normarc>
214 NORMARC instance installation
215
216 =item B<webinstall>
217 Running the webinstaller
218
219 =item B<pbuilder>
220 Updating the pbuilder environment
221
222 =item B<rnotes>
223 Generating release notes
224
225 =item B<stats>
226 Generating git statistics
227
228 =back
229
230 =back
231 =cut
232
233 =head2 Source description options
234
235 =over 8
236
237 =item B<--kohaclone, -k>
238
239 Kohaclone directory. Defaults to the current working directory
240
241 =item B<--branch>
242
243 The name of the git branch in use. Defaults to current git branch
244
245 =item B<--distribution>
246
247 The name of the distribution. Defaults to the current git branch
248
249 =item B<--version>
250
251 The version of Koha that is being created. Defaults to the version listed in
252 kohaversion.pl
253
254 =item B<--autoversion, -a>
255
256 Automatically include the git commit id and timestamp in the package version
257
258 =item B<--since>
259
260 Tag or commit from which to generate release notes and statistics. Defaults
261 to last tag on branch.
262
263 =back
264
265 =head2 Execution options
266
267 =over 8
268
269 =item B<--database>
270
271 Name of the MySQL database to use for tarball installs. Defaults to koharel
272
273 =item B<--user>
274
275 Name of the MySQL user for tarball installs. Defaults to koharel
276
277 =item B<--password>
278
279 Name of the MySQL password for tarball installs. Defaults to koharel
280
281 =item B<--maintainer-name>
282
283 The name of the maintainer. Defaults to global git config user.name
284
285 =item B<--maintainer-email>
286
287 The e-mail address of the maintainer. Defaults to the value of git config
288 --global user.email
289
290 =item B<--use-dist-rnotes>
291
292 Use the release notes included in the distribution. I<--rnotes> moust be
293 specified if this option is used.
294
295 =item B<--post-deploy-script>
296
297 Run the specified script at the end of the deploy phase with the summary
298 config file as an argument.
299
300 =back
301
302 =head2 Output options
303
304 =over 8
305
306 =item B<--build-result, -b>
307
308 Directory to put the output into. Defaults to ~/releases/[branch]/[version]
309
310 =item B<--errorlog>
311
312 File to store error information in. Defaults to [build-result]/errors.log
313
314 =item B<--rnotes, -r>
315
316 The name of the release notes file to generate or use (see I<--use-dist-rnotes>).
317 Defaults to [build-result]/release_notes.txt
318
319 =item B<--stats>
320
321 The base name of the files into which statistics should be placed.
322 Defaults to [build-result]/statistics (statistics will be generated
323 in .txt and .html format)
324
325 =item B<--tarball, -t>
326
327 The name of the tarball file to generate. Defaults to
328 [build-result]/koha-[branch]-[version].tar.gz
329
330 =item B<--alert>
331
332 E-mail address to which an an alert summarizing the result should be sent.
333
334 =item B<--email-template>
335
336 Template file for the release announcement e-mail. Defaults to
337 "announcement.eml.tt" in the same directory as this script
338
339 =item B<--bzlogin>
340
341 Your login on bugzilla. If provided, it will be used to retrieve
342 the comment 0 for each enhancement. This information will be
343 added in the release notes
344
345 =item B<--bzpassword>
346
347 Your password on bugzilla. Must be provided if you provide bzlogin
348
349 =back
350
351 =head2 Announcement options
352
353 =over 8
354
355 =item B<--email-recipients>
356
357 Who to generate the e-mail announcement for. Defaults to 
358 "koha@lists.katipo.co.nz, koha-devel@lists.koha-community.org"
359
360 =item B<--email-subject>
361
362 Subject of the generated e-mail announcement. Defaults to "New Koha version"
363
364 =item B<--email-file>
365
366 File to store the generated e-mail announcement in. Defaults to
367 [build-result]/announcement.eml
368
369 =item B<--email-template>
370
371 Template file for the release announcement e-mail. Defaults to
372 "announcement.eml.tt" in the same directory as this script
373
374 =back
375 =cut
376
377 my $options = GetOptions(
378     \%cmdline,
379
380     # General options
381     'help|h',     'quiet|q+',
382     'verbose|v+', 'config=s',
383
384     # Action control options
385     'clean|c', 'deploy|d',
386     'push|p:s@', 'release',
387     'sign|s', 'tag|g',
388     'skip-tests',
389     'skip-deb',        'skip-tgz',
390     'skip-install',    'skip-marc21',
391     'skip-unimarc',    'skip-normarc',
392     'skip-webinstall', 'skip-pbuilder',
393     'skip-rnotes',     'skip-stats',
394
395     # Source description options
396     'version=s', 'autoversion|a',
397     'kohaclone|k=s',
398     'branch=s', 'since=s',
399     'distribution=s',
400
401     # Execution options
402     'database=s',
403     'user=s', 'password=s',
404     'use-dist-rnotes',
405     'maintainer-name=s', 'maintainer-email=s',
406     'post-deploy-script=s@',
407
408     # Output options
409     'build-result|b=s', 'errorlog=s',
410     'tarball|t=s',      'rnotes|r=s',
411     'stats=s', 'alert=s',
412
413     # Announcement options
414     'email-file=s',
415     'email-recipients=s', 'email-subject=s',
416     'email-template=s',
417
418     # Bugzilla options
419     'bzlogin:s','bzpassword:s',
420 );
421
422 binmode( STDOUT, ":utf8" );
423
424 if ( $cmdline{help} ) {
425     usage();
426 }
427
428 if ( defined( $cmdline{config} ) && -f File::Spec->rel2abs( $cmdline{config} ) )
429 {
430     $config->read( $cmdline{config} );
431 }
432 foreach my $key ( keys %defaults ) {
433     $config->param( $key, $defaults{$key} ) unless $config->param($key);
434 }
435 foreach my $key ( keys %cmdline ) {
436     $config->param( $key, $cmdline{$key} );
437 }
438
439 my $starttime = time();
440
441 chdir $config->param('kohaclone')
442   if ( $config->param('kohaclone') && -d $config->param('kohaclone') );
443
444 my $reltools = File::Spec->rel2abs( dirname(__FILE__) );
445
446 $config->param( 'kohaclone', File::Spec->rel2abs( File::Spec->curdir() ) )
447   unless ( $config->param('kohaclone') && -d $config->param('kohaclone') );
448
449 $ENV{PERL5LIB} = $config->param('kohaclone');
450 my @marcflavours;
451 push @marcflavours, 'MARC21'  unless $config->param('skip-marc21');
452 push @marcflavours, 'UNIMARC' unless $config->param('skip-unimarc');
453 push @marcflavours, 'NORMARC' unless $config->param('skip-normarc');
454
455 set_default( 'branch', `git branch | grep '*' | sed -e 's/^* //'` );
456 my $dist = $config->param('branch');
457 $dist =~ s#[/_]#-#g;
458 set_default( 'distribution', $dist );
459
460 my $kohaversion = `grep 'VERSION = ' kohaversion.pl | sed -e "s/^[^']*'//" -e "s/';//"`;
461 set_default( 'version', $kohaversion );
462
463 set_default( 'maintainer-name', `git config --global --get user.name` );
464
465 set_default( 'maintainer-email', `git config --global --get user.email` );
466
467 set_default( 'build-result',
468         "$ENV{HOME}/releases/"
469       . $config->param('distribution') . '/'
470       . $config->param('version') );
471
472 make_path( $config->param('build-result') );
473
474 opendir( DIR, $config->param('build-result') );
475
476 while ( my $file = readdir(DIR) ) {
477
478     # We only want files
479     $file = $config->param('build-result') . "$file";
480     next unless ( -f $file );
481
482     unlink $file;
483 }
484
485 $ENV{TEST_QA} = 1;
486
487 if ( $config->param('tarball') =~ m#/# ) {
488     $config->param( 'tarball', '' )
489       unless ( -d dirname( $config->param('tarball') ) );
490 }
491 elsif ( $config->param('tarball') ) {
492     $config->param( 'tarball', build_result( $config->param('tarball') ) );
493 }
494
495 set_default( 'email-file', build_result('announcement.eml') );
496
497 set_default(
498     'tarball',
499     build_result(
500             'koha-'
501           . $config->param('distribution') . '-'
502           . $config->param('version')
503           . '.tar.gz'
504     )
505 );
506
507 set_default( 'rnotes', build_result('release_notes.txt') );
508
509 set_default( 'stats', build_result('statistics') );
510
511 my $lasttag = `git describe --abbrev=0`;
512 chomp $lasttag;
513
514 set_default( 'since', $lasttag );
515
516 set_default( 'errorlog', build_result('errors.log') );
517
518 unlink $config->param('tarball');
519 unlink $config->param('rnotes') unless $config->param('use-dist-rnotes');
520 unlink $config->param('errorlog');
521
522 print_log(
523     colored(
524         "Starting release test at "
525           . strftime( '%D %T', localtime($starttime) ),
526         'blue'
527     )
528 );
529 print_log( "\tBranch:  "
530       . $config->param('branch')
531       . "\n\tDistribution: "
532       . $config->param('distribution')
533       . "\n\tVersion: "
534       . $config->param('version')
535       . "\n" );
536
537 unless ( index $config->param('version'), $kohaversion ) {
538     $version_mismatch = 1;
539 }
540
541 unless ( $config->param('skip-tests') ) {
542     tap_task(
543         "Running unit tests",
544         0,
545         undef,
546         tap_dir( $config->param('kohaclone') . '/t' ),
547         tap_dir( $config->param('kohaclone') . '/t/db_dependent' ),
548         tap_dir( $config->param('kohaclone') . '/t/db_dependent/Labels' ),
549         $config->param('kohaclone') . '/xt/author/icondirectories.t',
550         $config->param('kohaclone') . '/xt/author/podcorrectness.t',
551         $config->param('kohaclone') . '/xt/author/translatable-templates.t',
552         $config->param('kohaclone') . '/xt/author/valid-templates.t',
553         $config->param('kohaclone') . '/xt/permissions.t',
554         $config->param('kohaclone') . '/xt/single_quotes.t',
555         $config->param('kohaclone') . '/xt/tt_valid.t'
556     );
557     $finished_tests = 'yes';
558 }
559
560 unless ( $config->param('skip-deb') ) {
561     unless ( $config->param('skip-pbuilder') ) {
562         print_log("Updating pbuilder...");
563         run_cmd("sudo pbuilder update --keyring '$reltools/debian.koha-community.org.gpg' 2>&1");
564         warn colored( "Error updating pbuilder. Continuing anyway.",
565             'bold red' )
566           if ($?);
567     }
568
569     $ENV{DEBEMAIL}    = $config->param('maintainer-email');
570     $ENV{DEBFULLNAME} = $config->param('maintainer-name');
571
572     my $extra_args = '';
573     $extra_args = ' --noautoversion' unless ( $config->param('autoversion') );
574     shell_task(
575         "Building packages",
576         "debian/build-git-snapshot --distribution="
577           . $config->param('distribution') . " -r "
578           . $config->param('build-result') . " -v "
579           . $config->param('version')
580           . "$extra_args 2>&1"
581     );
582
583     fail('Building package')
584       unless $output =~
585           m#^dpkg-deb: building package `koha-common' in `[^'`/]*/([^']*)'.$#m;
586     $config->param( 'package', build_result($1) );
587
588     fail('Building package') unless ( -f $config->param('package') );
589
590     $built_packages = 'yes';
591 }
592
593 unless ( $config->param('skip-tgz') ) {
594     print_log("Preparing release tarball...");
595
596     shell_task(
597         "Creating archive",
598         'git archive --format=tar --prefix=koha-'
599           . $config->param('version') . '/ '
600           . $config->param('branch')
601           . ' | gzip > '
602           . $config->param('tarball'),
603         1
604     );
605
606     $built_tarball = 'yes';
607
608     shell_task( "Signing archive", "gpg -sb " . $config->param('tarball'), 1 )
609       if ( $config->param('sign') );
610
611     shell_task(
612         "md5summing archive",
613         "md5sum "
614           . $config->param('tarball') . " > "
615           . $config->param('tarball') . ".MD5",
616         1
617     );
618
619     if ( $config->param('sign') ) {
620         shell_task( "Signing md5sum",
621             "gpg --clearsign " . $config->param('tarball') . ".MD5", 1 );
622         $signed_tarball = 'yes';
623     }
624
625     if ( $config->param('deploy') ) {
626         $config->param('staging', build_result('staging'));
627         mkdir $config->param('staging');
628         symlink $config->param('tarball'), $config->param('staging') . '/' . basename($config->param('tarball'));
629         symlink $config->param('tarball') . '.MD5', $config->param('staging') . '/' . basename($config->param('tarball') . '.MD5');
630         if ($signed_tarball) {
631             symlink $config->param('tarball') . '.MD5.asc', $config->param('staging') .'/' .  basename($config->param('tarball') . '.MD5.asc');
632             symlink $config->param('tarball') . '.sig', $config->param('staging') .'/' .  basename($config->param('tarball') . '.sig');
633         }
634     }
635 }
636
637 unless ( $config->param('skip-rnotes') || $config->param('use-dist-rnotes') ) {
638     print_log("Generating release notes...");
639     run_cmd(
640         "$reltools/get_bugs.pl -r "
641           . $config->param('rnotes') . " -t "
642           . $config->param('since') . " -v "
643           . $config->param('version')
644           .($config->param('bzlogin')?" -u ".$config->param('bzlogin')." -p ".$config->param('bzpassword'):"")
645           . " --verbose 2>&1"
646           );
647     warn colored( "Error generating release notes. Continuing anyway.",
648             'bold red' ) if ($?);
649 }
650
651 system('which gitdm 2>&1 > /dev/null');
652 $config->param('skip-stats', 1) if $?;
653 unless ( $config->param('skip-stats') ) {
654     shell_task(
655         "Generating statistics",
656         "git log -p -M " . $config->param('since')
657           . "..HEAD | gitdm -b $reltools -c $reltools/gitdm.config -u -s -a -o "
658           . $config->param('stats') . ".txt -h "
659           . $config->param('stats') . ".html 2>&1"
660     );
661 }
662
663 unless ( $config->param('skip-deb') || $config->param('skip-install') ) {
664     for my $flavour (@marcflavours) {
665         my $lflavour = lc $flavour;
666
667         print_log("Installing from package for $flavour...");
668         my ($lxc_ip, $ssh) = create_lxc();
669         ssh_task( $ssh, "Downloading package...", "wget -nv http://10.0.3.1" . $config->param('package') . ' 2>&1', '', 1 );
670         ssh_task( $ssh, "Installing package...", "sudo dpkg --no-debsig -i " . basename($config->param('package')) . ' 2>&1; sudo apt-get -y -f --force-yes install 2>&1', '', 1 );
671         ssh_task( $ssh, "Running koha-create for $flavour",
672             "sudo koha-create --marcflavor=$lflavour --create-db pkgrel 2>&1", '',
673             1 );
674
675         print_log("Unable to run webinstaller for $flavour package due to Koha breakage") if (! $config->param('skip-webinstall'));
676         unless ( $config->param('skip-webinstall') || 1 ) {
677             my $pkg_user = $ssh->capture("sudo xmlstarlet sel -t -v 'yazgfs/config/user' '/etc/koha/sites/pkgrel/koha-conf.xml'");
678             my $pkg_pass = $ssh->capture("sudo xmlstarlet sel -t -v 'yazgfs/config/pass' '/etc/koha/sites/pkgrel/koha-conf.xml'");
679             chomp $pkg_user;
680             chomp $pkg_pass;
681             my $harness_args = {
682                 test_args => [
683                     "http://$lxc_ip:8080", "http://$lxc_ip",
684                     "$flavour",              "$pkg_user",
685                     "$pkg_pass"
686                 ]
687             };
688             tap_task( "Running webinstaller for $flavour",
689                 1, $harness_args, "$reltools/install-fresh.pl" );
690
691             push @tested_package_installs, $flavour;
692         }
693     clean_lxc();
694     $lxc_host = '';
695     }
696 }
697
698 if ( $config->param('sign') && !$config->param('skip-deb') ) {
699     shell_task( "Signing packages", "debsign " . build_result('*.changes') );
700     $signed_packages = 'yes';
701 }
702
703 if ( $config->param('deploy') && !$config->param('skip-deb') ) {
704     shell_task(
705         "Importing packages to apt repo",
706         "dput koha " . build_result('*.changes')
707     );
708     $deployed = 'yes';
709 }
710
711 unless ( $config->param('skip-tgz') || $config->param('skip-install') ) {
712     for my $flavour (@marcflavours) {
713         my $lflavour = lc $flavour;
714         print_log("Installing from tarball for $flavour...");
715
716         my ($lxc_ip, $ssh) = create_lxc();
717         my $subdir = 'koha-' . $config->param('version');
718         ssh_task( $ssh, "Downloading tarball...", "wget -nv http://10.0.3.1" . $config->param('tarball') . ' 2>&1', '', 1 );
719         ssh_task( $ssh, "Untarring tarball...", "tar zxvf " . basename($config->param('tarball')) . ' 2>&1', '', 1 );
720         ssh_task( $ssh, "Installing dependencies...", "sudo apt-get -y --force-yes install `cat install_misc/ubuntu.12.04.packages | grep install | sed -e 's/install\$//' | tr -d ' \\t' | tr '\\n' ' '` 2>&1", $subdir, 1 );
721         my $env_vars = "DB_HOST=localhost DB_NAME=" . $config->param('database') . " DB_USER=" . $config->param('user') . " DB_PASS=" . $config->param('password') . " ZEBRA_MARC_FORMAT=$lflavour PERL_MM_USE_DEFAULT=1";
722         ssh_task( $ssh, "Running perl Makefile.PL for $flavour",
723             "$env_vars perl Makefile.PL 2>&1", $subdir, 1 );
724         ssh_task( $ssh, "Running make for $flavour...", "make 2>&1", $subdir, 1 );
725
726         ssh_task( $ssh, "Rewriting Apache config for $flavour",
727             "sed -i -e 's/<VirtualHost 127.0.1.1:80>/<VirtualHost *:80>/' -e 's/<VirtualHost 127.0.1.1:8080>/<VirtualHost *:8080>/' blib/KOHA_CONF_DIR/koha-httpd.conf",
728             $subdir, 1 );
729
730         ssh_task( $ssh, "Running make test for $flavour...", "make test 2>&1", $subdir, 1 );
731         ssh_task( $ssh, "Running make install for $flavour...",
732             "sudo make install 2>&1", $subdir, 1 );
733         ssh_task( $ssh, "Linking and loading Apache site for $flavour...", "sudo ln -s /etc/koha/koha-httpd.conf /etc/apache2/sites-available/koha && sudo a2ensite koha && sudo apache2ctl restart", '', 1 );
734
735         print_log("Unable to run webinstaller for $flavour tarball due to Koha breakage") if (! $config->param('skip-webinstall'));
736         unless ( $config->param('skip-webinstall') || 1 ) {
737             my $harness_args = {
738                 test_args => [
739                     "http://$lxc_ip:8080", "http://$lxc_ip",
740                     "$flavour",            $config->param('user'),
741                     $config->param('password')
742                 ]
743             };
744             tap_task( "Running webinstaller for $flavour",
745                 1, $harness_args, "$reltools/install-fresh.pl" );
746
747             push @tested_tarball_installs, $flavour;
748         }
749         clean_lxc();
750         $lxc_host = '';
751     }
752 }
753
754 if ( $config->param('tag') ) {
755     my $tag_action = $config->param('sign') ? '-s' : '-a';
756     shell_task(
757         "Tagging current commit",
758         "git tag $tag_action -m 'Koha release "
759           . $config->param('version') . "' v"
760           . $config->param('version') . " 2>&1"
761     );
762     $tagged = 'yes';
763 }
764
765 generate_email() unless $config->param('skip-rnotes');
766
767 if ( defined $config->param('push') ) {
768     foreach my $target ($config->param('push')) {
769         if ( $target =~ m/^([^:]*):(.*)$/ ) {
770             shell_task( "Pushing to branch $2 on remote $1",
771                 "git push $1 " . $config->param('branch') . ":$2" );
772             if ( $config->param('tag') ) {
773                 shell_task( "Pushing tag to remote $1",
774                     "git push $1 v" . $config->param('version') );
775             }
776         } else {
777             shell_task( "Pushing to default remote/branch",
778                 "git push" );
779             if ( $config->param('tag') ) {
780                 shell_task( "Pushing tag to default remote",
781                     "git push v" . $config->param('version') );
782             }
783         }
784     }
785     $pushed = 'yes';
786 }
787
788 my $configfile = build_result('summary.cfg');
789 $config->write($configfile);
790
791 if ( $config->param('deploy') && $config->param('post-deploy-script') ) {
792     foreach my $script ($config->param('post-deploy-script')) {
793         shell_task( "Running post-deploy script",
794             $script . ' ' . $configfile );
795     }
796 }
797
798 if ( $config->param('clean') ) {
799     clean_lxc();
800     remove_tree( $config->param('build-result') );
801     $cleaned = 'yes';
802 }
803
804 success();
805
806 sub build_result {
807     my @components = @_;
808     my $path       = $config->param('build-result');
809
810     foreach my $component (@components) {
811         $path .= "/$component";
812     }
813     return $path;
814 }
815
816 sub set_default {
817     my $key   = shift;
818     my $value = shift;
819
820     chomp($value);
821     $config->param( $key, $value ) unless $config->param($key);
822 }
823
824 sub clean_lxc {
825     shell_task( "Shutting down lxc container...", "sudo lxc-stop -n $lxc_host", 1 ) if $lxc_host;
826 }
827
828 sub summary {
829     my ($capsule_summary) = @_;
830     my $endtime = time();
831     my $totaltime = ceil( ( $endtime - $starttime ) * 1000 );
832     $starttime = strftime( '%D %T', localtime($starttime) );
833     $endtime   = strftime( '%D %T', localtime($endtime) );
834     my $skipped                = '';
835     my $tested_tarball_install = 'no';
836     my $tested_package_install = 'no';
837     $tested_tarball_install = join( ', ', @tested_tarball_installs )
838       if ( scalar(@tested_tarball_installs) );
839     $tested_package_install = join( ', ', @tested_package_installs )
840       if ( scalar(@tested_package_installs) );
841
842     my %vars = $config->vars();
843     foreach my $key ( sort keys %vars ) {
844         if ( $key =~ m/^skip-([a-z]+)$/ ) {
845             $skipped .= ", $1" if ( $config->param($key) );
846         }
847     }
848     $skipped =~ s/^, //;
849     $skipped = 'none' unless $skipped;
850     $config->param( 'package', 'none' )
851       unless ( -s $config->param('package') && not $config->param('skip-deb') );
852     $config->param( 'tarball', 'none' )
853       unless ( -s $config->param('tarball') && not $config->param('skip-tgz') );
854     $config->param( 'rnotes', 'none' )
855       unless ( -s $config->param('rnotes')
856         && not $config->param('skip-rnotes') );
857     return if $config->param('quiet') > 1;
858     my $branch  = $config->param('branch');
859     my $distribution  = $config->param('distribution');
860     my $version = $config->param('version');
861     my $maintainer =
862         $config->param('maintainer-name') . ' <'
863       . $config->param('maintainer-email') . '>';
864     my $tarball    = $config->param('tarball');
865     my $package    = $config->param('package');
866     my $rnotes     = $config->param('rnotes');
867     my $stats      = $config->param('stats');
868     my $emailfile  = $config->param('email-file');
869     my $logfile    = build_result('release-tool.log');
870     my $summary    = <<_SUMMARY_;
871 $capsule_summary
872
873 Release test report
874 =======================================================
875 Branch:                 $branch
876 Distribution:           $distribution
877 Version:                $version
878 Maintainer:             $maintainer
879 Run started at:         $starttime
880 Run ended at:           $endtime
881 Total run time:         $totaltime ms
882 Skipped:                $skipped
883 Finished tests:         $finished_tests
884 Built packages:         $built_packages
885 Tested package install: $tested_package_install
886 Deployed packages:      $deployed
887 Signed packages:        $signed_packages
888 Built tarball:          $built_tarball
889 Tested tarball install: $tested_tarball_install
890 Signed tarball:         $signed_tarball
891 Tagged git repository:  $tagged
892 Pushed git branch:      $pushed
893 Cleaned:                $cleaned
894 Tarball file:           $tarball
895 Package file:           $package
896 Release notes:          $rnotes
897 Statistics:             $stats\[.txt/.html\]
898 E-mail file:            $emailfile
899 Summary config file:    $configfile
900 Log file:               $logfile
901 _SUMMARY_
902
903     $summary .= colored( "Version mismatch between requested version " .
904       $config->param('version') . " and Koha version $kohaversion!", 'red' ) .  "\n"
905       if $version_mismatch;
906     $summary .= $capsule_summary;
907
908     print $summary unless $config->param('quiet') > 1;
909
910     $log .= "\n$summary";
911
912     open my $logfh, '>', $logfile;
913     print $logfh $log;
914     close($logfh);
915
916
917     if ( $config->param('alert') ) {
918         $summary =~ s/\x1b\[[0-9]*m//g;
919         $summary .= "=======================================================\n\n";
920         my $msg = MIME::Lite->new(
921             From    => $config->param('maintainer-email'),
922             To      => $config->param('alert'),
923             Subject => 'Release tool completed',
924             Data    => $summary,
925             Type    => 'multipart/mixed'
926         );
927         $msg->attach(
928             Type    => 'text/plain',
929             Data    => $summary
930         );
931         $log =~ s/\x1b\[[0-9]*m//g;
932         $msg->attach(
933             Type     => 'text/plain',
934             Data     => $log,
935             Filename => 'release-tool.log'
936         );
937         $msg->send;
938     }
939 }
940
941 sub success {
942     summary(colored( "Successfully finished release test", 'green' ) . "\n");
943
944     exit 0;
945 }
946
947 sub fail {
948     my $component = shift;
949     my $callback  = shift;
950
951     summary(colored( $component, 'bold red' ) . ' ' .
952       colored( " failed in release test", 'red' ) . "\n");
953
954     if ($output) {
955         open( my $errorlog, ">", $config->param('errorlog') )
956           or die "Unable to open error log for writing";
957         print $errorlog $output;
958         close($errorlog);
959     }
960
961     print colored( "Error report at " . $config->param('errorlog'), 'red' ),
962       "\n"
963       unless $config->param('quiet') > 1;
964
965     $callback->() if ( ref $callback eq 'CODE' );
966
967     exit 1;
968 }
969
970 sub print_log {
971     $log .= join(' ', @_) . "\n";
972     print @_, "\n" unless $config->param('quiet');
973 }
974
975 sub tap_dir {
976     my $directory = shift;
977     my @tests;
978
979     opendir( DIR, $directory );
980
981     while ( my $file = readdir(DIR) ) {
982
983         # We only want files
984         next unless ( -f "$directory/$file" );
985
986         # Use a regular expression to find files ending in .t
987         next unless ( $file =~ m/\.t$/ );
988         push @tests, "$directory/$file";
989     }
990     return sort @tests;
991 }
992
993 sub create_lxc {
994     # Sorry, creating the lxc container sucks. Deal with it.
995     print_log(" Creating lxc container...");
996     my $command = "sudo lxc-start-ephemeral -d -o ubuntu";
997     $log .= "> $command\n";
998     print colored( "> $command\n", 'cyan' )
999         if ( $config->param('verbose') >= 1 );
1000     my $pid = open( my $outputfh, "-|", "$command" )
1001         or die "Unable to run $command\n";
1002     while (<$outputfh>) {
1003         print $_ if ( $config->param('verbose') >= 2 );
1004         $output .= $_;
1005         if ($_ =~ /^([^ ]*) is running/) {
1006             $lxc_host = $1;
1007             last;
1008         }
1009     }
1010     close($outputfh);
1011
1012     my $lxc_ip = `lxc-ip -n $lxc_host`;
1013     chomp($lxc_ip);
1014     print_log(" Connecting to lxc container...");
1015     my $ssh = Net::OpenSSH->new("ubuntu\@$lxc_ip", master_opts => [-o => "StrictHostKeyChecking=no"]);
1016
1017     return ($lxc_ip, $ssh);
1018 }
1019
1020 sub run_cmd {
1021     my $command = shift;
1022     $log .= "> $command\n";
1023     print colored( "> $command\n", 'cyan' )
1024       if ( $config->param('verbose') >= 1 );
1025     my $pid = open( my $outputfh, "-|", "$command" )
1026       or die "Unable to run $command\n";
1027     while (<$outputfh>) {
1028         print $_ if ( $config->param('verbose') >= 2 );
1029         $output .= $_;
1030     }
1031     close($outputfh);
1032     $log .= $output . "\n";
1033 }
1034
1035 sub shell_task {
1036     my $message = shift;
1037     my $command = shift;
1038     my $callback;
1039     $callback = pop @_ if ( $#_ && ref $_[$#_] eq 'CODE' );
1040     my $subtask = shift || 0;
1041     my $logmsg = ( ' ' x $subtask ) . $message;
1042     print_log("$logmsg...");
1043     $output = '';
1044     run_cmd($command);
1045     fail( $message, $callback ) if ($?);
1046 }
1047
1048 sub ssh_task {
1049     my $ssh = shift;
1050     my $message = shift;
1051     my $command = shift;
1052     my $dir = shift;
1053     my $callback;
1054     $callback = pop @_ if ( $#_ && ref $_[$#_] eq 'CODE' );
1055     my $subtask = shift || 0;
1056     my $logmsg = ( ' ' x $subtask ) . $message;
1057     print_log("$logmsg...");
1058     $command = "cd $dir && $command" if $dir;
1059     $log .= "> $command\n";
1060     print colored( "> $command\n", 'cyan' )
1061       if ( $config->param('verbose') >= 1 );
1062     $output = '';
1063     my ($outputfh, $pid) = $ssh->pipe_out($command);
1064     die "Unable to run $command\n" unless $pid;
1065     while (<$outputfh>) {
1066         print $_ if ( $config->param('verbose') >= 2 );
1067         $output .= $_;
1068     }
1069     close($outputfh);
1070     $log .= $output . "\n";
1071     fail( $message, $callback ) if ($ssh->error);
1072 }
1073
1074 sub tap_task {
1075     my $message      = shift;
1076     my $subtask      = shift;
1077     my $harness_args = shift;
1078     my $callback;
1079     $callback = pop @_ if ( ref $_[$#_] eq 'CODE' );
1080     my @tests  = @_;
1081     my $logmsg = ( ' ' x $subtask ) . $message;
1082
1083     if ( $config->param('verbose') ) {
1084         $harness_args->{'verbosity'} = 1;
1085     }
1086     elsif ( $config->param('quiet') ) {
1087         $harness_args->{'verbosity'} = -1;
1088     }
1089     $harness_args->{'lib'}   = [ $config->param('kohaclone') ];
1090     $harness_args->{'merge'} = 1;
1091
1092     print_log("$logmsg...");
1093
1094     if ( $config->param('verbose') >= 1 ) {
1095         my $command = 'prove ';
1096         foreach my $test (@tests) {
1097             $command .= "$test ";
1098         }
1099         print colored( "> $command\n", 'cyan' );
1100         $log .= "> $command\n";
1101     }
1102
1103     my $pid = open( my $testfh, '-|' ) // die "Can't fork to run tests: $!\n";
1104     if ($pid) {
1105         while (<$testfh>) {
1106             print $_ if ( $config->param('verbose') >= 2 );
1107             $output .= $_;
1108         }
1109         $log .= $output . "\n";
1110         waitpid( $pid, 0 );
1111         fail( $message, $callback ) if ($?);
1112         close($testfh);
1113     }
1114     else {
1115         my $harness    = TAP::Harness->new($harness_args);
1116         my $aggregator = $harness->runtests(@tests);
1117         exit 1 if $aggregator->failed;
1118         exit 0;
1119     }
1120 }
1121
1122 sub process_tt_task {
1123     my $message  = shift;
1124     my $subtask  = shift;
1125     my $template = shift;
1126     my $args     = shift;
1127     my $logmsg   = ( ' ' x $subtask ) . $message;
1128
1129     print_log("$logmsg...");
1130
1131     my $tt = Template->new(
1132         {
1133             INCLUDE_PATH => "$reltools",
1134             ABSOLUTE     => 1,
1135         }
1136     );
1137     my $output;
1138     $tt->process( $template, $args, \$output ) || fail($message);
1139     return $output;
1140 }
1141
1142 sub generate_email {
1143     my $content = process_tt_task(
1144         'Generating e-mail',
1145         0,
1146         $config->param('email-template'),
1147         {
1148             VERSION  => $config->param('version'),
1149             RELNOTES => $config->param('rnotes')
1150         }
1151     );
1152     $content =~ s/^####.*$//m;
1153     my $msg = MIME::Lite->new(
1154         From    => $config->param('maintainer-email'),
1155         To      => $config->param('email-recipients'),
1156         Subject => $config->param('email-subject'),
1157         Data    => $content,
1158     );
1159     open( my $emailfh, ">", $config->param('email-file') );
1160     $msg->print($emailfh);
1161     close($emailfh);
1162 }
1163
1164 sub interrupt {
1165     undef $SIG{INT};
1166     warn "**** YOU INTERRUPTED THE SCRIPT ****\n";
1167     summary();
1168     die "**** YOU INTERRUPTED THE SCRIPT ****\n";
1169 }
1170
1171 =head1 DPUT CONFIGURATION
1172
1173 In order for the deploy step to work, you will need to have a signed apt repository
1174 set up. Once you have set up your repo, put the following in your  ~/.dput.cf:
1175
1176     [koha]
1177     method = local
1178     incoming = /home/apt/koha/incoming
1179     run_dinstall = 0
1180     post_upload_command = reprepro -b /home/apt/koha processincoming default
1181
1182 =head1 PBUILDER CONFIGURATION
1183
1184 Koha's C<build-git-snapshot> script uses uses L<pbuilder(8)> for building the
1185 Debian packages, so you will need a working pbuilder environment. To create
1186 one, you can use the following command:
1187
1188     sudo pbuilder create \
1189         --othermirror 'deb http://debian.koha-community.org/koha squeeze main' \
1190         --mirror http://ftp.debian.org/debian \
1191         --keyring '${release-tools-repo}/debian.koha-community.org.gpg'
1192
1193 =head1 LXC CONFIGURATION
1194
1195 Testing the Koha installation process requires an LXC container called "ubuntu"
1196 configured with the following features:
1197
1198 =over 8
1199
1200 =item Ubuntu Precise chosen as the distro
1201
1202 =item The following line in the sudoers file:
1203
1204     ubuntu  ALL=NOPASSWD: /usr/bin/make, /usr/bin/apt-get, /sbin/poweroff, \
1205     /bin/ln, /usr/sbin/apache2ctl, /usr/sbin/a2ensite, /usr/bin/dpkg, \
1206     /usr/sbin/koha-create, /usr/bin/xmlstarlet
1207
1208 =item The current user's SSH key must be in /home/ubuntu/.ssh/authorized_keys2
1209 in the container.
1210
1211 =item mysql-server must be installed and a database and user created for the
1212 tarball installation.
1213
1214 =item You must make your build result directory available via HTTP on the
1215 lxcbr interface. A configuration similar to the one nginx configuration below
1216 is recommended:
1217
1218     server {
1219         listen 10.0.3.1:80;
1220         server_name 10.0.3.1;
1221         root /var/www;
1222
1223         location /home/jcamins/releases {
1224             alias /home/jcamins/releases;
1225         }
1226     }
1227
1228 =item An apt cache set up with apt-cacher-ng is recommended to speed up the
1229 process.
1230
1231 =back
1232
1233 =head1 SEE ALSO
1234
1235 L<dch(1)>, L<dput(1)>, L<lxc(7)>, L<pbuilder(8)>, L<reprepro(1)>
1236
1237 =head1 AUTHOR
1238
1239 Jared Camins-Esakov <jcamins@cpbibliography.com>
1240
1241 =cut