App-prepare4release

 view release on metacpan or  search on metacpan

lib/App/prepare4release.pm  view on Meta::CPAN

		my $min = $class->resolve_min_perl_for_badge(
			$app->{config} // {}, $mf_content, $vf );
		$min = 'v5.10.0' unless defined $min && length $min;

		$inner = $class->build_pod_badge_markdown(
			$cwd, $app, $opts, $opts->{cpan} ? 1 : 0,
			$mf_snippets, $identity, $min
		);
	}

	open my $fh, '<:encoding(UTF-8)', $readme
		or croak "Cannot open README.md '$readme': $!";
	local $/;
	my $text = <$fh> // '';
	close $fh;

	$text = $class->_strip_readme_badge_markdown_block($text);

	# Trailing "\n\n" = mandatory blank line after the last badge line.
	my $block = $inner . "\n\n";

	my $new_text = $class->_insert_readme_badges_after_regen( $text, $block );
	return if $new_text eq $text;

	open my $out, '>:encoding(UTF-8)', $readme
		or croak "Cannot write README.md '$readme': $!";
	print {$out} $new_text;
	close $out;
	warn "[prepare4release] updated README badges in $readme\n" if $verbose;
	return;
}

sub strip_pod_badges_from_version_from {
	my ( $class, $vf, $verbose ) = @_;
	return unless $vf && -f $vf;

	my ( $code, $pod ) = $class->split_pm_code_and_pod($vf);
	return unless length $pod;
	return unless $pod =~ /PREPARE4RELEASE_BADGES/;

	my $new_pod = $pod;
	$new_pod =~ s{
		(?:^=begin\s+html\s*\R)?
		\s*<!--\s*PREPARE4RELEASE_BADGES\s*-->\s*
		.*?
		<!--\s*/PREPARE4RELEASE_BADGES\s*-->\s*
		(?:\R=end\s+html\s*)?
	}{}msx;

	return if $new_pod eq $pod;

	my $out = $code . "\n__END__\n" . $new_pod;
	open my $out_fh, '>:encoding(UTF-8)', $vf
		or croak "Cannot write '$vf': $!";
	print {$out_fh} $out;
	close $out_fh;
	warn "[prepare4release] removed legacy POD badge block from $vf\n" if $verbose;
	return;
}

sub list_files_for_eol_xt {
	my ( $class, $cwd ) = @_;
	my @out;

	for my $f (qw(Makefile.PL Build.PL cpanfile prepare4release.json)) {
		my $p = File::Spec->catfile( $cwd, $f );
		push @out, $f if -f $p;
	}

	push @out, map { File::Spec->abs2rel( $_, $cwd ) }
		$class->find_lib_pm_files($cwd);

	my $bin = File::Spec->catfile( $cwd, 'bin' );
	if ( -d $bin ) {
		opendir my $dh, $bin or croak "opendir bin: $!";
		while ( my $e = readdir $dh ) {
			next if $e =~ /^\./;
			my $rel = File::Spec->catfile( 'bin', $e );
			push @out, $rel if -f File::Spec->catfile( $cwd, $rel );
		}
		closedir $dh;
	}

	for my $td (qw(t xt)) {
		my $root = File::Spec->catfile( $cwd, $td );
		next unless -d $root;
		File::Find::find(
			{
				no_chdir => 1,
				wanted   => sub {
					return unless -f;
					return unless $File::Find::name =~ /\.(t|pm|pl)\z/;
					push @out, File::Spec->abs2rel( $File::Find::name, $cwd );
				},
			},
			$root
		);
	}

	my %seen;
	@out = grep { !$seen{$_}++ } sort @out;
	return @out;
}

