#!/usr/local/bin/perl
use v5.16;
use strict;
use warnings;
use open qw(:std :utf8);

=encoding utf8

=head1 NAME

release - give your Perl distros to the world

=head1 SYNOPSIS

	release [OPTIONS] [ LOCAL_FILE [ REMOTE_FILE ] ]

	# print a help message and exit
	release -h

	# try a dry run without uploading anything
	release -t

	# skip dist while checking
	release -D

	# skip kwalitee testing (e.g. a script distro)
	release -k

	# print debugging information
	release -d

	# print release number and exit
	release -v

	# set $ENV{AUTOMATED_TESTING} to a true value
	release -a

	# Enable parallel testing by setting $ENV{HARNESS_OPTIONS}
	release -j
	release -j8

=head1 DESCRIPTION

This is the prototype program for using C<Module::Release>. You should
modify it to fit your needs. If it doesn't do what you want, you can
change it however you like. This is how I like to release my modules,
and I'm happy to add features that do not get in my way. Beyond that,
you should write your own script to match your process.

This program automates Perl module releases. It makes the
distribution, tests it, checks that source control is up to date, tags
source control, and uploads it to the PAUSE.

By default this script assumes that you use Git as your source control
and relies mostly on L<Module::Release::Git>, which is an external
module. This distributes L<Module::Release::SVN>, but I haven't touched
that in years. There's still some code for CVS that I should probably
rip out.

=head2 Process

The release script checks many things before it actually releases the
file.  Some of these are annoying, but they are also the last line of
defense against releasing bad distributions.

=over 4

=item Read the configuration data

Look in the current working directory for C<.releaserc>.  See the
Configuration section.  If release cannot find the configuration file,
it dies.

=item Various sanity checks

These are related to various mistakes I've made at some point.

	* am I allowed to release from this branch (so, not an experimental branch)
	* have I listed all the prereqs?
	* is the version greater than what I've already released?

=item Test and make the distribution

Run make realclean, perl Makefile.PL, make test, make dist, make
disttest.  If testing fails, release dies.  make dist provides the
name of the distribution if LOCAL_FILE is not provided on the command
line. Too test the distribution against several perl binaries, see
the C<perls> configuration setting.

=item Check that source control is up-to-date

If there are modified files, added files, or extra files so that
source control complains, fail.

=item Upload to PAUSE

This program used to use FTP uploads, but PAUSE has turned off that feature.
Now it uploads through the web form.

=item Tag the repository

Use the version number (in the distribution name) to tag the
repository.  You should be able to checkout the code from any release.

=back

=head2 Command-line switches

=over 4

=item -a

=item --automated-testing

Set $ENV{AUTOMATED_TESTING} to true. You can also set C<automated_testing>
in the configuration file

=item -C

=item --skip-changes

Skip the Changes file. This is also the C<skip_changes> config option

=item -d

=item --skip-debug

Show debugging information. This is also the C<debug> config option

=item -D

=item --skip-dist

Skip building the dist. This is also the C<skip_dist> config option

=item -h

=item -?

=item --help

=item --usage

Print a help message then exit

=item -j [#]

=item --jobs[=#]

Enable parallel testing by defining the number of jobs Test::Harness can
execute in parallel by setting $ENV{HARNESS_OPTIONS} to C<j#>. You can also
set C<test_jobs> in the configuration file.

When not passing a count or count C<0>, this script will try to use
System::Info to get the CPU count. If it is not available or returns a
false value, the count will default to C<9>.

=item -k

=item --skip-kwalitee

Skip the kwalitee checks. You can also set the C<skip_kwalitee> directive
to a true value in the configuration file.

Have you considered just fixing the kwalitee though? :)

=item -m

=item --skip-manifest

Skip the manifest check. You can also set the C<skip_manifest> directive
to a true value in the configuration file.

=item -p

=item --skip-prereq

Skip the prereq checks. You can also set the skip_prereqs directive
to a true value in the configuration file.

Have you considered just fixing the prereqs though? :)

=item -t

=item --test-only

=item --dry-run

Run all checks then stop. Do not change any files or upload the distribution

=item -T

=item --skip-tests

