PageRenderTime 472ms CodeModel.GetById 14ms RepoModel.GetById 1ms app.codeStats 0ms

/transition/transition.php

https://github.com/antz29/transition
PHP | 484 lines | 288 code | 70 blank | 126 comment | 55 complexity | 403aef89ea42bd3353856866f94c1384 MD5 | raw file
  1. <?php
  2. /**
  3. * Transition
  4. *
  5. * This is the main Transition class that extends the Rebound library
  6. * which handles the command line interaction.
  7. *
  8. */
  9. class Transition extends Rebound {
  10. /**
  11. * Configure the given SETTING with the given VALUE
  12. *
  13. * Settings:
  14. *
  15. * CONFIG - Path to the transition config root.
  16. * SVN_ROOT - The path to the project root of the SVN repository.
  17. * DEV_GROUP - The unix group that all developers are a member of.
  18. *
  19. * This command should be run as root.
  20. */
  21. protected function action_set_config($setting,$value)
  22. {
  23. if (!file_exists(ROOT.'config.php')) {
  24. $settings = array();
  25. }
  26. else {
  27. $settings = include(ROOT.'config.php');
  28. }
  29. $settings[$setting] = $value;
  30. echo "Set {$setting} to '{$value}'\n";
  31. file_put_contents(ROOT.'config.php','<?php return '.var_export($settings,true).';');
  32. }
  33. /**
  34. * Return the current value of the given SETTING
  35. *
  36. * Settings:
  37. *
  38. * CONFIG - Path to the transition config root.
  39. *
  40. * SVN_ROOT - The path to the project root of the SVN repository.
  41. *
  42. * DEV_GROUP - The unix group that all developers are a member of.
  43. *
  44. */
  45. protected function action_get_config($setting)
  46. {
  47. $val = defined($setting) ? constant($setting) : 'Not Set';
  48. echo "{$setting}: {$val}\n";
  49. }
  50. /**
  51. * Deploy a specific release of a project to the specified target.
  52. *
  53. * @param string $project
  54. * @param string $release
  55. * @param string $target
  56. */
  57. protected function action_deploy($project,$release,$target) {
  58. if (!isset($project)) $this->error('You must specify a project.');
  59. if (!isset($release)) $this->error('You must specify a release.');
  60. if (!isset($target)) $this->error('You must specify a target.');
  61. $project_name = $project;
  62. // retrieve the project information and test to ensure that it has tasks to execute
  63. $project = $this->getProject($project);
  64. if (!count($project['tasks']))
  65. {
  66. $this->error("No tasks defined in project: '{$project['name']}'\nEdit the project XML config and add some deployment tasks.");
  67. }
  68. // define a tasks array and populate it with the tasks needed for the current target
  69. $tasks = array();
  70. foreach ($project['tasks'] as $task)
  71. {
  72. // explode the target string to get an array of targets
  73. $task['target'] = explode(',',$task['target']);
  74. // ensure there are some targets defined and that the current target is in the array
  75. if (!count($task['target'])) continue;
  76. if (!in_array($target,$task['target'])) continue;
  77. $task['params']['_target'] = $target;
  78. $task['params']['_project'] = $project_name;
  79. // add the task to the array
  80. $tasks[] = $task;
  81. }
  82. // iterate through the tasks until all the tasks are gone, removing a task
  83. // from the top of the array on each iteration
  84. while (count($tasks))
  85. {
  86. // remove a task from the array
  87. $task = array_shift($tasks);
  88. // execute the task, passing the current $release and if this is the
  89. // last task is the array.
  90. $this->execTask($task,$release,(count($tasks) == 0));
  91. }
  92. }
  93. /**
  94. * execTask
  95. *
  96. * Executes a given $task passing over the current $release and if the task is the $final task.
  97. * This $final parameter is used by the replicate task when copying to multiple targets to ensure
  98. * it only removes the source files after it has finished copying to all targets.
  99. *
  100. * @param string $task
  101. * @param string $release
  102. * @param boolean $final
  103. */
  104. private function execTask($task,$release,$final=false)
  105. {
  106. $task['params']['release'] = $release;
  107. $task['params']['_final'] = $final;
  108. $class = 'task_'.$task['type'];
  109. if (!class_exists($class))
  110. {
  111. $this->error("Task is undefined: '{$task['type']}'\nCheck the project XML config is correct.");
  112. }
  113. $t = new $class($task['params']);
  114. try
  115. {
  116. $t->exec();
  117. }
  118. catch (Exception $e)
  119. {
  120. $this->error("Task ended in error: '{$task['type']}'\n".$e->getMessage());
  121. }
  122. }
  123. /**
  124. * Show the configuration details of a project.
  125. */
  126. protected function action_show($project,$target=null) {
  127. if (!isset($project)) $this->error('You must specify a project.');
  128. $project = $this->getProject($project);
  129. print "Name: {$project['name']}\n";
  130. print "Tasks:\n";
  131. foreach ($project['tasks'] as $task) {
  132. if (isset($target)) {
  133. $task['target'] = explode(',',$task['target']);
  134. if (!in_array($target,$task['target'])) {
  135. continue;
  136. }
  137. $task['target'] = implode(',',$task['target']);
  138. }
  139. print " - {$task['type']}";
  140. print " / {$task['target']}\n";
  141. if (!isset($task['params'])) continue;
  142. foreach ($task['params'] as $name => $value) {
  143. print " - {$name} = {$value}\n";
  144. }
  145. }
  146. }
  147. /**
  148. * Returns the $project configuration information either as an associative array
  149. * or by providing bool true to $xml, the raw XML.
  150. *
  151. * @param string $project
  152. * @param bool $xml
  153. * @return string
  154. */
  155. private function getProject($project,$xml=null) {
  156. // generate the path the the project XML file
  157. $file = CONFIG.DS.$project.'.xml';
  158. // check if the project has been defined
  159. if (!file_exists($file)) {
  160. $this->error("Unknown project: '{$project}'\nType '{$this->_name} list' to list all projects.");
  161. }
  162. // if requesting raw XML, display this and return
  163. if (isset($xml)) {
  164. return file_get_contents($file);
  165. }
  166. // generate an associative array from the project xml file
  167. $project = array('name'=>$project);
  168. $xml = simplexml_load_file($file);
  169. $cnt = 0;
  170. foreach ($xml->task as $task) {
  171. $cnt++;
  172. $project['tasks'][$cnt]['type'] = (string) $task['type'];
  173. if (isset($task['target'])) {
  174. $project['tasks'][$cnt]['target'] = (string) $task['target'];
  175. }
  176. foreach ($task->param as $param) {
  177. if (!isset($param['value'])) {
  178. $project['tasks'][$cnt]['params'][(string) $param['name']] = (string) $param->value;
  179. }
  180. else {
  181. $project['tasks'][$cnt]['params'][(string) $param['name']] = (string) $param['value'];
  182. }
  183. }
  184. }
  185. return $project;
  186. }
  187. /**
  188. * Returns an of all the project.
  189. *
  190. * @return array
  191. */
  192. private function getProjects() {
  193. // create a glob
  194. $files = CONFIG.DS.'*.xml';
  195. // get the matching config files
  196. $list = glob($files);
  197. //iterate through the $list array getting value as a reference
  198. foreach ($list as &$project) {
  199. //explode the file basename on '.' to separate the file extension
  200. $project = explode('.',basename($project));
  201. //remove the file extension
  202. array_pop($project);
  203. //implode to get the project name
  204. $project = implode($project);
  205. //this process is designed to work with files with multiple '.' ie. somefile.something.xml
  206. }
  207. return $list;
  208. }
  209. /**
  210. * List all configured projects.
  211. */
  212. protected function action_list() {
  213. $list = $this->getProjects();
  214. if (!count($list)) {
  215. return;
  216. }
  217. echo implode("\n",$list)."\n";
  218. }
  219. /**
  220. * Tags and branches a new release as appropriate, optionally adding a comment
  221. * to describe the release.
  222. *
  223. * You should specify the project name ie. my_project
  224. *
  225. * Subprojects are automatically released with the release of a parent project.
  226. *
  227. * If the release is a bugfix release, there must be
  228. * an existing release branch where the fixes have been committed to HEAD.
  229. */
  230. protected function action_release($project,$release,$comment="") {
  231. if (!isset($project)) $this->error('You must specify an project.');
  232. if (!isset($release)) $this->error('You must specify a release.');
  233. $spl = explode('.',$release);
  234. if (count($spl) != 3) $this->error('Incorrectly formatted release string, must be of the format x.y.z');
  235. // For a bugfix release
  236. if ($spl[2] != 0)
  237. {
  238. // create the branch name from the requested release by forcing the third element to be 0.
  239. // this would convert 1.1.1 to 1.1.0 or 1.0.1 to 1.0.0.
  240. $spl[2] = 0;
  241. $branch = implode('.',$spl);
  242. //create the svn path tag and set the source path the be the branch
  243. $root = SVN_ROOT.'/'.$project.'/';
  244. $path = $root.'/branches/'.$branch;
  245. $t_target = $root.'/tags/'.$release;
  246. echo "Tagging project...\n";
  247. $this->svnCopy($path,$t_target,"Creating tag for {$release} release. {$comment}");
  248. $list = $this->svnList($root);
  249. $s_projects = array();
  250. if (in_array('subprojects/',$list)) {
  251. $list = $this->svnList($root.'/subprojects/');
  252. foreach ($list as $s_project) {
  253. $s_project = substr($s_project,0,-1);
  254. $s_projects[$s_project] = array(
  255. 'path' => $root.'/subprojects/'.$s_project.'/branches/'.$branch,
  256. 'tag' => $root.'/subprojects/'.$s_project.'/tags/'.$release
  257. );
  258. }
  259. }
  260. if (count($s_projects)) {
  261. echo "Tagging subprojects...\n";
  262. foreach ($s_projects as $name => $s_project) {
  263. echo " {$name}...\n";
  264. $this->svnCopy($s_project['path'],$s_project['tag'],"Creating subproject tag for {$name} {$release} release. {$comment}");
  265. }
  266. }
  267. }
  268. // For a major or minor release
  269. else
  270. {
  271. // create the svn paths for the branch and tag with the source path being trunk
  272. $root = SVN_ROOT.$project.'/';
  273. $path = $root.'trunk';
  274. $b_target = $root.'branches/'.$release;
  275. $t_target = $root.'tags/'.$release;
  276. $list = $this->svnList($root);
  277. $s_projects = array();
  278. if (in_array('subprojects/',$list)) {
  279. $list = $this->svnList($root.'/subprojects/');
  280. foreach ($list as $s_project) {
  281. $s_project = substr($s_project,0,-1);
  282. $s_projects[$s_project] = array(
  283. 'path' => $root.'/subprojects/'.$s_project.'/trunk',
  284. 'tag' => $root.'/subprojects/'.$s_project.'/tags/'.$release,
  285. 'branch' => $root.'/subprojects/'.$s_project.'/branches/'.$release
  286. );
  287. }
  288. }
  289. //root project
  290. echo "Branching / Tagging root project...\n";
  291. $this->svnCopy($path,$t_target,"Creating tag for {$release} release. {$comment}");
  292. $this->svnCopy($path,$b_target,"Creating branch for {$release} release. {$comment}");
  293. if (count($s_projects)) {
  294. echo "Branching / Tagging subprojects...\n";
  295. foreach ($s_projects as $name => $s_project) {
  296. echo " {$name}...\n";
  297. $this->svnCopy($s_project['path'],$s_project['tag'],"Creating subproject tag for {$name} {$release} release. {$comment}");
  298. $this->svnCopy($s_project['path'],$s_project['branch'],"Creating subproject branch for {$name} {$release} release. {$comment}");
  299. }
  300. }
  301. }
  302. }
  303. /**
  304. * Updates the transition config from svn.
  305. */
  306. protected function action_update() {
  307. $this->svnUpdate(CONFIG);
  308. }
  309. /**
  310. * Initializes the transition config from svn. Should be run as root.
  311. */
  312. protected function action_init($svn) {
  313. if (!isset($svn)) $this->error('You must specify an SVN path for the configuretion.');
  314. if (!stristr($svn,'://')) {
  315. $svn = SVN_ROOT.DS.$svn;
  316. }
  317. $this->svnCo($svn,CONFIG);
  318. $grp = DEV_GROUP;
  319. $CONFIG = CONFIG;
  320. shell_exec("chown -Rc root:{$grp} '{$CONFIG}'");
  321. shell_exec("chmod -Rc 775 '{$CONFIG}'");
  322. }
  323. private function svnCo($source,$target) {
  324. $svn_cmd = '"'.SVN.'"';
  325. $un = SVN_USER;
  326. $pw = SVN_PASS;
  327. $cmd = "{$svn_cmd} co --username \"{$un}\" --password \"{$pw}\" {$source} {$target}";
  328. passthru($cmd);
  329. }
  330. private function svnUpdate($path) {
  331. $svn_cmd = '"'.SVN.'"';
  332. $un = SVN_USER;
  333. $pw = SVN_PASS;
  334. $cmd = "{$svn_cmd} update --username \"{$un}\" --password \"{$pw}\" {$path}";
  335. passthru($cmd);
  336. }
  337. private function svnList($path) {
  338. $svn_cmd = '"'.SVN.'"';
  339. $un = SVN_USER;
  340. $pw = SVN_PASS;
  341. $list = "{$svn_cmd} list --username \"{$un}\" --password \"{$pw}\" {$path}";
  342. $list = shell_exec($list);
  343. $list = array_filter(explode("\n",$list));
  344. foreach ($list as &$item) {
  345. $item = trim($item);
  346. }
  347. return $list;
  348. }
  349. private function svnCopy($source,$target,$comment) {
  350. $svn_cmd = '"'.SVN.'"';
  351. $un = SVN_USER;
  352. $pw = SVN_PASS;
  353. $exec = $svn_cmd." copy --username \"{$un}\" --password \"{$pw}\" {$source} {$target} -m \"{$comment}\"";
  354. $out = shell_exec($exec);
  355. if (!stristr($out,"Committed revision")) {
  356. $this->error("Failed on svn copy. {$source} {$target}:\n{$out}");
  357. }
  358. return true;
  359. }
  360. /**
  361. * The init function call by Rebound at the start of a session.
  362. * It configures the Transition environment to execute tasks.
  363. *
  364. */
  365. protected function init() {}
  366. protected function preAction($action,$args)
  367. {
  368. if ($action == 'set_config') return;
  369. if (!file_exists(ROOT.'config.php')) {
  370. $this->error("This transition installation has not been configured.\nPlease configure the base settings with transition set_config.");
  371. }
  372. else {
  373. $settings = include(ROOT.'config.php');
  374. if (!isset($settings['CONFIG'])) $this->error('The CONFIG has not been set, please configure.');
  375. if (!isset($settings['SVN_ROOT'])) $this->error('The SVN_ROOT has not been set, please configure.');
  376. if (!isset($settings['DEV_GROUP'])) $this->error('The DEV_GROUP has not been set, please configure.');
  377. foreach ($settings as $setting => $value) {
  378. define($setting,$value);
  379. }
  380. }
  381. if (!defined('SVN')) {
  382. $svn = trim(shell_exec('which svn'));
  383. if (!$svn) $this->error('Could not find svn on path, please ensure it is installed correctly or set the SVN setting to define the full path.');
  384. }
  385. if (!defined('RSYNC')) {
  386. $svn = trim(shell_exec('which rsync'));
  387. if (!$svn) $this->error('Could not find rsync on path, please ensure it is installed correctly or set the RSYNC setting to define the full path.');
  388. }
  389. // If required attempt to create the TMP_ROOT root path.
  390. if (!file_exists(TMP_ROOT)) {
  391. if (!@mkdir(TMP_ROOT)) {
  392. $this->error('Failed to create temp folder at '.TMP_ROOT);
  393. }
  394. }
  395. // Create a unique session root path for this session
  396. define('SESSION_ROOT',TMP_ROOT.md5(uniqid()));
  397. // If there is an existing session root folder existing, remove it.
  398. if (file_exists(SESSION_ROOT)) {
  399. rmdir(SESSION_ROOT);
  400. }
  401. // Create the session root folder
  402. if (!mkdir(SESSION_ROOT)) {
  403. $this->error('Failed to create session root folder');
  404. }
  405. }
  406. /**
  407. * The shutdown function is executed by rebound at the end of the session.
  408. * Here we delete the session root.
  409. *
  410. */
  411. protected function shutdown() {
  412. system('rm -rf "'.SESSION_ROOT.'"');
  413. }
  414. }