Passed
Push — scrutinizer-migrate-to-new-eng... ( 58afd6 )
by Alexander
18:11
created

ReleaseController::actionRelease()   C

Complexity

Conditions 12
Paths 161

Size

Total Lines 62

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 62
rs 5.9957
c 0
b 0
f 0
cc 12
nc 161
nop 1

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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