Completed
Push — master ( 6e6d95...8ac45f )
by Alexander
13:11
created

build/controllers/ReleaseController.php (13 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * @link http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license http://www.yiiframework.com/license/
6
 */
7
8
namespace yii\build\controllers;
9
10
use Yii;
11
use yii\base\Exception;
12
use yii\console\Controller;
13
use yii\helpers\ArrayHelper;
14
use yii\helpers\Console;
15
use yii\helpers\FileHelper;
16
17
/**
18
 * ReleaseController is there to help preparing releases.
19
 *
20
 * Get a version overview:
21
 *
22
 *     ./build release/info
23
 *
24
 * run it with `--update` to fetch tags for all repos:
25
 *
26
 *     ./build release/info --update
27
 *
28
 * Make a framework release (apps are always in line with framework):
29
 *
30
 *     ./build release framework
31
 *     ./build release app-basic
32
 *     ./build release app-advanced
33
 *
34
 * Make an extension release (e.g. for redis):
35
 *
36
 *     ./build release redis
37
 *
38
 * Be sure to check the help info for individual sub-commands:
39
 *
40
 * @author Carsten Brandt <[email protected]>
41
 * @since 2.0
42
 */
43
class ReleaseController extends Controller
44
{
45
    public $defaultAction = 'release';
46
47
    /**
48
     * @var string base path to use for releases.
49
     */
50
    public $basePath;
51
    /**
52
     * @var bool whether to make actual changes. If true, it will run without changing or pushing anything.
53
     */
54
    public $dryRun = false;
55
    /**
56
     * @var bool whether to fetch latest tags.
57
     */
58
    public $update = false;
59
    /**
60
     * @var string override the default version. e.g. for major or patch releases.
61
     */
62
    public $version;
63
64
65
    public function options($actionID)
66
    {
67
        $options = ['basePath'];
68
        if ($actionID === 'release') {
69
            $options[] = 'dryRun';
70
            $options[] = 'version';
71
        } elseif ($actionID === 'sort-changelog') {
72
            $options[] = 'version';
73
        } elseif ($actionID === 'info') {
74
            $options[] = 'update';
75
        }
76
77
        return array_merge(parent::options($actionID), $options);
78
    }
79
80
81
    public function beforeAction($action)
82
    {
83
        if (!$this->interactive) {
84
            throw new Exception('Sorry, but releases should be run interactively to ensure you actually verify what you are doing ;)');
85
        }
86
        if ($this->basePath === null) {
87
            $this->basePath = \dirname(\dirname(__DIR__));
88
        }
89
        $this->basePath = rtrim($this->basePath, '\\/');
90
        return parent::beforeAction($action);
91
    }
92
93
    /**
94
     * Shows information about current framework and extension versions.
95
     */
96
    public function actionInfo()
97
    {
98
        $items = [
99
            'framework',
100
            'app-basic',
101
            'app-advanced',
102
        ];
103
        $extensionPath = "{$this->basePath}/extensions";
104
        foreach (scandir($extensionPath) as $extension) {
105
            if (ctype_alpha($extension) && is_dir($extensionPath . '/' . $extension)) {
106
                $items[] = $extension;
107
            }
108
        }
109
110
        if ($this->update) {
111
            foreach ($items as $item) {
112
                $this->stdout("fetching tags for $item...");
113
                if ($item === 'framework') {
114
                    $this->gitFetchTags((string)$this->basePath);
115
                } elseif (strncmp('app-', $item, 4) === 0) {
116
                    $this->gitFetchTags("{$this->basePath}/apps/" . substr($item, 4));
117
                } else {
118
                    $this->gitFetchTags("{$this->basePath}/extensions/$item");
119
                }
120
                $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
121
            }
122
        } else {
123
            $this->stdout("\nInformation may be outdated, re-run with `--update` to fetch latest tags.\n\n");
124
        }
125
126
        $versions = $this->getCurrentVersions($items);
127
        $nextVersions = $this->getNextVersions($versions, self::PATCH);
128
129
        // print version table
130
        $w = $this->minWidth(array_keys($versions));
131
        $this->stdout(str_repeat(' ', $w + 2) . "Current Version  Next Version\n", Console::BOLD);
132
        foreach ($versions as $ext => $version) {
133
            $this->stdout($ext . str_repeat(' ', $w + 3 - mb_strlen($ext)) . $version . '');
134
            $this->stdout(str_repeat(' ', 17 - mb_strlen($version)) . $nextVersions[$ext] . "\n");
135
        }
136
    }
137
138
    private function minWidth($a)
139
    {
140
        $w = 1;
141
        foreach ($a as $s) {
142
            if (($l = mb_strlen($s)) > $w) {
143
                $w = $l;
144
            }
145
        }
146
147
        return $w;
148
    }
149
150
    /**
151
     * Automation tool for making Yii framework and official extension releases.
152
     *
153
     * Usage:
154
     *
155
     * To make a release, make sure your git is clean (no uncommitted changes) and run the following command in
156
     * the yii dev repo root:
157
     *
158
     * ```
159
     * ./build/build release framework
160
     * ```
161
     *
162
     * or
163
     *
164
     * ```
165
     * ./build/build release redis,bootstrap,apidoc
166
     * ```
167
     *
168
     * You may use the `--dryRun` switch to test the command without changing or pushing anything:
169
     *
170
     * ```
171
     * ./build/build release redis --dryRun
172
     * ```
173
     *
174
     * The command will guide you through the complete release process including changing of files,
175
     * committing and pushing them. Each git command must be confirmed and can be skipped individually.
176
     * You may adjust changes in a separate shell or your IDE while the command is waiting for confirmation.
177
     *
178
     * @param array $what what do you want to release? this can either be:
179
     *
180
     * - an extension name such as `redis` or `bootstrap`,
181
     * - an application indicated by prefix `app-`, e.g. `app-basic`,
182
     * - or `framework` if you want to release a new version of the framework itself.
183
     *
184
     * @return int
185
     */
186
    public function actionRelease(array $what)
187
    {
188
        if (\count($what) > 1) {
189
            $this->stdout("Currently only one simultaneous release is supported.\n");
190
            return 1;
191
        }
192
193
        $this->stdout("This is the Yii release manager\n\n", Console::BOLD);
194
195
        if ($this->dryRun) {
196
            $this->stdout("Running in \"dry-run\" mode, nothing will actually be changed.\n\n", Console::BOLD, Console::FG_GREEN);
197
        }
198
199
        $this->validateWhat($what);
200
        $versions = $this->getCurrentVersions($what);
201
202
        if ($this->version !== null) {
203
            // if a version is explicitly given
204
            $newVersions = [];
205
            foreach ($versions as $k => $v) {
206
                $newVersions[$k] = $this->version;
207
            }
208
        } else {
209
            // otherwise get next patch or minor
210
            $newVersions = $this->getNextVersions($versions, self::PATCH);
211
        }
212
213
        $this->stdout("You are about to prepare a new release for the following things:\n\n");
214
        $this->printWhat($what, $newVersions, $versions);
215
        $this->stdout("\n");
216
217
        $this->stdout("Before you make a release briefly go over the changes and check if you spot obvious mistakes:\n\n", Console::BOLD);
218
        $gitDir = reset($what) === 'framework' ? 'framework/' : '';
219
        $gitVersion = $versions[reset($what)];
220
        if (strncmp('app-', reset($what), 4) !== 0) {
221
            $this->stdout("- no accidentally added CHANGELOG lines for other versions than this one?\n\n    git diff $gitVersion.. ${gitDir}CHANGELOG.md\n\n");
222
            $this->stdout("- are all new `@since` tags for this release version?\n");
223
        }
224
        $this->stdout("- other issues with code changes?\n\n    git diff -w $gitVersion.. ${gitDir}\n\n");
225
        $travisUrl = reset($what) === 'framework' ? '' : '-' . reset($what);
226
        $this->stdout("- are unit tests passing on travis? https://travis-ci.org/yiisoft/yii2$travisUrl/builds\n");
227
        $this->stdout("- also make sure the milestone on github is complete and no issues or PRs are left open.\n\n");
228
        $this->printWhatUrls($what, $versions);
229
        $this->stdout("\n");
230
231
        if (!$this->confirm('When you continue, this tool will run cleanup jobs and update the changelog as well as other files (locally). Continue?', false)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->confirm('When you...ly). Continue?', false) of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
232
            $this->stdout("Canceled.\n");
233
            return 1;
234
        }
235
236
        foreach ($what as $ext) {
237
            if ($ext === 'framework') {
238
                $this->releaseFramework("{$this->basePath}/framework", $newVersions['framework']);
239
            } elseif (strncmp('app-', $ext, 4) === 0) {
240
                $this->releaseApplication(substr($ext, 4), "{$this->basePath}/apps/" . substr($ext, 4), $newVersions[$ext]);
241
            } else {
242
                $this->releaseExtension($ext, "{$this->basePath}/extensions/$ext", $newVersions[$ext]);
243
            }
244
        }
245
246
        return 0;
247
    }
248
249
    /**
250
     * This will generate application packages for download page.
251
     *
252
     * Usage:
253
     *
254
     * ```
255
     * ./build/build release/package app-basic
256
     * ```
257
     *
258
     * @param array $what what do you want to package? this can either be:
259
     *
260
     * - an application indicated by prefix `app-`, e.g. `app-basic`,
261
     *
262
     * @return int
263
     */
264
    public function actionPackage(array $what)
265
    {
266
        $this->validateWhat($what, ['app']);
267
        $versions = $this->getCurrentVersions($what);
268
269
        $this->stdout("You are about to generate packages for the following things:\n\n");
270
        foreach ($what as $ext) {
271
            if (strncmp('app-', $ext, 4) === 0) {
272
                $this->stdout(' - ');
273
                $this->stdout(substr($ext, 4), Console::FG_RED);
274
                $this->stdout(' application version ');
275
            } elseif ($ext === 'framework') {
276
                $this->stdout(' - Yii Framework version ');
277
            } else {
278
                $this->stdout(' - ');
279
                $this->stdout($ext, Console::FG_RED);
280
                $this->stdout(' extension version ');
281
            }
282
            $this->stdout($versions[$ext], Console::BOLD);
283
            $this->stdout("\n");
284
        }
285
        $this->stdout("\n");
286
287
        $packagePath = "{$this->basePath}/packages";
288
        $this->stdout("Packages will be stored in $packagePath\n\n");
289
290
        if (!$this->confirm('Continue?', false)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->confirm('Continue?', false) of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
291
            $this->stdout("Canceled.\n");
292
            return 1;
293
        }
294
295
        foreach ($what as $ext) {
296
            if ($ext === 'framework') {
297
                throw new Exception('Can not package framework.');
298
            } elseif (strncmp('app-', $ext, 4) === 0) {
299
                $this->packageApplication(substr($ext, 4), $versions[$ext], $packagePath);
300
            } else {
301
                throw new Exception('Can not package extension.');
302
            }
303
        }
304
305
        $this->stdout("\ndone. verify the versions composer installed above and push it to github!\n\n");
306
307
        return 0;
308
    }
309
310
    /**
311
     * Sorts CHANGELOG for framework or extension.
312
     *
313
     * @param array $what what do you want to resort changelog for? this can either be:
314
     *
315
     * - an extension name such as `redis` or `bootstrap`,
316
     * - or `framework` if you want to release a new version of the framework itself.
317
     */
318
    public function actionSortChangelog(array $what)
319
    {
320
        if (\count($what) > 1) {
321
            $this->stdout("Currently only one simultaneous release is supported.\n");
322
            return 1;
323
        }
324
        $this->validateWhat($what, ['framework', 'ext'], false);
325
326
        $version = $this->version ?: array_values($this->getNextVersions($this->getCurrentVersions($what), self::PATCH))[0];
327
        $this->stdout('sorting CHANGELOG of ');
328
        $this->stdout(reset($what), Console::BOLD);
329
        $this->stdout(' for version ');
330
        $this->stdout($version, Console::BOLD);
331
        $this->stdout('...');
332
333
        $this->resortChangelogs($what, $version);
334
335
        $this->stdout("done.\n", Console::BOLD, Console::FG_GREEN);
336
    }
337
338
    protected function printWhat(array $what, $newVersions, $versions)
339
    {
340
        foreach ($what as $ext) {
341
            if (strncmp('app-', $ext, 4) === 0) {
342
                $this->stdout(' - ');
343
                $this->stdout(substr($ext, 4), Console::FG_RED);
344
                $this->stdout(' application version ');
345
            } elseif ($ext === 'framework') {
346
                $this->stdout(' - Yii Framework version ');
347
            } else {
348
                $this->stdout(' - ');
349
                $this->stdout($ext, Console::FG_RED);
350
                $this->stdout(' extension version ');
351
            }
352
            $this->stdout($newVersions[$ext], Console::BOLD);
353
            $this->stdout(", last release was {$versions[$ext]}\n");
354
        }
355
    }
356
357
    protected function printWhatUrls(array $what, $oldVersions)
358
    {
359
        foreach ($what as $ext) {
360
            if ($ext === 'framework') {
361
                $this->stdout("framework:    https://github.com/yiisoft/yii2-framework/compare/{$oldVersions[$ext]}...master\n");
362
                $this->stdout("app-basic:    https://github.com/yiisoft/yii2-app-basic/compare/{$oldVersions[$ext]}...master\n");
363
                $this->stdout("app-advanced: https://github.com/yiisoft/yii2-app-advanced/compare/{$oldVersions[$ext]}...master\n");
364
            } else {
365
                $this->stdout($ext, Console::FG_RED);
366
                $this->stdout(": https://github.com/yiisoft/yii2-$ext/compare/{$oldVersions[$ext]}...master\n");
367
            }
368
        }
369
    }
370
371
    /**
372
     * @param array $what list of items
373
     * @param array $limit list of things to allow, or empty to allow any, can be `app`, `framework`, `extension`
374
     * @param bool $ensureGitClean
375
     * @throws \yii\base\Exception
376
     */
377
    protected function validateWhat(array $what, $limit = [], $ensureGitClean = true)
378
    {
379
        foreach ($what as $w) {
380
            if (strncmp('app-', $w, 4) === 0) {
381
                if (!empty($limit) && !\in_array('app', $limit)) {
382
                    throw new Exception('Only the following types are allowed: ' . implode(', ', $limit) . "\n");
383
                }
384
                if (!is_dir($appPath = "{$this->basePath}/apps/" . substr($w, 4))) {
385
                    throw new Exception("Application path does not exist: \"{$appPath}\"\n");
386
                }
387
                if ($ensureGitClean) {
388
                    $this->ensureGitClean($appPath);
389
                }
390
            } elseif ($w === 'framework') {
391
                if (!empty($limit) && !\in_array('framework', $limit)) {
392
                    throw new Exception('Only the following types are allowed: ' . implode(', ', $limit) . "\n");
393
                }
394
                if (!is_dir($fwPath = "{$this->basePath}/framework")) {
395
                    throw new Exception("Framework path does not exist: \"{$this->basePath}/framework\"\n");
396
                }
397
                if ($ensureGitClean) {
398
                    $this->ensureGitClean($fwPath);
399
                }
400
            } else {
401
                if (!empty($limit) && !\in_array('ext', $limit)) {
402
                    throw new Exception('Only the following types are allowed: ' . implode(', ', $limit) . "\n");
403
                }
404
                if (!is_dir($extPath = "{$this->basePath}/extensions/$w")) {
405
                    throw new Exception("Extension path for \"$w\" does not exist: \"{$this->basePath}/extensions/$w\"\n");
406
                }
407
                if ($ensureGitClean) {
408
                    $this->ensureGitClean($extPath);
409
                }
410
            }
411
        }
412
    }
413
414
415
    protected function releaseFramework($frameworkPath, $version)
416
    {
417
        $this->stdout("\n");
418
        $this->stdout($h = "Preparing framework release version $version", Console::BOLD);
419
        $this->stdout("\n" . str_repeat('-', \strlen($h)) . "\n\n", Console::BOLD);
420
421
        if (!$this->confirm('Make sure you are on the right branch for this release and that it tracks the correct remote branch! Continue?')) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->confirm('Make sur...ote branch! Continue?') of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
422
            exit(1);
423
        }
424
        $this->runGit('git pull', $frameworkPath);
425
426
        // checks
427
428
        $this->stdout('check if framework composer.json matches yii2-dev composer.json...');
429
        $this->checkComposer($frameworkPath);
430
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
431
432
        // adjustments
433
434
        $this->stdout('prepare classmap...', Console::BOLD);
435
        $this->dryRun || Yii::$app->runAction('classmap', [$frameworkPath]);
436
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
437
438
        $this->stdout('updating mimetype magic file and mime aliases...', Console::BOLD);
439
        $this->dryRun || Yii::$app->runAction('mime-type', ["$frameworkPath/helpers/mimeTypes.php"], ["$frameworkPath/helpers/mimeAliases.php"]);
0 ignored issues
show
The call to Application::runAction() has too many arguments starting with array("{$frameworkPath}/helpers/mimeAliases.php").

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
440
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
441
442
        $this->stdout("fixing various PHPDoc style issues...\n", Console::BOLD);
443
        $this->dryRun || Yii::$app->runAction('php-doc/fix', [$frameworkPath]);
444
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
445
446
        $this->stdout("updating PHPDoc @property annotations...\n", Console::BOLD);
447
        $this->dryRun || Yii::$app->runAction('php-doc/property', [$frameworkPath]);
448
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
449
450
        $this->stdout('sorting changelogs...', Console::BOLD);
451
        $this->dryRun || $this->resortChangelogs(['framework'], $version);
452
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
453
454
        $this->stdout('closing changelogs...', Console::BOLD);
455
        $this->dryRun || $this->closeChangelogs(['framework'], $version);
456
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
457
458
        $this->stdout('updating Yii version...');
459
        $this->dryRun || $this->updateYiiVersion($frameworkPath, $version);
460
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
461
462
        $this->stdout("\nIn the following you can check the above changes using git diff.\n\n");
463
        do {
464
            $this->runGit('git diff --color', $frameworkPath);
465
            $this->stdout("\n\n\nCheck whether the above diff is okay, if not you may change things as needed before continuing.\n");
466
            $this->stdout("You may abort the program with Ctrl + C and reset the changes by running `git checkout -- .` in the repo.\n\n");
467
        } while (!$this->confirm('Type `yes` to continue, `no` to view git diff again. Continue?'));
468
469
        $this->stdout("\n\n");
470
        $this->stdout("    ****          RELEASE TIME!         ****\n", Console::FG_YELLOW, Console::BOLD);
471
        $this->stdout("    ****    Commit, Tag and Push it!    ****\n", Console::FG_YELLOW, Console::BOLD);
472
        $this->stdout("\n\nHint: if you decide 'no' for any of the following, the command will not be executed. You may manually run them later if needed. E.g. try the release locally without pushing it.\n\n");
473
474
        $this->stdout("Make sure to have your git set up for GPG signing. The following tag and commit should be signed.\n\n");
475
476
        $this->runGit("git commit -S -a -m \"release version $version\"", $frameworkPath);
477
        $this->runGit("git tag -s $version -m \"version $version\"", $frameworkPath);
478
        $this->runGit('git push', $frameworkPath);
479
        $this->runGit('git push --tags', $frameworkPath);
480
481
        $this->stdout("\n\n");
482
        $this->stdout('CONGRATULATIONS! You have just released ', Console::FG_YELLOW, Console::BOLD);
483
        $this->stdout('framework', Console::FG_RED, Console::BOLD);
484
        $this->stdout(' version ', Console::FG_YELLOW, Console::BOLD);
485
        $this->stdout($version, Console::BOLD);
486
        $this->stdout("!\n\n", Console::FG_YELLOW, Console::BOLD);
487
488
        // TODO release applications
489
        // $this->composerSetStability($what, $version);
490
491
492
//        $this->resortChangelogs($what, $version);
493
  //        $this->closeChangelogs($what, $version);
494
  //        $this->composerSetStability($what, $version);
495
  //        if (in_array('framework', $what)) {
496
  //            $this->updateYiiVersion($version);
497
  //        }
498
499
500
        // if done:
501
        //     * ./build/build release/done framework 2.0.0-dev 2.0.0-rc
502
        //     * ./build/build release/done redis 2.0.0-dev 2.0.0-rc
503
//            $this->openChangelogs($what, $nextVersion);
504
//            $this->composerSetStability($what, 'dev');
505
//            if (in_array('framework', $what)) {
506
//                $this->updateYiiVersion($devVersion);
507
//            }
508
509
510
511
        // prepare next release
512
513
        $this->stdout("Time to prepare the next release...\n\n", Console::FG_YELLOW, Console::BOLD);
514
515
        $this->stdout('opening changelogs...', Console::BOLD);
516
        $nextVersion = $this->getNextVersions(['framework' => $version], self::PATCH); // TODO support other versions
517
        $this->dryRun || $this->openChangelogs(['framework'], $nextVersion['framework']);
518
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
519
520
        $this->stdout('updating Yii version...');
521
        $this->dryRun || $this->updateYiiVersion($frameworkPath, $nextVersion['framework'] . '-dev');
522
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
523
524
525
        $this->stdout("\n");
526
        $this->runGit('git diff --color', $frameworkPath);
527
        $this->stdout("\n\n");
528
        $this->runGit('git commit -a -m "prepare for next release"', $frameworkPath);
529
        $this->runGit('git push', $frameworkPath);
530
531
        $this->stdout("\n\nDONE!", Console::FG_YELLOW, Console::BOLD);
532
533
        $this->stdout("\n\nThe following steps are left for you to do manually:\n\n");
534
        $nextVersion2 = $this->getNextVersions($nextVersion, self::PATCH); // TODO support other versions
535
        $this->stdout("- wait for your changes to be propagated to the repo and create a tag $version on  https://github.com/yiisoft/yii2-framework\n\n");
536
        $this->stdout("    git clone [email protected]:yiisoft/yii2-framework.git\n");
537
        $this->stdout("    cd yii2-framework/\n");
538
        $this->stdout("    export RELEASECOMMIT=$(git log --oneline |grep $version |grep -Po \"^[0-9a-f]+\")\n");
539
        $this->stdout("    git tag -s $version -m \"version $version\" \$RELEASECOMMIT\n");
540
        $this->stdout("    git tag --verify $version\n");
541
        $this->stdout("    git push --tags\n\n");
542
        $this->stdout("- close the $version milestone on github and open new ones for {$nextVersion['framework']} and {$nextVersion2['framework']}: https://github.com/yiisoft/yii2/milestones\n");
543
        $this->stdout("- create a release on github.\n");
544
        $this->stdout("- release news and announcement.\n");
545
        $this->stdout("- update the website (will be automated soon and is only relevant for the new website).\n");
546
        $this->stdout("\n");
547
        $this->stdout("- release applications: ./build/build release app-basic\n");
548
        $this->stdout("- release applications: ./build/build release app-advanced\n");
549
550
        $this->stdout("\n");
551
    }
552
553
    protected function releaseApplication($name, $path, $version)
554
    {
555
        $this->stdout("\n");
556
        $this->stdout($h = "Preparing release for application  $name  version $version", Console::BOLD);
557
        $this->stdout("\n" . str_repeat('-', \strlen($h)) . "\n\n", Console::BOLD);
558
559
        if (!$this->confirm('Make sure you are on the right branch for this release and that it tracks the correct remote branch! Continue?')) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->confirm('Make sur...ote branch! Continue?') of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
560
            exit(1);
561
        }
562
        $this->runGit('git pull', $path);
563
564
        // adjustments
565
566
        $this->stdout("fixing various PHPDoc style issues...\n", Console::BOLD);
567
        $this->setAppAliases($name, $path);
568
        $this->dryRun || Yii::$app->runAction('php-doc/fix', [$path, 'skipFrameworkRequirements' => true]);
569
        $this->resetAppAliases();
570
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
571
572
        $this->stdout("updating PHPDoc @property annotations...\n", Console::BOLD);
573
        $this->setAppAliases($name, $path);
574
        $this->dryRun || Yii::$app->runAction('php-doc/property', [$path, 'skipFrameworkRequirements' => true]);
575
        $this->resetAppAliases();
576
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
577
578
        $this->stdout("updating composer stability...\n", Console::BOLD);
579
        $this->dryRun || $this->composerSetStability(["app-$name"], $version);
580
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
581
582
        $this->stdout("\nIn the following you can check the above changes using git diff.\n\n");
583
        do {
584
            $this->runGit('git diff --color', $path);
585
            $this->stdout("\n\n\nCheck whether the above diff is okay, if not you may change things as needed before continuing.\n");
586
            $this->stdout("You may abort the program with Ctrl + C and reset the changes by running `git checkout -- .` in the repo.\n\n");
587
        } while (!$this->confirm('Type `yes` to continue, `no` to view git diff again. Continue?'));
588
589
        $this->stdout("\n\n");
590
        $this->stdout("    ****          RELEASE TIME!         ****\n", Console::FG_YELLOW, Console::BOLD);
591
        $this->stdout("    ****    Commit, Tag and Push it!    ****\n", Console::FG_YELLOW, Console::BOLD);
592
        $this->stdout("\n\nHint: if you decide 'no' for any of the following, the command will not be executed. You may manually run them later if needed. E.g. try the release locally without pushing it.\n\n");
593
594
        $this->stdout("Make sure to have your git set up for GPG signing. The following tag and commit should be signed.\n\n");
595
596
        $this->runGit("git commit -S -a -m \"release version $version\"", $path);
597
        $this->runGit("git tag -s $version -m \"version $version\"", $path);
598
        $this->runGit('git push', $path);
599
        $this->runGit('git push --tags', $path);
600
601
        $this->stdout("\n\n");
602
        $this->stdout('CONGRATULATIONS! You have just released application ', Console::FG_YELLOW, Console::BOLD);
603
        $this->stdout($name, Console::FG_RED, Console::BOLD);
604
        $this->stdout(' version ', Console::FG_YELLOW, Console::BOLD);
605
        $this->stdout($version, Console::BOLD);
606
        $this->stdout("!\n\n", Console::FG_YELLOW, Console::BOLD);
607
608
        // prepare next release
609
610
        $this->stdout("Time to prepare the next release...\n\n", Console::FG_YELLOW, Console::BOLD);
611
612
        $this->stdout("updating composer stability...\n", Console::BOLD);
613
        $this->dryRun || $this->composerSetStability(["app-$name"], 'dev');
614
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
615
616
        $nextVersion = $this->getNextVersions(["app-$name" => $version], self::PATCH); // TODO support other versions
617
618
        $this->stdout("\n");
619
        $this->runGit('git diff --color', $path);
620
        $this->stdout("\n\n");
621
        $this->runGit('git commit -a -m "prepare for next release"', $path);
622
        $this->runGit('git push', $path);
623
624
        $this->stdout("\n\nDONE!", Console::FG_YELLOW, Console::BOLD);
625
626
        $this->stdout("\n\nThe following steps are left for you to do manually:\n\n");
627
        $nextVersion2 = $this->getNextVersions($nextVersion, self::PATCH); // TODO support other versions
628
        $this->stdout("- close the $version milestone on github and open new ones for {$nextVersion["app-$name"]} and {$nextVersion2["app-$name"]}: https://github.com/yiisoft/yii2-app-$name/milestones\n");
629
        $this->stdout("- Create Application packages and upload them to github:  ./build release/package app-$name\n");
630
631
        $this->stdout("\n");
632
    }
