Completed
Push — 2.1 ( c952e8...98ed49 )
by Carsten
10:00
created

ReleaseController::resortChangelogs()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 9
rs 9.6666
c 0
b 0
f 0
cc 2
eloc 5
nc 2
nop 2
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
61
    public function options($actionID)
62
    {
63
        $options = ['basePath'];
64
        if ($actionID === 'release') {
65
            $options[] = 'dryRun';
66
        } elseif ($actionID === 'info') {
67
            $options[] = 'update';
68
        }
69
        return array_merge(parent::options($actionID), $options);
70
    }
71
72
73
    public function beforeAction($action)
74
    {
75
        if (!$this->interactive) {
76
            throw new Exception('Sorry, but releases should be run interactively to ensure you actually verify what you are doing ;)');
77
        }
78
        if ($this->basePath === null) {
79
            $this->basePath = dirname(dirname(__DIR__));
80
        }
81
        $this->basePath = rtrim($this->basePath, '\\/');
82
        return parent::beforeAction($action);
83
    }
84
85
    /**
86
     * Shows information about current framework and extension versions.
87
     */
88
    public function actionInfo()
89
    {
90
        $items = [
91
            'framework',
92
            'app-basic',
93
            'app-advanced',
94
        ];
95
        $extensionPath = "{$this->basePath}/extensions";
96
        foreach (scandir($extensionPath) as $extension) {
97
            if (ctype_alpha($extension) && is_dir($extensionPath . '/' . $extension)) {
98
                $items[] = $extension;
99
            }
100
        }
101
102
        if ($this->update) {
103
            foreach($items as $item) {
104
                $this->stdout("fetching tags for $item...");
105
                if ($item === 'framework') {
106
                    $this->gitFetchTags("{$this->basePath}");
107
                } elseif (strncmp('app-', $item, 4) === 0) {
108
                    $this->gitFetchTags("{$this->basePath}/apps/" . substr($item, 4));
109
                } else {
110
                    $this->gitFetchTags("{$this->basePath}/extensions/$item");
111
                }
112
                $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
113
            }
114
        } else {
115
            $this->stdout("\nInformation may be outdated, re-run with `--update` to fetch latest tags.\n\n");
116
        }
117
118
        $versions = $this->getCurrentVersions($items);
119
        $nextVersions = $this->getNextVersions($versions, self::PATCH);
120
121
        // print version table
122
        $w = $this->minWidth(array_keys($versions));
123
        $this->stdout(str_repeat(' ', $w + 2) . "Current Version  Next Version\n", Console::BOLD);
124
        foreach($versions as $ext => $version) {
125
            $this->stdout($ext . str_repeat(' ', $w + 3 - mb_strlen($ext)) . $version . "");
126
            $this->stdout(str_repeat(' ', 17 - mb_strlen($version)) . $nextVersions[$ext] . "\n");
127
        }
128
129
    }
130
131
    private function minWidth($a)
132
    {
133
        $w = 1;
134
        foreach($a as $s) {
135
            if (($l = mb_strlen($s)) > $w) {
136
                $w = $l;
137
            }
138
        }
139
        return $w;
140
    }
141
142
    /**
143
     * Automation tool for making Yii framework and official extension releases.
144
     *
145
     * Usage:
146
     *
147
     * To make a release, make sure your git is clean (no uncommitted changes) and run the following command in
148
     * the yii dev repo root:
149
     *
150
     * ```
151
     * ./build/build release framework
152
     * ```
153
     *
154
     * or
155
     *
156
     * ```
157
     * ./build/build release redis,bootstrap,apidoc
158
     * ```
159
     *
160
     * You may use the `--dryRun` switch to test the command without changing or pushing anything:
161
     *
162
     * ```
163
     * ./build/build release redis --dryRun
164
     * ```
165
     *
166
     * The command will guide you through the complete release process including changing of files,
167
     * committing and pushing them. Each git command must be confirmed and can be skipped individually.
168
     * You may adjust changes in a separate shell or your IDE while the command is waiting for confirmation.
169
     *
170
     * @param array $what what do you want to release? this can either be:
171
     *
172
     * - an extension name such as `redis` or `bootstrap`,
173
     * - an application indicated by prefix `app-`, e.g. `app-basic`,
174
     * - or `framework` if you want to release a new version of the framework itself.
175
     *
176
     * @return int
177
     */
178
    public function actionRelease(array $what)
