GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Push — master ( cd2d3a...6886d6 )
by Robert
09:38
created

ReleaseController::beforeAction()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 11
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 7
nc 3
nop 1
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 === 'info') {
72
            $options[] = 'update';
73
        }
74
        return array_merge(parent::options($actionID), $options);
75
    }
76
77
78
    public function beforeAction($action)
79
    {
80
        if (!$this->interactive) {
81
            throw new Exception('Sorry, but releases should be run interactively to ensure you actually verify what you are doing ;)');
82
        }
83
        if ($this->basePath === null) {
84
            $this->basePath = dirname(dirname(__DIR__));
85
        }
86
        $this->basePath = rtrim($this->basePath, '\\/');
87
        return parent::beforeAction($action);
88
    }
89
90
    /**
91
     * Shows information about current framework and extension versions.
92
     */
93
    public function actionInfo()
94
    {
95
        $items = [
96
            'framework',
97
            'app-basic',
98
            'app-advanced',
99
        ];
100
        $extensionPath = "{$this->basePath}/extensions";
101
        foreach (scandir($extensionPath) as $extension) {
102
            if (ctype_alpha($extension) && is_dir($extensionPath . '/' . $extension)) {
103
                $items[] = $extension;
104
            }
105
        }
106
107
        if ($this->update) {
108
            foreach($items as $item) {
109
                $this->stdout("fetching tags for $item...");
110
                if ($item === 'framework') {
111
                    $this->gitFetchTags("{$this->basePath}");
112
                } elseif (strncmp('app-', $item, 4) === 0) {
113
                    $this->gitFetchTags("{$this->basePath}/apps/" . substr($item, 4));
114
                } else {
115
                    $this->gitFetchTags("{$this->basePath}/extensions/$item");
116
                }
117
                $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
118
            }
119
        } else {
120
            $this->stdout("\nInformation may be outdated, re-run with `--update` to fetch latest tags.\n\n");
121
        }
122
123
        $versions = $this->getCurrentVersions($items);
124
        $nextVersions = $this->getNextVersions($versions, self::PATCH);
125
126
        // print version table
127
        $w = $this->minWidth(array_keys($versions));
128
        $this->stdout(str_repeat(' ', $w + 2) . "Current Version  Next Version\n", Console::BOLD);
129
        foreach($versions as $ext => $version) {
130
            $this->stdout($ext . str_repeat(' ', $w + 3 - mb_strlen($ext)) . $version . "");
131
            $this->stdout(str_repeat(' ', 17 - mb_strlen($version)) . $nextVersions[$ext] . "\n");
132
        }
133
134
    }
135
136
    private function minWidth($a)
137
    {
138
        $w = 1;
139
        foreach($a as $s) {
140
            if (($l = mb_strlen($s)) > $w) {
141
                $w = $l;
142
            }
143
        }
144
        return $w;
145
    }
146
147
    /**
148
     * Automation tool for making Yii framework and official extension releases.
149
     *
150
     * Usage:
151
     *
152
     * To make a release, make sure your git is clean (no uncommitted changes) and run the following command in
153
     * the yii dev repo root:
154
     *
155
     * ```
156
     * ./build/build release framework
157
     * ```
158
     *
159
     * or
160
     *
161
     * ```
162
     * ./build/build release redis,bootstrap,apidoc
163
     * ```
164
     *
165
     * You may use the `--dryRun` switch to test the command without changing or pushing anything:
166
     *
167
     * ```
168
     * ./build/build release redis --dryRun
169
     * ```
170
     *
171
     * The command will guide you through the complete release process including changing of files,
172
     * committing and pushing them. Each git command must be confirmed and can be skipped individually.
173
     * You may adjust changes in a separate shell or your IDE while the command is waiting for confirmation.
174
     *
175
     * @param array $what what do you want to release? this can either be:
176
     *
177
     * - an extension name such as `redis` or `bootstrap`,
178
     * - an application indicated by prefix `app-`, e.g. `app-basic`,
179
     * - or `framework` if you want to release a new version of the framework itself.
180
     *
181
     * @return int
182
     */
183
    public function actionRelease(array $what)
