Completed
Push — master ( 0e6c66...89f66a )
by Carsten
29:42 queued 19:52
created

ReleaseController::getNextVersions()   C

Complexity

Conditions 7
Paths 7

Size

Total Lines 29
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 29
rs 6.7272
c 0
b 0
f 0
cc 7
eloc 22
nc 7
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
     * @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
        $gitDir = reset($what) === 'framework' ? 'framework/' : '';
216
        $gitVersion = $versions[reset($what)];
217
        if (strncmp('app-', reset($what), 4) !== 0) {
218
            $this->stdout("- no accidentally added CHANGELOG lines for other versions than this one?\n\n    git diff $gitVersion.. ${gitDir}CHANGELOG.md\n\n");
219
            $this->stdout("- are all new `@since` tags for this relase version?\n");
220
        }
221
        $this->stdout("- other issues with code changes?\n\n    git diff -w $gitVersion.. ${gitDir}\n\n");
222
        $travisUrl = reset($what) === 'framework' ? '' : '-'.reset($what);
223
        $this->stdout("- are unit tests passing on travis? https://travis-ci.org/yiisoft/yii2$travisUrl/builds\n");
224
        $this->stdout("- also make sure the milestone on github is complete and no issues or PRs are left open.\n\n");
225
        $this->printWhatUrls($what, $versions);
226
        $this->stdout("\n");
227
228
        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...
229
            $this->stdout("Canceled.\n");
230
            return 1;
231
        }
232
233
        foreach($what as $ext) {
234
            if ($ext === 'framework') {
235
                $this->releaseFramework("{$this->basePath}/framework", $newVersions['framework']);
236
            } elseif (strncmp('app-', $ext, 4) === 0) {
237
                $this->releaseApplication(substr($ext, 4), "{$this->basePath}/apps/" . substr($ext, 4), $newVersions[$ext]);
238
            } else {
239
                $this->releaseExtension($ext, "{$this->basePath}/extensions/$ext", $newVersions[$ext]);
240
            }
241
        }
242
243
        return 0;
244
    }
245
246
    /**
247
     * This will generate application packages for download page.
248
     *
249
     * Usage:
250
     *
251
     * ```
252
     * ./build/build release/package app-basic
253
     * ```
254
     *
255
     * @param array $what what do you want to package? this can either be:
256
     *
257
     * - an application indicated by prefix `app-`, e.g. `app-basic`,
258
     *
259
     * @return int
260
     */
261
    public function actionPackage(array $what)
262
    {
263
        $this->validateWhat($what, ['app']);
264
        $versions = $this->getCurrentVersions($what);
265
266
        $this->stdout("You are about to generate packages for the following things:\n\n");
267
        foreach($what as $ext) {
268
            if (strncmp('app-', $ext, 4) === 0) {
269
                $this->stdout(" - ");
270
                $this->stdout(substr($ext, 4), Console::FG_RED);
271
                $this->stdout(" application version ");
272
            } elseif ($ext === 'framework') {
273
                $this->stdout(" - Yii Framework version ");
274
            } else {
275
                $this->stdout(" - ");
276
                $this->stdout($ext, Console::FG_RED);
277
                $this->stdout(" extension version ");
278
            }
279
            $this->stdout($versions[$ext], Console::BOLD);
280
            $this->stdout("\n");
281
        }
282
        $this->stdout("\n");
283
284
        $packagePath = "{$this->basePath}/packages";
285
        $this->stdout("Packages will be stored in $packagePath\n\n");
286
287
        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...
288
            $this->stdout("Canceled.\n");
289
            return 1;
290
        }
291
292
        foreach($what as $ext) {
293
            if ($ext === 'framework') {
294
                throw new Exception('Can not package framework.');
295
            } elseif (strncmp('app-', $ext, 4) === 0) {
296
                $this->packageApplication(substr($ext, 4), $versions[$ext], $packagePath);
297
            } else {
298
                throw new Exception('Can not package extension.');
299
            }
300
        }
301
302
        $this->stdout("\ndone. verify the versions composer installed above and push it to github!\n\n");
303
304
        return 0;
305
    }
306
307
    /**
308
     * Sorts CHANGELOG for framework or extension.
309
     *
310
     * @param array $what what do you want to resort changelog for? this can either be:
311
     *
312
     * - an extension name such as `redis` or `bootstrap`,
313
     * - or `framework` if you want to release a new version of the framework itself.
314
     */