633
634
    private $_oldAlias;
635
636
    protected function setAppAliases($app, $path)
637
    {
638
        $this->_oldAlias = Yii::getAlias('@app');
639
        switch ($app) {
640
            case 'basic':
641
                Yii::setAlias('@app', $path);
642
                break;
643
            case 'advanced':
644
                // setup @frontend, @backend etc...
645
                require "$path/common/config/bootstrap.php";
646
                break;
647
        }
648
    }
649
650
    protected function resetAppAliases()
651
    {
652
        Yii::setAlias('@app', $this->_oldAlias);
653
    }
654
655
    protected function packageApplication($name, $version, $packagePath)
656
    {
657
        FileHelper::createDirectory($packagePath);
658
659
        $this->runCommand("composer create-project yiisoft/yii2-app-$name $name $version", $packagePath);
660
        // clear cookie validation key in basic app
661
        if (is_file($configFile = "$packagePath/$name/config/web.php")) {
662
            $this->sed(
663
                "/'cookieValidationKey' => '.*?',/",
664
                "'cookieValidationKey' => '',",
665
                $configFile
666
            );
667
        }
668
        $this->runCommand("tar zcf yii-$name-app-$version.tgz $name", $packagePath);
669
    }
670
671
    protected function releaseExtension($name, $path, $version)