184
    {
185
        if (count($what) > 1) {
186
            $this->stdout("Currently only one simultaneous release is supported.\n");
187
            return 1;
188
        }
189
190
        $this->stdout("This is the Yii release manager\n\n", Console::BOLD);
191
192
        if ($this->dryRun) {
193
            $this->stdout("Running in \"dry-run\" mode, nothing will actually be changed.\n\n", Console::BOLD, Console::FG_GREEN);
194
        }
195
196
        $this->validateWhat($what);
197
        $versions = $this->getCurrentVersions($what);
198
199
        if ($this->version !== null) {
200
            // if a version is explicitly given
201
            $newVersions = [];
202
            foreach($versions as $k => $v) {
203
                $newVersions[$k] = $this->version;
204
            }
205
        } else {
206
            // otherwise get next patch or minor
207
            $newVersions = $this->getNextVersions($versions, self::PATCH);
208
        }
209
210
        $this->stdout("You are about to prepare a new release for the following things:\n\n");
211
        $this->printWhat($what, $newVersions, $versions);
212
        $this->stdout("\n");
213
214
        $this->stdout("Before you make a release briefly go over the changes and check if you spot obvious mistakes:\n\n", Console::BOLD);
215
        if (strncmp('app-', reset($what), 4) !== 0) {
216
            $this->stdout("- no accidentally added CHANGELOG lines for other versions than this one?\n");
217
            $this->stdout("- are all new `@since` tags for this relase version?\n");
218
        }
219
        $travisUrl = reset($what) === 'framework' ? '' : '-'.reset($what);
220
        $this->stdout("- are unit tests passing on travis? https://travis-ci.org/yiisoft/yii2$travisUrl/builds\n");
221
        $this->stdout("- other issues with code changes?\n");
222
        $this->stdout("- also make sure the milestone on github is complete and no issues or PRs are left open.\n\n");
223
        $this->printWhatUrls($what, $versions);
224
        $this->stdout("\n");
225
226
        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...
227
            $this->stdout("Canceled.\n");
228
            return 1;
229
        }
230
231
        foreach($what as $ext) {
232
            if ($ext === 'framework') {
233
                $this->releaseFramework("{$this->basePath}/framework", $newVersions['framework']);
234
            } elseif (strncmp('app-', $ext, 4) === 0) {
235
                $this->releaseApplication(substr($ext, 4), "{$this->basePath}/apps/" . substr($ext, 4), $newVersions[$ext]);
236
            } else {
237
                $this->releaseExtension($ext, "{$this->basePath}/extensions/$ext", $newVersions[$ext]);
238
            }
239
        }
240
241
        return 0;
242
    }
243
244
    /**
245
     * This will generate application packages for download page.
246
     *
247
     * Usage:
248
     *
249
     * ```
250
     * ./build/build release/package app-basic
251
     * ```
252
     *
253
     * @param array $what what do you want to package? this can either be:
254
     *
255
     * - an application indicated by prefix `app-`, e.g. `app-basic`,
256
     *
257
     * @return int
258
     */
259
    public function actionPackage(array $what)
260
    {
261
        $this->validateWhat($what, ['app']);
262
        $versions = $this->getCurrentVersions($what);
263
264
        $this->stdout("You are about to generate packages for the following things:\n\n");
265
        foreach($what as $ext) {
266
            if (strncmp('app-', $ext, 4) === 0) {
267
                $this->stdout(" - ");
268
                $this->stdout(substr($ext, 4), Console::FG_RED);
269
                $this->stdout(" application version ");
270
            } elseif ($ext === 'framework') {
271
                $this->stdout(" - Yii Framework version ");
272
            } else {
273
                $this->stdout(" - ");
274
                $this->stdout($ext, Console::FG_RED);
275
                $this->stdout(" extension version ");
276
            }
277
            $this->stdout($versions[$ext], Console::BOLD);
278
            $this->stdout("\n");
279
        }
280
        $this->stdout("\n");
281
282
        $packagePath = "{$this->basePath}/packages";
283
        $this->stdout("Packages will be stored in $packagePath\n\n");
284
285
        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...
286
            $this->stdout("Canceled.\n");
287
            return 1;
288
        }
289
290
        foreach($what as $ext) {
291
            if ($ext === 'framework') {
292
                throw new Exception('Can not package framework.');
293
            } elseif (strncmp('app-', $ext, 4) === 0) {
294
                $this->packageApplication(substr($ext, 4), $versions[$ext], $packagePath);
295
            } else {
296
                throw new Exception('Can not package extension.');
297
            }
298
        }
299
300
        $this->stdout("\ndone. verify the versions composer installed above and push it to github!\n\n");
301
302
        return 0;
303
    }
304
305
    /**
306
     * Sorts CHANGELOG for framework or extension.
307
     *
308
     * @param array $what what do you want to resort changelog for? this can either be:
309
     *
310
     * - an extension name such as `redis` or `bootstrap`,
311
     * - or `framework` if you want to release a new version of the framework itself.
312
     */
313
    public function actionSortChangelog(array $what)
314
    {
315
        if (count($what) > 1) {
316
            $this->stdout("Currently only one simultaneous release is supported.\n");
317
            return 1;
318
        }
319
        $this->validateWhat($what, ['framework', 'ext'], false);
320
321
        $version = reset($this->getNextVersions($this->getCurrentVersions($what), self::PATCH));
0 ignored issues
show
Bug introduced by
$this->getNextVersions($...ns($what), self::PATCH) cannot be passed to reset() as the parameter $array expects a reference.
Loading history...
322
        $this->stdout('sorting CHANGELOG of ');
323
        $this->stdout(reset($what), Console::BOLD);
324
        $this->stdout(" for version ");
325
        $this->stdout($version, Console::BOLD);
326
        $this->stdout("...");
327
328
        $this->resortChangelogs($what, $version);
329
330
        $this->stdout("done.\n", Console::BOLD, Console::FG_GREEN);
331
    }