315
    public function actionSortChangelog(array $what)
316
    {
317
        if (count($what) > 1) {
318
            $this->stdout("Currently only one simultaneous release is supported.\n");
319
            return 1;
320
        }
321
        $this->validateWhat($what, ['framework', 'ext'], false);
322
323
        $version = array_values($this->getNextVersions($this->getCurrentVersions($what), self::PATCH))[0];
324
        $this->stdout('sorting CHANGELOG of ');
325
        $this->stdout(reset($what), Console::BOLD);
326
        $this->stdout(" for version ");
327
        $this->stdout($version, Console::BOLD);
328
        $this->stdout("...");
329
330
        $this->resortChangelogs($what, $version);
331
332
        $this->stdout("done.\n", Console::BOLD, Console::FG_GREEN);
333
    }
334
335
    protected function printWhat(array $what, $newVersions, $versions)
336
    {
337
        foreach($what as $ext) {
338
            if (strncmp('app-', $ext, 4) === 0) {
339
                $this->stdout(" - ");
340
                $this->stdout(substr($ext, 4), Console::FG_RED);
341
                $this->stdout(" application version ");
342
            } elseif ($ext === 'framework') {
343
                $this->stdout(" - Yii Framework version ");
344
            } else {
345
                $this->stdout(" - ");
346
                $this->stdout($ext, Console::FG_RED);
347
                $this->stdout(" extension version ");
348
            }
349
            $this->stdout($newVersions[$ext], Console::BOLD);
350
            $this->stdout(", last release was {$versions[$ext]}\n");
351
        }
352
    }
353
354
    protected function printWhatUrls(array $what, $oldVersions)
355
    {
356
        foreach($what as $ext) {
357
            if ($ext === 'framework') {
358
                $this->stdout("framework:    https://github.com/yiisoft/yii2-framework/compare/{$oldVersions[$ext]}...master\n");
359
                $this->stdout("app-basic:    https://github.com/yiisoft/yii2-app-basic/compare/{$oldVersions[$ext]}...master\n");
360
                $this->stdout("app-advanced: https://github.com/yiisoft/yii2-app-advanced/compare/{$oldVersions[$ext]}...master\n");
361
            } else {
362
                $this->stdout($ext, Console::FG_RED);
363
                $this->stdout(": https://github.com/yiisoft/yii2-$ext/compare/{$oldVersions[$ext]}...master\n");
364
            }
365
        }
366
    }
367
368
    /**
369
     * @param array $what list of items
370
     * @param array $limit list of things to allow, or empty to allow any, can be `app`, `framework`, `extension`
371
     * @throws \yii\base\Exception
372
     */
373
    protected function validateWhat(array $what, $limit = [], $ensureGitClean = true)
374
    {
375
        foreach($what as $w) {
376
            if (strncmp('app-', $w, 4) === 0) {
377
                if (!empty($limit) && !in_array('app', $limit)) {
378
                    throw new Exception("Only the following types are allowed: ".implode(', ', $limit)."\n");
379
                }
380
                if (!is_dir($appPath = "{$this->basePath}/apps/" . substr($w, 4))) {
381
                    throw new Exception("Application path does not exist: \"{$appPath}\"\n");
382
                }
383
                if ($ensureGitClean) {
384
                    $this->ensureGitClean($appPath);
385
                }
386
            } elseif ($w === 'framework') {
387
                if (!empty($limit) && !in_array('framework', $limit)) {
388
                    throw new Exception("Only the following types are allowed: ".implode(', ', $limit)."\n");
389
                }
390
                if (!is_dir($fwPath = "{$this->basePath}/framework")) {
391
                    throw new Exception("Framework path does not exist: \"{$this->basePath}/framework\"\n");
392
                }
393
                if ($ensureGitClean) {
394
                    $this->ensureGitClean($fwPath);
395
                }
396
            } else {
397
                if (!empty($limit) && !in_array('ext', $limit)) {
398
                    throw new Exception("Only the following types are allowed: ".implode(', ', $limit)."\n");
399
                }
400
                if (!is_dir($extPath = "{$this->basePath}/extensions/$w")) {
401
                    throw new Exception("Extension path for \"$w\" does not exist: \"{$this->basePath}/extensions/$w\"\n");
402
                }
403
                if ($ensureGitClean) {
404
                    $this->ensureGitClean($extPath);
405
                }
406
            }
407
        }
408
    }