672
    {
673
        $this->stdout("\n");
674
        $this->stdout($h = "Preparing release for extension  $name  version $version", Console::BOLD);
675
        $this->stdout("\n" . str_repeat('-', \strlen($h)) . "\n\n", Console::BOLD);
676
677
        if (!$this->confirm('Make sure you are on the right branch for this release and that it tracks the correct remote branch! Continue?')) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->confirm('Make sur...ote branch! Continue?') of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
678
            exit(1);
679
        }
680
        $this->runGit('git pull', $path);
681
682
        // adjustments
683
684
        $this->stdout("fixing various PHPDoc style issues...\n", Console::BOLD);
685
        $this->dryRun || Yii::$app->runAction('php-doc/fix', [$path]);
686
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
687
688
        $this->stdout("updating PHPDoc @property annotations...\n", Console::BOLD);
689
        $this->dryRun || Yii::$app->runAction('php-doc/property', [$path]);
690
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
691
692
        $this->stdout('sorting changelogs...', Console::BOLD);
693
        $this->dryRun || $this->resortChangelogs([$name], $version);
694
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
695
696
        $this->stdout('closing changelogs...', Console::BOLD);
697
        $this->dryRun || $this->closeChangelogs([$name], $version);
698
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
699
700
        $this->stdout("\nIn the following you can check the above changes using git diff.\n\n");