332
333
    protected function printWhat(array $what, $newVersions, $versions)
334
    {
335
        foreach($what as $ext) {
336
            if (strncmp('app-', $ext, 4) === 0) {
337
                $this->stdout(" - ");
338
                $this->stdout(substr($ext, 4), Console::FG_RED);
339
                $this->stdout(" application version ");
340
            } elseif ($ext === 'framework') {
341
                $this->stdout(" - Yii Framework version ");
342
            } else {
343
                $this->stdout(" - ");
344
                $this->stdout($ext, Console::FG_RED);
345
                $this->stdout(" extension version ");
346
            }
347
            $this->stdout($newVersions[$ext], Console::BOLD);
348
            $this->stdout(", last release was {$versions[$ext]}\n");
349
        }
350
    }
351
352
    protected function printWhatUrls(array $what, $oldVersions)
353
    {
354
        foreach($what as $ext) {
355
            if ($ext === 'framework') {
356
                $this->stdout("framework:    https://github.com/yiisoft/yii2-framework/compare/{$oldVersions[$ext]}...master\n");
357
                $this->stdout("app-basic:    https://github.com/yiisoft/yii2-app-basic/compare/{$oldVersions[$ext]}...master\n");
358
                $this->stdout("app-advanced: https://github.com/yiisoft/yii2-app-advanced/compare/{$oldVersions[$ext]}...master\n");
359
            } else {
360
                $this->stdout($ext, Console::FG_RED);
361
                $this->stdout(": https://github.com/yiisoft/yii2-$ext/compare/{$oldVersions[$ext]}...master\n");
362
            }
363
        }
364
    }
365
366
    /**
367
     * @param array $what list of items
368
     * @param array $limit list of things to allow, or empty to allow any, can be `app`, `framework`, `extension`
369
     * @throws \yii\base\Exception
370
     */
371
    protected function validateWhat(array $what, $limit = [], $ensureGitClean = true)
372
    {
373
        foreach($what as $w) {
374
            if (strncmp('app-', $w, 4) === 0) {
375
                if (!empty($limit) && !in_array('app', $limit)) {
376
                    throw new Exception("Only the following types are allowed: ".implode(', ', $limit)."\n");
377
                }
378
                if (!is_dir($appPath = "{$this->basePath}/apps/" . substr($w, 4))) {
379
                    throw new Exception("Application path does not exist: \"{$appPath}\"\n");
380
                }
381
                if ($ensureGitClean) {
382
                    $this->ensureGitClean($appPath);
383
                }
384
            } elseif ($w === 'framework') {
385
                if (!empty($limit) && !in_array('framework', $limit)) {
386
                    throw new Exception("Only the following types are allowed: ".implode(', ', $limit)."\n");
387
                }
388
                if (!is_dir($fwPath = "{$this->basePath}/framework")) {
389
                    throw new Exception("Framework path does not exist: \"{$this->basePath}/framework\"\n");
390
                }
391
                if ($ensureGitClean) {
392
                    $this->ensureGitClean($fwPath);
393
                }
394
            } else {
395
                if (!empty($limit) && !in_array('ext', $limit)) {
396
                    throw new Exception("Only the following types are allowed: ".implode(', ', $limit)."\n");
397
                }
398
                if (!is_dir($extPath = "{$this->basePath}/extensions/$w")) {
399
                    throw new Exception("Extension path for \"$w\" does not exist: \"{$this->basePath}/extensions/$w\"\n");
400
                }
401
                if ($ensureGitClean) {
402
                    $this->ensureGitClean($extPath);
403
                }
404
            }
405
        }
406
    }
407
408
409
    protected function releaseFramework($frameworkPath, $version)
410
    {
411
        $this->stdout("\n");
412
        $this->stdout($h = "Preparing framework release version $version", Console::BOLD);
413
        $this->stdout("\n" . str_repeat('-', strlen($h)) . "\n\n", Console::BOLD);
414
415
        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...
416
            exit(1);
417
        }
418
        $this->runGit('git pull', $frameworkPath);
419
420
        // checks
421
422
        $this->stdout('check if framework composer.json matches yii2-dev composer.json...');
423
        $this->checkComposer($frameworkPath);
424
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
425
426
        // adjustments
427
428
        $this->stdout('prepare classmap...', Console::BOLD);
429
        $this->dryRun || Yii::$app->runAction('classmap', [$frameworkPath]);
430
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
431
432
        $this->stdout('updating mimetype magic file...', Console::BOLD);