sub ensure_xt_author_tests {
	my ( $class, $cwd, $verbose ) = @_;

	my $xtd = File::Spec->catfile( $cwd, 'xt', 'author' );
	make_path($xtd);

	my $pod_xt = File::Spec->catfile( $xtd, 'pod.t' );
	if ( !-e $pod_xt ) {
		my $body = <<'XT';
#!perl
use strict;
use warnings;
use Test2::V1;
use Test2::Tools::Basic qw(skip_all);

BEGIN {
	eval {
		require Test::Pod;
		Test::Pod->import;
		1;
	} or skip_all 'Test::Pod is required for author tests';
}

all_pod_files_ok();
XT
		$class->_write_if_absent( $pod_xt, $body, $verbose );
	}

	my $pc_xt = File::Spec->catfile( $xtd, 'pod-coverage.t' );
	if ( !-e $pc_xt ) {
		my $body = <<'XT';
#!perl
use strict;
use warnings;
use Test2::V1;
use Test2::Tools::Basic qw(skip_all);

BEGIN {
	eval {
		require Test::Pod::Coverage;
		Test::Pod::Coverage->import;
		1;
	} or skip_all 'Test::Pod::Coverage is required for author tests';
}

all_pod_coverage_ok();
XT
		$class->_write_if_absent( $pc_xt, $body, $verbose );
	}

	my @eol = $class->list_files_for_eol_xt($cwd);
	my $eol_xt = File::Spec->catfile( $xtd, 'eol.t' );
	if ( !-e $eol_xt ) {
		my $list = join "\n", map { '    ' . $_ } @eol;
		my $head = <<'EOL_HEAD';
#!perl
use strict;
use warnings;
use Test2::V1;
use Test2::Tools::Basic qw(skip_all done_testing);

BEGIN {
	eval {
		require Test::EOL;
		Test::EOL->import;
		1;
	} or skip_all 'Test::EOL is required for author tests';
}

my @files = qw(
EOL_HEAD
		my $tail = <<'EOL_TAIL';
);

eol_unix_ok($_) for @files;

done_testing;
EOL_TAIL
		my $body = $head . $list . $tail;
		$class->_write_if_absent( $eol_xt, $body, $verbose );
	}

	return;
}

sub _write_if_absent {
	my ( $class, $path, $body, $verbose ) = @_;
	open my $fh, '>:encoding(UTF-8)', $path
		or croak "Cannot write '$path': $!";
	print {$fh} $body;
	close $fh;
	warn "[prepare4release] wrote $path\n" if $verbose;
	return;
}

sub _collect_t_files {
	my ( $class, $cwd ) = @_;
	my @out;
	for my $root_name (qw(t xt)) {
		my $root = File::Spec->catfile( $cwd, $root_name );
		next unless -d $root;
		File::Find::find(
			{
				no_chdir => 1,
				wanted   => sub {
					return unless -f && /\.t\z/;
					push @out, File::Spec->abs2rel( $File::Find::name, $cwd );
				},
			},
			$root
		);
	}
	my %seen;
	@out = grep { !$seen{$_}++ } sort @out;
	return @out;
}

sub file_uses_legacy_assertion_framework {
	my ( $class, $path ) = @_;
	open my $fh, '<:encoding(UTF-8)', $path
		or return 0;
	while ( my $line = <$fh> ) {
		next if $line =~ /^\s*#/;
		next if $line =~ /^\s*=/;
		return 1 if $line =~ /^\s*use\s+Test::More\b/;
		return 1 if $line =~ /^\s*use\s+Test::Most\b/;
	}
	close $fh;
	return 0;
}

