Compare commits
6 Commits
cd691a3286
...
1ad1e9810c
Author | SHA1 | Date | |
---|---|---|---|
|
1ad1e9810c | ||
|
d68f49668d | ||
|
d4ad827471 | ||
|
cd7815e30c | ||
|
080add003c | ||
|
b3972b1062 |
@ -1,25 +1,30 @@
|
|||||||
# yaml-language-server: $schema=./changelog-schema.yaml
|
---
|
||||||
project:
|
project:
|
||||||
name: App::Changeman
|
name: App::Changeman
|
||||||
homepage: https://git.babyl.ca/yanick/App-Changelord
|
homepage: https://git.babyl.ca/yanick/App-Changelord
|
||||||
change_types:
|
|
||||||
- feat:
|
|
||||||
title: Features
|
|
||||||
level: minor
|
|
||||||
- fix:
|
|
||||||
title: Bug fixes
|
|
||||||
level: patch
|
|
||||||
releases:
|
releases:
|
||||||
|
- version: v3.0.2
|
||||||
|
date: 2022-06-17
|
||||||
|
changes: ~
|
||||||
|
- version: v3.0.1
|
||||||
|
date: 2022-06-17
|
||||||
|
changes: ~
|
||||||
|
- version: v3.0.0
|
||||||
|
date: 2022-06-17
|
||||||
|
changes: ~
|
||||||
|
- version: v2.0.0
|
||||||
|
date: 2022-06-17
|
||||||
|
changes: ~
|
||||||
- version: v1.2.3
|
- version: v1.2.3
|
||||||
date: 2022-01-02
|
date: 2022-01-02
|
||||||
changes:
|
changes:
|
||||||
- type: feat
|
- desc: doing the thing
|
||||||
desc: doing the thing
|
type: feat
|
||||||
- |
|
- |
|
||||||
## [2.0.0](https://github.com/yanick/json-schema-shorthand/compare/v1.0.0...v2.0.0) (2020-08-24)
|
## [2.0.0](https://github.com/yanick/json-schema-shorthand/compare/v1.0.0...v2.0.0) (2020-08-24)
|
||||||
|
|
||||||
|
|
||||||
### ⚠ BREAKING CHANGES
|
### BREAKING CHANGES
|
||||||
|
|
||||||
* things should continue to work as normal, but since to
|
* things should continue to work as normal, but since to
|
||||||
typescript is kinda of a big deal, I'm taking no chance.
|
typescript is kinda of a big deal, I'm taking no chance.
|
||||||
@ -64,14 +69,21 @@ releases:
|
|||||||
## 0.2.0 2017-01-03
|
## 0.2.0 2017-01-03
|
||||||
* Properties can be made required via a '!' suffix.
|
* Properties can be made required via a '!' suffix.
|
||||||
* Drop Mocha and Chai for TAP for testing.
|
* Drop Mocha and Chai for TAP for testing.
|
||||||
|
- version: 0.1.0
|
||||||
- date: 2016-08-01
|
date: 2016-08-01
|
||||||
version: 0.1.0
|
|
||||||
changes:
|
changes:
|
||||||
- Recurse down 'allOf', 'oneOf', 'anyOf', 'not'.
|
- "Recurse down 'allOf', 'oneOf', 'anyOf', 'not'."
|
||||||
- Add 'install' and 'synopsis' sections in doc.
|
- Add 'install' and 'synopsis' sections in doc.
|
||||||
|
- version: 0.0.1
|
||||||
- date: 2016-07-31
|
date: 2016-07-31
|
||||||
version: 0.0.1
|
|
||||||
changes:
|
changes:
|
||||||
- Initial release
|
- Initial release
|
||||||
|
change_types:
|
||||||
|
- feat:
|
||||||
|
level: minor
|
||||||
|
title: Features
|
||||||
|
keywords: []
|
||||||
|
- fix:
|
||||||
|
level: patch
|
||||||
|
title: Bug fixes
|
||||||
|
keywords: []
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package App::Changelord;
|
package App::Changelord;
|
||||||
|
|
||||||
|
# version next latest
|
||||||
|
|
||||||
use 5.36.0;
|
use 5.36.0;
|
||||||
|
|
||||||
use Moo;
|
use Moo;
|
||||||
@ -8,6 +10,8 @@ use YAML;
|
|||||||
|
|
||||||
use List::AllUtils qw/ pairmap partition_by /;
|
use List::AllUtils qw/ pairmap partition_by /;
|
||||||
|
|
||||||
|
use App::Changelord::Role::ChangeTypes;
|
||||||
|
|
||||||
option source => (
|
option source => (
|
||||||
is => 'ro',
|
is => 'ro',
|
||||||
format => 's',
|
format => 's',
|
||||||
@ -15,138 +19,21 @@ option source => (
|
|||||||
default => 'CHANGELOG.yml',
|
default => 'CHANGELOG.yml',
|
||||||
);
|
);
|
||||||
|
|
||||||
has changelog => (
|
has changelog => ( is => 'lazy' );
|
||||||
lazy => 1,
|
|
||||||
is => 'ro',
|
sub _build_changelog($self) {
|
||||||
default => sub($self) {
|
|
||||||
return YAML::LoadFile($self->source)
|
return YAML::LoadFile($self->source)
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
has change_types => (
|
|
||||||
is => 'ro',
|
|
||||||
default => sub($self) {
|
|
||||||
return [
|
|
||||||
{ title => 'Features' , level => 'minor', keywords => [ 'feat' ] } ,
|
|
||||||
{ title => 'Bug fixes' , level => 'patch', keywords => [ 'fix' ] },
|
|
||||||
]
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
sub render_header($self) {
|
|
||||||
|
|
||||||
my $output = "# Changelog";
|
|
||||||
|
|
||||||
my $name = $self->changelog->{project}{name};
|
|
||||||
|
|
||||||
my %links = ();
|
|
||||||
|
|
||||||
if( $self->changelog->{project}{homepage} ) {
|
|
||||||
$name = "[$name][homepage]";
|
|
||||||
$links{homepage} = $self->changelog->{project}{homepage};
|
|
||||||
}
|
|
||||||
|
|
||||||
$output .= " for $name" if $name;
|
|
||||||
|
|
||||||
if(%links) {
|
|
||||||
$output .= "\n\n";
|
|
||||||
$output .= $self->render_refs(%links);
|
|
||||||
}
|
|
||||||
|
|
||||||
$output .= "\n\n";
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sub render_refs($self,%links) {
|
with 'App::Changelord::Role::ChangeTypes';
|
||||||
my $output = '';
|
with 'App::Changelord::Role::Render';
|
||||||
|
|
||||||
for my $ref ( sort keys %links ) {
|
|
||||||
$output .= " [$ref]: $links{$ref}\n"
|
|
||||||
}
|
|
||||||
|
|
||||||
return $output . "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
sub as_markdown($self) {
|
|
||||||
my $changelog = $self->changelog;
|
|
||||||
|
|
||||||
my $output = $self->render_header;
|
|
||||||
|
|
||||||
my $n = 0;
|
|
||||||
$output .= join "\n", map { $self->render_release($_, $n++) } $changelog->{releases}->@*;
|
|
||||||
|
|
||||||
return $output;
|
|
||||||
}
|
|
||||||
|
|
||||||
sub render_release($self, $release, $n=0) {
|
|
||||||
|
|
||||||
# it's a string? Okay then!
|
|
||||||
return $release unless ref $release;
|
|
||||||
|
|
||||||
my $version = $release->{version} || ( $n ? '???' : 'NEXT' );
|
|
||||||
my $date = $release->{date};
|
|
||||||
|
|
||||||
my $output = '';
|
|
||||||
|
|
||||||
$output .= "## $version";
|
|
||||||
$output .= ", $date" if $date;
|
|
||||||
|
|
||||||
$output .= "\n";
|
|
||||||
|
|
||||||
if( $release->{changes} ) {
|
|
||||||
my @changes = map { ref ? $_ : { desc => $_ } } $release->{changes}->@*;
|
|
||||||
|
|
||||||
my @keywords = map { $_->{keywords}->@* } $self->change_types->@*;
|
|
||||||
|
|
||||||
# find the generics
|
|
||||||
my @generics = grep {
|
|
||||||
my $type = $_->{type};
|
|
||||||
|
|
||||||
my $res = !$type;
|
|
||||||
|
|
||||||
if( $type and not grep { $type eq $_} @keywords ) {
|
|
||||||
$res = 1;
|
|
||||||
warn "change type '$type' is not recognized\n";
|
|
||||||
}
|
|
||||||
$res;
|
|
||||||
} @changes;
|
|
||||||
|
|
||||||
|
|
||||||
$output .= "\n" if @generics;
|
|
||||||
$output .= " * $_->{desc}\n" for @generics;
|
|
||||||
|
|
||||||
my %keyword_mapping = map {
|
|
||||||
my $title = $_->{title};
|
|
||||||
map { $_ => $title } $_->{keywords}->@*;
|
|
||||||
} $self->change_types->@*;
|
|
||||||
|
|
||||||
|
|
||||||
my %groups = partition_by {
|
|
||||||
no warnings qw/ uninitialized /;
|
|
||||||
$keyword_mapping{$_->{type}} || ''
|
|
||||||
} @changes;
|
|
||||||
|
|
||||||
for my $type ( $self->change_types->@* ) {
|
|
||||||
my $c = $groups{$type->{title}} or next;
|
|
||||||
$output .= "\n### $type->{title}\n\n";
|
|
||||||
$output .= $self->render_change($_) for $c->@*;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return $output . "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
sub render_change($self, $change) {
|
|
||||||
return " * " . $change->{desc} . "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
sub run($self) {
|
sub run($self) {
|
||||||
no warnings 'utf8';
|
no warnings 'utf8';
|
||||||
print $self->as_markdown;
|
print $self->as_markdown;
|
||||||
}
|
}
|
||||||
|
|
||||||
subcommand 'schema' => 'App::Changelord::Command::Schema';
|
subcommand $_ => 'App::Changelord::Command::' . ucfirst $_
|
||||||
subcommand 'validate' => 'App::Changelord::Command::Validate';
|
for qw/ schema validate version bump init add/;
|
||||||
|
|
||||||
'end of App::Changeman';
|
1;
|
||||||
|
68
lib/App/Changelord/Command/Add.pm
Normal file
68
lib/App/Changelord/Command/Add.pm
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
package App::Changelord::Command::Add;
|
||||||
|
|
||||||
|
use 5.36.0;
|
||||||
|
|
||||||
|
use Moo;
|
||||||
|
use CLI::Osprey desc => 'add a change to the changelog';
|
||||||
|
|
||||||
|
use PerlX::Maybe;
|
||||||
|
use Path::Tiny;
|
||||||
|
use App::Changelord::Command::Init;
|
||||||
|
|
||||||
|
has changelog => ( is => 'lazy' );
|
||||||
|
|
||||||
|
sub _build_changelog ($self) { $self->parent_command->changelog }
|
||||||
|
|
||||||
|
# TODO validate the type
|
||||||
|
option type => (
|
||||||
|
format => 's',
|
||||||
|
doc => 'type of change',
|
||||||
|
is => 'ro',
|
||||||
|
);
|
||||||
|
|
||||||
|
option ticket => (
|
||||||
|
format => 's',
|
||||||
|
doc => 'associated ticket',
|
||||||
|
is => 'ro',
|
||||||
|
);
|
||||||
|
|
||||||
|
sub is_next($self,$release) {
|
||||||
|
my $version = $release->{version};
|
||||||
|
return !$version || $version eq 'NEXT';
|
||||||
|
}
|
||||||
|
|
||||||
|
sub next_release($self) {
|
||||||
|
my $changelog = $self->changelog;
|
||||||
|
|
||||||
|
my $release = $changelog->{releases}[0];
|
||||||
|
|
||||||
|
unless( $self->is_next($release) ) {
|
||||||
|
unshift $changelog->{releases}->@*,
|
||||||
|
$release = {
|
||||||
|
version => 'NEXT',
|
||||||
|
changes => [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return $release;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub save_changelog($self) {
|
||||||
|
my $src = $self->parent_command->source;
|
||||||
|
|
||||||
|
path($src)->spew( App::Changelord::Command::Init::serialize_changelog($self) );
|
||||||
|
}
|
||||||
|
|
||||||
|
sub run ($self) {
|
||||||
|
my $version = $self->next_release;
|
||||||
|
|
||||||
|
push $version->{changes}->@*, {
|
||||||
|
maybe type => $self->type,
|
||||||
|
maybe ticket => $self->ticket,
|
||||||
|
desc => join ' ', @ARGV,
|
||||||
|
};
|
||||||
|
|
||||||
|
$self->save_changelog;
|
||||||
|
}
|
||||||
|
|
||||||
|
'end of App::Changelog::Command::Add';
|
81
lib/App/Changelord/Command/Bump.pm
Normal file
81
lib/App/Changelord/Command/Bump.pm
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
package App::Changelord::Command::Bump;
|
||||||
|
|
||||||
|
use 5.36.0;
|
||||||
|
|
||||||
|
use Moo;
|
||||||
|
use CLI::Osprey desc => 'bump next version';
|
||||||
|
|
||||||
|
use Path::Tiny;
|
||||||
|
use JSON;
|
||||||
|
use YAML qw/ Bless /;
|
||||||
|
use List::AllUtils qw/ first min uniq /;
|
||||||
|
use Version::Dotted::Semantic;
|
||||||
|
|
||||||
|
with 'App::Changelord::Role::ChangeTypes';
|
||||||
|
|
||||||
|
has changelog => ( is => 'lazy' );
|
||||||
|
|
||||||
|
with 'App::Changelord::Role::Versions';
|
||||||
|
with 'App::Changelord::Role::Stats';
|
||||||
|
|
||||||
|
sub _build_changelog ($self) { $self->parent_command->changelog }
|
||||||
|
|
||||||
|
sub run ($self) {
|
||||||
|
my $bump = shift @ARGV;
|
||||||
|
|
||||||
|
if ( $bump and !grep { $_ eq $bump } qw/ minor major patch / ) {
|
||||||
|
die "invalid bump type '$bump', must be major, minor, or patch\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
my $version;
|
||||||
|
|
||||||
|
if ($bump) {
|
||||||
|
$version = Version::Dotted::Semantic->new( $self->latest_version );
|
||||||
|
$version->bump($bump);
|
||||||
|
$version = $version->stringify;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$version = $self->next_version;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $self->changelog->{releases}[0]{version}
|
||||||
|
and $self->changelog->{releases}[0]{version} ne 'NEXT' ) {
|
||||||
|
warn
|
||||||
|
"No change detected since last version, hope you know what you're doing.\n";
|
||||||
|
unshift $self->changelog->{releases}->@*, { version => 'NEXT', };
|
||||||
|
}
|
||||||
|
|
||||||
|
my @time = localtime;
|
||||||
|
|
||||||
|
$self->changelog->{releases}[0]{version} = $version;
|
||||||
|
$self->changelog->{releases}[0]{date} = sprintf "%d-%02d-%02d",
|
||||||
|
$time[5] + 1900, $time[4], $time[3];
|
||||||
|
|
||||||
|
if( $self->changelog->{project}{with_stats} ) {
|
||||||
|
push $self->changelog->{releases}[0]{changes}->@*, {
|
||||||
|
type => 'stats', desc => $self->stats
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
my $change = $self->changelog;
|
||||||
|
Bless($change)->keys(
|
||||||
|
[ uniq qw/
|
||||||
|
project releases change_types
|
||||||
|
/, sort keys %$change
|
||||||
|
] );
|
||||||
|
Bless( $change->{project} )->keys(
|
||||||
|
[ uniq qw/
|
||||||
|
name homepage
|
||||||
|
/, sort keys $change->{project}->%*
|
||||||
|
] );
|
||||||
|
|
||||||
|
for ( grep { ref } $change->{releases}->@* ) {
|
||||||
|
Bless($_)->keys( [ uniq qw/ version date changes /, sort keys %$_ ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
path( $self->parent_command->source )->spew( YAML::Dump($change) );
|
||||||
|
|
||||||
|
say "new version minted: $version";
|
||||||
|
}
|
||||||
|
|
||||||
|
'end of App::Changelog::Command::Bump';
|
65
lib/App/Changelord/Command/Init.pm
Normal file
65
lib/App/Changelord/Command/Init.pm
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
package App::Changelord::Command::Init;
|
||||||
|
|
||||||
|
use 5.36.0;
|
||||||
|
|
||||||
|
use Moo;
|
||||||
|
use CLI::Osprey desc => 'initialize new changelog source file';
|
||||||
|
|
||||||
|
use Path::Tiny;
|
||||||
|
use JSON;
|
||||||
|
use YAML qw/ Bless /;
|
||||||
|
use List::AllUtils qw/ first min uniq /;
|
||||||
|
use Version::Dotted::Semantic;
|
||||||
|
|
||||||
|
with 'App::Changelord::Role::ChangeTypes';
|
||||||
|
|
||||||
|
has changelog => ( is => 'lazy' );
|
||||||
|
|
||||||
|
with 'App::Changelord::Role::Versions';
|
||||||
|
|
||||||
|
sub _build_changelog ($self) { $self->parent_command->changelog }
|
||||||
|
|
||||||
|
sub serialize_changelog($self, $changelog = undef) {
|
||||||
|
|
||||||
|
$changelog //= $self->changelog;
|
||||||
|
|
||||||
|
Bless($changelog)->keys(
|
||||||
|
[ uniq qw/
|
||||||
|
project releases change_types
|
||||||
|
/, sort keys %$changelog
|
||||||
|
] );
|
||||||
|
Bless( $changelog->{project} )->keys(
|
||||||
|
[ uniq qw/
|
||||||
|
name homepage
|
||||||
|
/, sort keys $changelog->{project}->%*
|
||||||
|
] );
|
||||||
|
|
||||||
|
for ( grep { ref } $changelog->{releases}->@* ) {
|
||||||
|
Bless($_)->keys( [ uniq qw/ version date changes /, sort keys %$_ ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
return YAML::Dump($changelog);
|
||||||
|
}
|
||||||
|
|
||||||
|
sub run ($self) {
|
||||||
|
my $src = $self->parent_command->source;
|
||||||
|
die "file '$src' already exists, aborting\n" if -f $src;
|
||||||
|
|
||||||
|
my $change = {
|
||||||
|
project => {
|
||||||
|
name => undef,
|
||||||
|
homepage => undef,
|
||||||
|
with_stats => 'true',
|
||||||
|
ticket_url => undef,
|
||||||
|
},
|
||||||
|
change_types => $self->change_types,
|
||||||
|
releases => [
|
||||||
|
{ version => 'NEXT', changes => [] }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
path($src)->spew( $self->serialize_changelog($change) );
|
||||||
|
|
||||||
|
say "file '$src' created, enjoy!";
|
||||||
|
}
|
||||||
|
|
||||||
|
'end of App::Changelog::Command::Init';
|
@ -18,12 +18,9 @@ option json => (
|
|||||||
|
|
||||||
sub run($self) {
|
sub run($self) {
|
||||||
|
|
||||||
my $schema = path(__FILE__)->sibling('changelog-schema.yml')->slurp;
|
my $schema = YAML::Load(path(__FILE__)->sibling('changelog-schema.yml')->slurp);
|
||||||
|
|
||||||
$schema = JSON->new->pretty->encode(YAML::Load($schema));
|
|
||||||
|
|
||||||
print $schema;
|
|
||||||
|
|
||||||
|
print $self->json ? JSON->new->pretty->encode(YAML::Load($schema)) : YAML::Dump($schema);
|
||||||
}
|
}
|
||||||
|
|
||||||
'end of App::Changelog::Command::Schema';
|
'end of App::Changelog::Command::Schema';
|
||||||
|
67
lib/App/Changelord/Command/Version.pm
Normal file
67
lib/App/Changelord/Command/Version.pm
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
package App::Changelord::Command::Version;
|
||||||
|
# SYNOPSIS: output the latest / next version
|
||||||
|
|
||||||
|
use 5.36.0;
|
||||||
|
|
||||||
|
use Moo;
|
||||||
|
use CLI::Osprey
|
||||||
|
desc => 'output the latest/next version';
|
||||||
|
|
||||||
|
use Path::Tiny;
|
||||||
|
use JSON;
|
||||||
|
use YAML::XS;
|
||||||
|
use List::AllUtils qw/ first min /;
|
||||||
|
use Version::Dotted::Semantic;
|
||||||
|
|
||||||
|
with 'App::Changelord::Role::ChangeTypes';
|
||||||
|
|
||||||
|
has changelog => (
|
||||||
|
is => 'lazy'
|
||||||
|
);
|
||||||
|
|
||||||
|
sub _build_changelog($self){ $self->parent_command->changelog }
|
||||||
|
|
||||||
|
sub latest_version($self){
|
||||||
|
first { $_ } grep { $_ ne 'NEXT' } map { eval { $_->{version} } } $self->changelog->{releases}->@*;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub next_version($self) {
|
||||||
|
my $version = Version::Dotted::Semantic->new($self->latest_version // '0.0.0');
|
||||||
|
|
||||||
|
my $upcoming = $self->changelog->{releases}[0];
|
||||||
|
|
||||||
|
if( $upcoming->{version} and $upcoming->{version} ne 'NEXT') {
|
||||||
|
$upcoming = { changes => [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
my %mapping = map {
|
||||||
|
my $level = $_->{level};
|
||||||
|
map { $_ => $level } $_->{keywords}->@*
|
||||||
|
} $self->change_types->@*;
|
||||||
|
|
||||||
|
my $bump =min 2, map { $_ eq 'major' ? 0 : $_ eq 'minor' ? 1 : 2 } map { $mapping{$_->{type}} || 'patch' } $upcoming->{changes}->@*;
|
||||||
|
|
||||||
|
$version->bump($bump);
|
||||||
|
|
||||||
|
return $version->normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub run($self) {
|
||||||
|
my $param = shift @ARGV;
|
||||||
|
|
||||||
|
die "invalid parameter '$param', needs to be nothing, 'next' or 'latest'\n"
|
||||||
|
if $param and not grep { $param eq $_ } qw/ next latest /;
|
||||||
|
|
||||||
|
if(!$param) {
|
||||||
|
say "latest version: ", $self->latest_version;
|
||||||
|
say "next version: ", $self->next_version;
|
||||||
|
}
|
||||||
|
elsif( $param eq 'next' ) {
|
||||||
|
say $self->next_version;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
say $self->latest_version;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
'end of App::Changelog::Command::Version';
|
@ -6,30 +6,51 @@ properties:
|
|||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
homepage:
|
homepage:
|
||||||
type: string
|
type: [ string, 'null' ]
|
||||||
description: url of the project's homepage
|
description: url of the project's homepage
|
||||||
examples:
|
examples:
|
||||||
- https://github.com/yanick/app-changelord
|
- https://github.com/yanick/app-changelord
|
||||||
name:
|
name:
|
||||||
type: string
|
type: [ 'null', string ]
|
||||||
description: name of the project
|
description: name of the project
|
||||||
examples:
|
examples:
|
||||||
- App::Changelord
|
- App::Changelord
|
||||||
type:
|
ticket_url:
|
||||||
|
type: string
|
||||||
|
description: perl code that takes a ticket string (e.g. 'GH123') via the `$_` variable and turns it into a link.
|
||||||
|
examples:
|
||||||
|
- s!GH(\d+)!https://github.com/yanick/App-Changelord/issue/$1/
|
||||||
|
- /^\d+$/ ? "https://.../$_" : undef
|
||||||
|
with_stats:
|
||||||
|
description: if true, add git statistics when bumping the version.
|
||||||
|
change_types:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
type: object
|
type: object
|
||||||
# properties:
|
additionalProperties: false
|
||||||
# type: object
|
properties:
|
||||||
# properties:
|
keywords:
|
||||||
# level: { enum: [ major, minor, patch ] }
|
type: array
|
||||||
# title: { type: string }
|
items: { type: string }
|
||||||
|
level: { enum: [ major, minor, patch ] }
|
||||||
|
title: { type: string }
|
||||||
releases:
|
releases:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
oneOf:
|
oneOf:
|
||||||
- type: string
|
- type: string
|
||||||
- type: object
|
- type: object
|
||||||
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
version: { type: string }
|
version: { type: string }
|
||||||
date: { type: string }
|
date: { type: ['null',string] }
|
||||||
|
changes: { type: 'array', items: { $ref: '#/$defs/change' } }
|
||||||
|
$defs:
|
||||||
|
change:
|
||||||
|
type: object
|
||||||
|
required: [ desc ]
|
||||||
|
additionalProperties: false
|
||||||
|
properties:
|
||||||
|
desc: { type: string }
|
||||||
|
ticket: { type: [ string, 'null' ] }
|
||||||
|
type: { type: [ string, 'null' ] }
|
||||||
|
26
lib/App/Changelord/Role/ChangeTypes.pm
Normal file
26
lib/App/Changelord/Role/ChangeTypes.pm
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package App::Changelord::Role::ChangeTypes;
|
||||||
|
|
||||||
|
use v5.36.0;
|
||||||
|
|
||||||
|
use Moo::Role;
|
||||||
|
|
||||||
|
use feature 'try';
|
||||||
|
|
||||||
|
has change_types => (
|
||||||
|
is => 'lazy',
|
||||||
|
);
|
||||||
|
|
||||||
|
sub _build_change_types($self) {
|
||||||
|
no warnings;
|
||||||
|
return eval {
|
||||||
|
$self->changelog->{change_types};
|
||||||
|
} || [
|
||||||
|
{ title => 'Features' , level => 'minor', keywords => [ 'feat' ] } ,
|
||||||
|
{ title => 'Bug fixes' , level => 'patch', keywords => [ 'fix' ] },
|
||||||
|
{ title => 'Package maintenance' , level => 'patch', keywords => [ 'chore', 'maint', 'refactor' ] },
|
||||||
|
{ title => 'Statistics' , level => 'patch', keywords => [ 'stats' ] },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
1;
|
137
lib/App/Changelord/Role/Render.pm
Normal file
137
lib/App/Changelord/Role/Render.pm
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
package App::Changelord::Role::Render;
|
||||||
|
|
||||||
|
use v5.36.0;
|
||||||
|
|
||||||
|
use Moo::Role;
|
||||||
|
|
||||||
|
use List::AllUtils qw/ pairmap partition_by /;
|
||||||
|
|
||||||
|
sub render_header ($self) {
|
||||||
|
|
||||||
|
my $output = "# Changelog";
|
||||||
|
|
||||||
|
my $name = $self->changelog->{project}{name};
|
||||||
|
|
||||||
|
my %links = ();
|
||||||
|
|
||||||
|
if ( $self->changelog->{project}{homepage} ) {
|
||||||
|
$name = "[$name][homepage]";
|
||||||
|
$links{homepage} = $self->changelog->{project}{homepage};
|
||||||
|
}
|
||||||
|
|
||||||
|
$output .= " for $name" if $name;
|
||||||
|
|
||||||
|
if (%links) {
|
||||||
|
$output .= "\n\n";
|
||||||
|
$output .= $self->render_refs(%links);
|
||||||
|
}
|
||||||
|
|
||||||
|
$output .= "\n\n";
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
sub render_refs ( $self, %links ) {
|
||||||
|
my $output = '';
|
||||||
|
|
||||||
|
for my $ref ( sort keys %links ) {
|
||||||
|
$output .= " [$ref]: $links{$ref}\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $output . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
sub as_markdown ($self) {
|
||||||
|
my $changelog = $self->changelog;
|
||||||
|
|
||||||
|
my $output = $self->render_header;
|
||||||
|
|
||||||
|
my $n = 0;
|
||||||
|
$output .= join "\n",
|
||||||
|
map { $self->render_release( $_, $n++ ) } $changelog->{releases}->@*;
|
||||||
|
|
||||||
|
return $output;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub render_release ( $self, $release, $n = 0 ) {
|
||||||
|
|
||||||
|
# it's a string? Okay then!
|
||||||
|
return $release unless ref $release;
|
||||||
|
|
||||||
|
my $version = $release->{version} || ( $n ? '???' : 'NEXT' );
|
||||||
|
my $date = $release->{date};
|
||||||
|
|
||||||
|
my $output = '';
|
||||||
|
|
||||||
|
$output .= "## $version";
|
||||||
|
$output .= ", $date" if $date;
|
||||||
|
|
||||||
|
$output .= "\n";
|
||||||
|
|
||||||
|
if ( $release->{changes} ) {
|
||||||
|
my @changes =
|
||||||
|
map { ref ? $_ : { desc => $_ } } $release->{changes}->@*;
|
||||||
|
|
||||||
|
my @keywords = map { $_->{keywords}->@* } $self->change_types->@*;
|
||||||
|
|
||||||
|
# find the generics
|
||||||
|
my @generics = grep {
|
||||||
|
my $type = $_->{type};
|
||||||
|
|
||||||
|
my $res = !$type;
|
||||||
|
|
||||||
|
if ( $type and not grep { $type eq $_ } @keywords ) {
|
||||||
|
$res = 1;
|
||||||
|
warn "change type '$type' is not recognized\n";
|
||||||
|
}
|
||||||
|
$res;
|
||||||
|
} @changes;
|
||||||
|
|
||||||
|
$output .= "\n" if @generics;
|
||||||
|
$output .= join '', map { $self->render_change($_) } @generics;
|
||||||
|
|
||||||
|
my %keyword_mapping = map {
|
||||||
|
my $title = $_->{title};
|
||||||
|
map { $_ => $title } $_->{keywords}->@*;
|
||||||
|
} $self->change_types->@*;
|
||||||
|
|
||||||
|
my %groups = partition_by {
|
||||||
|
no warnings qw/ uninitialized /;
|
||||||
|
$keyword_mapping{ $_->{type} } || ''
|
||||||
|
}
|
||||||
|
@changes;
|
||||||
|
|
||||||
|
for my $type ( $self->change_types->@* ) {
|
||||||
|
my $c = $groups{ $type->{title} } or next;
|
||||||
|
$output .= "\n### $type->{title}\n\n";
|
||||||
|
$output .= $self->render_change($_) for $c->@*;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
my $links = '';
|
||||||
|
$output =~ s/(\n \[.*?\]: .*?)\n/$links .= $1;''/gem;
|
||||||
|
|
||||||
|
return $output . $links . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
sub render_change ( $self, $change ) {
|
||||||
|
my $out = " * " . $change->{desc};
|
||||||
|
|
||||||
|
my $link = "";
|
||||||
|
|
||||||
|
if ( $change->{ticket} ) {
|
||||||
|
$out .= " [$change->{ticket}]";
|
||||||
|
if ( $self->changelog->{project}{ticket_url} ) {
|
||||||
|
local $_ = $change->{ticket};
|
||||||
|
eval $self->changelog->{project}{ticket_url};
|
||||||
|
warn $@ if $@;
|
||||||
|
if ($_) {
|
||||||
|
$link = " [$change->{ticket}]: $_";
|
||||||
|
$out .= "\n\n$link";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $out . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
1;
|
48
lib/App/Changelord/Role/Stats.pm
Normal file
48
lib/App/Changelord/Role/Stats.pm
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
package App::Changelord::Role::Stats;
|
||||||
|
|
||||||
|
use v5.36.0;
|
||||||
|
|
||||||
|
use Git::Repository;
|
||||||
|
|
||||||
|
use Moo::Role;
|
||||||
|
|
||||||
|
use feature 'try'; no warnings qw/ experimental /;
|
||||||
|
|
||||||
|
requires 'changelog';
|
||||||
|
|
||||||
|
# stolen from Dist::Zilla::Plugin::ChangeStats::Git
|
||||||
|
|
||||||
|
has repo => (
|
||||||
|
is => 'ro',
|
||||||
|
default => sub { Git::Repository->new( work_tree => '.' ) },
|
||||||
|
);
|
||||||
|
|
||||||
|
has stats => (
|
||||||
|
is => 'lazy' );
|
||||||
|
|
||||||
|
sub _build_stats ($self) {
|
||||||
|
my $comparison_data = $self->_get_comparison_data or return;
|
||||||
|
|
||||||
|
my $stats = 'code churn: ' . $comparison_data;
|
||||||
|
return $stats =~ s/\s+/ /gr;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub _get_comparison_data($self) {
|
||||||
|
|
||||||
|
# HEAD versus previous release
|
||||||
|
# What are we diffing against? :)
|
||||||
|
my $previous = $self->changelog->{releases}->@* > 1
|
||||||
|
? $self->changelog->{releases}[1]{version}
|
||||||
|
: '4b825dc642cb6eb9a060e54bf8d69288fbee4904'; # empty tree
|
||||||
|
|
||||||
|
my $output = eval {
|
||||||
|
$self->repo->run( 'diff', '--shortstat', $previous, 'HEAD')
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
warn "could not gather stats: $@\n" if $@;
|
||||||
|
|
||||||
|
return $output;
|
||||||
|
}
|
||||||
|
|
||||||
|
1;
|
39
lib/App/Changelord/Role/Versions.pm
Normal file
39
lib/App/Changelord/Role/Versions.pm
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package App::Changelord::Role::Versions;
|
||||||
|
|
||||||
|
use v5.36.0;
|
||||||
|
|
||||||
|
use List::AllUtils qw/ first min /;
|
||||||
|
use Version::Dotted::Semantic;
|
||||||
|
|
||||||
|
use Moo::Role;
|
||||||
|
|
||||||
|
use feature 'try';
|
||||||
|
|
||||||
|
requires 'changelog';
|
||||||
|
|
||||||
|
sub latest_version($self){
|
||||||
|
first { $_ } grep { $_ ne 'NEXT' } map { eval { $_->{version} } } $self->changelog->{releases}->@*;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub next_version($self) {
|
||||||
|
my $version = Version::Dotted::Semantic->new($self->latest_version // '0.0.0');
|
||||||
|
|
||||||
|
my $upcoming = $self->changelog->{releases}[0];
|
||||||
|
|
||||||
|
if( $upcoming->{version} and $upcoming->{version} ne 'NEXT') {
|
||||||
|
$upcoming = { changes => [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
my %mapping = map {
|
||||||
|
my $level = $_->{level};
|
||||||
|
map { $_ => $level } $_->{keywords}->@*
|
||||||
|
} $self->change_types->@*;
|
||||||
|
|
||||||
|
my $bump =min 2, map { $_ eq 'major' ? 0 : $_ eq 'minor' ? 1 : 2 } map { $mapping{$_->{type}} || 'patch' } $upcoming->{changes}->@*;
|
||||||
|
|
||||||
|
$version->bump($bump);
|
||||||
|
|
||||||
|
return $version->normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
1;
|
69
t/render.t
Normal file
69
t/render.t
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
use Test2::V0;
|
||||||
|
|
||||||
|
use v5.36.0;
|
||||||
|
|
||||||
|
package TestMe {
|
||||||
|
use Moo;
|
||||||
|
with 'App::Changelord::Role::Render';
|
||||||
|
with 'App::Changelord::Role::ChangeTypes';
|
||||||
|
|
||||||
|
has changelog => (
|
||||||
|
is => 'ro',
|
||||||
|
default => sub {{
|
||||||
|
project => { ticket_url => undef }
|
||||||
|
}}
|
||||||
|
);
|
||||||
|
|
||||||
|
sub set_url($self, $url) {
|
||||||
|
$self->changelog->{project}{ticket_url} = $url;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
my $test = TestMe->new();
|
||||||
|
|
||||||
|
is $test->render_change( { desc => 'blah', ticket => 'GT123' } ),
|
||||||
|
<<'END', "no ticket_url";
|
||||||
|
* blah [GT123]
|
||||||
|
END
|
||||||
|
|
||||||
|
$test->set_url( 's!GT!https://github.com/yanick/App-Changelord/issue/!' );
|
||||||
|
|
||||||
|
is $test->render_change( { desc => 'blah', ticket => 'GT123' } ),
|
||||||
|
<<'END', 'with a ticket_url';
|
||||||
|
* blah [GT123]
|
||||||
|
|
||||||
|
[GT123]: https://github.com/yanick/App-Changelord/issue/123
|
||||||
|
END
|
||||||
|
|
||||||
|
$test->set_url( '$_ = undef' );
|
||||||
|
|
||||||
|
is $test->render_change( { desc => 'blah', ticket => 'GT123' } ),
|
||||||
|
<<'END', 'with a ticket_url, but returns nothing';
|
||||||
|
* blah [GT123]
|
||||||
|
END
|
||||||
|
|
||||||
|
subtest 'all links go at the bottom' => sub {
|
||||||
|
$test->set_url( 's!^!link://!' );
|
||||||
|
|
||||||
|
is $test->render_release({
|
||||||
|
changes => [
|
||||||
|
{ desc => 'this', ticket => 1 },
|
||||||
|
{ desc => 'that', ticket => 2 },
|
||||||
|
{ desc => 'else' },
|
||||||
|
]
|
||||||
|
}), <<'END';
|
||||||
|
## NEXT
|
||||||
|
|
||||||
|
* this [1]
|
||||||
|
* that [2]
|
||||||
|
* else
|
||||||
|
|
||||||
|
[1]: link://1
|
||||||
|
[2]: link://2
|
||||||
|
END
|
||||||
|
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
done_testing;
|
27
t/stats.t
Normal file
27
t/stats.t
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
use Test2::V0;
|
||||||
|
|
||||||
|
use v5.36.0;
|
||||||
|
|
||||||
|
package TestMe {
|
||||||
|
use Moo;
|
||||||
|
|
||||||
|
has changelog => (
|
||||||
|
is => 'ro',
|
||||||
|
default => sub {{
|
||||||
|
project => { ticket_url => undef },
|
||||||
|
releases => [
|
||||||
|
{ },
|
||||||
|
]
|
||||||
|
}}
|
||||||
|
);
|
||||||
|
|
||||||
|
with 'App::Changelord::Role::Stats';
|
||||||
|
with 'App::Changelord::Role::ChangeTypes';
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
my $test = TestMe->new;
|
||||||
|
|
||||||
|
like $test->stats => qr/code churn: /;
|
||||||
|
|
||||||
|
done_testing;
|
26
t/versions.t
Normal file
26
t/versions.t
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
use 5.36.0;
|
||||||
|
|
||||||
|
use Test2::V0;
|
||||||
|
|
||||||
|
use App::Changelord::Command::Version;
|
||||||
|
|
||||||
|
my $version = App::Changelord::Command::Version->new(
|
||||||
|
changelog => {
|
||||||
|
releases => [
|
||||||
|
{ version => 'NEXT' },
|
||||||
|
{ version => 'v1.2.3' },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
is $version->latest_version => 'v1.2.3';
|
||||||
|
|
||||||
|
is $version->next_version => 'v1.2.4';
|
||||||
|
|
||||||
|
$version->{changelog}{releases}[0]{changes} = [
|
||||||
|
{ type => 'feat' }
|
||||||
|
];
|
||||||
|
|
||||||
|
is $version->next_version => 'v1.3.0';
|
||||||
|
|
||||||
|
done_testing();
|
Loading…
Reference in New Issue
Block a user