433
        $this->dryRun || Yii::$app->runAction('mime-type', ["$frameworkPath/helpers/mimeTypes.php"]);
434
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
435
436
        $this->stdout("fixing various PHPdoc style issues...\n", Console::BOLD);
437
        $this->dryRun || Yii::$app->runAction('php-doc/fix', [$frameworkPath]);
438
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
439
440
        $this->stdout("updating PHPdoc @property annotations...\n", Console::BOLD);
441
        $this->dryRun || Yii::$app->runAction('php-doc/property', [$frameworkPath]);
442
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
443
444
        $this->stdout('sorting changelogs...', Console::BOLD);
445
        $this->dryRun || $this->resortChangelogs(['framework'], $version);
446
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
447
448
        $this->stdout('closing changelogs...', Console::BOLD);
449
        $this->dryRun || $this->closeChangelogs(['framework'], $version);
450
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
451
452
        $this->stdout('updating Yii version...');
453
        $this->dryRun || $this->updateYiiVersion($frameworkPath, $version);
454
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
455
456
        $this->stdout("\nIn the following you can check the above changes using git diff.\n\n");
457
        do {
458
            $this->runGit("git diff --color", $frameworkPath);
459
            $this->stdout("\n\n\nCheck whether the above diff is okay, if not you may change things as needed before continuing.\n");
460
            $this->stdout("You may abort the program with Ctrl + C and reset the changes by running `git checkout -- .` in the repo.\n\n");
461
        } while(!$this->confirm("Type `yes` to continue, `no` to view git diff again. Continue?"));
462
463
        $this->stdout("\n\n");
464
        $this->stdout("    ****          RELEASE TIME!         ****\n", Console::FG_YELLOW, Console::BOLD);
465
        $this->stdout("    ****    Commit, Tag and Push it!    ****\n", Console::FG_YELLOW, Console::BOLD);
466
        $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");
467
468
        $this->runGit("git commit -a -m \"release version $version\"", $frameworkPath);
469
        $this->runGit("git tag -a $version -m \"version $version\"", $frameworkPath);
470
        $this->runGit("git push", $frameworkPath);
471
        $this->runGit("git push --tags", $frameworkPath);
472
473
        $this->stdout("\n\n");
474
        $this->stdout("CONGRATULATIONS! You have just released ", Console::FG_YELLOW, Console::BOLD);
475
        $this->stdout('framework', Console::FG_RED, Console::BOLD);
476
        $this->stdout(" version ", Console::FG_YELLOW, Console::BOLD);
477
        $this->stdout($version, Console::BOLD);
478
        $this->stdout("!\n\n", Console::FG_YELLOW, Console::BOLD);
479
480
        // TODO release applications
481
        // $this->composerSetStability($what, $version);
482
483
484
//        $this->resortChangelogs($what, $version);
485
  //        $this->closeChangelogs($what, $version);
486
  //        $this->composerSetStability($what, $version);
487
  //        if (in_array('framework', $what)) {
488
  //            $this->updateYiiVersion($version);
489
  //        }
490
491
492
        // if done:
493
        //     * ./build/build release/done framework 2.0.0-dev 2.0.0-rc
494
        //     * ./build/build release/done redis 2.0.0-dev 2.0.0-rc
495
//            $this->openChangelogs($what, $nextVersion);
496
//            $this->composerSetStability($what, 'dev');
497
//            if (in_array('framework', $what)) {
498
//                $this->updateYiiVersion($devVersion);
499
//            }
500
501
502
503
        // prepare next release
504
505
        $this->stdout("Time to prepare the next release...\n\n", Console::FG_YELLOW, Console::BOLD);
506
507
        $this->stdout('opening changelogs...', Console::BOLD);
508
        $nextVersion = $this->getNextVersions(['framework' => $version], self::PATCH); // TODO support other versions
509
        $this->dryRun || $this->openChangelogs(['framework'], $nextVersion['framework']);
510
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
511
512
        $this->stdout('updating Yii version...');
513
        $this->dryRun || $this->updateYiiVersion($frameworkPath, $nextVersion['framework'] . '-dev');
514
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
515
516
517
        $this->stdout("\n");
518
        $this->runGit("git diff --color", $frameworkPath);
519
        $this->stdout("\n\n");
520
        $this->runGit("git commit -a -m \"prepare for next release\"", $frameworkPath);
521
        $this->runGit("git push", $frameworkPath);
522
523
        $this->stdout("\n\nDONE!", Console::FG_YELLOW, Console::BOLD);
524
525
        $this->stdout("\n\nThe following steps are left for you to do manually:\n\n");
526
        $nextVersion2 = $this->getNextVersions($nextVersion, self::PATCH); // TODO support other versions
527
        $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");
528
        $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");
529
        $this->stdout("- create a release on github.\n");
530
        $this->stdout("- release news and announcement.\n");