179
    {
180
        if (count($what) > 1) {
181
            $this->stdout("Currently only one simultaneous release is supported.\n");
182
            return 1;
183
        }
184
185
        $this->stdout("This is the Yii release manager\n\n", Console::BOLD);
186
187
        if ($this->dryRun) {
188
            $this->stdout("Running in \"dry-run\" mode, nothing will actually be changed.\n\n", Console::BOLD, Console::FG_GREEN);
189
        }
190
191
        $this->validateWhat($what);
192
        $versions = $this->getCurrentVersions($what);
193
        $newVersions = $this->getNextVersions($versions, self::PATCH);// TODO add support for minor
194
195
        $this->stdout("You are about to prepare a new release for the following things:\n\n");
196
        $this->printWhat($what, $newVersions, $versions);
197
        $this->stdout("\n");
198
199
        $this->stdout("Before you make a release briefly go over the changes and check if you spot obvious mistakes:\n\n", Console::BOLD);
200
        if (strncmp('app-', reset($what), 4) !== 0) {
201
            $this->stdout("- no accidentally added CHANGELOG lines for other versions than this one?\n");
202
            $this->stdout("- are all new `@since` tags for this relase version?\n");
203
        }
204
        $travisUrl = reset($what) === 'framework' ? '' : '-'.reset($what);
205
        $this->stdout("- are unit tests passing on travis? https://travis-ci.org/yiisoft/yii2$travisUrl/builds\n");
206
        $this->stdout("- other issues with code changes?\n");
207
        $this->stdout("- also make sure the milestone on github is complete and no issues or PRs are left open.\n\n");
208
        $this->printWhatUrls($what, $versions);
209
        $this->stdout("\n");
210
211
        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...
212
            $this->stdout("Canceled.\n");
213
            return 1;
214
        }
215
216
        foreach($what as $ext) {
217
            if ($ext === 'framework') {
218
                $this->releaseFramework("{$this->basePath}/framework", $newVersions['framework']);
219
            } elseif (strncmp('app-', $ext, 4) === 0) {
220
                $this->releaseApplication(substr($ext, 4), "{$this->basePath}/apps/" . substr($ext, 4), $newVersions[$ext]);
221
            } else {
222
                $this->releaseExtension($ext, "{$this->basePath}/extensions/$ext", $newVersions[$ext]);
223
            }
224
        }
225
226
        return 0;
227
    }
228
229
    /**
230
     * This will generate application packages for download page.
231
     *
232
     * Usage:
233
     *
234
     * ```
235
     * ./build/build release/package app-basic
236
     * ```
237
     *
238
     * @param array $what what do you want to package? this can either be:
239
     *
240
     * - an application indicated by prefix `app-`, e.g. `app-basic`,
241
     *
242
     * @return int
243
     */
244
    public function actionPackage(array $what)
245
    {
246
        $this->validateWhat($what, ['app']);
247
        $versions = $this->getCurrentVersions($what);
248
249
        $this->stdout("You are about to generate packages for the following things:\n\n");
250
        foreach($what as $ext) {
251
            if (strncmp('app-', $ext, 4) === 0) {
252
                $this->stdout(" - ");
253
                $this->stdout(substr($ext, 4), Console::FG_RED);
254
                $this->stdout(" application version ");
255
            } elseif ($ext === 'framework') {
256
                $this->stdout(" - Yii Framework version ");
257
            } else {
258
                $this->stdout(" - ");
259
                $this->stdout($ext, Console::FG_RED);
260
                $this->stdout(" extension version ");
261
            }
262
            $this->stdout($versions[$ext], Console::BOLD);
263
            $this->stdout("\n");
264
        }
265
        $this->stdout("\n");
266
267
        $packagePath = "{$this->basePath}/packages";
268
        $this->stdout("Packages will be stored in $packagePath\n\n");
269
270
        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...
271
            $this->stdout("Canceled.\n");
272
            return 1;
273
        }
274
275
        foreach($what as $ext) {
276
            if ($ext === 'framework') {
277
                throw new Exception('Can not package framework.');
278
            } elseif (strncmp('app-', $ext, 4) === 0) {
279
                $this->packageApplication(substr($ext, 4), $versions[$ext], $packagePath);
280
            } else {
281
                throw new Exception('Can not package extension.');
282
            }
283
        }
284
285
        $this->stdout("\ndone. verify the versions composer installed above and push it to github!\n\n");
286
287
        return 0;
288
    }
289
290
    protected function printWhat(array $what, $newVersions, $versions)
291
    {
292
        foreach($what as $ext) {
293
            if (strncmp('app-', $ext, 4) === 0) {
294
                $this->stdout(" - ");
295
                $this->stdout(substr($ext, 4), Console::FG_RED);
296
                $this->stdout(" application version ");
297
            } elseif ($ext === 'framework') {
298
                $this->stdout(" - Yii Framework version ");
299
            } else {
300
                $this->stdout(" - ");
301
                $this->stdout($ext, Console::FG_RED);
302
                $this->stdout(" extension version ");
303
            }
304
            $this->stdout($newVersions[$ext], Console::BOLD);
305
            $this->stdout(", last release was {$versions[$ext]}\n");
306
        }
307
    }
