File Coverage

blib/lib/Dist/Zilla/Plugin/GitHub/CreateRelease.pm
Criterion Covered Total %
statement 45 151 29.8
branch 0 50 0.0
condition 0 9 0.0
subroutine 15 28 53.5
pod 3 3 100.0
total 63 241 26.1


line stmt bran cond sub pod time code
1 1     1   1942 use strict;
  1         3  
  1         31  
2 1     1   5 use warnings;
  1         2  
  1         58  
3              
4             package Dist::Zilla::Plugin::GitHub::CreateRelease;
5             our $VERSION = '0.0002'; # VERSION
6              
7             # ABSTRACT: Create a GitHub Release
8              
9 1     1   528 use Pithub::Repos::Releases;
  1         306761  
  1         36  
10 1     1   1162 use Config::Identity::GitHub;
  1         47124  
  1         40  
11 1     1   574 use Git::Wrapper;
  1         25079  
  1         36  
12 1     1   8 use File::Basename;
  1         2  
  1         67  
13 1     1   6 use URI;
  1         3  
  1         31  
14 1     1   6 use URI::Escape qw( uri_unescape );
  1         3  
  1         55  
15 1     1   507 use File::Slurper qw/read_text read_binary/;
  1         4844  
  1         68  
16 1     1   8 use Exporter qw(import);
  1         2  
  1         29  
17 1     1   1079 use Moose;
  1         558662  
  1         10  
18             with 'Dist::Zilla::Role::AfterRelease';
19              
20 1     1   8638 use namespace::autoclean;
  1         9024  
  1         4  
21              
22             has hash_alg => (is => 'ro', default => 'sha256');
23             has repo => (is => 'ro');
24             has branch => (is => 'ro', default => 'main');
25             has title_template => (is => 'ro', default => 'Version RELEASE - TRIAL CPAN release');
26             has notes_as_code => (is => 'ro', default => 1);
27             has github_notes => (is => 'ro', default => 0);
28             has notes_from => (is => 'ro', default => 'SignReleaseNotes');
29             has notes_file => (is => 'ro', default => 'Release-VERSION');
30             has draft => (is => 'ro', default => 0);
31             has add_checksum => (is => 'ro', default => 1);
32              
33 1     1   144 use constant false => \0;
  1         2  
  1         1478  