409
410
411
    protected function releaseFramework($frameworkPath, $version)
412
    {
413
        $this->stdout("\n");
414
        $this->stdout($h = "Preparing framework release version $version", Console::BOLD);
415
        $this->stdout("\n" . str_repeat('-', strlen($h)) . "\n\n", Console::BOLD);
416
417
        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...
418
            exit(1);
419
        }
420
        $this->runGit('git pull', $frameworkPath);
421
422
        // checks
423
424
        $this->stdout('check if framework composer.json matches yii2-dev composer.json...');
425
        $this->checkComposer($frameworkPath);
426
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
427
428
        // adjustments
429
430
        $this->stdout('prepare classmap...', Console::BOLD);
431
        $this->dryRun || Yii::$app->runAction('classmap', [$frameworkPath]);
432
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
433
434
        $this->stdout('updating mimetype magic file...', Console::BOLD);
435
        $this->dryRun || Yii::$app->runAction('mime-type', ["$frameworkPath/helpers/mimeTypes.php"]);
436
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
437
438
        $this->stdout("fixing various PHPdoc style issues...\n", Console::BOLD);
439
        $this->dryRun || Yii::$app->runAction('php-doc/fix', [$frameworkPath]);
440
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
441
442
        $this->stdout("updating PHPdoc @property annotations...\n", Console::BOLD);
443
        $this->dryRun || Yii::$app->runAction('php-doc/property', [$frameworkPath]);
444
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
445
446
        $this->stdout('sorting changelogs...', Console::BOLD);
447
        $this->dryRun || $this->resortChangelogs(['framework'], $version);
448
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
449
450
        $this->stdout('closing changelogs...', Console::BOLD);
451
        $this->dryRun || $this->closeChangelogs(['framework'], $version);
452
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
453
454
        $this->stdout('updating Yii version...');
455
        $this->dryRun || $this->updateYiiVersion($frameworkPath, $version);
456
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
457
458
        $this->stdout("\nIn the following you can check the above changes using git diff.\n\n");
459
        do {
460
            $this->runGit("git diff --color", $frameworkPath);
461
            $this->stdout("\n\n\nCheck whether the above diff is okay, if not you may change things as needed before continuing.\n");
462
            $this->stdout("You may abort the program with Ctrl + C and reset the changes by running `git checkout -- .` in the repo.\n\n");
463
        } while(!$this->confirm("Type `yes` to continue, `no` to view git diff again. Continue?"));
464
465
        $this->stdout("\n\n");
466
        $this->stdout("    ****          RELEASE TIME!         ****\n", Console::FG_YELLOW, Console::BOLD);
467
        $this->stdout("    ****    Commit, Tag and Push it!    ****\n", Console::FG_YELLOW, Console::BOLD);
468
        $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");
469
470
        $this->stdout("Make sure to have your git set up for GPG signing. The following tag and commit should be signed.\n\n");
471
472
        $this->runGit("git commit -S -a -m \"release version $version\"", $frameworkPath);
473
        $this->runGit("git tag -s $version -m \"version $version\"", $frameworkPath);
474
        $this->runGit("git push", $frameworkPath);
475
        $this->runGit("git push --tags", $frameworkPath);
476
477
        $this->stdout("\n\n");
478
        $this->stdout("CONGRATULATIONS! You have just released ", Console::FG_YELLOW, Console::BOLD);
479
        $this->stdout('framework', Console::FG_RED, Console::BOLD);
480
        $this->stdout(" version ", Console::FG_YELLOW, Console::BOLD);
481
        $this->stdout($version, Console::BOLD);
482
        $this->stdout("!\n\n", Console::FG_YELLOW, Console::BOLD);
483
484
        // TODO release applications
485
        // $this->composerSetStability($what, $version);
486
487
488
//        $this->resortChangelogs($what, $version);
489
  //        $this->closeChangelogs($what, $version);
490
  //        $this->composerSetStability($what, $version);
491
  //        if (in_array('framework', $what)) {
492
  //            $this->updateYiiVersion($version);
493
  //        }
494
495
496
        // if done:
497
        //     * ./build/build release/done framework 2.0.0-dev 2.0.0-rc
498
        //     * ./build/build release/done redis 2.0.0-dev 2.0.0-rc
499
//            $this->openChangelogs($what, $nextVersion);
500
//            $this->composerSetStability($what, 'dev');
501
//            if (in_array('framework', $what)) {
502
//                $this->updateYiiVersion($devVersion);
503
//            }
504
505
506
507
        // prepare next release