701
        do {
702
            $this->runGit('git diff --color', $path);
703
            $this->stdout("\n\n\nCheck whether the above diff is okay, if not you may change things as needed before continuing.\n");
704
            $this->stdout("You may abort the program with Ctrl + C and reset the changes by running `git checkout -- .` in the repo.\n\n");
705
        } while (!$this->confirm('Type `yes` to continue, `no` to view git diff again. Continue?'));
706
707
        $this->stdout("\n\n");
708
        $this->stdout("    ****          RELEASE TIME!         ****\n", Console::FG_YELLOW, Console::BOLD);
709
        $this->stdout("    ****    Commit, Tag and Push it!    ****\n", Console::FG_YELLOW, Console::BOLD);
710
        $this->stdout("\n\nHint: if you decide 'no' for any of the following, the command will not be executed. You may manually run them later if needed. E.g. try the release locally without pushing it.\n\n");
711
712
        $this->stdout("Make sure to have your git set up for GPG signing. The following tag and commit should be signed.\n\n");
713
714
        $this->runGit("git commit -S -a -m \"release version $version\"", $path);
715
        $this->runGit("git tag -s $version -m \"version $version\"", $path);
716
        $this->runGit('git push', $path);
717
        $this->runGit('git push --tags', $path);
718
719
        $this->stdout("\n\n");