34 0     0 1   sub true () { return \1 } # note the empty prototype
35              
36             sub _create_release {
37 0     0     my $self = shift;
38 0           my $tag = shift;
39 0           my $branch = shift;
40 0           my $title = shift;
41 0           my $notes = shift;
42 0           my $filename = shift;
43 0           my $cpan_tar = shift;
44              
45 0           my %identity = Config::Identity::GitHub->load_check;
46              
47 0 0         die "Unable to load github token from ~/.github-identity" if (! defined $identity{token});
48              
49             my $releases = Pithub::Repos::Releases->new(
50             user => $identity{login} || $self->{username},
51             repo => $self->_get_repo_name() || $self->{repo},
52             token => $identity{token},
53 0   0       );
      0        
54 0 0         die "Unable to instantiate Pithub::Repos::Releases" if (! defined $releases);
55              
56             my $release = $releases->create(
57             data => {
58             tag_name => "$tag",
59             target_commitish => $branch,
60             name => $title,
61             body => $notes,
62             draft => $self->{draft} ? true : false,
63             prerelease => $self->zilla->is_trial ? true : false,
64 0 0         generate_release_notes => $self->{github_notes} ? true : false,
    0          
    0          
65             }
66             );
67 0 0         die "Unable to create GitHub release\n" if (! defined $release->content->{id});
68              
69 0           $self->log("Release created at $releases->{repo} for $identity{login}");
70              
71             my $asset = $releases->assets->create(
72             release_id => $release->content->{id},
73 0           name => $filename,
74             data => $cpan_tar,
75             content_type => 'application/gzip',
76             );
77              
78 0 0         if ($asset->code eq '201') {
79 0           $self->log("CPAN archive appended to GitHub release: $tag");
80             } else {
81 0           $self->log("Unable to append CPAN archive GitHub release: $tag");
82             }
83              
84             }
85              
86             sub _get_repo_name {
87 0     0     my $self;
88              
89 0           my $git = Git::Wrapper->new('./');
90 0           my @url = $git->RUN('config', '--get', 'remote.origin.url');
91              
92             #FIXME there must be a better way...
93 0           my $basename = uri_unescape( basename(URI->new( $url[0])->path));
94 0           $basename =~ s/.git//;
95              
96 0           return $basename;
97              
98             }
99              
100             sub _generate_release_notes {
101 0     0     my $self = shift;
102 0           my $filename = shift;
103 0           my $notes;
104              
105 0 0         return "" if (! $self->{add_checksum});
106              
107 0           $notes = $self->_get_checksum($filename);
108              
109 0           return $self->_as_code($notes);
110             }
111              
112             sub _get_notes_from_changes {
113 0     0     my $self = shift;
114 0           my $filename = shift;
115              
116 0           my $git = Git::Wrapper->new('./');
117 0           my @tags = $git->RUN('for-each-ref', 'refs/tags/*', '--sort=-taggerdate', '--count=2', '--format=%(refname:short)');
118              
119 0           my $vers = $tags[0];
120 0           my $prev = $tags[1];
121              
122 0           my $file = read_text($self->{notes_file});
123 0           my @lines = split /\n/, $file;
124 0           my $print = 0;
125 0           my $notes = "";
126 0           foreach my $line (@lines) {
127 0 0         $print = 1 if ($line =~ /^$vers/);
128 0 0         $print = 0 if ($line =~ /^$prev/);
129 0 0         $notes .= $line . "\n" if $print;
130             }
131 0 0         return $self->_as_code($notes) if (! $self->{add_checksum});
132              
133 0           $notes .= $self->_get_checksum($filename);
134              
135 0           return $self->_as_code($notes);
136             }
137              
138             sub _get_notes_from_file {
139 0     0     my $self = shift;
140 0           my $filename = shift;
141              
142 0           my $version = $self->_get_version();
143              
144 0           my $notes_file = $self->{notes_file};
145 0           $notes_file =~ s/VERSION/$version/;
146              
147 0           my $notes = read_text($notes_file);
148              
149 0 0         return $self->_as_code($notes) if (! $self->{add_checksum});
150              
151 0 0         return $self->_as_code($notes) if ($self->{notes_from} eq 'SignReleaseNotes');
152              
153 0           $notes .= $self->_get_checksum($filename);
154              
155 0           return $self->_as_code($notes);
156              
157             }
158              
159             sub after_release {
160 0     0 1   my $self = shift;
161 0           my $filename = shift;
162              
163 0           my $tag = _get_git_tag();
164 0           my $branch = $self->{branch};
165 0           my $title = $self->{title_template};
166              
167 0           $title =~ s/RELEASE/$tag/;
168 0 0         $title =~ s/TRIAL/Official/ if (!$self->zilla->is_trial);
169              
170 0           my $notes;
171              
172 0 0 0       if ($self->{notes_from} eq 'SignReleaseNotes' or $self->{notes_from} eq 'FromFile') {
    0          
    0          
173 0           $notes = $self->_get_notes_from_file($filename);
174             } elsif ($self->{notes_from} eq 'ChangeLog') {
175 0           $notes = $self->_get_notes_from_changes($filename);
176             } elsif ($self->{notes_from} eq 'GitHub::CreateRelease') {
177 0           $notes = $self->_generate_release_notes($filename);
178             }
179              
180 0           my $cpan_tar = read_binary($filename);
181              
182 0           my($basename, $dirs, $suffix) = fileparse($filename);
183              
184 0           $self->_create_release($tag, $branch, $title, $notes, $basename, $cpan_tar);
185             }
186              
187             sub _as_code {
188 0     0     my $self = shift;
189 0           my $text = shift;
190              
191 0 0         return '```' . "\n" . $text . "\n" . '```' if $self->{notes_as_code};
192 0           return $text;
193             }
194              
195             sub _get_git_tag {
196 0     0     my $self = shift;
197              
198 0           my $git = Git::Wrapper->new('./');
199              
200 0           my @tags = $git->RUN('for-each-ref', 'refs/tags/*', '--sort=-taggerdate', '--count=1', '--format=%(refname:short)');
201              
202 0           return $tags[0];
203             }
204              
205             sub _get_checksum {
206 0     0     my $self = shift;
207 0           my $filename = shift;
208              
209 1     1   1186 use Digest::SHA;
  1         4906  
  1         451  
210 0           my $sha = Digest::SHA->new($self->{hash_alg});
211 0           my $digest;
212 0 0         if ( -e $filename ) {
213 0 0         open my $fh, '<:raw', $filename or die "$filename: $!";
214 0           $sha->addfile($fh);
215 0           $digest = $sha->hexdigest;
216             }
217              
218 0           my $checksum = uc($self->{hash_alg}) . " hash of CPAN release\n";
219 0           $checksum .= "\n";
220 0           $checksum .= "$digest *$filename\n";
221 0           $checksum .= "\n";
222              
223 0           return $checksum;
224             }
225              
226             sub _get_version {
227 0     0     my ($self) = @_;
228              
229 0           return $self->{zilla}->version;
230             }
231              
232             sub _get_name {
233 0     0     my ($self, $filename) = @_;
234              
235 0           $filename =~ s/-+\d+.*$//g;
236 0           $filename =~ s/-/::/g;
237 0           return $filename;
238             }
239              
240             sub BUILDARGS {
241 0     0 1   my $self = shift;
242 0 0         my $args = @_ == 1 ? shift : {@_};
243              
244 0 0         if (not exists $args->{notes_file}) {
245 0 0         $args->{notes_file} = 'Changes' if ($args->{notes_from} eq 'ChangeLog');
246 0 0         $args->{notes_file} = 'Release-VERSION' if ($args->{notes_from} eq 'SignReleaseNotes');
247             }
248 0           $args;
249             }
250              
251 1     1   9 no Moose;
  1         3  
  1         9  