508
509
        $this->stdout("Time to prepare the next release...\n\n", Console::FG_YELLOW, Console::BOLD);
510
511
        $this->stdout('opening changelogs...', Console::BOLD);
512
        $nextVersion = $this->getNextVersions(['framework' => $version], self::PATCH); // TODO support other versions
513
        $this->dryRun || $this->openChangelogs(['framework'], $nextVersion['framework']);
514
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
515
516
        $this->stdout('updating Yii version...');
517
        $this->dryRun || $this->updateYiiVersion($frameworkPath, $nextVersion['framework'] . '-dev');
518
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
519
520
521
        $this->stdout("\n");
522
        $this->runGit("git diff --color", $frameworkPath);
523
        $this->stdout("\n\n");
524
        $this->runGit("git commit -a -m \"prepare for next release\"", $frameworkPath);
525
        $this->runGit("git push", $frameworkPath);
526
527
        $this->stdout("\n\nDONE!", Console::FG_YELLOW, Console::BOLD);
528
529
        $this->stdout("\n\nThe following steps are left for you to do manually:\n\n");
530
        $nextVersion2 = $this->getNextVersions($nextVersion, self::PATCH); // TODO support other versions
531
        $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");
532
        $this->stdout("    git clone [email protected]:yiisoft/yii2-framework.git\n");
533
        $this->stdout("    cd yii2-framework/\n");
534
        $this->stdout("    export RELEASECOMMIT=$(git log --oneline |grep $version |grep -Po \"^[0-9a-f]+\")\n");
535
        $this->stdout("    git tag -s $version -m \"version $version\" \$RELEASECOMMIT\n");
536
        $this->stdout("    git tag --verify $version\n");
537
        $this->stdout("    git push --tags\n\n");
538
        $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");
539
        $this->stdout("- create a release on github.\n");
540
        $this->stdout("- release news and announcement.\n");
541
        $this->stdout("- update the website (will be automated soon and is only relevant for the new website).\n");
542
        $this->stdout("\n");
543
        $this->stdout("- release applications: ./build/build release app-basic\n");
544
        $this->stdout("- release applications: ./build/build release app-advanced\n");
545
546
        $this->stdout("\n");
547
548
    }
549
550
    protected function releaseApplication($name, $path, $version)
551
    {
552
        $this->stdout("\n");
553
        $this->stdout($h = "Preparing release for application  $name  version $version", Console::BOLD);
554
        $this->stdout("\n" . str_repeat('-', strlen($h)) . "\n\n", Console::BOLD);
555
556
        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...
557
            exit(1);
558
        }
559
        $this->runGit('git pull', $path);
560
561
        // adjustments
562
563
        $this->stdout("fixing various PHPdoc style issues...\n", Console::BOLD);
564
        $this->setAppAliases($name, $path);
565
        $this->dryRun || Yii::$app->runAction('php-doc/fix', [$path, 'skipFrameworkRequirements' => true]);
566
        $this->resetAppAliases();
567
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
568
569
        $this->stdout("updating PHPdoc @property annotations...\n", Console::BOLD);
570
        $this->setAppAliases($name, $path);
571
        $this->dryRun || Yii::$app->runAction('php-doc/property', [$path, 'skipFrameworkRequirements' => true]);
572
        $this->resetAppAliases();
573
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
574
575
        $this->stdout("updating composer stability...\n", Console::BOLD);
576
        $this->dryRun || $this->composerSetStability(["app-$name"], $version);
577
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
578
579
        $this->stdout("\nIn the following you can check the above changes using git diff.\n\n");
580
        do {
581
            $this->runGit("git diff --color", $path);
582
            $this->stdout("\n\n\nCheck whether the above diff is okay, if not you may change things as needed before continuing.\n");
583
            $this->stdout("You may abort the program with Ctrl + C and reset the changes by running `git checkout -- .` in the repo.\n\n");
584
        } while(!$this->confirm("Type `yes` to continue, `no` to view git diff again. Continue?"));
585
586
        $this->stdout("\n\n");
587
        $this->stdout("    ****          RELEASE TIME!         ****\n", Console::FG_YELLOW, Console::BOLD);
588
        $this->stdout("    ****    Commit, Tag and Push it!    ****\n", Console::FG_YELLOW, Console::BOLD);