Skip the tests. This is useful when you just want to upload

=item -v

=item --version

Print the program name and version then exit

=item -V

=item --skip-version

Skip the version check against CPAN

=back

=head2 Configuration

The release script uses a configuration file in the current working
directory.  The file name is F<.releaserc>.

release's own F<.releaserc> looks like this:

	cpan_user BDFOY

If you would like to test with multiple perl binaries (version 1.21
and later), list them as a colon-separated list in the C<perls>
setting:

	perls /usr/local/bin/perl5.6.2:/usr/local/bin/perl5.10.0

release does not test the perls in any particular order.

=over 4

=item allow_glob_in_perls

If true, expand globs in the C<perls> configuration.

Default: 0

=item allow_branches

A comma-separated list of literal branch names that are allowed
to release (requires Module::Release::Git 1.016).

=item allow_branches_regex

A Perl pattern to match version control branches that are allowed
to release (requires Module::Release::Git 1.016).

=item automated_testing

Set C<automated_testing> to the value you want for the
$ENV{AUTOMATED_TESTING} setting. By default this is 0, so
testing is started in interactive mode.

=item commit_message_format

A sprintf format for the message when C<release> commits its changes.
So far this takes one C<%s> placeholder for the module version
(requires Module::Release::Git 1.016).

=item cpan_user

The PAUSE user

=item dry_run

When true, stop before releasing the dist. This is also the C<-t> switch.

=item test_jobs

If set, define the number of parallel jobs Test::Harness may use to run
the tests. If C<0>, try to get the CPU count using System::Info. If that
is not available or returns a false value, default to C<9>.

=item perls

A colon-separated list of perls to test with. If C<allow_glob_in_perls> is
a true value, globs in paths will expand to their paths so you can specify
many perls.

Default: the perl that's running C<release>.

=item release_subclass

DEPRECATED AND REMOVED. You should really just write your own
release script. Fork this one even!

=item required

An optional list of comma-separated modules that are required to be
available. These are checked I<before> building C<Makefile>.

	required DBI,Text::CSV_XS,SQL::Statement

A list like this can be handy when testing against a lot of perl
versions (see C<perls> above) in which some do and some do not have
all required modules installed. Another good reason to have this is
when testing against I<optional> modules.

=item skip_changes

Skip updating the changes. This is also the C<-C> switch.

=item skip_cpan_version_check

Skip comparing the local module version to the previous release version.
This is also the C<-V> switch.

=item skip_kwalitee

Set to a true value to skip kwalitee checks (such as for a script
distribution with no modules in it). This is also the C<-k> command-line
switch.

=item skip_manifest

Set to a true value, it skips checking the MANIFEST file. This is also
the C<-m> command-line switch.

=item skip_prereqs

Set C<skip_prereqs> to 1 if you don't want to run the Test::Prereq
checks. By default this is 0 and C<release> will try to check
prerequisites. This is also the C<-p> command-line switch.

=item skip_tests

Set C<skip_tests> to 1 if you don't want to run any of the tests. This
is also the C<-T> command-line switch.

=back

=head2 Environment

=over 4

=item * AUTOMATED_TESTING

Module::Release doesn't do anything with this other than set it for
Test::Harness.

=item * CPAN_PASS

release reads the C<CPAN_PASS> environment variable to set the
password for PAUSE.  Of course, you don't need to set the password for
a system you're not uploading to.

=item * RELEASE_DEBUG

The C<RELEASE_DEBUG> environment variable sets the debugging value,
which is 0 by default.  Set C<RELEASE_DEBUG> to a true value to get
debugging output.

=item * PERL

The C<PERL> environment variable sets the path to perl for use in the
make; otherwise, the perl used to run release will be used.

=item * RELEASE_OPTS

A string representing options to add to the command line.

=back

=head1 TO DO

=over 4

=item * break out functional groups into modules.

=item * more plugins!

=back

=head1 SOURCE AVAILABILITY

This source is in GitHub as part of the Module::Release project:

	https://github.com/briandfoy/module-release

=head1 AUTHOR

brian d foy, C<< <bdfoy@cpan.org> >>