720
        $this->stdout('CONGRATULATIONS! You have just released extension ', Console::FG_YELLOW, Console::BOLD);
721
        $this->stdout($name, Console::FG_RED, Console::BOLD);
722
        $this->stdout(' version ', Console::FG_YELLOW, Console::BOLD);
723
        $this->stdout($version, Console::BOLD);
724
        $this->stdout("!\n\n", Console::FG_YELLOW, Console::BOLD);
725
726
        // prepare next release
727
728
        $this->stdout("Time to prepare the next release...\n\n", Console::FG_YELLOW, Console::BOLD);
729
730
        $this->stdout('opening changelogs...', Console::BOLD);
731
        $nextVersion = $this->getNextVersions([$name => $version], self::PATCH); // TODO support other versions
732
        $this->dryRun || $this->openChangelogs([$name], $nextVersion[$name]);
733
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
734
735
        $this->stdout("\n");
736
        $this->runGit('git diff --color', $path);
737
        $this->stdout("\n\n");
738
        $this->runGit('git commit -a -m "prepare for next release"', $path);
739
        $this->runGit('git push', $path);
740
741
        $this->stdout("\n\nDONE!", Console::FG_YELLOW, Console::BOLD);
742
743
        $this->stdout("\n\nThe following steps are left for you to do manually:\n\n");