589
        $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");
590
591
        $this->stdout("Make sure to have your git set up for GPG signing. The following tag and commit should be signed.\n\n");
592
593
        $this->runGit("git commit -S -a -m \"release version $version\"", $path);
594
        $this->runGit("git tag -s $version -m \"version $version\"", $path);
595
        $this->runGit("git push", $path);
596
        $this->runGit("git push --tags", $path);
597
598
        $this->stdout("\n\n");
599
        $this->stdout("CONGRATULATIONS! You have just released application ", Console::FG_YELLOW, Console::BOLD);
600
        $this->stdout($name, Console::FG_RED, Console::BOLD);
601
        $this->stdout(" version ", Console::FG_YELLOW, Console::BOLD);
602
        $this->stdout($version, Console::BOLD);
603
        $this->stdout("!\n\n", Console::FG_YELLOW, Console::BOLD);
604
605
        // prepare next release
606
607
        $this->stdout("Time to prepare the next release...\n\n", Console::FG_YELLOW, Console::BOLD);
608
609
        $this->stdout("updating composer stability...\n", Console::BOLD);
610
        $this->dryRun || $this->composerSetStability(["app-$name"], 'dev');
611
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
612
613
        $nextVersion = $this->getNextVersions(["app-$name" => $version], self::PATCH); // TODO support other versions
614
615
        $this->stdout("\n");
616
        $this->runGit("git diff --color", $path);
617
        $this->stdout("\n\n");
618
        $this->runGit("git commit -a -m \"prepare for next release\"", $path);
619
        $this->runGit("git push", $path);
620
621
        $this->stdout("\n\nDONE!", Console::FG_YELLOW, Console::BOLD);
622
623
        $this->stdout("\n\nThe following steps are left for you to do manually:\n\n");
624
        $nextVersion2 = $this->getNextVersions($nextVersion, self::PATCH); // TODO support other versions
625
        $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");
626
        $this->stdout("- Create Application packages and upload them to github:  ./build release/package app-$name\n");
627
628
        $this->stdout("\n");
629
    }
630
631
    private $_oldAlias;
632
633
    protected function setAppAliases($app, $path)
634
    {
635
        $this->_oldAlias = Yii::getAlias('@app');
636
        switch($app) {
637
            case 'basic':
638
                Yii::setAlias('@app', $path);
639
                break;
640
            case 'advanced':
641
                // setup @frontend, @backend etc...
642
                require("$path/common/config/bootstrap.php");
643
                break;
644
        }
645
    }
646
647
    protected function resetAppAliases()
648
    {
649
        Yii::setAlias('@app', $this->_oldAlias);
650
    }
651
652
    protected function packageApplication($name, $version, $packagePath)
653
    {
654
        FileHelper::createDirectory($packagePath);
655
656
        $this->runCommand("composer create-project yiisoft/yii2-app-$name $name $version", $packagePath);
657
        // clear cookie validation key in basic app
658
        if (is_file($configFile = "$packagePath/$name/config/web.php")) {
659
            $this->sed(
660
                "/'cookieValidationKey' => '.*?',/",
661
                "'cookieValidationKey' => '',",
662
                $configFile
663
            );
664
        }
665
        $this->runCommand("tar zcf yii-$name-app-$version.tgz $name", $packagePath);
666
    }
667
668
    protected function releaseExtension($name, $path, $version)
669
    {
670
        $this->stdout("\n");
671
        $this->stdout($h = "Preparing release for extension  $name  version $version", Console::BOLD);
672
        $this->stdout("\n" . str_repeat('-', strlen($h)) . "\n\n", Console::BOLD);
673
674
        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...
675
            exit(1);
676
        }
677
        $this->runGit('git pull', $path);
678
679
        // adjustments
680
681
        $this->stdout("fixing various PHPdoc style issues...\n", Console::BOLD);
682
        $this->dryRun || Yii::$app->runAction('php-doc/fix', [$path]);
683
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
684
685
        $this->stdout("updating PHPdoc @property annotations...\n", Console::BOLD);
686
        $this->dryRun || Yii::$app->runAction('php-doc/property', [$path]);
687
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
688
689
        $this->stdout('sorting changelogs...', Console::BOLD);
690
        $this->dryRun || $this->resortChangelogs([$name], $version);
691
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
692
693
        $this->stdout('closing changelogs...', Console::BOLD);
694
        $this->dryRun || $this->closeChangelogs([$name], $version);
