UNPKG

7.93 kBJavaScriptView Raw
1'use strict'
2
3const ora = require('ora')
4const axios = require('axios')
5const chalk = require('chalk')
6const execa = require('execa')
7
8const GITLAB_HOST = 'https://gitlab.weibo.cn'
9const fetch = axios.create({
10 baseURL: `${GITLAB_HOST}/api/v4/`
11})
12
13// 内部调试用
14// 当为 true 时,发布到 ci dev 环境
15// marauder 发布时,请确保关闭
16const DEBUG = false
17
18function 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
40async function doCIJob(repoName, tagName) {
41 const pid = encodeURIComponent(repoName)
42 const spinner = ora(`Searching job...`).start()
43 let job = null
44
45 try {
46 // job 创建需要时间,因此循环请求
47 job = await replayAsync(getTestJob, data => data)(pid, tagName)
48 } catch (e) {
49 // fetch error
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 // job 就绪需要时间,因此循环请求
80 job = await replayAsync(getJobInfo, assertReady, 10, 1500)(pid, job.id)
81
82 // return job
83 return await playJob(pid, job.id, spinner)
84 } catch (e) {
85 // fetch error
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
98async function getJobInfo(pid, jobId) {
99 const rep = await fetch.get(`/projects/${pid}/jobs/${jobId}`)
100
101 return rep.data
102}
103
104async 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
116async function playJob(pid, jobId, spinner) {
117 // replay 操作将会创建新 job
118 // 为了更具拓展,这里统一使用新 job
119 const { data: job } = await fetch.post(`/projects/${pid}/jobs/${jobId}/play`)
120
121 // https://docs.gitlab.com/ee/ci/pipelines.html#ordering-of-jobs-in-pipeline-graphs
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
147function 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
159function 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
168async 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 // push commit
176 await execa('git', ['push', 'origin', branchName])
177 } catch (e) {
178 // 回滚 commit
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
188async 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 // 回滚 tag
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
211async 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
223async 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
242module.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 // /SINA_MFE/snhy
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}