744
        $nextVersion2 = $this->getNextVersions($nextVersion, self::PATCH); // TODO support other versions
745
        $this->stdout("- close the $version milestone on github and open new ones for {$nextVersion[$name]} and {$nextVersion2[$name]}: https://github.com/yiisoft/yii2-$name/milestones\n");
746
        $this->stdout("- release news and announcement.\n");
747
        $this->stdout("- update the website (will be automated soon and is only relevant for the new website).\n");
748
749
        $this->stdout("\n");
750
    }
751
752
753
    protected function runCommand($cmd, $path)
754
    {
755
        $this->stdout("running  $cmd  ...", Console::BOLD);
756
        if ($this->dryRun) {
757
            $this->stdout("dry run, command `$cmd` not executed.\n");
758
            return;
759
        }
760
        chdir($path);
761
        exec($cmd, $output, $ret);
762
        if ($ret != 0) {
0 ignored issues
show
It seems like you are loosely comparing $ret of type integer|null to 0; this is ambiguous as not only 0 == 0 is true, but null == 0 is true, too. Consider using a strict comparison ===.
Loading history...
763
            echo implode("\n", $output);
764
            throw new Exception("Command \"$cmd\" failed with code " . $ret);
765
        }
766
        $this->stdout("\ndone.\n", Console::BOLD, Console::FG_GREEN);
767
    }