695
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
696
697
        $this->stdout("\nIn the following you can check the above changes using git diff.\n\n");
698
        do {
699
            $this->runGit("git diff --color", $path);
700
            $this->stdout("\n\n\nCheck whether the above diff is okay, if not you may change things as needed before continuing.\n");
701
            $this->stdout("You may abort the program with Ctrl + C and reset the changes by running `git checkout -- .` in the repo.\n\n");
702
        } while(!$this->confirm("Type `yes` to continue, `no` to view git diff again. Continue?"));
703
704
        $this->stdout("\n\n");
705
        $this->stdout("    ****          RELEASE TIME!         ****\n", Console::FG_YELLOW, Console::BOLD);
706
        $this->stdout("    ****    Commit, Tag and Push it!    ****\n", Console::FG_YELLOW, Console::BOLD);
707
        $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");
708
709
        $this->stdout("Make sure to have your git set up for GPG signing. The following tag and commit should be signed.\n\n");
710
711
        $this->runGit("git commit -S -a -m \"release version $version\"", $path);
712
        $this->runGit("git tag -s $version -m \"version $version\"", $path);
713
        $this->runGit("git push", $path);
714
        $this->runGit("git push --tags", $path);
715
716
        $this->stdout("\n\n");
717
        $this->stdout("CONGRATULATIONS! You have just released extension ", Console::FG_YELLOW, Console::BOLD);
718
        $this->stdout($name, Console::FG_RED, Console::BOLD);
719
        $this->stdout(" version ", Console::FG_YELLOW, Console::BOLD);
720
        $this->stdout($version, Console::BOLD);
721
        $this->stdout("!\n\n", Console::FG_YELLOW, Console::BOLD);
722
723
        // prepare next release
724
725
        $this->stdout("Time to prepare the next release...\n\n", Console::FG_YELLOW, Console::BOLD);
726
727
        $this->stdout('opening changelogs...', Console::BOLD);
728
        $nextVersion = $this->getNextVersions([$name => $version], self::PATCH); // TODO support other versions
729
        $this->dryRun || $this->openChangelogs([$name], $nextVersion[$name]);
730
        $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD);
731
732
        $this->stdout("\n");
733
        $this->runGit("git diff --color", $path);
734
        $this->stdout("\n\n");
735
        $this->runGit("git commit -a -m \"prepare for next release\"", $path);
736
        $this->runGit("git push", $path);
737
738
        $this->stdout("\n\nDONE!", Console::FG_YELLOW, Console::BOLD);
739
740
        $this->stdout("\n\nThe following steps are left for you to do manually:\n\n");
741
        $nextVersion2 = $this->getNextVersions($nextVersion, self::PATCH); // TODO support other versions
742
        $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");
743
        $this->stdout("- release news and announcement.\n");
744
        $this->stdout("- update the website (will be automated soon and is only relevant for the new website).\n");
745
746
        $this->stdout("\n");
747
    }
748
749
750
    protected function runCommand($cmd, $path)
751
    {
752
        $this->stdout("running  $cmd  ...", Console::BOLD);
753
        if ($this->dryRun) {
754
            $this->stdout("dry run, command `$cmd` not executed.\n");
755
            return;
756
        }
757
        chdir($path);
758
        exec($cmd, $output, $ret);
759
        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...
760
            echo implode("\n", $output);
761
            throw new Exception("Command \"$cmd\" failed with code " . $ret);
762
        }
763
        $this->stdout("\ndone.\n", Console::BOLD, Console::FG_GREEN);
764
    }
765
766
    protected function runGit($cmd, $path)
767
    {
768
        if ($this->confirm("Run `$cmd`?", true)) {
769
            if ($this->dryRun) {
770
                $this->stdout("dry run, command `$cmd` not executed.\n");
771
                return;
772
            }
773
            chdir($path);
774
            exec($cmd, $output, $ret);
775
            echo implode("\n", $output);
776
            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...
777
                throw new Exception("Command \"$cmd\" failed with code " . $ret);
778
            }
779
            echo "\n";
780
        }
781
    }
782
783
    protected function ensureGitClean($path)
784
    {
785
        chdir($path);
786
        exec('git status --porcelain -uno', $changes, $ret);
787
        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...
788
            throw new Exception('Command "git status --porcelain -uno" failed with code ' . $ret);
789
        }