=head1 COPYRIGHT AND LICENSE

Copyright © 2002-2021, brian d foy <bdfoy@cpan.org>. All rights reserved.

You may use this software under the same terms as Perl itself.

=head1 CREDITS

Ken Williams turned the original release(1) script into a module.

Andy Lester contributed to the module and script.

H. Merijn Brand submitted patches to work with 5.005, to create
the automated_testing and required features and parallel testing.

=cut

use Getopt::Long qw(:config noignorecase bundling);
use Module::Release 2.131;
my $script_version = '2.131';

my $class = "Module::Release";


# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
$ENV{RELEASE_OPTS} = defined $ENV{RELEASE_OPTS} ? $ENV{RELEASE_OPTS} : '';
$ENV{RELEASE_OPTS} =~ s/\A\s+|\s+\z//g;

my @extra_opts = split /\s+/, $ENV{RELEASE_OPTS};
push @ARGV, @extra_opts;

my %opts;
GetOptions(
    'v|version'            => sub { say "$0 version $script_version"; exit },
    'h|help|usage|?'       => sub { usage(0) },

    'a|automated-testing!' => \$opts{a},
    'C|skip-changes!'      => \$opts{C},
    'd|skip-debug!'        => \$opts{d},
    'D|skip-dist!'         => \$opts{D},
    'j|jobs:0'             => \$opts{j},
    'k|skip-kwalitee!'     => \$opts{k},
    'm|skip-manifest!'     => \$opts{'m'},
    'p|skip-prereq!'       => \$opts{p},
    't|test-only|dry-run!' => \$opts{t},
    'T|skip-tests!'        => \$opts{T},
    'V|skip-version!'      => \$opts{V},
    ) or usage(1);

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

sub usage {
	my $err = shift and select STDERR;
	print <<"USE";

Use: release -aCdDhkmptTvV [-j[N]] [ LOCAL_FILE [ REMOTE_FILE ] ]

Will upload current release LOCAL_FILE, naming it REMOTE_FILE.  Will
get LOCAL_FILE and REMOTE_FILE automatically (using same name for
both) if not supplied.

	-a   Set AUTOMATED_TESTING to true
	-C   Skip Changes file (useful for re-running botched releases)
	-d   Print extra debugging information
	-D   Skip building the dist
	-h   This help
	-jN  Enable parallel tests
	-k   Skip kwalitee check
	-m   Skip the MANIFEST check
	-p   Skip the orereqs check
	-t   Just make and test distribution, don't tag/upload
	-T   Skip tests
	-v   Print the script version number and exit
	-V   Skip the version check

The program works in the current directory, and looks for a .releaserc
or releaserc file and the environment for its preferences.  See
`perldoc $0`, for more information.

USE

	exit 2;
	}

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# get the release object
my %params;
$params{local}  = shift @ARGV if @ARGV;

if( @ARGV ) {
	$params{remote} = shift @ARGV;
	}
elsif( $params{local} ) {
	$params{remote} = $params{local};
	}

$params{debug} = 1 if( $opts{d} );

my $release = $class->new( %params );

$release->_debug( "release $script_version, using $class "
	.  $class->VERSION . "\n" );

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# load whatever will handle source control
{
my @vcs = (
	[ '.git'       => "Module::Release::Git" ],
	[ '.gitignore' => "Module::Release::Git" ],
	[ '.svn'       => "Module::Release::SVN" ],
	[ 'CVS'        => "Module::Release::CVS" ],
	);

foreach my $vcs ( @vcs ) {
	next unless -e $vcs->[0];
	my $module = $vcs->[1];
	$release->_debug( "I see an $vcs->[0] directory, so I'm loading $module\n" );
	$release->load_mixin( $module );
	$release->_die( "Could not load $module: $@\n" ) if $@;
	last;
	}
}

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Are we on the right branch? I've made mistakes where I released
# from an experimental branch, and I don't want to do that again.
if( $release->can('is_allowed_branch') ) {  # New in Module::Release::Git 1.016
	$release->_print("============ Checking for allowed branch\n");
	my $branch = $release->vcs_branch;
	unless( $release->is_allowed_branch ) {
		$release->_die( "Configuration blocks release from branch <$branch>\n" );
		}
	$release->_print( "Branch <$branch> is allowed to release\n" );
	}

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Will we upload to PAUSE?
if( $release->config->cpan_user ) { # not a dry run
	$release->load_mixin( 'Module::Release::PAUSE' );
	}

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Are we checking prereqs?
$release->load_mixin( 'Module::Release::Prereq' );