531
        $this->stdout("- update the website (will be automated soon and is only relevant for the new website).\n");
532
        $this->stdout("\n");
533
        $this->stdout("- release applications: ./build/build release app-basic\n");
534
        $this->stdout("- release applications: ./build/build release app-advanced\n");
535
536
        $this->stdout("\n");
537
538
    }
539
540
    protected function releaseApplication($name, $path, $version)
541
    {
542
        $this->stdout("\n");
543
        $this->stdout($h = "Preparing release for application  $name  version $version", Console::BOLD);
544
        $this->stdout("\n" . str_repeat('-', strlen($h)) . "\n\n", Console::BOLD);
545
546
        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...
547
            exit(1);
548
        }
549
        $this->runGit('git pull', $path);
550
551
        // adjustments
552
553
        $this->stdout("fixing various PHPdoc style issues...\n", Console::BOLD);
554
        $this->setAppAliases($name, $path);
555
        $this->dryRun || Yii::$app->runAction('php-doc/fix', [$path, 'skipFrameworkRequirements' => true]);
556
        $this->resetAppAliases();
557
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
558
559
        $this->stdout("updating PHPdoc @property annotations...\n", Console::BOLD);
560
        $this->setAppAliases($name, $path);
561
        $this->dryRun || Yii::$app->runAction('php-doc/property', [$path, 'skipFrameworkRequirements' => true]);
562
        $this->resetAppAliases();
563
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
564
565
        $this->stdout("updating composer stability...\n", Console::BOLD);
566
        $this->dryRun || $this->composerSetStability(["app-$name"], $version);
567
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
568
569
        $this->stdout("\nIn the following you can check the above changes using git diff.\n\n");
570
        do {
571
            $this->runGit("git diff --color", $path);
572
            $this->stdout("\n\n\nCheck whether the above diff is okay, if not you may change things as needed before continuing.\n");
573
            $this->stdout("You may abort the program with Ctrl + C and reset the changes by running `git checkout -- .` in the repo.\n\n");
574
        } while(!$this->confirm("Type `yes` to continue, `no` to view git diff again. Continue?"));
575
576
        $this->stdout("\n\n");
577
        $this->stdout("    ****          RELEASE TIME!         ****\n", Console::FG_YELLOW, Console::BOLD);
578
        $this->stdout("    ****    Commit, Tag and Push it!    ****\n", Console::FG_YELLOW, Console::BOLD);
579
        $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");
580
581
        $this->runGit("git commit -a -m \"release version $version\"", $path);
582
        $this->runGit("git tag -a $version -m \"version $version\"", $path);
583
        $this->runGit("git push", $path);
584
        $this->runGit("git push --tags", $path);
585
586
        $this->stdout("\n\n");
587
        $this->stdout("CONGRATULATIONS! You have just released application ", Console::FG_YELLOW, Console::BOLD);
588
        $this->stdout($name, Console::FG_RED, Console::BOLD);
589
        $this->stdout(" version ", Console::FG_YELLOW, Console::BOLD);
590
        $this->stdout($version, Console::BOLD);
591
        $this->stdout("!\n\n", Console::FG_YELLOW, Console::BOLD);
592
593
        // prepare next release
594
595
        $this->stdout("Time to prepare the next release...\n\n", Console::FG_YELLOW, Console::BOLD);
596
597
        $this->stdout("updating composer stability...\n", Console::BOLD);
598
        $this->dryRun || $this->composerSetStability(["app-$name"], 'dev');
599
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
600
601
        $nextVersion = $this->getNextVersions(["app-$name" => $version], self::PATCH); // TODO support other versions
602
603
        $this->stdout("\n");
604
        $this->runGit("git diff --color", $path);
605
        $this->stdout("\n\n");
606
        $this->runGit("git commit -a -m \"prepare for next release\"", $path);
607
        $this->runGit("git push", $path);
608
609
        $this->stdout("\n\nDONE!", Console::FG_YELLOW, Console::BOLD);
610
611
        $this->stdout("\n\nThe following steps are left for you to do manually:\n\n");
612
        $nextVersion2 = $this->getNextVersions($nextVersion, self::PATCH); // TODO support other versions
613
        $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");
614
        $this->stdout("- Create Application packages and upload them to github:  ./build release/package app-$name\n");
615
616
        $this->stdout("\n");
617
    }
618
619
    private $_oldAlias;
620
621
    protected function setAppAliases($app, $path)
622
    {
623
        $this->_oldAlias = Yii::getAlias('@app');
624
        switch($app) {
625
            case 'basic':
626
                Yii::setAlias('@app', $path);
627
                break;
628
            case 'advanced':
629
                // setup @frontend, @backend etc...
630
                require("$path/common/config/bootstrap.php");
631
                break;
632
        }
633
    }
634
635
    protected function resetAppAliases()