790
        if (!empty($changes)) {
791
            throw new Exception("You have uncommitted changes in $path: " . print_r($changes, true));
792
        }
793
    }
794
795
    protected function gitFetchTags($path)
796
    {
797
        chdir($path);
798
        exec('git fetch --tags', $output, $ret);
799
        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...
800
            throw new Exception('Command "git fetch --tags" failed with code ' . $ret);
801
        }
802
    }
803
804
805
    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...
806
    {
807
        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...
808
            exit;
809
        }
810
    }
811
812
813
    protected function closeChangelogs($what, $version)
814
    {
815
        $v = str_replace('\\-', '[\\- ]', preg_quote($version, '/'));
816
        $headline = $version . ' ' . date('F d, Y');
817
        $this->sed(
818
            '/'.$v.' under development\n(-+?)\n/',
819
            $headline . "\n" . str_repeat('-', strlen($headline)) . "\n",
820
            $this->getChangelogs($what)
821
        );
822
    }
823
824
    protected function openChangelogs($what, $version)
825
    {
826
        $headline = "\n$version under development\n";
827
        $headline .= str_repeat('-', strlen($headline) - 2) . "\n\n- no changes in this release.\n";
828
        foreach($this->getChangelogs($what) as $file) {
829
            $lines = explode("\n", file_get_contents($file));
830
            $hl = [
831
                array_shift($lines),
832
                array_shift($lines),
833
            ];
834
            array_unshift($lines, $headline);
835
836
            file_put_contents($file, implode("\n", array_merge($hl, $lines)));
837
        }
838
    }
839
840
    protected function resortChangelogs($what, $version)
841
    {
842
        foreach($this->getChangelogs($what) as $file) {
843
            // split the file into relevant parts
844
            list($start, $changelog, $end) = $this->splitChangelog($file, $version);
845
            $changelog = $this->resortChangelog($changelog);
846
            file_put_contents($file, implode("\n", array_merge($start, $changelog, $end)));
847
        }
848
    }
849
850
    /**
851
     * Extract changelog content for a specific version
852
     */
853
    protected function splitChangelog($file, $version)
854
    {
855
        $lines = explode("\n", file_get_contents($file));
856
857
        // split the file into relevant parts
858
        $start = [];
859
        $changelog = [];
860
        $end = [];
861
862
        $state = 'start';
863
        foreach($lines as $l => $line) {
864
            // starting from the changelogs headline
865
            if (isset($lines[$l-2]) && strpos($lines[$l-2], $version) !== false &&
866
                isset($lines[$l-1]) && strncmp($lines[$l-1], '---', 3) === 0) {
867
                $state = 'changelog';
868
            }
869
            if ($state === 'changelog' && isset($lines[$l+1]) && strncmp($lines[$l+1], '---', 3) === 0) {
870
                $state = 'end';
871
            }
872
            // add continued lines to the last item to keep them together
873
            if (!empty(${$state}) && trim($line !== '') && strpos($line, '- ') !== 0) {
874
                end(${$state});
875
                ${$state}[key(${$state})] .= "\n" . $line;
876
            } else {
877
                ${$state}[] = $line;
878
            }
879
        }
880
        return [$start, $changelog, $end];
881
    }
882
883
    /**
884
     * Ensure sorting of the changelog lines
885
     */
886
    protected function resortChangelog($changelog)
887
    {
888
        // cleanup whitespace
889
        foreach($changelog as $i => $line) {
890
            $changelog[$i] = rtrim($line);
891
        }
892
        $changelog = array_filter($changelog);
893
894
        $i = 0;
895
        ArrayHelper::multisort($changelog, function($line) use (&$i) {
896
            if (preg_match('/^- (Chg|Enh|Bug|New)( #\d+(, #\d+)*)?: .+/', $line, $m)) {
897
                $o = ['Bug' => 'C', 'Enh' => 'D', 'Chg' => 'E', 'New' => 'F'];
898
                return $o[$m[1]] . ' ' . (!empty($m[2]) ? $m[2] : 'AAAA' . $i++);
899
            }
900
            return 'B' . $i++;
901
        }, SORT_ASC, SORT_NATURAL);
902
903
        // re-add leading and trailing lines
904
        array_unshift($changelog, '');
905
        $changelog[] = '';
906
        $changelog[] = '';
907
908
        return $changelog;
909
    }
910
911
    protected function getChangelogs($what)