308
309
    protected function printWhatUrls(array $what, $oldVersions)
310
    {
311
        foreach($what as $ext) {
312
            if ($ext === 'framework') {
313
                $this->stdout("framework:    https://github.com/yiisoft/yii2-framework/compare/{$oldVersions[$ext]}...master\n");
314
                $this->stdout("app-basic:    https://github.com/yiisoft/yii2-app-basic/compare/{$oldVersions[$ext]}...master\n");
315
                $this->stdout("app-advanced: https://github.com/yiisoft/yii2-app-advanced/compare/{$oldVersions[$ext]}...master\n");
316
            } else {
317
                $this->stdout($ext, Console::FG_RED);
318
                $this->stdout(": https://github.com/yiisoft/yii2-$ext/compare/{$oldVersions[$ext]}...master\n");
319
            }
320
        }
321
    }
322
323
    /**
324
     * @param array $what list of items
325
     * @param array $limit list of things to allow, or empty to allow any, can be `app`, `framework`, `extension`
326
     * @throws \yii\base\Exception
327
     */
328
    protected function validateWhat(array $what, $limit = [])
329
    {
330
        foreach($what as $w) {
331
            if (strncmp('app-', $w, 4) === 0) {
332
                if (!empty($limit) && !in_array('app', $limit)) {
333
                    throw new Exception("Only the following types are allowed: ".implode(', ', $limit)."\n");
334
                }
335
                if (!is_dir($appPath = "{$this->basePath}/apps/" . substr($w, 4))) {
336
                    throw new Exception("Application path does not exist: \"{$appPath}\"\n");
337
                }
338
                $this->ensureGitClean($appPath);
339
            } elseif ($w === 'framework') {
340
                if (!empty($limit) && !in_array('framework', $limit)) {
341
                    throw new Exception("Only the following types are allowed: ".implode(', ', $limit)."\n");
342
                }
343
                if (!is_dir($fwPath = "{$this->basePath}/framework")) {
344
                    throw new Exception("Framework path does not exist: \"{$this->basePath}/framework\"\n");
345
                }
346
                $this->ensureGitClean($fwPath);
347
            } else {
348
                if (!empty($limit) && !in_array('ext', $limit)) {
349
                    throw new Exception("Only the following types are allowed: ".implode(', ', $limit)."\n");
350
                }
351
                if (!is_dir($extPath = "{$this->basePath}/extensions/$w")) {
352
                    throw new Exception("Extension path for \"$w\" does not exist: \"{$this->basePath}/extensions/$w\"\n");
353
                }
354
                $this->ensureGitClean($extPath);
355
            }
356
        }
357
    }
358
359
360
    protected function releaseFramework($frameworkPath, $version)