636
    {
637
        Yii::setAlias('@app', $this->_oldAlias);
0 ignored issues
show
Bug introduced by
It seems like $this->_oldAlias can also be of type boolean; however, yii\BaseYii::setAlias() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
638
    }
639
640
    protected function packageApplication($name, $version, $packagePath)
641
    {
642
        FileHelper::createDirectory($packagePath);
643
644
        $this->runCommand("composer create-project yiisoft/yii2-app-$name $name $version", $packagePath);
645
        // clear cookie validation key in basic app
646
        if (is_file($configFile = "$packagePath/$name/config/web.php")) {
647
            $this->sed(
648
                "/'cookieValidationKey' => '.*?',/",
649
                "'cookieValidationKey' => '',",
650
                $configFile
651
            );
652
        }
653
        $this->runCommand("tar zcf yii-$name-app-$version.tgz $name", $packagePath);
654
    }
655
656
    protected function releaseExtension($name, $path, $version)
657
    {
658
        $this->stdout("\n");
659
        $this->stdout($h = "Preparing release for extension  $name  version $version", Console::BOLD);
660
        $this->stdout("\n" . str_repeat('-', strlen($h)) . "\n\n", Console::BOLD);
661
662
        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...
663
            exit(1);
664
        }
665
        $this->runGit('git pull', $path);
666
667
        // adjustments
668
669
        $this->stdout("fixing various PHPdoc style issues...\n", Console::BOLD);
670
        $this->dryRun || Yii::$app->runAction('php-doc/fix', [$path]);
671
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
672
673
        $this->stdout("updating PHPdoc @property annotations...\n", Console::BOLD);
674
        $this->dryRun || Yii::$app->runAction('php-doc/property', [$path]);
675
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
676
677
        $this->stdout('sorting changelogs...', Console::BOLD);
678
        $this->dryRun || $this->resortChangelogs([$name], $version);
679
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
680
681
        $this->stdout('closing changelogs...', Console::BOLD);
682
        $this->dryRun || $this->closeChangelogs([$name], $version);
683
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
684
685
        $this->stdout("\nIn the following you can check the above changes using git diff.\n\n");
686
        do {
687
            $this->runGit("git diff --color", $path);
688
            $this->stdout("\n\n\nCheck whether the above diff is okay, if not you may change things as needed before continuing.\n");
689
            $this->stdout("You may abort the program with Ctrl + C and reset the changes by running `git checkout -- .` in the repo.\n\n");
690
        } while(!$this->confirm("Type `yes` to continue, `no` to view git diff again. Continue?"));
691
692
        $this->stdout("\n\n");
693
        $this->stdout("    ****          RELEASE TIME!         ****\n", Console::FG_YELLOW, Console::BOLD);
694
        $this->stdout("    ****    Commit, Tag and Push it!    ****\n", Console::FG_YELLOW, Console::BOLD);
695
        $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");
696
697
        $this->runGit("git commit -a -m \"release version $version\"", $path);
698
        $this->runGit("git tag -a $version -m \"version $version\"", $path);
699
        $this->runGit("git push", $path);
700
        $this->runGit("git push --tags", $path);
701
702
        $this->stdout("\n\n");
703
        $this->stdout("CONGRATULATIONS! You have just released extension ", Console::FG_YELLOW, Console::BOLD);
704
        $this->stdout($name, Console::FG_RED, Console::BOLD);
705
        $this->stdout(" version ", Console::FG_YELLOW, Console::BOLD);
706
        $this->stdout($version, Console::BOLD);
707
        $this->stdout("!\n\n", Console::FG_YELLOW, Console::BOLD);
708
709
        // prepare next release
710
711
        $this->stdout("Time to prepare the next release...\n\n", Console::FG_YELLOW, Console::BOLD);
712
713
        $this->stdout('opening changelogs...', Console::BOLD);
714
        $nextVersion = $this->getNextVersions([$name => $version], self::PATCH); // TODO support other versions
715
        $this->dryRun || $this->openChangelogs([$name], $nextVersion[$name]);
716
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
717
718
        $this->stdout("\n");
719
        $this->runGit("git diff --color", $path);
720
        $this->stdout("\n\n");
721
        $this->runGit("git commit -a -m \"prepare for next release\"", $path);
722
        $this->runGit("git push", $path);
723
724
        $this->stdout("\n\nDONE!", Console::FG_YELLOW, Console::BOLD);
725
726
        $this->stdout("\n\nThe following steps are left for you to do manually:\n\n");
727
        $nextVersion2 = $this->getNextVersions($nextVersion, self::PATCH); // TODO support other versions
728
        $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");
729
        $this->stdout("- release news and announcement.\n");
730
        $this->stdout("- update the website (will be automated soon and is only relevant for the new website).\n");
731
732
        $this->stdout("\n");
733
    }
734
735
736
    protected function runCommand($cmd, $path)