912
    {
913
        $changelogs = [];
914
        if (in_array('framework', $what)) {
915
            $changelogs[] = $this->getFrameworkChangelog();
916
        }
917
918
        return array_merge($changelogs, $this->getExtensionChangelogs($what));
919
    }
920
921
    protected function getFrameworkChangelog()
922
    {
923
        return $this->basePath . '/framework/CHANGELOG.md';
924
    }
925
926
    protected function getExtensionChangelogs($what)
927
    {
928
        return array_filter(glob($this->basePath . '/extensions/*/CHANGELOG.md'), function($elem) use ($what) {
929
            foreach($what as $ext) {
930
                if (strpos($elem, "extensions/$ext/CHANGELOG.md") !== false) {
931
                    return true;
932
                }
933
            }
934
            return false;
935
        });
936
    }
937
938
    protected function composerSetStability($what, $version)
939
    {
940
        $apps = [];
941
        if (in_array('app-advanced', $what)) {
942
            $apps[] = $this->basePath . '/apps/advanced/composer.json';
943
        }
944
        if (in_array('app-basic', $what)) {
945
            $apps[] = $this->basePath . '/apps/basic/composer.json';
946
        }
947
        if (in_array('app-benchmark', $what)) {
948
            $apps[] = $this->basePath . '/apps/benchmark/composer.json';
949
        }
950
        if (empty($apps)) {
951
            return;
952
        }
953
954
        $stability = 'stable';
955
        if (strpos($version, 'alpha') !== false) {
956
            $stability = 'alpha';
957
        } elseif (strpos($version, 'beta') !== false) {
958
            $stability = 'beta';
959
        } elseif (strpos($version, 'rc') !== false) {
960
            $stability = 'RC';
961
        } elseif (strpos($version, 'dev') !== false) {
962
            $stability = 'dev';
963
        }
964
965
        $this->sed(
966
            '/"minimum-stability": "(.+?)",/',
967
            '"minimum-stability": "' . $stability . '",',
968
            $apps
969
        );
970
    }
971
972
    protected function updateYiiVersion($frameworkPath, $version)
973
    {
974
        $this->sed(
975
            '/function getVersion\(\)\n    \{\n        return \'(.+?)\';/',
976
            "function getVersion()\n    {\n        return '$version';",
977
            $frameworkPath . '/BaseYii.php');
978
    }
979
980
    protected function sed($pattern, $replace, $files)
981
    {
982
        foreach((array) $files as $file) {
983
            file_put_contents($file, preg_replace($pattern, $replace, file_get_contents($file)));
984
        }
985
    }
986
987
    protected function getCurrentVersions(array $what)
988
    {
989
        $versions = [];
990
        foreach($what as $ext) {
991
            if ($ext === 'framework') {
992
                chdir("{$this->basePath}/framework");
993
            } elseif (strncmp('app-', $ext, 4) === 0) {
994
                chdir("{$this->basePath}/apps/" . substr($ext, 4));
995
            } else {
996
                chdir("{$this->basePath}/extensions/$ext");
997
            }
998
            $tags = [];
999
            exec('git tag', $tags, $ret);
1000
            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...
1001
                throw new Exception('Command "git tag" failed with code ' . $ret);
1002
            }
1003
            rsort($tags, SORT_NATURAL); // TODO this can not deal with alpha/beta/rc...
1004
            $versions[$ext] = reset($tags);
1005
        }
1006
        return $versions;
1007
    }
1008
1009
    const MINOR = 'minor';
1010
    const PATCH = 'patch';
1011
1012
    protected function getNextVersions(array $versions, $type)
1013
    {
1014
        foreach($versions as $k => $v) {
1015
            if (empty($v)) {
1016
                $versions[$k] = '2.0.0';
1017
                continue;
1018
            }
1019
            $parts = explode('.', $v);
1020
            switch($type) {
1021
                case self::MINOR:
1022
                    $parts[1]++;
1023
                    $parts[2] = 0;
1024
                    if (isset($parts[3])) {
1025
                        unset($parts[3]);
1026
                    }
1027
                    break;
1028
                case self::PATCH:
1029
                    $parts[2]++;
1030
                    if (isset($parts[3])) {
1031
                        unset($parts[3]);
1032
                    }
1033
                    break;
1034
                default:
1035
                    throw new Exception('Unknown version type.');
1036
            }
1037
            $versions[$k] = implode('.', $parts);
1038
        }
1039
        return $versions;
1040
    }
1041
}
1042