Passed
Push — master ( 64745c...3e25ae )
by Stefano
08:55 queued 06:43
created

ChangeLogCommand::buildOptionParser()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 7
c 1
b 0
f 0
dl 0
loc 12
rs 10
cc 1
nc 1
nop 1
1
<?php
2
declare(strict_types=1);
3
4
/**
5
 * BEdita, API-first content management framework
6
 * Copyright 2021 ChannelWeb Srl, Chialab Srl
7
 *
8
 * This file is part of BEdita: you can redistribute it and/or modify
9
 * it under the terms of the GNU Lesser General Public License as published
10
 * by the Free Software Foundation, either version 3 of the License, or
11
 * (at your option) any later version.
12
 *
13
 * See LICENSE.LGPL or <http://gnu.org/licenses/lgpl-3.0.html> for more details.
14
 */
15
16
namespace BEdita\DevTools\Command;
17
18
use Cake\Command\Command;
19
use Cake\Console\Arguments;
20
use Cake\Console\ConsoleIo;
21
use Cake\Console\ConsoleOptionParser;
22
use Cake\Core\Configure;
23
use Cake\Core\InstanceConfigTrait;
24
use Cake\Http\Client;
25
use Cake\Utility\Hash;
26
27
/**
28
 * Command to generate a changelog snippet to prepare a new relase.
29
 */
30
class ChangeLogCommand extends Command
31
{
32
    use InstanceConfigTrait;
33
34
    /**
35
     * Default configuration for command.
36
     *
37
     * @var array
38
     */
39
    protected $_defaultConfig = [
40
        // Classification filter on labels
41
        'filter' => [
42
            'integration' => [
43
                'Topic - Integration',
44
                'Topic - Tests',
45
            ],
46
            'api' => [
47
                'Topic - API',
48
            ],
49
            'core' => [
50
                'Topic - Core',
51
                'Topic - Database',
52
                'Topic - Authentication',
53
                'Topic - ORM',
54
            ],
55
        ],
56
        // Changelog sections
57
        'sections' => [
58
            'api' => 'API',
59
            'core' => 'Core',
60
            'integration' => 'Integration',
61
            'other' => 'Other',
62
        ],
63
        // Issues search url
64
        'url' => 'https://api.github.com/search/issues',
65
        // HTTP client configuration
66
        'client' => [],
67
    ];
68
69
    /**
70
     * @inheritDoc
71
     */
72
    protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
73
    {
74
        $parser->addArgument('from', [
75
                'help' => 'Read closed PRs from this date in "YYYY-MM-DD" format',
76
                'required' => true,
77
            ])
78
            ->addArgument('version', [
79
                'help' => 'Version to release',
80
                'required' => true,
81
            ]);
82
83
        return $parser;
84
    }
85
86
    /**
87
     * @inheritDoc
88
     */
89
    public function initialize(): void
90
    {
91
        $config = (array)Configure::read('ChangeLog');
92
        $this->setConfig($config);
93
    }
94
95
    /**
96
     * Read closed PRs from github and create changelog file.
97
     *
98
     * @param \Cake\Console\Arguments $args The command arguments.
99
     * @param \Cake\Console\ConsoleIo $io The console io
100
     * @return null|int The exit code or null for success
101
     */
102
    public function execute(Arguments $args, ConsoleIo $io): ?int
103
    {
104
        $from = $args->getArgument('from');
105
        $io->out("Reading closed PRs from from $from");
106
107
        $json = $this->fetchPrs((string)$from);
108
        $items = (array)Hash::get($json, 'items');
109
        $version = (string)$args->getArgument('version');
110
        $items = $this->filterItems($items, $version);
111
        $io->out(sprintf('Loaded %d prs', count($items)));
112
113
        $changeLog = $this->createChangeLog($items);
114
        $this->saveChangeLog($changeLog, $version);
115
116
        $io->out('Changelog created. Bye.');
117
118
        return null;
119
    }
120
121
    /**
122
     * Filter items by Milestone: major version of item mileston should match requested version (4 o 5 for instance).
123
     *
124
     * @param array $items Changelog items
125
     * @param string $version Release version
126
     * @return array
127
     */
128
    protected function filterItems(array $items, string $version): array
129
    {
130
        $major = substr($version, 0, (int)strpos($version, '.'));
131
132
        return array_filter(
133
            $items,
134
            function ($item) use ($major) {
135
                /** @var string $milestone */
136
                $milestone = Hash::get($item, 'milestone.title');
137
                $milestone = str_replace('-', '.', $milestone);
138
                $v = substr($milestone, 0, (int)strpos($milestone, '.'));
139
140
                return $v == $major;
141
            }
142
        );
143
    }
144
145
    /**
146
     * Fetch pull requests from Github
147
     *
148
     * @param string $from From date.
149
     * @return array
150
     */
151
    protected function fetchPrs(string $from): array
152
    {
153
        $client = new Client((array)$this->getConfig('client'));
154
        $query = [
155
            'q' => sprintf('is:pr draft:false repo:bedita/bedita merged:>%s', $from),
156
            'sort' => '-closed',
157
            'per_page' => 100,
158
        ];
159
        $headers = ['Accept' => 'application/vnd.github.v3+json'];
160
        /** @var string $url */
161
        $url = $this->getConfig('url');
162
        $response = $client->get($url, $query, compact('headers'));
163
164
        return (array)$response->getJson();
165
    }
166
167
    /**
168
     * Create changelog array with classified pull requests
169
     *
170
     * @param array $items PR items
171
     * @return array
172
     */
173
    protected function createChangeLog(array $items): array
174
    {
175
        $res = [];
176
        foreach ($items as $item) {
177
            $labels = Hash::extract($item, 'labels.{n}.name');
178
            $type = $this->classify((array)$labels);
179
            $res[$type][] = $item;
180
        }
181
182
        return $res;
183
    }
184
185
    /**
186
     * Classify PR from its labels
187
     *
188
     * @param array $labels Labels array
189
     * @return string
190
     */
191
    protected function classify(array $labels): string
192
    {
193
        foreach ((array)$this->getConfig('filter') as $name => $data) {
194
            if (!empty(array_intersect($labels, (array)$data))) {
195
                return $name;
196
            }
197
        }
198
199
        return 'other';
200
    }
201
202
    /**
203
     * Save changelog to file
204
     *
205
     * @param array $changeLog Changelog items
206
     * @param string $version Version released
207
     * @return void
208
     */
209
    protected function saveChangeLog(array $changeLog, string $version): void
210
    {
211
        $out = sprintf("## Version %s - Cactus\n", $version);
212
213
        foreach ((array)$this->getConfig('sections') as $name => $label) {
214
            /** @var string $label */
215
            $out .= sprintf("\n### %s changes (%s)\n\n", $label, (string)$version);
216
            $out .= $this->loglines((array)Hash::get($changeLog, $name));
217
        }
218
219
        file_put_contents(sprintf('changelog-%s.md', $version), $out);
220
    }
221
222
    /**
223
     * Section changelog lines
224
     *
225
     * @param array $items Changelog items
226
     * @return string
227
     */
228
    protected function loglines(array $items): string
229
    {
230
        $res = '';
231
        foreach ($items as $item) {
232
            $id = (string)$item['number'];
233
            $url = (string)$item['html_url'];
234
            $title = (string)$item['title'];
235
            $res .= sprintf("* [#%s](%s) %s\n", $id, $url, $title);
236
        }
237
238
        return $res;
239
    }
240
}
241