sub warn_legacy_test_frameworks {
	my ( $class, $cwd ) = @_;
	my %legacy_ok = map { $_ => 1 } qw(
		xt/author/cpants.t
		xt/author/pause-permissions.t
		xt/author/pod-coverage.t
		xt/author/version.t
	);
	my @bad;

lib/App/prepare4release.pm  view on Meta::CPAN


  use App::prepare4release;
  App::prepare4release->run(@ARGV);

=head1 DESCRIPTION

Run from the distribution root (where F<prepare4release.json> and F<Makefile.PL>
live). The tool:

=over 4

=item *

Loads F<prepare4release.json> and resolves C<module_name> / C<version> / C<dist_name>
when omitted (from F<Makefile.PL> and the main F<.pm>). Invalid JSON logs a warning
and behaves like an empty object. Root keys such as C<author>, C<abstract>,
C<license>, C<min_perl_version>, C<module_name>, C<version_from>, and C<exe_files>
are copied into F<Makefile.PL> C<WriteMakefile(...)> when set (see L</CONFIGURATION FILE>).

=item *

Patches F<Makefile.PL>: C<META_MERGE> (C<repository> and C<bugtracker> URLs), and
a marked C<MY::postamble> block (between C<# BEGIN PREPARE4RELEASE_POSTAMBLE> and
C<# END PREPARE4RELEASE_POSTAMBLE>) that runs C<pod2github> when C<--github> or
C<--gitlab> was used (else C<pod2markdown>), then F<maint/inject-readme-badges.pl>
(a standalone Perl script regenerated each run, core modules only) so C<make README.md>
reapplies the same shields without depending on C<App::prepare4release>. The block
is refreshed on each run to match the current C<pod2*> choice; the script embeds the
frozen badge Markdown for the chosen C<--github> / C<--gitlab> / C<--cpan> flags.

=item *

When C<--github> or C<--gitlab> is set, ensures CI workflow files exist (see
L</Continuous integration>).

=item *

Regenerates F<README.md> from the F<VERSION_FROM> module (C<make README.md> when
F<Makefile> exists, otherwise C<pod2github> or C<pod2markdown>), then injects
Markdown shield lines (C<[![Alt](image)](link)>) into F<README.md> after the first
title block (runs of C<#> headings) or before C<# NAME> when that is the first
heading. The F<Makefile.PL> postamble runs F<maint/inject-readme-badges.pl> after
C<pod2github>/C<pod2markdown> so badges stay in sync without a runtime dependency
on this distribution. Strips any
legacy badge block from POD after C<__END__>. License and minimum Perl badges
are always added; with C<--cpan>, also Repology, CPAN version, and cpants. The
GitHub Actions CI badge is added only with C<--github>; the GitLab pipeline badge
only with C<--gitlab> (host from C<git.server>, else from C<git.repo> URL, else
C<gitlab.com>). License shield (always blue) uses the same key as ExtUtils::MakeMaker
(F<Makefile.PL> C<LICENSE>), or the type inferred from a root F<LICENSE> file when
present; the link is the repository F<LICENSE> blob when that file exists and
C<--github> or C<--gitlab> is set (branch from C<git.default_branch>, default
C<main>), otherwise the usual canonical license URL. Minimum Perl on the shield
comes from C<min_perl_version> / C<perl_min> in the JSON file, else
F<Makefile.PL> C<MIN_PERL_VERSION>, else the stricter of makefile and main module
(as for CI).

=item *

Creates author tests under F<xt/author/> when missing: C<pod.t> (L<Test::Pod>),
C<eol.t> (L<Test::EOL>), C<pod-coverage.t> (L<Test::Pod::Coverage>), using
L<Test2::V1>.

=item *

With C<--cpan>, after the steps above: ensures F<LICENSE> exists. The license
I<type> is taken from F<Makefile.PL> C<LICENSE> (via the same snippet scan as
elsewhere in this tool); if that is missing, I<perl> (same terms as Perl 5) is
assumed. The file text is downloaded from official upstream sources (for
C<perl>, the F<Artistic> and F<Copying> files from the Perl 5 repository; for
C<apache_2>, C<mit>, C<gpl_3>, etc., the canonical license URLs). If a fetch
fails, a short built-in fallback is written. If F<README> is missing but
F<README.md> exists, writes a short stub F<README> pointing readers to
F<README.md>. Creates a default F<MANIFEST.SKIP> when none is present (skipping
F<blib/>, F<cover_db/>, F<nytprof/>, tarballs, F<.git/>, etc.); runs
C<perl Makefile.PL>, copies F<MYMETA.*> to F<META.*>, and C<make manifest> so
F<MANIFEST> matches the tree for CPAN packaging.

=item *

Warns when any F<t/*.t> or F<xt/**/*.t> file starts with C<use Test::More> or
C<use Test::Most> (legacy assertion frameworks). Prefer L<Test2::V1> or
L<Test2::Tools::Spec>.

=item *

Scans F<lib/>, F<bin/>, F<maint/>, F<t/>, and optionally F<xt/> for C<use> /
C<require> and compares with C<PREREQ_PM> / C<TEST_REQUIRES> in F<Makefile.PL>.
Core modules for the target minimum Perl are skipped unless a minimum module
version is given on the C<use> line (see L<Module::CoreList>). By default only a
warning is printed; C<--sync-deps> or C<dependencies.sync> in
F<prepare4release.json> updates F<Makefile.PL> and appends to F<cpanfile> when
present. C<dependencies.skip> disables the check.

=back

=head1 README badge injector (F<maint/inject-readme-badges.pl>)

The C<MY::postamble> fragment cannot hold large, self-contained Perl I<sub>s:
C<ExtUtils::MakeMaker> expects that section to expand into Makefile rules, and
keeping badge logic only in F<Makefile.PL> would either duplicate a lot of text
or imply loading this distribution at C<make README.md> time. Instead,
C<prepare4release> writes F<maint/inject-readme-badges.pl>, a small, generated
program (core modules only) that strips prior shield lines and inserts the
frozen Markdown block computed on the last run (same flags as C<--github> /
C<--gitlab> / C<--cpan>). Downstream distributions should I<commit> that file
with the rest of the tree so C<make README.md> works in a clean clone and the
file is included in the CPAN tarball like any other tracked asset. Re-run
C<prepare4release> after changing repository URLs, license, or badge-related
options so the script and F<README.md> stay consistent. No runtime dependency on
C<App::prepare4release> is added to the target module.

=head1 CONFIGURATION FILE

File name: F<prepare4release.json> (in the distribution root).

An empty file or whitespace-only file is treated as an empty JSON object C<{}>.
Invalid JSON logs a warning and is treated as C<{}>.

=over 4



( run in 1.427 second using v1.01-cache-2.11-cpan-8f98c5d2c55 )