361
    {
362
        $this->stdout("\n");
363
        $this->stdout($h = "Preparing framework release version $version", Console::BOLD);
364
        $this->stdout("\n" . str_repeat('-', strlen($h)) . "\n\n", Console::BOLD);
365
366
        $this->runGit('git checkout master', $frameworkPath); // TODO add compatibility for other release branches
367
        $this->runGit('git pull', $frameworkPath); // TODO add compatibility for other release branches
368
369
        // checks
370
371
        $this->stdout('check if framework composer.json matches yii2-dev composer.json...');
372
        $this->checkComposer($frameworkPath);
373
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
374
375
        // adjustments
376
377
        $this->stdout('prepare classmap...', Console::BOLD);
378
        $this->dryRun || Yii::$app->runAction('classmap', [$frameworkPath]);
379
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
380
381
        $this->stdout('updating mimetype magic file...', Console::BOLD);
382
        $this->dryRun || Yii::$app->runAction('mime-type', ["$frameworkPath/helpers/mimeTypes.php"]);
383
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
384
385
        $this->stdout("fixing various PHPdoc style issues...\n", Console::BOLD);
386
        $this->dryRun || Yii::$app->runAction('php-doc/fix', [$frameworkPath]);
387
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
388
389
        $this->stdout("updating PHPdoc @property annotations...\n", Console::BOLD);
390
        $this->dryRun || Yii::$app->runAction('php-doc/property', [$frameworkPath]);
391
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
392
393
        $this->stdout('sorting changelogs...', Console::BOLD);
394
        $this->dryRun || $this->resortChangelogs(['framework'], $version);
395
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
396
397
        $this->stdout('closing changelogs...', Console::BOLD);
398
        $this->dryRun || $this->closeChangelogs(['framework'], $version);
399
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
400
401
        $this->stdout('updating Yii version...');
402
        $this->dryRun || $this->updateYiiVersion($frameworkPath, $version);
403
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
404
405
        $this->stdout("\nIn the following you can check the above changes using git diff.\n\n");
406
        do {
407
            $this->runGit("git diff --color", $frameworkPath);
408
            $this->stdout("\n\n\nCheck whether the above diff is okay, if not you may change things as needed before continuing.\n");
409
            $this->stdout("You may abort the program with Ctrl + C and reset the changes by running `git checkout -- .` in the repo.\n\n");
410
        } while(!$this->confirm("Type `yes` to continue, `no` to view git diff again. Continue?"));
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->confirm('Type `ye...diff again. 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...
411
412
        $this->stdout("\n\n");
413
        $this->stdout("    ****          RELEASE TIME!         ****\n", Console::FG_YELLOW, Console::BOLD);
414
        $this->stdout("    ****    Commit, Tag and Push it!    ****\n", Console::FG_YELLOW, Console::BOLD);
415
        $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");
416
417
        $this->runGit("git commit -a -m \"release version $version\"", $frameworkPath);
418
        $this->runGit("git tag -a $version -m\"version $version\"", $frameworkPath);
419
        $this->runGit("git push origin master", $frameworkPath);
420
        $this->runGit("git push --tags", $frameworkPath);
421
422
        $this->stdout("\n\n");
423
        $this->stdout("CONGRATULATIONS! You have just released extension ", Console::FG_YELLOW, Console::BOLD);
424
        $this->stdout('framework', Console::FG_RED, Console::BOLD);
425
        $this->stdout(" version ", Console::FG_YELLOW, Console::BOLD);
426
        $this->stdout($version, Console::BOLD);
427
        $this->stdout("!\n\n", Console::FG_YELLOW, Console::BOLD);
428
429
        // TODO release applications
430
        // $this->composerSetStability($what, $version);
431
432
433
//        $this->resortChangelogs($what, $version);
434
  //        $this->closeChangelogs($what, $version);
435
  //        $this->composerSetStability($what, $version);
436
  //        if (in_array('framework', $what)) {
437
  //            $this->updateYiiVersion($version);
438
  //        }
439
440
441
        // if done:
442
        //     * ./build/build release/done framework 2.0.0-dev 2.0.0-rc
443
        //     * ./build/build release/done redis 2.0.0-dev 2.0.0-rc
444
//            $this->openChangelogs($what, $nextVersion);
445
//            $this->composerSetStability($what, 'dev');
446
//            if (in_array('framework', $what)) {
447
//                $this->updateYiiVersion($devVersion);
448
//            }
449
450
451
452
        // prepare next release
453
454
        $this->stdout("Time to prepare the next release...\n\n", Console::FG_YELLOW, Console::BOLD);
455
456
        $this->stdout('opening changelogs...', Console::BOLD);
457
        $nextVersion = $this->getNextVersions(['framework' => $version], self::PATCH); // TODO support other versions
458
        $this->dryRun || $this->openChangelogs(['framework'], $nextVersion['framework']);
459
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
460
461
        $this->stdout('updating Yii version...');
462
        $this->dryRun || $this->updateYiiVersion($frameworkPath, $nextVersion['framework'] . '-dev');
463
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
464
465
466
        $this->stdout("\n");
467
        $this->runGit("git diff --color", $frameworkPath);
468
        $this->stdout("\n\n");
469
        $this->runGit("git commit -a -m \"prepare for next release\"", $frameworkPath);
470
        $this->runGit("git push origin master", $frameworkPath);
471
472
        $this->stdout("\n\nDONE!", Console::FG_YELLOW, Console::BOLD);
473
474
        $this->stdout("\n\nThe following steps are left for you to do manually:\n\n");
475
        $nextVersion2 = $this->getNextVersions($nextVersion, self::PATCH); // TODO support other versions
476
        $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");
477
        $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");
478
        $this->stdout("- create a release on github.\n");
479
        $this->stdout("- release news and announcement.\n");
480
        $this->stdout("- update the website (will be automated soon and is only relevant for the new website).\n");
481
        $this->stdout("\n");
482
        $this->stdout("- release applications: ./build/build release app-basic\n");
483
        $this->stdout("- release applications: ./build/build release app-advanced\n");
484
485
        $this->stdout("\n");
486
487
    }
488
489
    protected function releaseApplication($name, $path, $version)
490
    {
491
        $this->stdout("\n");
492
        $this->stdout($h = "Preparing release for application  $name  version $version", Console::BOLD);
493
        $this->stdout("\n" . str_repeat('-', strlen($h)) . "\n\n", Console::BOLD);
494
495
        $this->runGit('git checkout master', $path); // TODO add compatibility for other release branches
496
        $this->runGit('git pull', $path); // TODO add compatibility for other release branches
497
498
        // adjustments
499
500
        $this->stdout("fixing various PHPdoc style issues...\n", Console::BOLD);
501
        $this->setAppAliases($name, $path);
502
        $this->dryRun || Yii::$app->runAction('php-doc/fix', [$path, 'skipFrameworkRequirements' => true]);
503
        $this->resetAppAliases();
504
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
505
506
        $this->stdout("updating PHPdoc @property annotations...\n", Console::BOLD);
507
        $this->setAppAliases($name, $path);
508
        $this->dryRun || Yii::$app->runAction('php-doc/property', [$path, 'skipFrameworkRequirements' => true]);
509
        $this->resetAppAliases();
510
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
511
512
        $this->stdout("updating composer stability...\n", Console::BOLD);
513
        $this->dryRun || $this->composerSetStability(["app-$name"], $version);
514
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
515
516
        $this->stdout("\nIn the following you can check the above changes using git diff.\n\n");
517
        do {
518
            $this->runGit("git diff --color", $path);
519
            $this->stdout("\n\n\nCheck whether the above diff is okay, if not you may change things as needed before continuing.\n");
520
            $this->stdout("You may abort the program with Ctrl + C and reset the changes by running `git checkout -- .` in the repo.\n\n");
521
        } while(!$this->confirm("Type `yes` to continue, `no` to view git diff again. Continue?"));
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->confirm('Type `ye...diff again. 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...
522
523
        $this->stdout("\n\n");
524
        $this->stdout("    ****          RELEASE TIME!         ****\n", Console::FG_YELLOW, Console::BOLD);
525
        $this->stdout("    ****    Commit, Tag and Push it!    ****\n", Console::FG_YELLOW, Console::BOLD);
526
        $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");
527
528
        $this->runGit("git commit -a -m \"release version $version\"", $path);
529
        $this->runGit("git tag -a $version -m\"version $version\"", $path);
530
        $this->runGit("git push origin master", $path);
531
        $this->runGit("git push --tags", $path);
532
533
        $this->stdout("\n\n");
534
        $this->stdout("CONGRATULATIONS! You have just released application ", Console::FG_YELLOW, Console::BOLD);
535
        $this->stdout($name, Console::FG_RED, Console::BOLD);
536
        $this->stdout(" version ", Console::FG_YELLOW, Console::BOLD);
537
        $this->stdout($version, Console::BOLD);
538
        $this->stdout("!\n\n", Console::FG_YELLOW, Console::BOLD);
539
540
        // prepare next release
541
542
        $this->stdout("Time to prepare the next release...\n\n", Console::FG_YELLOW, Console::BOLD);
543
544
        $this->stdout("updating composer stability...\n", Console::BOLD);
545
        $this->dryRun || $this->composerSetStability(["app-$name"], 'dev');
546
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
547
548
        $nextVersion = $this->getNextVersions(["app-$name" => $version], self::PATCH); // TODO support other versions
549
550
        $this->stdout("\n");
551
        $this->runGit("git diff --color", $path);
552
        $this->stdout("\n\n");
553
        $this->runGit("git commit -a -m \"prepare for next release\"", $path);
554
        $this->runGit("git push origin master", $path);
555
556
        $this->stdout("\n\nDONE!", Console::FG_YELLOW, Console::BOLD);
557
558
        $this->stdout("\n\nThe following steps are left for you to do manually:\n\n");
559
        $nextVersion2 = $this->getNextVersions($nextVersion, self::PATCH); // TODO support other versions
560
        $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");
561
        $this->stdout("- Create Application packages and upload them to github:  ./build release/package app-$name\n");
562
563
        $this->stdout("\n");
564
    }
565
566
    private $_oldAlias;
567
568
    protected function setAppAliases($app, $path)
569
    {
570
        $this->_oldAlias = Yii::getAlias('@app');
571
        switch($app) {
572
            case 'basic':
573
                Yii::setAlias('@app', $path);
574
                break;
575
            case 'advanced':
576
                // setup @frontend, @backend etc...
577
                require("$path/common/config/bootstrap.php");
578
                break;
579
        }
580
    }
581
582
    protected function resetAppAliases()
583
    {
584
        Yii::setAlias('@app', $this->_oldAlias);
585
    }
586
587
    protected function packageApplication($name, $version, $packagePath)
588
    {
589
        FileHelper::createDirectory($packagePath);
590
591
        $this->runCommand("composer create-project yiisoft/yii2-app-$name $name $version", $packagePath);
592
        // clear cookie validation key in basic app
593
        if (is_file($configFile = "$packagePath/$name/config/web.php")) {
594
            $this->sed(
595
                "/'cookieValidationKey' => '.*?',/",
596
                "'cookieValidationKey' => '',",
597
                $configFile
598
            );
599
        }
600
        $this->runCommand("tar zcf yii-$name-app-$version.tgz $name", $packagePath);
601
    }
602
603
    protected function releaseExtension($name, $path, $version)
604
    {
605
        $this->stdout("\n");
606
        $this->stdout($h = "Preparing release for extension  $name  version $version", Console::BOLD);
607
        $this->stdout("\n" . str_repeat('-', strlen($h)) . "\n\n", Console::BOLD);
608
609
        $this->runGit('git checkout master', $path); // TODO add compatibility for other release branches
610
        $this->runGit('git pull', $path); // TODO add compatibility for other release branches
611
612
        // adjustments
613
614
        $this->stdout("fixing various PHPdoc style issues...\n", Console::BOLD);
615
        $this->dryRun || Yii::$app->runAction('php-doc/fix', [$path]);
616
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
617
618
        $this->stdout("updating PHPdoc @property annotations...\n", Console::BOLD);
619
        $this->dryRun || Yii::$app->runAction('php-doc/property', [$path]);
620
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
621
622
        $this->stdout('sorting changelogs...', Console::BOLD);
623
        $this->dryRun || $this->resortChangelogs([$name], $version);
624
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
625
626
        $this->stdout('closing changelogs...', Console::BOLD);
627
        $this->dryRun || $this->closeChangelogs([$name], $version);
628
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
629
630
        $this->stdout("\nIn the following you can check the above changes using git diff.\n\n");
631
        do {
632
            $this->runGit("git diff --color", $path);
633
            $this->stdout("\n\n\nCheck whether the above diff is okay, if not you may change things as needed before continuing.\n");
634
            $this->stdout("You may abort the program with Ctrl + C and reset the changes by running `git checkout -- .` in the repo.\n\n");
635
        } while(!$this->confirm("Type `yes` to continue, `no` to view git diff again. Continue?"));
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->confirm('Type `ye...diff again. 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...
636
637
        $this->stdout("\n\n");
638
        $this->stdout("    ****          RELEASE TIME!         ****\n", Console::FG_YELLOW, Console::BOLD);
639
        $this->stdout("    ****    Commit, Tag and Push it!    ****\n", Console::FG_YELLOW, Console::BOLD);
640
        $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");
641
642
        $this->runGit("git commit -a -m \"release version $version\"", $path);
643
        $this->runGit("git tag -a $version -m\"version $version\"", $path);
644
        $this->runGit("git push origin master", $path);
645
        $this->runGit("git push --tags", $path);
646
647
        $this->stdout("\n\n");
648
        $this->stdout("CONGRATULATIONS! You have just released extension ", Console::FG_YELLOW, Console::BOLD);
649
        $this->stdout($name, Console::FG_RED, Console::BOLD);
650
        $this->stdout(" version ", Console::FG_YELLOW, Console::BOLD);
651
        $this->stdout($version, Console::BOLD);
652
        $this->stdout("!\n\n", Console::FG_YELLOW, Console::BOLD);
653
654
        // prepare next release
655
656
        $this->stdout("Time to prepare the next release...\n\n", Console::FG_YELLOW, Console::BOLD);
657
658
        $this->stdout('opening changelogs...', Console::BOLD);
659
        $nextVersion = $this->getNextVersions([$name => $version], self::PATCH); // TODO support other versions
660
        $this->dryRun || $this->openChangelogs([$name], $nextVersion[$name]);
661
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
662
663
        $this->stdout("\n");
664
        $this->runGit("git diff --color", $path);
665
        $this->stdout("\n\n");
666
        $this->runGit("git commit -a -m \"prepare for next release\"", $path);
667
        $this->runGit("git push origin master", $path);
668
669
        $this->stdout("\n\nDONE!", Console::FG_YELLOW, Console::BOLD);
670
671
        $this->stdout("\n\nThe following steps are left for you to do manually:\n\n");
672
        $nextVersion2 = $this->getNextVersions($nextVersion, self::PATCH); // TODO support other versions
673
        $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");
674
        $this->stdout("- release news and announcement.\n");
675
        $this->stdout("- update the website (will be automated soon and is only relevant for the new website).\n");
676
677
        $this->stdout("\n");
678
    }
679
680
681
    protected function runCommand($cmd, $path)
682
    {
683
        $this->stdout("running  $cmd  ...", Console::BOLD);
684
        if ($this->dryRun) {
685
            $this->stdout("dry run, command `$cmd` not executed.\n");
686
            return;
687
        }
688
        chdir($path);
689
        exec($cmd, $output, $ret);
690
        if ($ret != 0) {
0 ignored issues
show
Bug introduced by
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...
691
            echo implode("\n", $output);
692
            throw new Exception("Command \"$cmd\" failed with code " . $ret);
693
        }
694
        $this->stdout("\ndone.\n", Console::BOLD, Console::FG_GREEN);
695
    }
696
697
    protected function runGit($cmd, $path)
698
    {
699
        if ($this->confirm("Run `$cmd`?", true)) {
700
            if ($this->dryRun) {
701
                $this->stdout("dry run, command `$cmd` not executed.\n");
702
                return;
703
            }
704
            chdir($path);
705
            exec($cmd, $output, $ret);
706
            echo implode("\n", $output);
707
            if ($ret != 0) {
0 ignored issues
show
Bug introduced by
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...
708
                throw new Exception("Command \"$cmd\" failed with code " . $ret);
709
            }
710
            echo "\n";
711
        }
712
    }
713
714
    protected function ensureGitClean($path)
715
    {
716
        chdir($path);
717
        exec('git status --porcelain -uno', $changes, $ret);
718
        if ($ret != 0) {
0 ignored issues
show
Bug introduced by
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...
719
            throw new Exception('Command "git status --porcelain -uno" failed with code ' . $ret);
720
        }
721
        if (!empty($changes)) {
722
            throw new Exception("You have uncommitted changes in $path: " . print_r($changes, true));
723
        }
724
    }
725
726
    protected function gitFetchTags($path)
727
    {
728
        chdir($path);
729
        exec('git fetch --tags', $output, $ret);
730
        if ($ret != 0) {
0 ignored issues
show
Bug introduced by
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...
731
            throw new Exception('Command "git fetch --tags" failed with code ' . $ret);
732
        }
733
    }
734
735
736
    protected function checkComposer($fwPath)
0 ignored issues
show
Unused Code introduced by
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...
737
    {
738
        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...
739
            exit;
740
        }
741
    }
742
743
744
    protected function closeChangelogs($what, $version)
745
    {
746
        $v = str_replace('\\-', '[\\- ]', preg_quote($version, '/'));
747
        $headline = $version . ' ' . date('F d, Y');
748
        $this->sed(
749
            '/'.$v.' under development\n(-+?)\n/',
750
            $headline . "\n" . str_repeat('-', strlen($headline)) . "\n",
751
            $this->getChangelogs($what)
752
        );
753
    }
754
755
    protected function openChangelogs($what, $version)
756
    {
757
        $headline = "\n$version under development\n";
758
        $headline .= str_repeat('-', strlen($headline) - 2) . "\n\n- no changes in this release.\n";
759
        foreach($this->getChangelogs($what) as $file) {
760
            $lines = explode("\n", file_get_contents($file));
761
            $hl = [
762
                array_shift($lines),
763
                array_shift($lines),
764
            ];
765
            array_unshift($lines, $headline);
766
767
            file_put_contents($file, implode("\n", array_merge($hl, $lines)));
768
        }
769
    }
770
771
    protected function resortChangelogs($what, $version)
772
    {
773
        foreach($this->getChangelogs($what) as $file) {
774
            // split the file into relevant parts
775
            list($start, $changelog, $end) = $this->splitChangelog($file, $version);
776
            $changelog = $this->resortChangelog($changelog);
777
            file_put_contents($file, implode("\n", array_merge($start, $changelog, $end)));
778
        }
779
    }
780
781
    /**
782
     * Extract changelog content for a specific version
783
     */
784
    protected function splitChangelog($file, $version)
785
    {
786
        $lines = explode("\n", file_get_contents($file));
787
788
        // split the file into relevant parts
789
        $start = [];
790
        $changelog = [];
791
        $end = [];
792
793
        $state = 'start';
794
        foreach($lines as $l => $line) {
795
            // starting from the changelogs headline
796
            if (isset($lines[$l-2]) && strpos($lines[$l-2], $version) !== false &&
797
                isset($lines[$l-1]) && strncmp($lines[$l-1], '---', 3) === 0) {
798
                $state = 'changelog';
799
            }
800
            if ($state === 'changelog' && isset($lines[$l+1]) && strncmp($lines[$l+1], '---', 3) === 0) {
801
                $state = 'end';
802
            }
803
            ${$state}[] = $line;
804
        }
805
        return [$start, $changelog, $end];
806
    }
807
808
    /**
809
     * Ensure sorting of the changelog lines
810
     */
811
    protected function resortChangelog($changelog)
812
    {
813
        // cleanup whitespace
814
        foreach($changelog as $i => $line) {
815
            $changelog[$i] = rtrim($line);
816
        }
817
        $changelog = array_filter($changelog);
818
819
        $i = 0;
820
        ArrayHelper::multisort($changelog, function($line) use (&$i) {
821
            if (preg_match('/^- (Chg|Enh|Bug|New)( #\d+(, #\d+)*)?: .+$/', $line, $m)) {
822
                $o = ['Bug' => 'C', 'Enh' => 'D', 'Chg' => 'E', 'New' => 'F'];
823
                return $o[$m[1]] . ' ' . (!empty($m[2]) ? $m[2] : 'AAAA' . $i++);
824
            }
825
            return 'B' . $i++;
826
        }, SORT_ASC, SORT_NATURAL);
827
828
        // re-add leading and trailing lines
829
        array_unshift($changelog, '');
830
        $changelog[] = '';
831
        $changelog[] = '';
832
833
        return $changelog;
834
    }
835
836
    protected function getChangelogs($what)
837
    {
838
        $changelogs = [];
839
        if (in_array('framework', $what)) {
840
            $changelogs[] = $this->getFrameworkChangelog();
841
        }
842
843
        return array_merge($changelogs, $this->getExtensionChangelogs($what));
844
    }
845
846
    protected function getFrameworkChangelog()
847
    {
848
        return $this->basePath . '/framework/CHANGELOG.md';
849
    }
850
851
    protected function getExtensionChangelogs($what)
852
    {
853
        return array_filter(glob($this->basePath . '/extensions/*/CHANGELOG.md'), function($elem) use ($what) {
854
            foreach($what as $ext) {
855
                if (strpos($elem, "extensions/$ext/CHANGELOG.md") !== false) {
856
                    return true;
857
                }
858
            }
859
            return false;
860
        });
861
    }
862
863
    protected function composerSetStability($what, $version)
864
    {
865
        $apps = [];
866
        if (in_array('app-advanced', $what)) {
867
            $apps[] = $this->basePath . '/apps/advanced/composer.json';
868
        }
869
        if (in_array('app-basic', $what)) {
870
            $apps[] = $this->basePath . '/apps/basic/composer.json';
871
        }
872
        if (in_array('app-benchmark', $what)) {
873
            $apps[] = $this->basePath . '/apps/benchmark/composer.json';
874
        }
875
        if (empty($apps)) {
876
            return;
877
        }
878
879
        $stability = 'stable';
880
        if (strpos($version, 'alpha') !== false) {
881
            $stability = 'alpha';
882
        } elseif (strpos($version, 'beta') !== false) {
883
            $stability = 'beta';
884
        } elseif (strpos($version, 'rc') !== false) {
885
            $stability = 'RC';
886
        } elseif (strpos($version, 'dev') !== false) {
887
            $stability = 'dev';
888
        }
889
890
        $this->sed(
891
            '/"minimum-stability": "(.+?)",/',
892
            '"minimum-stability": "' . $stability . '",',
893
            $apps
894
        );
895
    }
896
897
    protected function updateYiiVersion($frameworkPath, $version)
898
    {
899
        $this->sed(
900
            '/function getVersion\(\)\n    \{\n        return \'(.+?)\';/',
901
            "function getVersion()\n    {\n        return '$version';",
902
            $frameworkPath . '/BaseYii.php');
903
    }
904
905
    protected function sed($pattern, $replace, $files)
906
    {
907
        foreach((array) $files as $file) {
908
            file_put_contents($file, preg_replace($pattern, $replace, file_get_contents($file)));
909
        }
910
    }
911
912
    protected function getCurrentVersions(array $what)
913
    {
914
        $versions = [];
915
        foreach($what as $ext) {
916
            if ($ext === 'framework') {
917
                chdir("{$this->basePath}/framework");
918
            } elseif (strncmp('app-', $ext, 4) === 0) {
919
                chdir("{$this->basePath}/apps/" . substr($ext, 4));
920
            } else {
921
                chdir("{$this->basePath}/extensions/$ext");
922
            }
923
            $tags = [];
924
            exec('git tag', $tags, $ret);
925
            if ($ret != 0) {
0 ignored issues
show
Bug introduced by
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...
926
                throw new Exception('Command "git tag" failed with code ' . $ret);
927
            }
928
            rsort($tags, SORT_NATURAL); // TODO this can not deal with alpha/beta/rc...
929
            $versions[$ext] = reset($tags);
930
        }
931
        return $versions;
932
    }
933
934
    const MINOR = 'minor';
935
    const PATCH = 'patch';
936
937
    protected function getNextVersions(array $versions, $type)
938
    {
939
        foreach($versions as $k => $v) {
940
            if (empty($v)) {
941
                $versions[$k] = '2.0.0';
942
                continue;
943
            }
944
            $parts = explode('.', $v);
945
            switch($type) {
946
                case self::MINOR:
947
                    $parts[1]++;
948
                    break;
949
                case self::PATCH:
950
                    $parts[2]++;
951
                    break;
952
                default:
953
                    throw new Exception('Unknown version type.');
954
            }
955
            $versions[$k] = implode('.', $parts);
956
        }
957
        return $versions;
958
    }
959
}
960