768
769
    protected function runGit($cmd, $path)
770
    {
771
        if ($this->confirm("Run `$cmd`?", true)) {
772
            if ($this->dryRun) {
773
                $this->stdout("dry run, command `$cmd` not executed.\n");
774
                return;
775
            }
776
            chdir($path);
777
            exec($cmd, $output, $ret);
778
            echo implode("\n", $output);
779
            if ($ret != 0) {
0 ignored issues
show
It seems like you are loosely comparing $ret of type integer|null to 0; this is ambiguous as not only 0 == 0 is true, but null == 0 is true, too. Consider using a strict comparison ===.
Loading history...
780
                throw new Exception("Command \"$cmd\" failed with code " . $ret);
781
            }
782
            echo "\n";
783
        }
784
    }
785
786
    protected function ensureGitClean($path)
787
    {
788
        chdir($path);
789
        exec('git status --porcelain -uno', $changes, $ret);
790
        if ($ret != 0) {
0 ignored issues
show
It seems like you are loosely comparing $ret of type integer|null to 0; this is ambiguous as not only 0 == 0 is true, but null == 0 is true, too. Consider using a strict comparison ===.
Loading history...
791
            throw new Exception('Command "git status --porcelain -uno" failed with code ' . $ret);
792
        }
793
        if (!empty($changes)) {
794
            throw new Exception("You have uncommitted changes in $path: " . print_r($changes, true));
795
        }
796
    }
797
798
    protected function gitFetchTags($path)
799
    {
800
        try {
801
            chdir($path);
802
        } catch (\yii\base\ErrorException $e) {
803
            throw new Exception('Failed to getch git tags in ' . $path . ': ' . $e->getMessage());
804
        }
805
        exec('git fetch --tags', $output, $ret);
806
        if ($ret != 0) {
0 ignored issues
show
It seems like you are loosely comparing $ret of type integer|null to 0; this is ambiguous as not only 0 == 0 is true, but null == 0 is true, too. Consider using a strict comparison ===.
Loading history...
807
            throw new Exception('Command "git fetch --tags" failed with code ' . $ret);
808
        }
809
    }
810
811
812
    protected function checkComposer($fwPath)
0 ignored issues
show
The parameter $fwPath is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
813
    {
814
        if (!$this->confirm("\nNot yet automated: Please check if composer.json dependencies in framework dir match the one in repo root. Continue?", false)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->confirm(' Not yet...oot. Continue?', false) of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
815
            exit;
816
        }
817
    }
818
819
820
    protected function closeChangelogs($what, $version)
821
    {
822
        $v = str_replace('\\-', '[\\- ]', preg_quote($version, '/'));
823
        $headline = $version . ' ' . date('F d, Y');
824
        $this->sed(
825
            '/' . $v . ' under development\R(-+?)\R/',
826
            $headline . "\n" . str_repeat('-', \strlen($headline)) . "\n",
827
            $this->getChangelogs($what)
828
        );
829
    }
830
831
    protected function openChangelogs($what, $version)
832
    {
833
        $headline = "\n$version under development\n";
834
        $headline .= str_repeat('-', \strlen($headline) - 2) . "\n\n- no changes in this release.\n";
835
        foreach ($this->getChangelogs($what) as $file) {
836
            $lines = explode("\n", file_get_contents($file));
837
            $hl = [
838
                array_shift($lines),
839
                array_shift($lines),
840
            ];
841
            array_unshift($lines, $headline);
842
843
            file_put_contents($file, implode("\n", array_merge($hl, $lines)));
844
        }
845
    }
846
847
    protected function resortChangelogs($what, $version)
848
    {
849
        foreach ($this->getChangelogs($what) as $file) {
850
            // split the file into relevant parts
851
            list($start, $changelog, $end) = $this->splitChangelog($file, $version);
852
            $changelog = $this->resortChangelog($changelog);
853
            file_put_contents($file, implode("\n", array_merge($start, $changelog, $end)));
854
        }
855
    }
856
857
    /**
858
     * Extract changelog content for a specific version.
859
     * @param string $file
860
     * @param string $version
861
     * @return array
862
     */
863
    protected function splitChangelog($file, $version)
864
    {
865
        $lines = explode("\n", file_get_contents($file));
866
867
        // split the file into relevant parts
868
        $start = [];
869
        $changelog = [];
870
        $end = [];
871
872
        $state = 'start';
873
        foreach ($lines as $l => $line) {
874
            // starting from the changelogs headline
875
            if (isset($lines[$l - 2]) && strpos($lines[$l - 2], $version) !== false &&
876
                isset($lines[$l - 1]) && strncmp($lines[$l - 1], '---', 3) === 0) {
877
                $state = 'changelog';
878
            }
879
            if ($state === 'changelog' && isset($lines[$l + 1]) && strncmp($lines[$l + 1], '---', 3) === 0) {
880
                $state = 'end';
881
            }
882
            // add continued lines to the last item to keep them together
883
            if (!empty(${$state}) && trim($line) !== '' && strncmp($line, '- ', 2) !== 0) {
884
                end(${$state});
885
                ${$state}[key(${$state})] .= "\n" . $line;
886
            } else {
887
                ${$state}[] = $line;
888
            }
889
        }
890
891
        return [$start, $changelog, $end];
892
    }
893
894
    /**
895
     * Ensure sorting of the changelog lines.
896
     * @param string[] $changelog
897
     * @return string[]
898
     */
899
    protected function resortChangelog($changelog)
