├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── composer.json
└── src
├── Git.php
├── Mercurial.php
├── SelfUpdateController.php
├── Shell.php
├── ShellResult.php
├── VersionControlSystem.php
├── VersionControlSystemInterface.php
└── views
└── selfUpdateConfig.php
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | Yii 2 Project Self Update extension Change Log
2 | ==============================================
3 |
4 | 1.0.3, November 3, 2017
5 | -----------------------
6 |
7 | - Bug: Usage of deprecated `yii\base\Object` changed to `yii\base\BaseObject` allowing compatibility with PHP 7.2 (klimov-paul)
8 | - Enh: Usage of deprecated exit code constants of `yii\console\Controller` changed to `yii\console\ExitCode` ones (klimov-paul)
9 |
10 |
11 | 1.0.2, December 8, 2016
12 | -----------------------
13 |
14 | - Enh #5: Added `SelfUpdateController::$composerOptions` allowing setup of additional options for `composer install` command (klimov-paul)
15 |
16 |
17 | 1.0.1, November 24, 2016
18 | ------------------------
19 |
20 | - Enh #4: Added `SelfUpdateController::$reportFrom` allowing setup of the report sender email address (klimov-paul)
21 |
22 |
23 | 1.0.0, February 11, 2016
24 | ------------------------
25 |
26 | - Initial release.
27 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The Yii framework is free software. It is released under the terms of
2 | the following BSD License.
3 |
4 | Copyright © 2015 by Yii2tech (https://github.com/yii2tech)
5 | All rights reserved.
6 |
7 | Redistribution and use in source and binary forms, with or without
8 | modification, are permitted provided that the following conditions
9 | are met:
10 |
11 | * Redistributions of source code must retain the above copyright
12 | notice, this list of conditions and the following disclaimer.
13 | * Redistributions in binary form must reproduce the above copyright
14 | notice, this list of conditions and the following disclaimer in
15 | the documentation and/or other materials provided with the
16 | distribution.
17 | * Neither the name of Yii2tech nor the names of its
18 | contributors may be used to endorse or promote products derived
19 | from this software without specific prior written permission.
20 |
21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
22 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
23 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
24 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
25 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
26 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
27 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
28 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
29 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
30 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
31 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32 | POSSIBILITY OF SUCH DAMAGE.
33 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
Project Self Update Extension for Yii 2
6 |
7 |
8 |
9 | This extension allows automatic project updating in case its source code is maintained via version control system, such
10 | as [GIT](https://git-scm.com/) or [Mercurial](https://mercurial.selenic.com/). Such update includes following steps:
11 | - check if there are any changes at VSC remote repository
12 | - link web server web directories to the stubs, while project update is running
13 | - apply remote VCS changes
14 | - update 'vendor' directory via Composer
15 | - clear application cache and temporary directories
16 | - perform additional actions, like applying database migrations
17 | - link web server web directories to the project web directories, once update is complete
18 | - notify developer(s) about update result via email
19 |
20 | > Note: this solution is very basic and may not suite for the complex project update workflow. You may consider
21 | usage of more sophisticated tools like [Phing](https://www.phing.info/). However, this extension may be used as a part
22 | of such solution.
23 |
24 | For license information check the [LICENSE](LICENSE.md)-file.
25 |
26 | [](https://packagist.org/packages/yii2tech/selfupdate)
27 | [](https://packagist.org/packages/yii2tech/selfupdate)
28 | [](https://travis-ci.org/yii2tech/selfupdate)
29 |
30 |
31 | Requirements
32 | ------------
33 |
34 | This extension requires Linux OS.
35 |
36 |
37 | Installation
38 | ------------
39 |
40 | The preferred way to install this extension is through [composer](http://getcomposer.org/download/).
41 |
42 | Either run
43 |
44 | ```
45 | php composer.phar require --prefer-dist yii2tech/selfupdate
46 | ```
47 |
48 | or add
49 |
50 | ```json
51 | "yii2tech/selfupdate": "*"
52 | ```
53 |
54 | to the require section of your composer.json.
55 |
56 |
57 | Usage
58 | -----
59 |
60 | This extension provides special console controller [[yii2tech\selfupdate\SelfUpdateController]], which allows automatic updating of
61 | the project, if its source code is maintained via version control system.
62 | In order to enable this controller in your project, you should add it to your console application `controllerMap` at
63 | configuration file:
64 |
65 | ```php
66 | return [
67 | 'controllerMap' => [
68 | 'self-update' => 'yii2tech\selfupdate\SelfUpdateController'
69 | ],
70 | // ...
71 | ];
72 | ```
73 |
74 | Now you should able to use 'self-update' command via console:
75 |
76 | ```
77 | yii self-update
78 | ```
79 |
80 |
81 | ## Project preparation
82 |
83 | In order to use 'self-update' command, you should perform several preparations in your project, allowing
84 | certain shell commands to be executed in non-interactive (without user prompt) mode.
85 |
86 | First of all, you should clone (checkout) your project from version control system and switch project working copy
87 | to the branch, which should be used at this particular server. Using GIT this actions can be performed via following commands:
88 |
89 | ```
90 | cd /path/to/my/project
91 | git clone git@my-git-server.com/myproject.git
92 | git checkout production
93 | ```
94 |
95 | > Attention: you need to configure your VCS (or at least your project working copy) in the way interacting with remote
96 | repository does NOT require user prompt, like input of username or password! This can be achieved using authentication
97 | keys or 'remember password' feature.
98 |
99 | Then you should make project operational performing all necessary actions for its initial deployment, like running
100 | 'composer install', creating necessary directories and so on.
101 |
102 |
103 | ## Using self-update command
104 |
105 | Once project is setup you need to create a configuration for its updating. This can be done using 'self-update/config'
106 | command:
107 |
108 | ```
109 | yii self-update/config @app/config/self-update.php
110 | ```
111 |
112 | This will generate configuration file, which should be manually adjusted depending on the particular project structure
113 | and server environment. For the common project such configuration file may look like following:
114 |
115 | ```php
116 | [
121 | 'developer@domain.com',
122 | ],
123 | // Mailer component to be used
124 | 'mailer' => 'mailer',
125 | // Mutex component to be used
126 | 'mutex' => 'mutex',
127 | // path to project root directory (VCS root directory)
128 | 'projectRootPath' => '@app',
129 | // web path stubs configuration
130 | 'webPaths' => [
131 | [
132 | 'path' => '@app/web',
133 | 'link' => '@app/httpdocs',
134 | 'stub' => '@app/webstub',
135 | ],
136 | ],
137 | // cache components to be flushed
138 | 'cache' => [
139 | 'cache'
140 | ],
141 | // temporary directories, which should be cleared after project update
142 | 'tmpDirectories' => [
143 | '@app/web/assets',
144 | '@runtime/URI',
145 | '@runtime/HTML',
146 | '@runtime/debug',
147 | ],
148 | // list of shell commands, which should be executed after project update
149 | 'afterUpdateCommands' => [
150 | 'php ' . escapeshellarg($_SERVER['SCRIPT_FILENAME']) . ' migrate/up --interactive=0',
151 | ],
152 | ];
153 | ```
154 |
155 | Please refer to [[\yii2tech\selfupdate\SelfUpdateController]] for particular option information.
156 |
157 | Once you have made all necessary adjustments at configuration file, you can run 'self-update/perform' command with it:
158 |
159 | ```
160 | yii self-update @app/config/self-update.php
161 | ```
162 |
163 | You may setup default configuration file name inside the `controllerMap` specification via [[yii2tech\selfupdate\SelfUpdateController::$configFile]]:
164 |
165 | ```php
166 | return [
167 | 'controllerMap' => [
168 | 'self-update' => [
169 | 'class' => 'yii2tech\selfupdate\SelfUpdateController',
170 | 'configFile' => '@app/config/self-update.php',
171 | ]
172 | ],
173 | // ...
174 | ];
175 | ```
176 |
177 | Then invocation of the self-update command will be much more clear:
178 |
179 | ```
180 | yii self-update
181 | ```
182 |
183 | > Note: it is not necessary to create a separated configuration file: you can configure all necessary fields of
184 | [[yii2tech\selfupdate\SelfUpdateController]] inside `controllerMap` specification, but such approach is not recommended.
185 |
186 |
187 | Self Update Workflow
188 | --------------------
189 |
190 | While running, [[yii2tech\selfupdate\SelfUpdateController]] performs following steps:
191 |
192 | - check if there are any changes at VSC remote repository
193 | - link web server web directories to the stubs, while project update is running
194 | - apply remote VCS changes
195 | - update 'vendor' directory via Composer
196 | - clear application cache and temporary directories
197 | - perform additional actions, like applying database migrations
198 | - link web server web directories to the project web directories, once update is complete
199 | - notify developer(s) about update result via email
200 |
201 | At the first stage there is a check for any changes in the remote repository. If there is no changes in remote
202 | repository for the current project VCS working copy branch, no further actions will be performed!
203 |
204 | If remote changes detected, the symbolic links pointing to the project '@web' directory will be switched to another
205 | directory, which should contain a 'stub' - some static HTML page, which says something like 'Application is under the
206 | maintenance, please check again later'. Although, usage of such stub it is up to you, it is recommended, because actual
207 | project update may take significant time before being complete.
208 | Project web directory will be linked back instead of stub, only after all update actions are performed.
209 |
210 | During update itself VCS remote changes are applied, `vendor` directory is updated via Composer, specified temporary
211 | directories will be cleared and cache flushed.
212 |
213 | > Note: in order for Composer be able to apply necessary changes, the 'composer.lock' file should be tracked by version
214 | control system!
215 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "yii2tech/selfupdate",
3 | "description": "Basic extension for the Yii2 project self-update from VCS",
4 | "keywords": ["yii2", "selfupdate", "update", "deployment", "continuous", "integration", "VCS", "GIT", "Mercurial"],
5 | "type": "yii2-extension",
6 | "license": "BSD-3-Clause",
7 | "support": {
8 | "issues": "https://github.com/yii2tech/selfupdate/issues",
9 | "forum": "http://www.yiiframework.com/forum/",
10 | "wiki": "https://github.com/yii2tech/selfupdate/wiki",
11 | "source": "https://github.com/yii2tech/selfupdate"
12 | },
13 | "authors": [
14 | {
15 | "name": "Paul Klimov",
16 | "email": "klimov.paul@gmail.com"
17 | }
18 | ],
19 | "require": {
20 | "yiisoft/yii2": "~2.0.13"
21 | },
22 | "repositories": [
23 | {
24 | "type": "composer",
25 | "url": "https://asset-packagist.org"
26 | }
27 | ],
28 | "autoload": {
29 | "psr-4": {"yii2tech\\selfupdate\\": "src"}
30 | },
31 | "extra": {
32 | "branch-alias": {
33 | "dev-master": "1.0.x-dev"
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Git.php:
--------------------------------------------------------------------------------
1 |
18 | * @since 1.0
19 | */
20 | class Git extends VersionControlSystem
21 | {
22 | /**
23 | * @var string path to the 'git' bin command.
24 | * By default simple 'git' is used assuming it available as global shell command.
25 | * It could be '/usr/bin/git' for example.
26 | */
27 | public $binPath = 'git';
28 | /**
29 | * @var string name of the GIT remote, which should be used to get changes.
30 | */
31 | public $remoteName = 'origin';
32 |
33 |
34 | /**
35 | * Returns currently active GIT branch name for the project.
36 | * @param string $projectRoot VCS project root directory path.
37 | * @return string branch name.
38 | * @throws Exception on failure.
39 | */
40 | public function getCurrentBranch($projectRoot)
41 | {
42 | $result = Shell::execute('(cd {projectRoot}; {binPath} branch)', [
43 | '{binPath}' => $this->binPath,
44 | '{projectRoot}' => $projectRoot,
45 | ]);
46 | foreach ($result->outputLines as $line) {
47 | if (($pos = stripos($line, '* ')) === 0) {
48 | return trim(substr($line, $pos + 2));
49 | }
50 | }
51 | throw new Exception('Unable to detect current GIT branch: ' . $result->toString());
52 | }
53 |
54 | /**
55 | * Checks, if there are some changes in remote repository.
56 | * @param string $projectRoot VCS project root directory path.
57 | * @param string $log if parameter passed it will be filled with related log string.
58 | * @return bool whether there are changes in remote repository.
59 | */
60 | public function hasRemoteChanges($projectRoot, &$log = null)
61 | {
62 | $placeholders = [
63 | '{binPath}' => $this->binPath,
64 | '{projectRoot}' => $projectRoot,
65 | '{remote}' => $this->remoteName,
66 | '{branch}' => $this->getCurrentBranch($projectRoot),
67 | ];
68 |
69 | $fetchResult = Shell::execute('(cd {projectRoot}; {binPath} fetch {remote})', $placeholders);
70 | $log = $fetchResult->toString() . "\n";
71 |
72 | $result = Shell::execute('(cd {projectRoot}; {binPath} diff --numstat HEAD {remote}/{branch})', $placeholders);
73 | $log .= $result->toString();
74 | return ($result->isOk() && !$result->isOutputEmpty());
75 | }
76 |
77 | /**
78 | * Applies changes from remote repository.
79 | * @param string $projectRoot VCS project root directory path.
80 | * @param string $log if parameter passed it will be filled with related log string.
81 | * @return bool whether the changes have been applied successfully.
82 | */
83 | public function applyRemoteChanges($projectRoot, &$log = null)
84 | {
85 | $result = Shell::execute('(cd {projectRoot}; {binPath} merge {remote}/{branch})', [
86 | '{binPath}' => $this->binPath,
87 | '{projectRoot}' => $projectRoot,
88 | '{remote}' => $this->remoteName,
89 | '{branch}' => $this->getCurrentBranch($projectRoot),
90 | ]);
91 | $log = $result->toString();
92 | return $result->isOk();
93 | }
94 | }
--------------------------------------------------------------------------------
/src/Mercurial.php:
--------------------------------------------------------------------------------
1 |
16 | * @since 1.0
17 | */
18 | class Mercurial extends VersionControlSystem
19 | {
20 | /**
21 | * @var string path to the 'hg' bin command.
22 | * By default simple 'hg' is used assuming it available as global shell command.
23 | * It could be '/usr/bin/hg' for example.
24 | */
25 | public $binPath = 'hg';
26 |
27 |
28 | /**
29 | * Returns currently active Mercurial branch name for the project.
30 | * @param string $projectRoot VCS project root directory path.
31 | * @return string branch name.
32 | */
33 | public function getCurrentBranch($projectRoot)
34 | {
35 | $result = Shell::execute('(cd {projectRoot}; {binPath} branch)', [
36 | '{binPath}' => $this->binPath,
37 | '{projectRoot}' => $projectRoot,
38 | ]);
39 | return $result->outputLines[0];
40 | }
41 |
42 | /**
43 | * Checks, if there are some changes in remote repository.
44 | * @param string $projectRoot VCS project root directory path.
45 | * @param string $log if parameter passed it will be filled with related log string.
46 | * @return bool whether there are changes in remote repository.
47 | */
48 | public function hasRemoteChanges($projectRoot, &$log = null)
49 | {
50 | $result = Shell::execute("(cd {projectRoot}; {binPath} incoming -b {branch} --newest-first --limit 1)", [
51 | '{binPath}' => $this->binPath,
52 | '{projectRoot}' => $projectRoot,
53 | '{branch}' => $this->getCurrentBranch($projectRoot),
54 | ]);
55 | $log = $result->toString();
56 | return $result->isOk();
57 | }
58 |
59 | /**
60 | * Applies changes from remote repository.
61 | * @param string $projectRoot VCS project root directory path.
62 | * @param string $log if parameter passed it will be filled with related log string.
63 | * @return bool whether the changes have been applied successfully.
64 | */
65 | public function applyRemoteChanges($projectRoot, &$log = null)
66 | {
67 | $result = Shell::execute('(cd {projectRoot}; {binPath} pull -b {branch} -u)', [
68 | '{binPath}' => $this->binPath,
69 | '{projectRoot}' => $projectRoot,
70 | '{branch}' => $this->getCurrentBranch($projectRoot),
71 | ]);
72 | $log = $result->toString();
73 | return $result->isOk();
74 | }
75 | }
--------------------------------------------------------------------------------
/src/SelfUpdateController.php:
--------------------------------------------------------------------------------
1 |
45 | * @since 1.0
46 | */
47 | class SelfUpdateController extends Controller
48 | {
49 | /**
50 | * @var string controller default action ID.
51 | */
52 | public $defaultAction = 'perform';
53 | /**
54 | * @var array list of email addresses, which should be used to send execution reports.
55 | */
56 | public $emails = [];
57 | /**
58 | * @var Mutex|array|string the mutex object or the application component ID of the mutex.
59 | * After the controller object is created, if you want to change this property, you should only assign it
60 | * with a mutex connection object.
61 | */
62 | public $mutex = 'mutex';
63 | /**
64 | * @var string path to project root directory, which means VCS root directory. Path aliases can be use here.
65 | */
66 | public $projectRootPath = '@app';
67 | /**
68 | * @var array project web path stubs configuration.
69 | * Each path configuration should have following keys:
70 | *
71 | * - 'path': string, path to web root folder
72 | * - 'link': string, path for the symbolic link, which should point to the web root
73 | * - 'stub': string, path to folder, which contains stub for the web
74 | *
75 | * Yii aliases can be used for all these keys.
76 | * For example:
77 | *
78 | * ```php
79 | * [
80 | * [
81 | * 'path' => '@app/web',
82 | * 'link' => '@app/httpdocs',
83 | * 'stub' => '@app/webstub',
84 | * ]
85 | * ]
86 | * ```
87 | */
88 | public $webPaths = [];
89 | /**
90 | * @var string|array list of cache application components, for which [[Cache::flush()]] method should be invoked.
91 | * Component ids, instances or array configurations can be used here.
92 | */
93 | public $cache;
94 | /**
95 | * @var array list of temporary directories, which should be cleared after project update.
96 | * Path aliases can be used here. For example:
97 | *
98 | * ```php
99 | * [
100 | * '@app/web/assets',
101 | * '@runtime/URI',
102 | * '@runtime/HTML',
103 | * '@runtime/debug',
104 | * ]
105 | * ```
106 | */
107 | public $tmpDirectories = [];
108 | /**
109 | * @var array list of commands, which should be executed before project update begins.
110 | * If command is a string it will be executed as shell command, otherwise as PHP callback.
111 | * For example:
112 | *
113 | * ```php
114 | * [
115 | * 'mysqldump -h localhost -u root myproject > /path/to/backup/myproject.sql'
116 | * ],
117 | * ```
118 | */
119 | public $beforeUpdateCommands = [];
120 | /**
121 | * @var array list of shell commands, which should be executed after project update.
122 | * If command is a string it will be executed as shell command, otherwise as PHP callback.
123 | * For example:
124 | *
125 | * ```php
126 | * [
127 | * 'php /path/to/project/yii migrate/up --interactive=0'
128 | * ],
129 | * ```
130 | */
131 | public $afterUpdateCommands = [];
132 | /**
133 | * @var array list of keywords, which presence in the shell command output is considered as
134 | * its execution error.
135 | */
136 | public $shellResponseErrorKeywords = [
137 | 'error',
138 | 'exception',
139 | 'ошибка',
140 | ];
141 | /**
142 | * @var array list of possible version control systems (VCS) in format: `vcsFolderName => classConfig`.
143 | * VCS will be detected automatically based on which folder is available inside [[$projectRootPath]]
144 | */
145 | public $versionControlSystems = [
146 | '.git' => [
147 | 'class' => 'yii2tech\selfupdate\Git'
148 | ],
149 | '.hg' => [
150 | 'class' => 'yii2tech\selfupdate\Mercurial'
151 | ],
152 | ];
153 | /**
154 | * @var array composer command options.
155 | * @see Shell::buildOptions() for valid syntax on specifying this value.
156 | * For example:
157 | *
158 | * ```php
159 | * [
160 | * 'prefer-dist',
161 | * 'no-dev',
162 | * ]
163 | * ```
164 | *
165 | * Note, that `no-interaction` option will be added automatically to the options list.
166 | *
167 | * @since 1.0.2
168 | */
169 | public $composerOptions = [];
170 | /**
171 | * @var string path to the 'composer' bin command.
172 | * By default simple 'composer' is used, assuming it available as global shell command.
173 | * Path alias can be used here. For example: '@app/composer.phar'.
174 | */
175 | public $composerBinPath = 'composer';
176 | /**
177 | * @var array list of composer install root paths (the ones containing 'composer.json').
178 | * Path aliases can be used here.
179 | */
180 | public $composerRootPaths = [
181 | '@app'
182 | ];
183 | /**
184 | * @var \yii\mail\MailerInterface|array|string the mailer object or the application component ID of the mailer.
185 | * It will be used to send notification messages to [[$emails]].
186 | * If not set or sending email via this component fails, the fallback to the plain PHP `mail()` function will be used instead.
187 | */
188 | public $mailer;
189 | /**
190 | * @var string configuration file name. Settings from this file will be merged with the default ones.
191 | * Such configuration file can be created, using action 'config'.
192 | * Path alias can be used here, for example: '@app/config/self-update.php'.
193 | */
194 | public $configFile;
195 |
196 | /**
197 | * @var array list of log entries.
198 | * @see log()
199 | */
200 | private $logLines = [];
201 | /**
202 | * @var string name of the host, which will be used in reports.
203 | */
204 | private $_hostName;
205 | /**
206 | * @var string email address, which should be used to send report email messages.
207 | */
208 | private $_reportFrom;
209 |
210 |
211 | /**
212 | * Performs project update from VCS.
213 | * @param string|null $configFile the path or alias of the configuration file.
214 | * You may use the "config" command to generate
215 | * this file and then customize it for your needs.
216 | * @throws Exception on failure
217 | * @return int CLI exit code
218 | */
219 | public function actionPerform($configFile = null)
220 | {
221 | if (empty($configFile)) {
222 | $configFile = $this->configFile;
223 | }
224 | if (!empty($configFile)) {
225 | $configFile = Yii::getAlias($configFile);
226 | if (!is_file($configFile)) {
227 | throw new Exception("The configuration file does not exist: $configFile");
228 | }
229 | $this->log("Reading configuration from: $configFile");
230 | Yii::configure($this, require $configFile);
231 | }
232 |
233 | if (!$this->acquireMutex()) {
234 | $this->stderr("Execution terminated: command is already running.\n", Console::FG_RED);
235 | return ExitCode::UNSPECIFIED_ERROR;
236 | }
237 |
238 | try {
239 | $this->normalizeWebPaths();
240 |
241 | $projectRootPath = Yii::getAlias($this->projectRootPath);
242 |
243 | $versionControlSystem = $this->detectVersionControlSystem($projectRootPath);
244 |
245 | $changesDetected = $versionControlSystem->hasRemoteChanges($projectRootPath, $log);
246 | $this->log($log);
247 |
248 | if ($changesDetected) {
249 | $this->linkWebStubs();
250 |
251 | $this->executeCommands($this->beforeUpdateCommands);
252 |
253 | $versionControlSystem->applyRemoteChanges($projectRootPath, $log);
254 | $this->log($log);
255 |
256 | $this->updateVendor();
257 | $this->flushCache();
258 | $this->clearTmpDirectories();
259 |
260 | $this->executeCommands($this->afterUpdateCommands);
261 |
262 | $this->linkWebPaths();
263 |
264 | $this->reportSuccess();
265 | } else {
266 | $this->log('No changes detected. Project is already up-to-date.');
267 | }
268 |
269 | } catch (\Exception $exception) {
270 | $this->log($exception->getMessage());
271 | $this->reportFail();
272 |
273 | $this->releaseMutex();
274 | return ExitCode::UNSPECIFIED_ERROR;
275 | }
276 |
277 | $this->releaseMutex();
278 | return ExitCode::OK;
279 | }
280 |
281 | /**
282 | * Creates a configuration file for the "perform" command.
283 | *
284 | * The generated configuration file contains detailed instructions on
285 | * how to customize it to fit for your needs. After customization,
286 | * you may use this configuration file with the "perform" command.
287 | *
288 | * @param string $fileName output file name or alias.
289 | * @return int CLI exit code.
290 | */
291 | public function actionConfig($fileName)
292 | {
293 | $fileName = Yii::getAlias($fileName);
294 | if (file_exists($fileName)) {
295 | if (!$this->confirm("File '{$fileName}' already exists. Do you wish to overwrite it?")) {
296 | return ExitCode::OK;
297 | }
298 | }
299 | copy(Yii::getAlias('@yii2tech/selfupdate/views/selfUpdateConfig.php'), $fileName);
300 | $this->stdout("Configuration file template created at '{$fileName}' . \n\n", Console::FG_GREEN);
301 | return ExitCode::OK;
302 | }
303 |
304 | /**
305 | * Acquires current action lock.
306 | * @return bool lock acquiring result.
307 | */
308 | protected function acquireMutex()
309 | {
310 | $this->mutex = Instance::ensure($this->mutex, Mutex::className());
311 | return $this->mutex->acquire($this->composeMutexName());
312 | }
313 |
314 | /**
315 | * Release current action lock.
316 | * @return bool lock release result.
317 | */
318 | protected function releaseMutex()
319 | {
320 | return $this->mutex->release($this->composeMutexName());
321 | }
322 |
323 | /**
324 | * Composes the mutex name.
325 | * @return string mutex name.
326 | */
327 | protected function composeMutexName()
328 | {
329 | return get_class($this) . '::' . $this->action->getUniqueId();
330 | }
331 |
332 | /**
333 | * Links web roots to the stub directories.
334 | * @see webPaths
335 | */
336 | protected function linkWebStubs()
337 | {
338 | foreach ($this->webPaths as $webPath) {
339 | if (is_link($webPath['link'])) {
340 | unlink($webPath['link']);
341 | }
342 | symlink($webPath['stub'], $webPath['link']);
343 | }
344 | }
345 |
346 | /**
347 | * Links web roots to the actual web directories.
348 | * @see webPaths
349 | */
350 | protected function linkWebPaths()
351 | {
352 | foreach ($this->webPaths as $webPath) {
353 | if (is_link($webPath['link'])) {
354 | unlink($webPath['link']);
355 | }
356 | symlink($webPath['path'], $webPath['link']);
357 | }
358 | }
359 |
360 | /**
361 | * Normalizes [[$webPaths]] value.
362 | * @throws InvalidConfigException on invalid configuration.
363 | */
364 | protected function normalizeWebPaths()
365 | {
366 | $rawWebPaths = $this->webPaths;
367 | $webPaths = [];
368 | foreach ($rawWebPaths as $rawWebPath) {
369 | if (!isset($rawWebPath['path'], $rawWebPath['link'], $rawWebPath['stub'])) {
370 | throw new InvalidConfigException("Web path configuration should contain keys: 'path', 'link', 'stub'");
371 | }
372 | $webPath = [
373 | 'path' => Yii::getAlias($rawWebPath['path']),
374 | 'link' => Yii::getAlias($rawWebPath['link']),
375 | 'stub' => Yii::getAlias($rawWebPath['stub']),
376 | ];
377 | if (!is_dir($webPath['path'])) {
378 | throw new InvalidConfigException("'{$webPath['path']}' ('{$rawWebPath['path']}') is not a directory.");
379 | }
380 | if (!is_dir($webPath['stub'])) {
381 | throw new InvalidConfigException("'{$webPath['stub']}' ('{$rawWebPath['stub']}') is not a directory.");
382 | }
383 | if (!is_link($webPath['link'])) {
384 | throw new InvalidConfigException("'{$webPath['link']}' ('{$rawWebPath['link']}') is not a symbolic link.");
385 | }
386 | if (!in_array(readlink($webPath['link']), [$webPath['path'], $webPath['stub']])) {
387 | throw new InvalidConfigException("'{$webPath['link']}' ('{$rawWebPath['link']}') does not pointing to actual web or stub directory.");
388 | }
389 | $webPaths[] = $webPath;
390 | }
391 | $this->webPaths = $webPaths;
392 | }
393 |
394 | /**
395 | * Flushes cache for all components specified at [[$cache]].
396 | */
397 | protected function flushCache()
398 | {
399 | if (!empty($this->cache)) {
400 | foreach ((array)$this->cache as $cache) {
401 | $cache = Instance::ensure($cache, Cache::className());
402 | $cache->flush();
403 | }
404 | $this->log('Cache flushed.');
405 | }
406 | }
407 |
408 | /**
409 | * Clears all directories specified via [[$tmpDirectories]].
410 | */
411 | protected function clearTmpDirectories()
412 | {
413 | foreach ($this->tmpDirectories as $path) {
414 | $realPath = Yii::getAlias($path);
415 | $this->clearDirectory($realPath);
416 | $this->log("Directory '{$realPath}' cleared.");
417 | }
418 | }
419 |
420 | /**
421 | * Clears specified directory.
422 | * @param string $dir directory to be cleared.
423 | */
424 | protected function clearDirectory($dir)
425 | {
426 | if (!is_dir($dir)) {
427 | return;
428 | }
429 | if (!($handle = opendir($dir))) {
430 | return;
431 | }
432 | $specialFileNames = [
433 | '.htaccess',
434 | '.gitignore',
435 | '.gitkeep',
436 | '.hgignore',
437 | '.hgkeep',
438 | ];
439 | while (($file = readdir($handle)) !== false) {
440 | if ($file === '.' || $file === '..') {
441 | continue;
442 | }
443 | if (in_array($file, $specialFileNames)) {
444 | continue;
445 | }
446 | $path = $dir . DIRECTORY_SEPARATOR . $file;
447 | if (is_dir($path)) {
448 | FileHelper::removeDirectory($path);
449 | } else {
450 | unlink($path);
451 | }
452 | }
453 | closedir($handle);
454 | }
455 |
456 | /**
457 | * Performs vendors update via Composer at all [[$composerRootPaths]].
458 | */
459 | protected function updateVendor()
460 | {
461 | $options = Shell::buildOptions(array_merge($this->composerOptions, ['no-interaction']));
462 | foreach ($this->composerRootPaths as $path) {
463 | $this->execShellCommand('(cd {composerRoot}; {composer} install ' . $options . ')', [
464 | '{composerRoot}' => Yii::getAlias($path),
465 | '{composer}' => Yii::getAlias($this->composerBinPath),
466 | ]);
467 | }
468 | }
469 |
470 | /**
471 | * Detects version control system used for the project.
472 | * @param string $path project root path.
473 | * @return VersionControlSystemInterface version control system instance.
474 | * @throws InvalidConfigException on failure.
475 | */
476 | protected function detectVersionControlSystem($path)
477 | {
478 | foreach ($this->versionControlSystems as $folderName => $config) {
479 | if (file_exists($path . DIRECTORY_SEPARATOR . $folderName)) {
480 | return Yii::createObject($config);
481 | }
482 | }
483 | throw new InvalidConfigException("Unable to detect version control system: neither of '" . implode(', ', array_keys($this->versionControlSystems)) . "' is present under '{$path}'.");
484 | }
485 |
486 | /**
487 | * @param string $hostName server hostname.
488 | */
489 | public function setHostName($hostName)
490 | {
491 | $this->_hostName = $hostName;
492 | }
493 |
494 | /**
495 | * @return string server hostname.
496 | */
497 | public function getHostName()
498 | {
499 | if ($this->_hostName === null) {
500 | $hostName = @exec('hostname');
501 | if (empty($hostName)) {
502 | $this->_hostName = Inflector::slug(Yii::$app->name) . '.com';
503 | } else {
504 | $this->_hostName = $hostName;
505 | }
506 | }
507 | return $this->_hostName;
508 | }
509 |
510 | /**
511 | * @return string email address, which should be used to send report email messages.
512 | */
513 | public function getReportFrom()
514 | {
515 | if ($this->_reportFrom === null) {
516 | $userName = @exec('whoami');
517 | if (empty($userName)) {
518 | $userName = Inflector::slug(Yii::$app->name);
519 | }
520 | $hostName = $this->getHostName();
521 | $this->_reportFrom = $userName . '@' . $hostName;
522 | }
523 | return $this->_reportFrom;
524 | }
525 |
526 | /**
527 | * @param string $reportFrom email address, which should be used to send report email messages.
528 | */
529 | public function setReportFrom($reportFrom)
530 | {
531 | $this->_reportFrom = $reportFrom;
532 | }
533 |
534 | /**
535 | * @return string current date string.
536 | */
537 | public function getCurrentDate()
538 | {
539 | return date('Y-m-d H:i:s');
540 | }
541 |
542 | /**
543 | * Logs the message
544 | * @param string $message log message.
545 | */
546 | protected function log($message)
547 | {
548 | $this->logLines[] = $message;
549 | $this->stdout($message . "\n\n");
550 | }
551 |
552 | /**
553 | * Flushes log lines, returning them.
554 | * @return array log lines.
555 | */
556 | protected function flushLog()
557 | {
558 | $logLines = $this->logLines;
559 | $this->logLines = [];
560 | return $logLines;
561 | }
562 |
563 | /**
564 | * Executes list of given commands.
565 | * @param array $commands commands to be executed.
566 | * @throws InvalidConfigException on invalid commands specification.
567 | */
568 | protected function executeCommands(array $commands)
569 | {
570 | foreach ($commands as $command) {
571 | if (is_string($command)) {
572 | $this->execShellCommand($command);
573 | } elseif (is_callable($command)) {
574 | $this->log(call_user_func($command));
575 | } else {
576 | throw new InvalidConfigException('Command should be a string or a valid PHP callback');
577 | }
578 | }
579 | }
580 |
581 | /**
582 | * Executes shell command.
583 | * @param string $command command text.
584 | * @return string command output.
585 | * @param array $placeholders placeholders to be replaced using `escapeshellarg()` in format: `placeholder => value`.
586 | * @throws Exception on failure.
587 | */
588 | protected function execShellCommand($command, array $placeholders = [])
589 | {
590 | $result = Shell::execute($command, $placeholders);
591 | $this->log($result->toString());
592 |
593 | $output = $result->getOutput();
594 | if (!$result->isOk()) {
595 | throw new Exception("Execution of '{$result->command}' failed: exit code = '{$result->exitCode}': \nOutput: \n{$output}");
596 | }
597 | foreach ($this->shellResponseErrorKeywords as $errorKeyword) {
598 | if (stripos($output, $errorKeyword) !== false) {
599 | throw new Exception("Execution of '{$result->command}' failed! \nOutput: \n{$output}");
600 | }
601 | }
602 | return $output;
603 | }
604 |
605 | /**
606 | * Sends report about success.
607 | */
608 | protected function reportSuccess()
609 | {
610 | $this->reportResult('Update success');
611 | }
612 |
613 | /**
614 | * Sends report about failure.
615 | */
616 | protected function reportFail()
617 | {
618 | $this->reportResult('UPDATE FAILED');
619 | }
620 |
621 | /**
622 | * Sends execution report.
623 | * Report message content will be composed from log messages.
624 | * @param string $subjectPrefix report message subject.
625 | */
626 | protected function reportResult($subjectPrefix)
627 | {
628 | $emails = $this->emails;
629 | if (!empty($emails)) {
630 | $hostName = $this->getHostName();
631 | $from = $this->getReportFrom();
632 | $subject = $subjectPrefix . ': ' . $hostName . ' at ' . $this->getCurrentDate();
633 | $message = implode("\n", $this->flushLog());
634 | foreach ($emails as $email) {
635 | $this->sendEmail($from, $email, $subject, $message);
636 | }
637 | }
638 | }
639 |
640 | /**
641 | * Sends an email.
642 | * @param string $from sender email address
643 | * @param string $email single email address
644 | * @param string $subject email subject
645 | * @param string $message email content
646 | * @return bool success.
647 | */
648 | protected function sendEmail($from, $email, $subject, $message)
649 | {
650 | if ($this->mailer === null) {
651 | return $this->sendEmailFallback($from, $email, $subject, $message);
652 | }
653 |
654 | try {
655 | /* @var $mailer \yii\mail\MailerInterface|BaseMailer */
656 | $mailer = Instance::ensure($this->mailer, 'yii\mail\MailerInterface');
657 | if ($mailer instanceof BaseMailer) {
658 | $mailer->useFileTransport = false; // ensure mailer is not in test mode
659 | }
660 | return $mailer->compose()
661 | ->setFrom($from)
662 | ->setTo($email)
663 | ->setSubject($subject)
664 | ->setTextBody($message)
665 | ->send();
666 | } catch (\Exception $exception) {
667 | $this->log($exception->getMessage());
668 | return $this->sendEmailFallback($from, $email, $subject, $message);
669 | }
670 | }
671 |
672 | /**
673 | * Sends an email via plain PHP `mail()` function.
674 | * @param string $from sender email address
675 | * @param string $email single email address
676 | * @param string $subject email subject
677 | * @param string $message email content
678 | * @return bool success.
679 | */
680 | protected function sendEmailFallback($from, $email, $subject, $message)
681 | {
682 | $headers = [
683 | "MIME-Version: 1.0",
684 | "Content-Type: text/plain; charset=UTF-8",
685 | ];
686 | $subject = '=?UTF-8?B?' . base64_encode($subject) . '?=';
687 |
688 | $matches = [];
689 | preg_match_all('/([^<]*)<([^>]*)>/iu', $from, $matches);
690 | if (isset($matches[1][0],$matches[2][0])) {
691 | $name = '=?UTF-8?B?' . base64_encode(trim($matches[1][0])) . '?=';
692 | $from = trim($matches[2][0]);
693 | $headers[] = "From: {$name} <{$from}>";
694 | } else {
695 | $headers[] = "From: {$from}";
696 | }
697 | $headers[] = "Reply-To: {$from}";
698 |
699 | return mail($email, $subject, $message, implode("\n", $headers));
700 | }
701 | }
--------------------------------------------------------------------------------
/src/Shell.php:
--------------------------------------------------------------------------------
1 |
16 | * @since 1.0
17 | */
18 | class Shell
19 | {
20 | /**
21 | * Executes shell command.
22 | * @param string $command command to be executed.
23 | * @param array $placeholders placeholders to be replaced using `escapeshellarg()` in format: placeholder => value.
24 | * @return ShellResult execution result.
25 | */
26 | public static function execute($command, array $placeholders = [])
27 | {
28 | if (!empty($placeholders)) {
29 | $command = strtr($command, array_map('escapeshellarg', $placeholders));
30 | }
31 | $result = new ShellResult();
32 | $result->command = $command;
33 | exec($command . ' 2>&1', $result->outputLines, $result->exitCode);
34 | return $result;
35 | }
36 |
37 | /**
38 | * Builds shell command options string from array.
39 | * Option, which does not use any value should be specified as an array value, option with value should
40 | * be specified as key-value pair, where key is an option name and value - option value.
41 | * Option name will be automatically prefixed with `--` in case it has not already.
42 | *
43 | * For example:
44 | *
45 | * ```php
46 | * [
47 | * 'verbose',
48 | * 'username' => 'root'
49 | * ]
50 | * ```
51 | *
52 | * @param array $options options specification.
53 | * @return string options string.
54 | * @since 1.0.2
55 | */
56 | public static function buildOptions(array $options)
57 | {
58 | $parts = [];
59 | foreach ($options as $key => $value) {
60 | if (is_int($key)) {
61 | $parts[] = self::normalizeOptionName($value);
62 | } else {
63 | $parts[] = self::normalizeOptionName($key) . '=' . escapeshellarg($value);
64 | }
65 | }
66 | return implode(' ', $parts);
67 | }
68 |
69 | /**
70 | * Normalizes shell command option name, adding leading `-` if necessary.
71 | * @param string $name raw option name.
72 | * @return string normalized option name.
73 | * @since 1.0.2
74 | */
75 | private static function normalizeOptionName($name)
76 | {
77 | if (strpos($name, '-') !== 0) {
78 | return '--' . $name;
79 | }
80 | return $name;
81 | }
82 | }
--------------------------------------------------------------------------------
/src/ShellResult.php:
--------------------------------------------------------------------------------
1 |
19 | * @since 1.0
20 | */
21 | class ShellResult extends BaseObject
22 | {
23 | /**
24 | * @var string command being executed.
25 | */
26 | public $command;
27 | /**
28 | * @var int shell command execution exit code
29 | */
30 | public $exitCode;
31 | /**
32 | * @var array shell command output lines.
33 | */
34 | public $outputLines = [];
35 |
36 |
37 | /**
38 | * @param string $glue lines glue.
39 | * @return string shell command output.
40 | */
41 | public function getOutput($glue = "\n")
42 | {
43 | return implode($glue, $this->outputLines);
44 | }
45 |
46 | /**
47 | * @return bool whether exit code is OK.
48 | */
49 | public function isOk()
50 | {
51 | return $this->exitCode === 0;
52 | }
53 |
54 | /**
55 | * @return bool whether command execution produced empty output.
56 | */
57 | public function isOutputEmpty()
58 | {
59 | return empty($this->outputLines);
60 | }
61 |
62 | /**
63 | * Checks if output contains given string
64 | * @param string $string needle string.
65 | * @return bool whether output contains given string.
66 | */
67 | public function isOutputContains($string)
68 | {
69 | return stripos($this->getOutput(), $string) !== false;
70 | }
71 |
72 | /**
73 | * Checks if output matches give regular expression.
74 | * @param string $pattern regular expression
75 | * @return bool whether output matches given regular expression.
76 | */
77 | public function isOutputMatches($pattern)
78 | {
79 | return preg_match($pattern, $this->getOutput()) > 0;
80 | }
81 |
82 | /**
83 | * @return string string representation of this object.
84 | */
85 | public function toString()
86 | {
87 | return $this->command . "\n" . $this->getOutput() . "\n" . 'Exit code: ' . $this->exitCode;
88 | }
89 |
90 | /**
91 | * PHP magic method that returns the string representation of this object.
92 | * @return string the string representation of this object.
93 | */
94 | public function __toString()
95 | {
96 | // __toString cannot throw exception
97 | // use trigger_error to bypass this limitation
98 | try {
99 | return $this->toString();
100 | } catch (\Exception $e) {
101 | ErrorHandler::convertExceptionToError($e);
102 | return '';
103 | }
104 | }
105 | }
--------------------------------------------------------------------------------
/src/VersionControlSystem.php:
--------------------------------------------------------------------------------
1 |
16 | * @since 1.0
17 | */
18 | abstract class VersionControlSystem extends BaseObject implements VersionControlSystemInterface
19 | {
20 | }
--------------------------------------------------------------------------------
/src/VersionControlSystemInterface.php:
--------------------------------------------------------------------------------
1 |
15 | * @since 1.0
16 | */
17 | interface VersionControlSystemInterface
18 | {
19 | /**
20 | * Checks, if there are some changes in remote repository.
21 | * @param string $projectRoot VCS project root directory path.
22 | * @param string $log if parameter passed it will be filled with related log string.
23 | * @return bool whether there are changes in remote repository.
24 | */
25 | public function hasRemoteChanges($projectRoot, &$log = null);
26 |
27 | /**
28 | * Applies changes from remote repository.
29 | * @param string $projectRoot VCS project root directory path.
30 | * @param string $log if parameter passed it will be filled with related log string.
31 | * @return bool whether the changes have been applied successfully.
32 | */
33 | public function applyRemoteChanges($projectRoot, &$log = null);
34 | }
--------------------------------------------------------------------------------
/src/views/selfUpdateConfig.php:
--------------------------------------------------------------------------------
1 | [
13 | //'developer@domain.com',
14 | ],
15 | // Mailer component to be used
16 | 'mailer' => 'mailer',
17 | // Mutex component to be used
18 | 'mutex' => 'mutex',
19 | // path to project root directory (VCS root directory)
20 | 'projectRootPath' => '@app',
21 | // web path stubs configuration
22 | 'webPaths' => [
23 | [
24 | 'path' => '@app/web',
25 | 'link' => '@app/httpdocs',
26 | 'stub' => '@app/webstub',
27 | ],
28 | ],
29 | // cache components to be flushed
30 | 'cache' => [
31 | 'cache'
32 | ],
33 | // temporary directories, which should be cleared after project update
34 | 'tmpDirectories' => [
35 | '@app/web/assets',
36 | '@runtime/URI',
37 | '@runtime/HTML',
38 | '@runtime/debug',
39 | ],
40 | // list of commands, which should be executed before project update begins
41 | 'beforeUpdateCommands' => [],
42 | // list of shell commands, which should be executed after project update
43 | 'afterUpdateCommands' => [
44 | 'php ' . escapeshellarg($_SERVER['SCRIPT_FILENAME']) . ' migrate/up --interactive=0',
45 | ],
46 | // adjust Composer settings, if necessary :
47 | 'composerOptions' => YII_ENV === 'dev' ? [] : ['no-dev'],
48 | /*'composerBinPath' => 'composer',
49 | 'composerRootPaths' => [
50 | '@app'
51 | ],*/
52 | // adjust report settings, if necessary :
53 | //'hostName' => 'myproject.com',
54 | //'reportFrom' => 'root@myproject.com',
55 | ];
56 |
--------------------------------------------------------------------------------