model.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  1. <?php
  2. /**
  3. * The model file of ci module of ZenTaoPMS.
  4. *
  5. * @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.zentao.net)
  6. * @license ZPL(https://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
  7. * @author Yanyi Cao <caoyanyi@easycorp.ltd>
  8. * @package ci
  9. * @link https://www.zentao.net
  10. */
  11. class ciModel extends model
  12. {
  13. /**
  14. * Set menu.
  15. *
  16. * @param int $repoID
  17. * @access public
  18. * @return void
  19. */
  20. public function setMenu($repoID = 0)
  21. {
  22. if($repoID) $this->session->set('repoID', $repoID);
  23. $homeMenuModule = array('gitlab', 'gogs', 'gitea', 'jenkins', 'sonarqube');
  24. if(!in_array("{$this->app->moduleName}", $homeMenuModule)) common::setMenuVars('devops', (int)$this->session->repoID);
  25. if($this->session->repoID)
  26. {
  27. $repo = $this->loadModel('repo')->getByID($this->session->repoID);
  28. if(!empty($repo) && !in_array(strtolower($repo->SCM), $this->config->repo->gitServiceList)) unset($this->lang->devops->menu->mr);
  29. if(!$repo || !in_array($repo->SCM, $this->config->repo->notSyncSCM)) unset($this->lang->devops->menu->tag);
  30. if(!$repo || !in_array($repo->SCM, $this->config->repo->notSyncSCM)) unset($this->lang->devops->menu->branch);
  31. }
  32. }
  33. /**
  34. * 向jenkins发送请求以检查构建状态。
  35. * Send a request to jenkins to check build status.
  36. *
  37. * @param int $compileID
  38. * @access public
  39. * @return bool
  40. */
  41. public function checkCompileStatus($compileID = 0)
  42. {
  43. $compiles = $this->dao->select('compile.*, job.engine,job.pipeline, pipeline.name as jenkinsName,job.server,pipeline.url,pipeline.account,pipeline.token,pipeline.password')
  44. ->from(TABLE_COMPILE)->alias('compile')
  45. ->leftJoin(TABLE_JOB)->alias('job')->on('compile.job=job.id')
  46. ->leftJoin(TABLE_PIPELINE)->alias('pipeline')->on('job.server=pipeline.id')
  47. ->where('compile.status')->notIN('success, failure, create_fail, timeout, canceled')
  48. ->andWhere('compile.createdDate')->gt(date(DT_DATETIME1, strtotime("-1 day")))
  49. ->beginIf($compileID)->andWhere('compile.id')->eq($compileID)->fi()
  50. ->fetchAll();
  51. $notCompileMR = $this->dao->select('jobID,id')
  52. ->from(TABLE_MR)
  53. ->where('jobID')->gt(0)
  54. ->andWhere('compileStatus')->eq('created')
  55. ->fetchPairs();
  56. foreach($compiles as $compile)
  57. {
  58. $MRID = zget($notCompileMR, $compile->job, 0);
  59. $this->syncCompileStatus($compile, $MRID);
  60. }
  61. return !dao::isError();
  62. }
  63. /**
  64. * 根据编译ID获取编译信息。
  65. * Get compile by ID.
  66. *
  67. * @param int $compileID
  68. * @access public
  69. * @return object|false
  70. */
  71. public function getCompileByID($compileID)
  72. {
  73. return $this->dao->select('t1.*, t2.pipeline,t2.product,t2.frame,t2.server,t3.name as jenkinsName,t3.url,t3.account,t3.token,t3.password')->from(TABLE_COMPILE)->alias('t1')
  74. ->leftJoin(TABLE_JOB)->alias('t2')->on('t1.job=t2.id')
  75. ->leftJoin(TABLE_PIPELINE)->alias('t3')->on('t2.server=t3.id')
  76. ->where('t1.id')->eq($compileID)
  77. ->fetch();
  78. }
  79. /**
  80. * 保存编译信息到数据库。
  81. * Save compile info to database.
  82. *
  83. * @param string $response
  84. * @param object $compile
  85. * @param string $userPWD
  86. * @param string $jenkinsServer
  87. * @access public
  88. * @return bool
  89. */
  90. public function saveCompile($response, $compile, $userPWD, $jenkinsServer)
  91. {
  92. if(strripos($response, "404") > -1)
  93. {
  94. $jenkinsServer = strpos($compile->pipeline, '/job/') === 0 ? $jenkinsServer . $compile->pipeline : $jenkinsServer . '/job/' . $compile->pipeline;
  95. $infoUrl = sprintf("%s/api/xml?tree=builds[id,number,result,queueId]&xpath=//build[queueId=%s]", $jenkinsServer, $compile->queue);
  96. $response = common::http($infoUrl, '', array(CURLOPT_USERPWD => $userPWD));
  97. if($response && strpos($response, "<") === 0)
  98. {
  99. $buildInfo = simplexml_load_string($response);
  100. if(empty($buildInfo)) return false;
  101. $buildNumber = strtolower($buildInfo->number->__toString());
  102. if(empty($buildNumber)) return false;
  103. $result = strtolower($buildInfo->result->__toString());
  104. if(empty($result)) return false;
  105. $this->updateBuildStatus($compile, $result);
  106. $logUrl = sprintf('%s/%s/consoleText', $jenkinsServer, $buildNumber);
  107. $response = common::http($logUrl, '', array(CURLOPT_USERPWD => $userPWD));
  108. $this->dao->update(TABLE_COMPILE)->set('logs')->eq($response)->where('id')->eq($compile->id)->exec();
  109. }
  110. }
  111. else
  112. {
  113. $queueInfo = @json_decode($response);
  114. $buildInfo = null;
  115. if($queueInfo && !empty($queueInfo->executable))
  116. {
  117. $buildUrl = $queueInfo->executable->url . 'api/json?pretty=true';
  118. $response = common::http($buildUrl, '', array(CURLOPT_USERPWD => $userPWD));
  119. $buildInfo = json_decode($response);
  120. }
  121. elseif(stripos($response, 'not found') !== false)
  122. {
  123. $job = strpos($compile->pipeline, '/job/') !== false ? $compile->pipeline : '/job/' . $compile->pipeline;
  124. $buildUrl = "{$compile->url}{$job}/api/json?depth=1";
  125. $response = json_decode(common::http($buildUrl, '', array(CURLOPT_USERPWD => $userPWD)));
  126. if($response)
  127. {
  128. foreach($response->builds as $build)
  129. {
  130. if($build->queueId == $compile->queue)
  131. {
  132. $buildInfo = $build;
  133. break;
  134. }
  135. }
  136. }
  137. }
  138. if($buildInfo)
  139. {
  140. if(!empty($buildInfo->building))
  141. {
  142. $this->updateBuildStatus($compile, 'building');
  143. }
  144. else
  145. {
  146. if(empty($buildInfo->result)) return false;
  147. $result = strtolower($buildInfo->result);
  148. $this->updateBuildStatus($compile, $result);
  149. $logUrl = $buildInfo->url . 'logText/progressiveText/api/json';
  150. $response = common::http($logUrl, '', array(CURLOPT_USERPWD => $userPWD));
  151. $this->dao->update(TABLE_COMPILE)->set('logs')->eq($response)->where('id')->eq($compile->id)->exec();
  152. }
  153. }
  154. }
  155. return !dao::isError();
  156. }
  157. /**
  158. * 同步构建状态。
  159. * Sync compile status.
  160. *
  161. * @param object $compile
  162. * @param int $MRID
  163. * @access public
  164. * @return bool
  165. */
  166. public function syncCompileStatus($compile, $MRID = 0)
  167. {
  168. /* Max retry times is: 5. */
  169. if($compile->times >= 5)
  170. {
  171. $this->updateBuildStatus($compile, 'failure');
  172. /* Added merge request result push to xuanxuan. */
  173. if($MRID) $this->loadModel('message')->send('mr', $MRID, 'compilefail', 0);
  174. return false;
  175. }
  176. if($compile->engine == 'gitlab') return $this->syncGitlabTaskStatus($compile);
  177. $jenkinsServer = $compile->url;
  178. $jenkinsPassword = $compile->token ? $compile->token : base64_decode($compile->password);
  179. $userPWD = "{$compile->account}:{$jenkinsPassword}";
  180. $queueUrl = sprintf('%s/queue/item/%s/api/json', $jenkinsServer, $compile->queue);
  181. $response = common::http($queueUrl, '', array(CURLOPT_USERPWD => $userPWD));
  182. $result = '';
  183. if($this->app->rawModule != 'mr') $this->dao->update(TABLE_COMPILE)->set('times = times + 1')->where('id')->eq($compile->id)->exec();
  184. $this->saveCompile($response, $compile, $userPWD, $jenkinsServer);
  185. if($MRID && in_array($result, array('success', 'failure')))
  186. {
  187. $actionType = $result == 'success' ? 'compilepass' : 'compilefail';
  188. $this->loadModel('message')->send('mr', $MRID, $actionType, 0);
  189. }
  190. return !dao::isError();
  191. }
  192. /**
  193. * 同步gitlab任务状态。
  194. * Sync gitlab task status.
  195. *
  196. * @param object $compile
  197. * @access public
  198. * @return bool
  199. */
  200. public function syncGitlabTaskStatus($compile)
  201. {
  202. /* The value of `$compile->pipeline` is like `'{"project":"46","reference":"master"}'` in current design. */
  203. $pipeline = json_decode($compile->pipeline);
  204. $compile->project = isset($pipeline->project) ? (int)$pipeline->project : (int)$compile->pipeline;
  205. $now = helper::now();
  206. $pipeline = $this->loadModel('gitlab')->apiGetSinglePipeline($compile->server, $compile->project, $compile->queue);
  207. if(!isset($pipeline->id) || isset($pipeline->message)) /* The pipeline is not available. */
  208. {
  209. $this->dao->update(TABLE_COMPILE)->set('status')->eq('failure')->where('id')->eq($compile->id)->exec();
  210. $this->dao->update(TABLE_JOB)->set('lastExec')->eq($now)->set('lastStatus')->eq('create_fail')->where('id')->eq($compile->job)->exec();
  211. return false;
  212. }
  213. $jobs = $this->gitlab->apiGetJobs($compile->server, $compile->project, $compile->queue);
  214. $data = new stdclass;
  215. $data->status = $pipeline->status;
  216. $data->updateDate = $now;
  217. $data->logs = '';
  218. foreach($jobs as $job)
  219. {
  220. if(empty($job->duration) || $job->duration == '') $job->duration = '-';
  221. $data->logs .= "<font style='font-weight:bold'>&gt;&gt;&gt; Job: $job->name, Stage: $job->stage, Status: $job->status, Duration: $job->duration Sec\r\n </font>";
  222. $data->logs .= "Job URL: <a href=\"$job->web_url\" target='_blank'>$job->web_url</a> \r\n";
  223. $data->logs .= $this->transformAnsiToHtml($this->gitlab->apiGetJobLog($compile->server, $compile->project, $job->id));
  224. }
  225. $this->dao->update(TABLE_COMPILE)->data($data)->where('id')->eq($compile->id)->exec();
  226. $this->dao->update(TABLE_JOB)->set('lastExec')->eq($now)->set('lastStatus')->eq($pipeline->status)->where('id')->eq($compile->job)->exec();
  227. /* Send mr message by compile status. */
  228. $relateMR = $this->dao->select('*')->from(TABLE_MR)->where('compileID')->eq($compile->id)->fetch();
  229. if($relateMR)
  230. {
  231. if($data->status == 'success') $this->loadModel('action')->create('mr', $relateMR->id, 'compilePass');
  232. if($data->status == 'failed') $this->loadModel('action')->create('mr', $relateMR->id, 'compileFail');
  233. }
  234. return !dao::isError();
  235. }
  236. /**
  237. * 把ansi文本转换成html样式。
  238. * Transform ansi text to html.
  239. *
  240. * @param string $text
  241. * @access public
  242. * @return string
  243. */
  244. public function transformAnsiToHtml($text)
  245. {
  246. $text = preg_replace("/\x1B\[31;40m/", '<font style="color: red">', $text);
  247. $text = preg_replace("/\x1B\[32;1m/", '<font style="color: green">', $text);
  248. $text = preg_replace("/\x1B\[32;1m/", '<font style="color: green">', $text);
  249. $text = preg_replace("/\x1B\[36;1m/", '<font style="color: cyan">', $text);
  250. $text = preg_replace("/\x1B\[0;33m/", '<font style="color: yellow">', $text);
  251. $text = preg_replace("/\x1B\[1m/", '<font style="font-weight:bold">', $text);
  252. $text = preg_replace("/\x1B\[0;m/", '</font><br>', $text);
  253. return preg_replace("/\x1B\[0K/", '<br>', $text);
  254. }
  255. /**
  256. * 更新流水线构建状态。
  257. * Update ci build status.
  258. *
  259. * @param object $build
  260. * @param string $status
  261. * @access public
  262. * @return bool
  263. */
  264. public function updateBuildStatus($build, $status)
  265. {
  266. $this->dao->update(TABLE_COMPILE)->set('status')->eq($status)->set('updateDate')->eq(helper::now())->where('id')->eq($build->id)->exec();
  267. $this->dao->update(TABLE_JOB)->set('lastExec')->eq(helper::now())->set('lastStatus')->eq($status)->where('id')->eq($build->job)->exec();
  268. if($status == 'building') return false;
  269. $relateMR = $this->dao->select('*')->from(TABLE_MR)->where('compileID')->eq($build->id)->fetch();
  270. if(empty($relateMR)) return false;
  271. if($status != 'success')
  272. {
  273. $newMR = new stdclass();
  274. $newMR->status = 'closed';
  275. $newMR->mergeStatus = 'cannot_merge_by_fail';
  276. $newMR->compileStatus = $status;
  277. $this->dao->update(TABLE_MR)->data($newMR)->where('id')->eq($relateMR->id)->exec();
  278. }
  279. elseif(isset($relateMR->synced) && $relateMR->synced == '0')
  280. {
  281. $rawMR = $this->loadModel('mr')->apiCreateMR($relateMR->hostID, $relateMR);
  282. if(!empty($rawMR->iid))
  283. {
  284. $newMR = new stdclass();
  285. $newMR->mriid = $rawMR->iid;
  286. $newMR->status = $rawMR->state;
  287. $newMR->synced = '1';
  288. $this->dao->update(TABLE_MR)->data($newMR)->where('id')->eq($relateMR->id)->exec();
  289. $this->mr->linkObjects($relateMR);
  290. }
  291. }
  292. return !dao::isError();
  293. }
  294. /**
  295. * 发起一个请求。
  296. * Send request.
  297. *
  298. * @param string $url
  299. * @param object $data
  300. * @param string $userPWD
  301. * @access public
  302. * @return string|int
  303. */
  304. public function sendRequest($url, $data, $userPWD = '')
  305. {
  306. if(!empty($data->PARAM_TAG)) $data->PARAM_REVISION = '';
  307. $response = common::http($url, $data, array(CURLOPT_HEADER => true, CURLOPT_USERPWD => $userPWD));
  308. if(preg_match("!Location: .*item/(.*)/!i", $response, $matches)) return $matches[1];
  309. return 0;
  310. }
  311. /**
  312. * 根据ztf结果更新测试单。
  313. * Save test task for ztf.
  314. *
  315. * @param string $testType unit|func
  316. * @param int $productID
  317. * @param int $compileID
  318. * @param int $taskID
  319. * @param string $name
  320. * @access public
  321. * @return int|bool
  322. */
  323. public function saveTestTaskForZtf($testType = 'unit', $productID = 0, $compileID = 0, $taskID = 0, $name = '')
  324. {
  325. $this->loadModel('testtask');
  326. if(!empty($taskID))
  327. {
  328. $testtask = $this->testtask->getByID($taskID);
  329. if(!$testtask) return false;
  330. $this->dao->update(TABLE_TESTTASK)->set('auto')->eq(strtolower($testType))->where('id')->eq($taskID)->exec();
  331. }
  332. else
  333. {
  334. if(empty($productID)) return false;
  335. $lastProject = $this->dao->select('t2.id,t2.project')->from(TABLE_PROJECTPRODUCT)->alias('t1')
  336. ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project=t2.id')
  337. ->where('t1.product')->eq($productID)
  338. ->andWhere('t2.deleted')->eq(0)
  339. ->andWhere('t2.project')->ne('0')
  340. ->orderBy('t2.id desc')
  341. ->limit(1)
  342. ->fetch();
  343. $testtask = new stdclass();
  344. $testtask->product = $productID;
  345. $testtask->name = !empty($name) ? $name : sprintf($this->lang->testtask->titleOfAuto, date('Y-m-d H:i:s'));
  346. $testtask->owner = $this->app->user->account;
  347. $testtask->project = $lastProject ? $lastProject->project : 0;
  348. $testtask->execution = $lastProject ? $lastProject->id : 0;
  349. $testtask->build = 'trunk';
  350. $testtask->auto = strtolower($testType);
  351. $testtask->begin = date('Y-m-d');
  352. $testtask->end = date('Y-m-d', time() + 24 * 3600);
  353. $testtask->status = 'done';
  354. $testtask->createdBy = $this->app->user->account;
  355. $testtask->createdDate = helper::now();
  356. $this->dao->insert(TABLE_TESTTASK)->data($testtask)->exec();
  357. $taskID = $this->dao->lastInsertId();
  358. $this->loadModel('action')->create('testtask', $taskID, 'opened');
  359. }
  360. if($compileID) $this->dao->update(TABLE_COMPILE)->set('testtask')->eq($taskID)->where('id')->eq($compileID)->exec();
  361. if(dao::isError()) return dao::isError();
  362. return $taskID;
  363. }
  364. }