900
    {
901
        // cleanup whitespace
902
        foreach ($changelog as $i => $line) {
903
            $changelog[$i] = rtrim($line);
904
        }
905
        $changelog = array_filter($changelog);
906
907
        $i = 0;
908
        ArrayHelper::multisort($changelog, function ($line) use (&$i) {
909
            if (preg_match('/^- (Chg|Enh|Bug|New)( #\d+(, #\d+)*)?: .+/', $line, $m)) {
910
                $o = ['Bug' => 'C', 'Enh' => 'D', 'Chg' => 'E', 'New' => 'F'];
911
                return $o[$m[1]] . ' ' . (!empty($m[2]) ? $m[2] : 'AAAA' . $i++);
912
            }
913
914
            return 'B' . $i++;
915
        }, SORT_ASC, SORT_NATURAL);
916
917
        // re-add leading and trailing lines
918
        array_unshift($changelog, '');
919
        $changelog[] = '';
920
        $changelog[] = '';
921
922
        return $changelog;
923
    }
924
925
    protected function getChangelogs($what)
926
    {
927
        $changelogs = [];
928
        if (\in_array('framework', $what)) {
929
            $changelogs[] = $this->getFrameworkChangelog();
930
        }
931
932
        return array_merge($changelogs, $this->getExtensionChangelogs($what));
933
    }
934
935
    protected function getFrameworkChangelog()
936
    {
937
        return $this->basePath . '/framework/CHANGELOG.md';
938
    }
939
940
    protected function getExtensionChangelogs($what)
941
    {
942
        return array_filter(glob($this->basePath . '/extensions/*/CHANGELOG.md'), function ($elem) use ($what) {
943
            foreach ($what as $ext) {
944
                if (strpos($elem, "extensions/$ext/CHANGELOG.md") !== false) {
945
                    return true;
946
                }
947
            }
948
949
            return false;
950
        });
951
    }
952
953
    protected function composerSetStability($what, $version)
954
    {
955
        $apps = [];
956
        if (\in_array('app-advanced', $what)) {
957
            $apps[] = $this->basePath . '/apps/advanced/composer.json';
958
        }
959
        if (\in_array('app-basic', $what)) {
960
            $apps[] = $this->basePath . '/apps/basic/composer.json';
961
        }
962
        if (\in_array('app-benchmark', $what)) {
963
            $apps[] = $this->basePath . '/apps/benchmark/composer.json';
964
        }
965
        if (empty($apps)) {
966
            return;
967
        }
968
969
        $stability = 'stable';
970
        if (strpos($version, 'alpha') !== false) {
971
            $stability = 'alpha';
972
        } elseif (strpos($version, 'beta') !== false) {
973
            $stability = 'beta';
974
        } elseif (strpos($version, 'rc') !== false) {
975
            $stability = 'RC';
976
        } elseif (strpos($version, 'dev') !== false) {
977
            $stability = 'dev';
978
        }
979
980
        $this->sed(
981
            '/"minimum-stability": "(.+?)",/',
982
            '"minimum-stability": "' . $stability . '",',
983
            $apps
984
        );
985
    }
986
987
    protected function updateYiiVersion($frameworkPath, $version)
988
    {
989
        $this->sed(
990
            '/function getVersion\(\)\R    \{\R        return \'(.+?)\';/',
991
            "function getVersion()\n    {\n        return '$version';",
992
            $frameworkPath . '/BaseYii.php');
993
    }
994
995
    protected function sed($pattern, $replace, $files)
996
    {
997
        foreach ((array) $files as $file) {
998
            file_put_contents($file, preg_replace($pattern, $replace, file_get_contents($file)));
999
        }
1000
    }
1001
1002
    protected function getCurrentVersions(array $what)
1003
    {
1004
        $versions = [];
1005
        foreach ($what as $ext) {
1006
            if ($ext === 'framework') {
1007
                chdir("{$this->basePath}/framework");
1008
            } elseif (strncmp('app-', $ext, 4) === 0) {
1009
                chdir("{$this->basePath}/apps/" . substr($ext, 4));
1010
            } else {
1011
                chdir("{$this->basePath}/extensions/$ext");
1012
            }
1013
            $tags = [];
1014
            exec('git tag', $tags, $ret);
1015
            if ($ret != 0) {
0 ignored issues
show
It seems like you are loosely comparing $ret of type integer|null to 0; this is ambiguous as not only 0 == 0 is true, but null == 0 is true, too. Consider using a strict comparison ===.
Loading history...
1016
                throw new Exception('Command "git tag" failed with code ' . $ret);
1017
            }
1018
            rsort($tags, SORT_NATURAL); // TODO this can not deal with alpha/beta/rc...
1019
            $versions[$ext] = reset($tags);
1020
        }
1021
1022
        return $versions;
1023
    }
1024
1025
    const MINOR = 'minor';
1026
    const PATCH = 'patch';
1027
1028
    protected function getNextVersions(array $versions, $type)
1029
    {
1030
        foreach ($versions as $k => $v) {
1031
            if (empty($v)) {
1032
                $versions[$k] = '2.0.0';
1033
                continue;
1034
            }
1035
            $parts = explode('.', $v);
1036
            switch ($type) {
1037
                case self::MINOR:
1038
                    $parts[1]++;
1039
                    $parts[2] = 0;
1040
                    if (isset($parts[3])) {
1041
                        unset($parts[3]);
1042
                    }
1043
                    break;
1044
                case self::PATCH:
1045
                    $parts[2]++;
1046
                    if (isset($parts[3])) {
1047
                        unset($parts[3]);
1048
                    }
1049
                    break;
1050
                default:
1051
                    throw new Exception('Unknown version type.');
1052
            }
1053
            $versions[$k] = implode('.', $parts);
1054
        }
1055
1056
        return $versions;
1057
    }
1058
}
1059