my $skip_prereqs = $opts{p} // $release->config->skip_prereqs;
my $skip_dist    = $opts{D} // $release->config->skip_dist;
my $required     =             $release->config->required // "";

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Set automated testing from command line, config, environment, or default
{
no warnings 'uninitialized';

$ENV{AUTOMATED_TESTING} = (
	grep { defined } (
		$opts{a}, $release->config->automated_testing,
		$ENV{AUTOMATED_TESTING}, 0 )
		)[0];

$release->_debug( "Automated testing is $ENV{AUTOMATED_TESTING}; -a was $opts{a};" .
	" automated_testing was " . $release->config->automated_testing . ";\n" );
}

my $test_jobs = $opts{j} // $release->config->test_jobs;
if( defined $test_jobs ) {
	$test_jobs ||= eval {
		require System::Info;
		System::Info->new->get_core_count;
		} || 9;
	$ENV{HARNESS_OPTIONS} = join ':' => grep { length }
		(split /:/ => ($ENV{HARNESS_OPTIONS} // "")),
		"j$test_jobs";
$release->_debug( "Will use HARNESS_OPTIONS '$ENV{HARNESS_OPTIONS}' during tests\n" );
}

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Check that this is a version later than the one of CPAN
unless( $opts{V} // $release->config->skip_cpan_version_check ) {
	$release->_print("============ Checking CPAN versions\n");
	$release->load_mixin( 'Module::Release::MetaCPAN' );

	# need to make the dist to guess the name, then look up that
	# name in MetaCPAN
	$release->dist;
	$release->_print("Module name is <". $release->module_name . ">\n");

	my $cpan_v_default = '<undef>';

	my $cpan = $release->cpan_version // $cpan_v_default;
	my $dist = eval { $release->dist_version };
	my $dist_v_error = $@;

	$release->_print("CPAN Version: $cpan\n");
	$release->_print("Dist Version: $dist\n");

	if( length $dist_v_error ) {
		$release->_die( "Could not get dist version: $dist_v_error\n" );
		}
	elsif( $cpan eq '<undef>' ) {
		$release->_die( "Could not get CPAN version.\nSkip this CPAN version check with -V if you are sure you want to continue\n" );
		}
	elsif( version->parse($cpan) >= version->parse($dist) ) {
		$release->_die( "Local version <$dist> is not greater than current version <$cpan>\n" );
		}
	}


# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# check kwalitee
unless( $opts{k} // $release->config->skip_kwalitee  ) {
	$release->load_mixin( 'Module::Release::Kwalitee' );
	$release->_print("============ Testing for kwalitee\n");
	$release->clean;
	$release->build_makefile;
	$release->make;
	$release->dist;
	$release->check_kwalitee;
	}
else {
	$opts{t} or $release->_print( "Skipping kwalitee checks. Shame on you!\n" );
	}

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# check MANIFEST. When we run 'make manifest' nothing should be
# added or removed from MANIFEST
unless( $opts{'m'} // $release->config->skip_manifest ) {
	$release->_print("============ Checking MANIFEST\n");
	$release->load_mixin('Module::Release::MANIFEST');
	$release->clean;
	$release->build_makefile;
	$release->make;
	$release->check_MANIFEST;
	}


# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# check source repository (but do not commit)
# This should happen after everything else because I like to use
# release as my testing tool before I checkin. It will test but
# not release if VCS is dirty.
$release->_print("============ Checking source repository\n");

$release->check_vcs;

my $Version;
if ($skip_dist) {
	$Version = "?"; # Get from ?
	}
else {
	$release->dist unless( $release->local_file and -e $release->local_file );
	$Version = eval { $release->dist_version } // 'error?';
	}

$release->_debug( "dist version is <$Version>\n" );


# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# test with a bunch of perls
unless( $opts{T} // $release->config->skip_tests ) {
	my $old_perl = $release->get_perl;

	my @perls = $release->perls;
	my ($n, $N) = (1, scalar @perls);
	PERL: foreach my $perl ( @perls ) {
		$release->_print("============ Testing with $perl (", $n++, "/$N)\n");
		$release->set_perl( $perl ) or next;

		$release->clean;

		foreach my $mod ( grep m/\S/ => split m/\s*,\s*/ => $required ) {
			system "$perl -M$mod -e1 >/dev/null 2>&1";
			if( $? ) {
				warn "Prereq $mod not available\n";
				next PERL;
				}
			}

		$release->build_makefile;
		$release->make;
		$release->test;

		unless( $skip_prereqs ) {
			$release->check_prereqs;
			}
		else {
			$opts{t} or $release->_print( "Skipping prereq checks. Shame on you!\n" );
			}

		unless( $skip_dist ) {
			$release->dist;
			$release->disttest;
			}
		}

	$release->set_perl( $old_perl );
	}


# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# exit if this is a dry run. Everything following this changes
# things or uploads. Don't leave anything behind.
if( $opts{t} // $release->config->dry_run ) {
	$release->_print("============ Cleaning up\n");
	$release->_print( "This is a dry run, so stopping. Cleaning up.\n" );
	$release->distclean;
	unlink glob( '*.tar *.tgz *.tar.gz *.tar.bz *.tar.bz2 *.tbz *.zip' );
	exit;
	}


# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# CPAN::Spec::Changes
$release->_print("============ Getting Changes\n");
unless( $release->debug or $opts{C} or $release->config->skip_changes ) {
	$release->show_recent_contributors;

	my $changes = "Changes";
	my $bak     = $changes . ".bak";

	die "Changes file does not exist!\n" unless -e $changes;

	$release->_print( "\n", "-" x 73, "\n", "Enter Changes section\n\n> " );
	my $str = $Version . ' ' . $release->get_release_date . "\n";

	while( <STDIN> ) {
		$_ =~ s/\A \s* (\*|-)? \s*/- /; # Make bullets (-)
		$_ =~ s/\A(\S)/\t$1/;  # always indent

		$str .= $_;
		$release->_print( "> " );
		}

	$str .= "\n";

	rename $changes, $bak or die "Could not backup $changes. $!\n";

	open my $in,  '<:encoding(UTF-8)', $bak or $release->_die( "Could not read old $changes file! $!\n" );
	open my $out, '>:encoding(UTF-8)', $changes;

	while( <$in> ) {
		print $out $_;
		last unless m/\S/;
		}

	print $out $str;

	print $out $_ while( <$in> );

	close $in;
	close $out;

	my $command = do {
		   if( -d 'CVS'  ) { 'cvs commit -m' }
		elsif( -d '.svn' ) { 'svn commit -m' }
		elsif( -d '.git' ) { 'git commit -a -m' }
		};

	my $commit_message = do {
		if( $release->can( 'vcs_commit_message' ) ) { # Module::Release::Git 1.016
			$release->vcs_commit_message( { version => $Version } );
			}
		else {
			sprintf '* for release %s', $Version;
			}
		};

	my $vcs_commit = `$command "$commit_message" 2>&1`;

	$release->_print( $vcs_commit );
	}
else {
	$release->_print( "Skipping Changes file" );
	}


# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Build the release in preparation for uploading
$release->_print("============ Preparing to release\n");
$release->clean;
$release->touch_all_in_manifest;
$release->build_makefile;
$release->make;
$release->dist;

$release->check_for_passwords;

$release->_debug( "This is where I should release stuff\n" );

while( $release->should_upload_to_pause ) {
	$release->load_mixin( 'Module::Release::WebUpload::Mojo' );
	$release->_print("============ Uploading to PAUSE\n");
	last if $release->debug;
	$release->web_upload;
	last;
	}

$release->vcs_tag unless $release->debug;


# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
$release->_print("============ Cleaning up\n");
$release->clean;
$release->vcs_exit;
$release->_print( "Done.\n" );

__END__