737
    {
738
        $this->stdout("running  $cmd  ...", Console::BOLD);
739
        if ($this->dryRun) {
740
            $this->stdout("dry run, command `$cmd` not executed.\n");
741
            return;
742
        }
743
        chdir($path);
744
        exec($cmd, $output, $ret);
745
        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...
746
            echo implode("\n", $output);
747
            throw new Exception("Command \"$cmd\" failed with code " . $ret);
748
        }
749
        $this->stdout("\ndone.\n", Console::BOLD, Console::FG_GREEN);
750
    }
751
752
    protected function runGit($cmd, $path)
753
    {
754
        if ($this->confirm("Run `$cmd`?", true)) {
755
            if ($this->dryRun) {
756
                $this->stdout("dry run, command `$cmd` not executed.\n");
757
                return;
758
            }
759
            chdir($path);
760
            exec($cmd, $output, $ret);
761
            echo implode("\n", $output);
762
            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...
763
                throw new Exception("Command \"$cmd\" failed with code " . $ret);
764
            }
765
            echo "\n";
766
        }
767
    }
768
769
    protected function ensureGitClean($path)
770
    {
771
        chdir($path);
772
        exec('git status --porcelain -uno', $changes, $ret);
773
        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...
774
            throw new Exception('Command "git status --porcelain -uno" failed with code ' . $ret);
775
        }
776
        if (!empty($changes)) {
777
            throw new Exception("You have uncommitted changes in $path: " . print_r($changes, true));
778
        }
779
    }
780
781
    protected function gitFetchTags($path)
782
    {
783
        chdir($path);
784
        exec('git fetch --tags', $output, $ret);
785
        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...
786
            throw new Exception('Command "git fetch --tags" failed with code ' . $ret);
787
        }
788
    }
789
790
791
    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...
792
    {
793
        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...
794
            exit;
795
        }
796
    }
797
798
799
    protected function closeChangelogs($what, $version)
800
    {
801
        $v = str_replace('\\-', '[\\- ]', preg_quote($version, '/'));
802
        $headline = $version . ' ' . date('F d, Y');
803
        $this->sed(
804
            '/'.$v.' under development\n(-+?)\n/',
805
            $headline . "\n" . str_repeat('-', strlen($headline)) . "\n",
806
            $this->getChangelogs($what)
807
        );
808
    }
809
810
    protected function openChangelogs($what, $version)
811
    {
812
        $headline = "\n$version under development\n";
813
        $headline .= str_repeat('-', strlen($headline) - 2) . "\n\n- no changes in this release.\n";
814
        foreach($this->getChangelogs($what) as $file) {
815
            $lines = explode("\n", file_get_contents($file));
816
            $hl = [
817
                array_shift($lines),
818
                array_shift($lines),
819
            ];
820
            array_unshift($lines, $headline);
821
822
            file_put_contents($file, implode("\n", array_merge($hl, $lines)));
823
        }
824
    }
825
826
    protected function resortChangelogs($what, $version)
827
    {
828
        foreach($this->getChangelogs($what) as $file) {
829
            // split the file into relevant parts
830
            list($start, $changelog, $end) = $this->splitChangelog($file, $version);
831
            $changelog = $this->resortChangelog($changelog);
832
            file_put_contents($file, implode("\n", array_merge($start, $changelog, $end)));
833
        }
834
    }
835
836
    /**
837
     * Extract changelog content for a specific version
838
     */
839
    protected function splitChangelog($file, $version)
840
    {
841
        $lines = explode("\n", file_get_contents($file));
842
843
        // split the file into relevant parts
844
        $start = [];
845
        $changelog = [];
846
        $end = [];
847
848
        $state = 'start';
849
        foreach($lines as $l => $line) {
850
            // starting from the changelogs headline
851
            if (isset($lines[$l-2]) && strpos($lines[$l-2], $version) !== false &&
852
                isset($lines[$l-1]) && strncmp($lines[$l-1], '---', 3) === 0) {
853
                $state = 'changelog';
854
            }
855
            if ($state === 'changelog' && isset($lines[$l+1]) && strncmp($lines[$l+1], '---', 3) === 0) {
856
                $state = 'end';
857
            }
858
            ${$state}[] = $line;
859
        }
860
        return [$start, $changelog, $end];
861
    }
862
863
    /**
864
     * Ensure sorting of the changelog lines
865
     */
866
    protected function resortChangelog($changelog)
