1 | 'use strict'
|
2 |
|
3 | const ora = require('ora')
|
4 | const axios = require('axios')
|
5 | const chalk = require('chalk')
|
6 | const execa = require('execa')
|
7 |
|
8 | const GITLAB_HOST = 'https://gitlab.weibo.cn'
|
9 | const fetch = axios.create({
|
10 | baseURL: `${GITLAB_HOST}/api/v4/`
|
11 | })
|
12 |
|
13 |
|
14 |
|
15 |
|
16 | const DEBUG = false
|
17 |
|
18 | function replayAsync(fn, assertFn, maxLoop = 10, wait = 1000) {
|
19 | return (...args) => {
|
20 | let cycles = 0
|
21 |
|
22 | return new Promise(async function tillTheWorldEnds(resolve, reject) {
|
23 | let res = null
|
24 | let isEndTime = false
|
25 |
|
26 | try {
|
27 | res = await fn.apply(fn, args)
|
28 | isEndTime = assertFn(res) || ++cycles > maxLoop
|
29 | } catch (e) {
|
30 | return reject(e)
|
31 | }
|
32 |
|
33 | return isEndTime
|
34 | ? resolve(res)
|
35 | : setTimeout(tillTheWorldEnds, wait, resolve)
|
36 | })
|
37 | }
|
38 | }
|
39 |
|
40 | async function doCIJob(repoName, tagName) {
|
41 | const pid = encodeURIComponent(repoName)
|
42 | const spinner = ora(`Searching job...`).start()
|
43 | let job = null
|
44 |
|
45 | try {
|
46 |
|
47 | job = await replayAsync(getTestJob, data => data)(pid, tagName)
|
48 | } catch (e) {
|
49 |
|
50 | if (e.response) {
|
51 | spinner.fail('Searching job: ' + e.response.data.error + '\n')
|
52 | console.log(chalk.red(e.response.data.error_description), '\n')
|
53 | } else {
|
54 | spinner.fail('Searching job\n')
|
55 | console.log(chalk.red(e), '\n')
|
56 | }
|
57 |
|
58 | throw new Error(e)
|
59 | }
|
60 |
|
61 | if (!job) {
|
62 | spinner.fail('未匹配到 CI 任务,请更新 gitlab-ci.yml\n')
|
63 |
|
64 | console.log(
|
65 | chalk.yellow(
|
66 | 'https://raw.githubusercontent.com/SinaMFE/marauder-template/master/.gitlab-ci.yml'
|
67 | ),
|
68 | '\n'
|
69 | )
|
70 |
|
71 | throw new Error()
|
72 | }
|
73 |
|
74 | spinner.text = `Running job #${job.id}...`
|
75 |
|
76 | try {
|
77 | const assertReady = data => data.status != 'created'
|
78 |
|
79 |
|
80 | job = await replayAsync(getJobInfo, assertReady, 10, 1500)(pid, job.id)
|
81 |
|
82 |
|
83 | return await playJob(pid, job.id, spinner)
|
84 | } catch (e) {
|
85 |
|
86 | if (e.response) {
|
87 | spinner.fail('Running job: ' + e.response.data.error + '\n')
|
88 | console.log(chalk.red(e.response.data.error_description), '\n')
|
89 | } else {
|
90 | spinner.fail('Running job\n')
|
91 | console.log(chalk.red(e), '\n')
|
92 | }
|
93 |
|
94 | throw new Error(e)
|
95 | }
|
96 | }
|
97 |
|
98 | async function getJobInfo(pid, jobId) {
|
99 | const rep = await fetch.get(`/projects/${pid}/jobs/${jobId}`)
|
100 |
|
101 | return rep.data
|
102 | }
|
103 |
|
104 | async function getTestJob(pid, tagName) {
|
105 | const { data: jobs } = await fetch.get(`/projects/${pid}/jobs`)
|
106 |
|
107 | return jobs.find(job => {
|
108 | const isTargetTag = job.ref == tagName
|
109 | const isTestJob = job.stage == (DEBUG ? 'dev' : 'test')
|
110 | const isSimulate = job.name == (DEBUG ? 'dev' : 'simulate')
|
111 |
|
112 | return isTargetTag && isTestJob && isSimulate
|
113 | })
|
114 | }
|
115 |
|
116 | async function playJob(pid, jobId, spinner) {
|
117 |
|
118 |
|
119 | const { data: job } = await fetch.post(`/projects/${pid}/jobs/${jobId}/play`)
|
120 |
|
121 |
|
122 | let status = job.status
|
123 | let traceBuffer = ''
|
124 |
|
125 | while (status == 'pending' || status == 'running') {
|
126 | const { status: runningStatus } = await getJobInfo(pid, job.id)
|
127 | const { data: trace } = await fetch.get(
|
128 | `/projects/${pid}/jobs/${job.id}/trace`
|
129 | )
|
130 | const output = trace.replace(traceBuffer, '')
|
131 |
|
132 | status = runningStatus
|
133 |
|
134 | if (!output) continue
|
135 |
|
136 | if (!traceBuffer && trace) spinner.stop()
|
137 |
|
138 | traceBuffer = trace
|
139 | console.log(output)
|
140 | }
|
141 |
|
142 | spinner.stop()
|
143 |
|
144 | return job
|
145 | }
|
146 |
|
147 | function getPushErrTip(error) {
|
148 | const msg = ['\n😲 操作已回滚']
|
149 |
|
150 | if (error.includes('connect to host')) {
|
151 | msg.push('请检查您的网络连接')
|
152 | } else if (error.includes('git pull')) {
|
153 | msg.push('检测到远程分支更新,请先执行 git pull 操作')
|
154 | }
|
155 |
|
156 | return chalk.yellow(msg.join(','))
|
157 | }
|
158 |
|
159 | function checkRepo(remote, branch) {
|
160 | if (!remote) throw new Error('请设置远程仓库')
|
161 |
|
162 | if (remote.indexOf('http') > -1) throw new Error('请配置 ssh 仓库地址')
|
163 |
|
164 | if (!DEBUG && branch != 'master')
|
165 | throw new Error(chalk.red('🚧 请在 master 分支上执行 test 发布操作'))
|
166 | }
|
167 |
|
168 | async function pushBuildCommit(branchName, verInfo) {
|
169 | const spinner = ora('Add commit...').start()
|
170 | const commitInfo = await addCommit(verInfo)
|
171 |
|
172 | spinner.text = 'Pushing commits...'
|
173 |
|
174 | try {
|
175 |
|
176 | await execa('git', ['push', 'origin', branchName])
|
177 | } catch (e) {
|
178 |
|
179 | await execa('git', ['reset', 'HEAD~'])
|
180 | spinner.stop()
|
181 |
|
182 | throw new Error(e.stderr + getPushErrTip(e.stderr))
|
183 | }
|
184 |
|
185 | spinner.succeed(commitInfo + '\n')
|
186 | }
|
187 |
|
188 | async function pushBuildTag(tagName, tagMsg, repoUrl) {
|
189 | const spinner = ora('Add tag...').start()
|
190 | await execa('git', ['tag', '-a', tagName, '-m', tagMsg])
|
191 |
|
192 | spinner.text = `Pushing tag #${tagName}...`
|
193 |
|
194 | try {
|
195 | await execa('git', ['push', 'origin', tagName])
|
196 | } catch (e) {
|
197 |
|
198 | await execa('git', ['tag', '-d', tagName])
|
199 | spinner.stop()
|
200 |
|
201 | const tip = ['\n😲 操作已回滚,请手动发布:', `${repoUrl}/tags/new`].join(
|
202 | '\n'
|
203 | )
|
204 |
|
205 | throw new Error(e.stderr + chalk.yellow(tip))
|
206 | }
|
207 |
|
208 | spinner.stop()
|
209 | }
|
210 |
|
211 | async function addCommit(verInfo) {
|
212 | await execa('git', ['add', '.'])
|
213 |
|
214 | const { stdout: commitInfo } = await execa('git', [
|
215 | 'commit',
|
216 | '-m',
|
217 | `[TEST] v${verInfo}`
|
218 | ])
|
219 |
|
220 | return commitInfo
|
221 | }
|
222 |
|
223 | async function showManualTip(repoUrl, type = 'token') {
|
224 | const { stdout: lastCommit } = await execa('git', ['rev-parse', 'HEAD'])
|
225 | const commitPage = chalk.yellow(`${repoUrl}/commit/${lastCommit}`)
|
226 |
|
227 | if (type == 'token') {
|
228 | console.log(chalk.red('未配置 CI 访问权限,请手动发布:'))
|
229 | console.log(commitPage, '\n')
|
230 |
|
231 | console.log(
|
232 | '推荐在 marauder.config.ciConfig 中配置 privateToken 以启用自动化发布\n'
|
233 | )
|
234 | console.log('Private Token 生成链接:')
|
235 | console.log(chalk.yellow(`${GITLAB_HOST}/profile/personal_access_tokens`))
|
236 | } else if (type == 'ci') {
|
237 | console.log(chalk.red('任务失败,请手动发布:'))
|
238 | console.log(commitPage, '\n')
|
239 | }
|
240 | }
|
241 |
|
242 | module.exports = async function hybridTestPublish(entry, testMsg) {
|
243 | const path = require('path')
|
244 | const config = require('../../config')
|
245 | const { URL } = require('url')
|
246 | const { version } = require(config.paths.packageJson)
|
247 |
|
248 | const tagPrefix = `tag__${entry}__`
|
249 | const verInfo = `${version}-${Date.now()}`
|
250 | const tagName = tagPrefix + verInfo
|
251 | const tagMsg = testMsg || `test ${entry} v${verInfo}`
|
252 |
|
253 | console.log('----------- Test Publish -----------\n')
|
254 |
|
255 | const { stdout: branchName } = await execa('git', [
|
256 | 'symbolic-ref',
|
257 | '--short',
|
258 | 'HEAD'
|
259 | ])
|
260 | const { stdout: remoteUrl } = await execa('git', [
|
261 | 'config',
|
262 | '--get',
|
263 | 'remote.origin.url'
|
264 | ])
|
265 |
|
266 | checkRepo(remoteUrl, branchName)
|
267 |
|
268 | const baseRepoName = path.basename(remoteUrl, '.git')
|
269 |
|
270 | const fullRepoName = new URL(remoteUrl).pathname
|
271 | .replace(/\.git/, '')
|
272 | .replace('/', '')
|
273 | const repoUrl = GITLAB_HOST + '/' + fullRepoName
|
274 |
|
275 | await pushBuildCommit(branchName, verInfo)
|
276 |
|
277 | await pushBuildTag(tagName, tagMsg, repoUrl)
|
278 |
|
279 | console.log(chalk.yellow('Tag: ' + tagName))
|
280 | console.log(chalk.yellow('Msg: ' + tagMsg), '\n')
|
281 |
|
282 | if (!config.ciConfig || !config.ciConfig.privateToken) {
|
283 | return await showManualTip(repoUrl, 'token')
|
284 | }
|
285 |
|
286 | fetch.defaults.headers.common['Private-Token'] = config.ciConfig.privateToken
|
287 | console.log('-------------- CI job --------------\n')
|
288 |
|
289 | try {
|
290 | const job = await doCIJob(fullRepoName, tagName)
|
291 |
|
292 | console.log(chalk.bgGreen(' DONE '), `${repoUrl}/-/jobs/${job.id}`)
|
293 | } catch (e) {
|
294 | const { stdout: lastCommit } = await execa('git', ['rev-parse', 'HEAD'])
|
295 |
|
296 | DEBUG && console.log(e)
|
297 | await showManualTip(repoUrl, 'ci')
|
298 | }
|
299 | }
|