252              
253             __PACKAGE__->meta->make_immutable;
254             1;
255              
256             __END__
257              
258             =pod
259              
260             =encoding UTF-8
261              
262             =head1 NAME
263              
264             Dist::Zilla::Plugin::GitHub::CreateRelease - Create a GitHub Release
265              
266             =head1 VERSION
267              
268             version 0.0002
269              
270             =head1 SYNOPSIS
271              
272             In your F<dist.ini>:
273              
274             [GitHub::CreateRelease]
275             repo = github_repo_name ; optional
276             branch = main ; default = main
277             notes_as_code = 1 ; default = 1 (true)
278             notes_from = SignReleaseNotes ; default = SignReleaseNotes
279             notes_file = Release-VERSION ; default = Release-VERSION
280             github_notes = 0 ; default = 0 (false)
281             draft = 0 ; default = 0 (false)
282             hash_alg = sha256 ; default = sha256
283             add_checksum = 1 ; default = 1 (true)
284             title_template = Version RELEASE - TRIAL CPAN release ; this is the default
285              
286             =head1 DESCRIPTION
287              
288             This plugin will create a GitHub Release and attach a copy of the
289             cpan release archive to the Release.
290              
291             The release notes can be generated based on the notes_from value.
292              
293             This plugin should appear after any other AfterBuild plugin in your C<dist.ini> file.
294             If you are using SignReleaseNotes as the notes_from it should be after the
295             SignReleaseNotes plugin.
296              
297             =head1 Required Plugins
298              
299             This plugin requires that your Dist::Zilla configuration do the following:
300              
301             1. Create a release
302             2. Tag the release in your git repository
303             3. Push the commits (and tags) to GitHub
304              
305             There are numerous combinations of Dist::Zilla plugins that can perform those
306             functions.
307              
308             =head1 GITHUB API AUTHENTICATION
309              
310             This module uses Config::Identity::GitHub to access the GitHub API credentials.
311              
312             You need to create a file in your home directory named B<.github-identity>. It
313             requires the following fields:
314              
315             login github_username
316             token github_....
317              
318             The GitHub API has a lot of options for the generation of Personal Access Tokens.
319              
320             At minimum you will need a personal access token with "Write" access to "Contents".
321             It allows write access to Repository contents, commits, branches, downloads,
322             releases, and merges.
323              
324             Config::Identity::GitHub supports a gpg encrypted B<.github-identity> file. It is
325             recommended that you implement encryption for the B<.github-identity> file. If you
326             have gpg configured you can encrypt the file:
327              
328             # Encrypt it to ~/.github-identity.asc
329             gpg -ea -r you@example.com ~/.github-identity
330             # Cat ~/.github-identity.asc to verify it is encrypted
331             cat ~/.github-identity.asc
332             # Verify you can decrypt the file
333             gpg -d ~/.github-identity.asc
334             # Replace the clear text version (uncomment next line)
335             # mv ~/.github-identity.asc ~/.github-identity
336              
337             =head1 ATTRIBUTES
338              
339             =over
340              
341             =item hash_alg
342              
343             A string value for the B<Digest::SHA> supported hash algorithm to use for the hash of the
344             cpan upload file.
345              
346             =item repo
347              
348             A string value that specifies the name of the github repository. The module determines the
349             name based on the remote url but this setting can override the name that is detected.
350              
351             =item branch
352              
353             A string value that specifies the branch. It defaults to B<main> if not specified.
354              
355             =item title_template
356              
357             A string value that specifies the format of the Title used for the release. If the
358             title includes B<VERSION> it is replaced with the version number of the release. If the
359             title includes B<TRIAL> it is replaced with Official or Trial depending on whether --trial
360             was specified.
361              
362             The default value is "Version VERSION - TRIAL CPAN release"
363              
364             =item notes_as_code
365              
366             An integer value specifying true/false. If the value is true (not 0) the notes are surrounded by
367             the github code markup "```" ... "```".
368              
369             =item github_notes
370              
371             An integer value specifying true/false. If the value is true (not 0) the api call instructs
372             github to add a link to the changes in the release.
373              
374             =item notes_from
375              
376             A string value that specifies how to obtain the Notes for the Release. The valid values
377             are:
378              
379             =over
380              
381             =item SignReleaseNotes
382              
383             =item ChangeLog
384              
385             =item FromFile
386              
387             =item GitHub::CreateRelease
388              
389             =back
390              
391             =item notes_file
392              
393             A string value specifying the name template of the notes file that should be read
394             for to obtain the notes. It is used for if the B<note_from> is one of:
395              
396             SignReleaseNotes
397             FromFile
398             ChangeLog
399              
400             The default is B<Release-VERSION> and VERSION is replaced by the module version
401             number if it exists.
402              
403             =item draft
404              
405             An integer value specifying true/false. If the value is true (not 0) the api call instructs
406             github that the Release is a draft. You must publish it via the github webpage to make
407             it active.
408              
409             =back
410              
411             =head1 METHODS
412              
413             =over
414              
415             =item after_release
416              
417             The main processing function that is called automatically after the release is complete.
418              
419             =item true
420              
421             Boolean true constant
422              
423             =item false
424              
425             Boolean false constant
426              
427             =back
428              
429             =head1 AUTHOR
430              
431             Timothy Legge <timlegge@cpan.org>
432              
433             =head1 COPYRIGHT AND LICENSE
434              
435             This software is copyright (c) 2023 by Timothy Legge.
436              
437             This is free software; you can redistribute it and/or modify it under
438             the same terms as the Perl 5 programming language system itself.
439              
440             =head1 AUTHOR
441              
442             Timothy Legge
443              
444             =head1 COPYRIGHT AND LICENSE
445              
446             This software is copyright (c) 2023 by Timothy Legge.
447              
448             This is free software; you can redistribute it and/or modify it under
449             the same terms as the Perl 5 programming language system itself.
450              
451             =cut