867
    {
868
        // cleanup whitespace
869
        foreach($changelog as $i => $line) {
870
            $changelog[$i] = rtrim($line);
871
        }
872
        $changelog = array_filter($changelog);
873
874
        $i = 0;
875
        ArrayHelper::multisort($changelog, function($line) use (&$i) {
876
            if (preg_match('/^- (Chg|Enh|Bug|New)( #\d+(, #\d+)*)?: .+$/', $line, $m)) {
877
                $o = ['Bug' => 'C', 'Enh' => 'D', 'Chg' => 'E', 'New' => 'F'];
878
                return $o[$m[1]] . ' ' . (!empty($m[2]) ? $m[2] : 'AAAA' . $i++);
879
            }
880
            return 'B' . $i++;
881
        }, SORT_ASC, SORT_NATURAL);
882
883
        // re-add leading and trailing lines
884
        array_unshift($changelog, '');
885
        $changelog[] = '';
886
        $changelog[] = '';
887
888
        return $changelog;
889
    }
890
891
    protected function getChangelogs($what)
892
    {
893
        $changelogs = [];
894
        if (in_array('framework', $what)) {
895
            $changelogs[] = $this->getFrameworkChangelog();
896
        }
897
898
        return array_merge($changelogs, $this->getExtensionChangelogs($what));
899
    }
900
901
    protected function getFrameworkChangelog()
902
    {
903
        return $this->basePath . '/framework/CHANGELOG.md';
904
    }
905
906
    protected function getExtensionChangelogs($what)
907
    {
908
        return array_filter(glob($this->basePath . '/extensions/*/CHANGELOG.md'), function($elem) use ($what) {
909
            foreach($what as $ext) {
910
                if (strpos($elem, "extensions/$ext/CHANGELOG.md") !== false) {
911
                    return true;
912
                }
913
            }
914
            return false;
915
        });
916
    }
917
918
    protected function composerSetStability($what, $version)
919
    {
920
        $apps = [];
921
        if (in_array('app-advanced', $what)) {
922
            $apps[] = $this->basePath . '/apps/advanced/composer.json';
923
        }
924
        if (in_array('app-basic', $what)) {
925
            $apps[] = $this->basePath . '/apps/basic/composer.json';
926
        }
927
        if (in_array('app-benchmark', $what)) {
928
            $apps[] = $this->basePath . '/apps/benchmark/composer.json';
929
        }
930
        if (empty($apps)) {
931
            return;
932
        }
933
934
        $stability = 'stable';
935
        if (strpos($version, 'alpha') !== false) {
936
            $stability = 'alpha';
937
        } elseif (strpos($version, 'beta') !== false) {
938
            $stability = 'beta';
939
        } elseif (strpos($version, 'rc') !== false) {
940
            $stability = 'RC';
941
        } elseif (strpos($version, 'dev') !== false) {
942
            $stability = 'dev';
943
        }
944
945
        $this->sed(
946
            '/"minimum-stability": "(.+?)",/',
947
            '"minimum-stability": "' . $stability . '",',
948
            $apps
949
        );
950
    }
951
952
    protected function updateYiiVersion($frameworkPath, $version)
953
    {
954
        $this->sed(
955
            '/function getVersion\(\)\n    \{\n        return \'(.+?)\';/',
956
            "function getVersion()\n    {\n        return '$version';",
957
            $frameworkPath . '/BaseYii.php');
958
    }
959
960
    protected function sed($pattern, $replace, $files)
961
    {
962
        foreach((array) $files as $file) {
963
            file_put_contents($file, preg_replace($pattern, $replace, file_get_contents($file)));
964
        }
965
    }
966
967
    protected function getCurrentVersions(array $what)
968
    {
969
        $versions = [];
970
        foreach($what as $ext) {
971
            if ($ext === 'framework') {
972
                chdir("{$this->basePath}/framework");
973
            } elseif (strncmp('app-', $ext, 4) === 0) {
974
                chdir("{$this->basePath}/apps/" . substr($ext, 4));
975
            } else {
976
                chdir("{$this->basePath}/extensions/$ext");
977
            }
978
            $tags = [];
979
            exec('git tag', $tags, $ret);
980
            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...
981
                throw new Exception('Command "git tag" failed with code ' . $ret);
982
            }
983
            rsort($tags, SORT_NATURAL); // TODO this can not deal with alpha/beta/rc...
984
            $versions[$ext] = reset($tags);
985
        }
986
        return $versions;
987
    }
988
989
    const MINOR = 'minor';
990
    const PATCH = 'patch';
991
992
    protected function getNextVersions(array $versions, $type)
993
    {
994
        foreach($versions as $k => $v) {
995
            if (empty($v)) {
996
                $versions[$k] = '2.0.0';
997
                continue;
998
            }
999
            $parts = explode('.', $v);
1000
            switch($type) {
1001
                case self::MINOR:
1002
                    $parts[1]++;
1003
                    $parts[2] = 0;
1004
                    break;
1005
                case self::PATCH:
1006
                    $parts[2]++;
1007
                    break;
1008
                default:
1009
                    throw new Exception('Unknown version type.');
1010
            }
1011
            $versions[$k] = implode('.', $parts);
1012
        }
1013
        return $versions;
1014
    }
1015
}
1016