@taole/deploy-helper 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.mjs CHANGED
@@ -109,11 +109,11 @@ async function main() {
109
109
  console.log(`command: scp {file} {dest} 复制文件{file}到OfficialSite测试服务器{dest}`);
110
110
  console.log(`command: scpevt {file} 复制文件{file}到events测试服务器{file}`);
111
111
  console.log(`command: scpevt {file} {dest} 复制文件{file}到events测试服务器{dest}`);
112
- console.log(`command: pipeline {pipelineName|pipelineId} 触发流水线{pipelineName|pipelineId}`);
112
+ console.log(`command: pipeline {pipelineName|pipelineId} [branch] 触发流水线{pipelineName|pipelineId}, 指定分支(可省略)`);
113
113
  process.exit(0);
114
114
  }
115
115
  log("deploy-helper start");
116
-
116
+
117
117
  // 其他命令
118
118
  if (command === "init") {
119
119
  await initConfigJson();
@@ -162,11 +162,12 @@ async function main() {
162
162
  log(`pipeline参数不能为空`);
163
163
  process.exit(1);
164
164
  }
165
+ const branch = process.argv[4] || "";
165
166
  try {
166
- await triggerPipeline(pipelineName, true);
167
+ await triggerPipeline(pipelineName, { waitResult: true, branch });
167
168
  process.exit(0);
168
169
  } catch (error) {
169
- log(`触发流水线失败: ${error}`);
170
+ log(`触发流水线失败: `, error);
170
171
  process.exit(1);
171
172
  }
172
173
 
@@ -181,12 +182,6 @@ async function main() {
181
182
  const config = await loadConfig();
182
183
  // log(`config`, config);
183
184
 
184
- // 检查配置格式
185
- if (!config.assets || !config.assets.dest || !config.entry || !config.entry.dest) {
186
- log(`配置格式错误, 请检查deploy.config.js或deploy.config.json`);
187
- return;
188
- }
189
-
190
185
  const result = hasChangeMe(JSON.stringify(config));
191
186
  if (result) {
192
187
  log(`配置中存在默认值(CHANGE_ME), 请先修改配置`);
@@ -194,8 +189,10 @@ async function main() {
194
189
  }
195
190
 
196
191
 
192
+ const needHandleAssets = config.assets && config.assets.dest;
193
+
197
194
  // 需要处理entry
198
- const needHandleEntry = mode === 'prod' || (mode === 'test' && !config.entry.onlyProd);
195
+ const needHandleEntry = config.entry && config.entry.dest && (mode === 'prod' || (mode === 'test' && !config.entry.onlyProd));
199
196
  const entryTestBranch = (config.entry && config.entry.testBranch) || "test";
200
197
  const entryProdBranch = (config.entry && config.entry.prodBranch) || "master";
201
198
  const entryTargetBranch = mode === 'prod' ? entryProdBranch : entryTestBranch;
@@ -208,24 +205,34 @@ async function main() {
208
205
 
209
206
  // 检查项目
210
207
  // 1. 检查项目是否存在
211
- const assetsDest = join(workDir, config.assets.dest);
212
- const entryDest = join(workDir, config.entry.dest);
213
- if (!fs.existsSync(assetsDest)) {
214
- log(`assets.dest路径不存在: ${assetsDest}, 请先创建`);
215
- return;
208
+ let assetsDest = null;
209
+ let entryDest = null;
210
+ if (needHandleAssets) {
211
+ assetsDest = join(workDir, config.assets.dest);
212
+ if (!fs.existsSync(assetsDest)) {
213
+ log(`assets.dest路径不存在: ${assetsDest}, 请先创建`);
214
+ process.exit(1);
215
+ }
216
216
  }
217
217
  // entry.dest
218
- if (needHandleEntry && !fs.existsSync(entryDest)) {
219
- log(`entry.dest路径不存在: ${entryDest}, 请先创建`);
220
- return;
218
+ if (needHandleEntry) {
219
+ entryDest = join(workDir, config.entry.dest);
220
+ if (!fs.existsSync(entryDest)) {
221
+ log(`entry.dest路径不存在: ${entryDest}, 请先创建`);
222
+ process.exit(1);
223
+ }
221
224
  }
222
225
 
223
226
  // 2. 检查项目是否clean
224
- const assetsGit = simpleGit(assetsDest);
225
- let assetsStatus = await assetsGit.status();
226
- if (!assetsStatus.isClean()) {
227
- log(`${assetsDest}目前有未提交内容, 请先处理`);
228
- return;
227
+ let assetsGit = null;
228
+ let assetsStatus = null;
229
+ if (needHandleAssets) {
230
+ assetsGit = simpleGit(assetsDest);
231
+ assetsStatus = await assetsGit.status();
232
+ if (!assetsStatus.isClean()) {
233
+ log(`${assetsDest}目前有未提交内容, 请先处理`);
234
+ return;
235
+ }
229
236
  }
230
237
 
231
238
  let entryGit;
@@ -255,7 +262,7 @@ async function main() {
255
262
 
256
263
  // 3. 检查assets项目分支,并切换至master分支
257
264
  // assets项目固定使用master分支
258
- if (assetsStatus.current !== "master") {
265
+ if (needHandleAssets && assetsStatus.current !== "master") {
259
266
  log(`${assetsDest}当前分支不是master, 切换至master分支`);
260
267
  await assetsGit.checkout("master");
261
268
  log(`${assetsDest}切换至master分支成功`);
@@ -267,9 +274,11 @@ async function main() {
267
274
  }
268
275
 
269
276
  // 4. 执行git pull
270
- log(`${assetsDest}执行git pull`);
271
- await assetsGit.pull();
272
- log(`${assetsDest} git pull done`);
277
+ if (assetsGit) {
278
+ log(`${assetsDest}执行git pull`);
279
+ await assetsGit.pull();
280
+ log(`${assetsDest} git pull done`);
281
+ }
273
282
  if (entryGit) {
274
283
  log(`${entryDest}执行git pull`);
275
284
  await entryGit.pull();
@@ -278,24 +287,26 @@ async function main() {
278
287
 
279
288
  // 5. 复制构建产物
280
289
  // 5.1 复制assets
281
- let assetsFilesCount = 0;
282
- const assetsFiles = config.assets.files;
283
- for (const [src, dest] of Object.entries(assetsFiles)) {
284
- const srcPath = join(workDir, src);
285
- if (!fs.existsSync(srcPath)) {
286
- log(`${srcPath}不存在,请确认构建产物是否正确`);
287
- return;
288
- }
289
- const destPath = join(assetsDest, dest);
290
- if (fs.statSync(srcPath).isDirectory()) {
291
- fs.cpSync(srcPath, destPath, { recursive: true });
292
- } else {
293
- fs.copyFileSync(srcPath, destPath);
290
+ if (needHandleAssets) {
291
+ let assetsFilesCount = 0;
292
+ const assetsFiles = config.assets.files;
293
+ for (const [src, dest] of Object.entries(assetsFiles)) {
294
+ const srcPath = join(workDir, src);
295
+ if (!fs.existsSync(srcPath)) {
296
+ log(`${srcPath}不存在,请确认构建产物是否正确`);
297
+ return;
298
+ }
299
+ const destPath = join(assetsDest, dest);
300
+ if (fs.statSync(srcPath).isDirectory()) {
301
+ fs.cpSync(srcPath, destPath, { recursive: true });
302
+ } else {
303
+ fs.copyFileSync(srcPath, destPath);
304
+ }
305
+ log(`assets copy: ${srcPath} -> ${destPath}`);
306
+ assetsFilesCount++;
294
307
  }
295
- log(`assets copy: ${srcPath} -> ${destPath}`);
296
- assetsFilesCount++;
308
+ // log(`assets copy done.`);
297
309
  }
298
- // log(`assets copy done.`);
299
310
  // 5.2 复制entry
300
311
  if (needHandleEntry) {
301
312
  const entryFiles = config.entry.files;
@@ -319,18 +330,19 @@ async function main() {
319
330
  // 6. 提交
320
331
  let canPushAssets = false;
321
332
  let canPushEntry = false;
322
- assetsStatus = await assetsGit.status();
323
- if (!assetsStatus.isClean()) {
324
- canPushAssets = true;
325
- await assetsGit.add(".");
326
- const assetsCommit = `${config.assets.commit || "feat:部署资源文件"} by deploy-helper`;
327
- const assetsCommitResult = await assetsGit.commit(assetsCommit);
328
- log(`assets commit: ${assetsCommit}`);
329
- log(`assets commit: ${JSON.stringify(assetsCommitResult.summary)}`);
330
- } else {
331
- log(`${assetsDest} 未发现修改内容,跳过提交`);
333
+ if (needHandleAssets) {
334
+ assetsStatus = await assetsGit.status();
335
+ if (!assetsStatus.isClean()) {
336
+ canPushAssets = true;
337
+ await assetsGit.add(".");
338
+ const assetsCommit = `${config.assets.commit || "feat:部署资源文件"} by deploy-helper`;
339
+ const assetsCommitResult = await assetsGit.commit(assetsCommit);
340
+ log(`assets commit: ${assetsCommit}`);
341
+ log(`assets commit: ${JSON.stringify(assetsCommitResult.summary)}`);
342
+ } else {
343
+ log(`${assetsDest} 未发现修改内容,跳过提交`);
344
+ }
332
345
  }
333
-
334
346
  if (needHandleEntry) {
335
347
  entryStatus = await entryGit.status();
336
348
  if (!entryStatus.isClean()) {
@@ -26,27 +26,27 @@ function getCurrentJob(runDetail) {
26
26
  const stages = runDetail.stages;
27
27
 
28
28
  // 找到当前阶段
29
- for(let i = 0; i < stages.length; i++) {
29
+ for (let i = 0; i < stages.length; i++) {
30
30
  const stage = stages[i];
31
31
  currentStage = stage;
32
32
  const stageStatus = stage.stageInfo.status;
33
- if(stageStatus === "RUNNING" || stageStatus === "WAITING" || stageStatus === "FAIL") {
33
+ if (stageStatus === "INIT" || stageStatus === "RUNNING" || stageStatus === "WAITING" || stageStatus === "FAIL") {
34
34
  break;
35
35
  }
36
36
  }
37
37
 
38
- if(currentStage) {
38
+ if (currentStage) {
39
39
  const jobs = currentStage.stageInfo.jobs;
40
- for(let i = 0; i < jobs.length; i++) {
40
+ for (let i = 0; i < jobs.length; i++) {
41
41
  const job = jobs[i];
42
42
  currentJob = job;
43
43
  const jobStatus = job.status;
44
- if(jobStatus === "RUNNING" || jobStatus === "WAITING" || jobStatus === "FAIL") {
44
+ if (jobStatus === "INIT" || jobStatus === "RUNNING" || jobStatus === "WAITING" || jobStatus === "FAIL") {
45
45
  break;
46
46
  }
47
47
  }
48
48
  }
49
- return {stage: currentStage, job: currentJob};
49
+ return { stage: currentStage, job: currentJob };
50
50
  }
51
51
 
52
52
  /**
@@ -155,24 +155,35 @@ async function getPipelineInfoByName(name) {
155
155
  * @param {number} interval 轮询间隔时间
156
156
  * @returns 流水线执行详情
157
157
  */
158
- async function waitPipelineRunFinish(pipelineID, runId, interval = 3000) {
158
+ async function waitPipelineRunFinish(pipelineID, runId, interval = 5000) {
159
159
  const pipelineRunDetailUrl = `https://flow.aliyun.com/pipelines/${pipelineID}/builds/${runId}`;
160
160
  log(`开始轮询至流水线执行完成`);
161
161
  let runDetail = await getPipelineRunFunc(organizationId, pipelineID, runId);
162
- while (runDetail.status === "RUNNING" || runDetail.status === "WAITING") {
162
+ let passFailedCount = 0;
163
+ let passFailedMaxCount = 3;
164
+ while (runDetail.status === "RUNNING" || runDetail.status === "WAITING" || runDetail.status === "INIT") {
163
165
  const { stage, job } = getCurrentJob(runDetail);
164
166
  log(`流水线执行状态: ${runDetail.status},阶段: ${stage.name},任务: ${job.name},等${interval / 1000}秒后重新轮询`);
165
167
  if (runDetail.status === "WAITING") {
166
168
  if (stage && job) {
167
169
  log(`尝试自动通过人工卡点`);
168
- const isPass = await passPipelineJob(pipelineID, runId, job.id);
169
- if (isPass) {
170
- log(`人工卡点通过`);
171
- } else {
172
- log(`人工卡点通过失败`);
173
- break;
170
+ try {
171
+ const isPass = await passPipelineJob(pipelineID, runId, job.id);
172
+ if (isPass) {
173
+ log(`人工卡点通过`);
174
+ } else {
175
+ log(`人工卡点通过失败`);
176
+ passFailedCount++;
177
+ }
178
+ } catch (error) {
179
+ passFailedCount++;
180
+ log(`人工卡点通过失败`, error);
174
181
  }
175
182
  }
183
+ if (passFailedCount >= passFailedMaxCount) {
184
+ log(`人工卡点通过失败次数超过最大值, 放弃`);
185
+ break;
186
+ }
176
187
  }
177
188
  await new Promise(resolve => setTimeout(resolve, interval));
178
189
  runDetail = await getPipelineRunFunc(organizationId, pipelineID, runId);
@@ -185,33 +196,78 @@ async function waitPipelineRunFinish(pipelineID, runId, interval = 3000) {
185
196
  return runDetail;
186
197
  }
187
198
 
199
+ /**
200
+ * 获取流水线信息和源码仓库信息, 不指定分支则不返回源码仓库信息
201
+ * @param {*} pipelineName 流水线名称或id
202
+ * @param {*} targetBranch 指定分支, 如果为空,则不指定分支
203
+ * @returns 流水线信息和源码仓库信息, 触发流水线参数
204
+ */
205
+ async function getPipelineInfoDetail(pipelineName, targetBranch = ""){
206
+ let pipelineID = "";
207
+ let pipelineInfo = null;
208
+ let repoInfo = null;
209
+ let hasDetail = false;
210
+ if (/^\d+$/.test(pipelineName)) {
211
+ pipelineID = pipelineName;
212
+ pipelineInfo = await getPipelineFunc(organizationId, pipelineID);
213
+ pipelineInfo.id = pipelineID;
214
+ hasDetail = true;
215
+ } else {
216
+ const info = await getPipelineInfoByName(pipelineName);
217
+ pipelineInfo = info;
218
+ pipelineID = info.id;
219
+ hasDetail = false;
220
+ }
221
+ // 如果需要指定分支,则需要获取源码仓库信息
222
+ if (targetBranch) {
223
+ if (!hasDetail) {
224
+ pipelineInfo = await getPipelineFunc(organizationId, pipelineID);
225
+ pipelineInfo.id = pipelineID;
226
+ }
227
+ if (pipelineInfo && pipelineInfo.pipelineConfig && Array.isArray(pipelineInfo.pipelineConfig.sources)) {
228
+ repoInfo = pipelineInfo.pipelineConfig.sources[0];
229
+ }
230
+ }
231
+
232
+ if (targetBranch && !repoInfo) {
233
+ throw new Error(`未找到源码仓库信息,请检查流水线配置是否正确`);
234
+ }
235
+ const params = {
236
+ "envs": {
237
+ "Change_Log": "deploy-helper触发", // 此处不可以带空格,否则会报错
238
+ },
239
+ "needCreateBranch": false,
240
+ "comment": "triggered by deploy-helper"
241
+ }
242
+ if (targetBranch && repoInfo) {
243
+ params.runningBranchs = {
244
+ [`${repoInfo.data.repo}`]: targetBranch
245
+ }
246
+ log(`指定分支: ${targetBranch}, 源码仓库: ${repoInfo.data.repo}`);
247
+ }
248
+ const runParams = {
249
+ params: JSON.stringify(params)
250
+ };
251
+ return { pipelineInfo, repoInfo, runParams };
252
+ }
188
253
 
189
254
  /**
190
255
  * 触发流水线
191
256
  * @param {*} pipelineName 流水线名称或id
192
257
  * @param {*} waitResult 是否等待流水线执行完成
193
258
  */
194
- export async function triggerPipeline(pipelineName, waitResult = false) {
259
+ export async function triggerPipeline(pipelineName, opts = {}) {
260
+ const { waitResult = false } = opts;
195
261
  const yunxiaoToken = getYunxiaoToken({});
196
262
  if (!yunxiaoToken) {
197
263
  throw new Error(`未设置云效token, 请检查云效token是否正确`);
198
264
  }
199
- let pipelineID = "";
200
- let name = pipelineName;
201
- if (/^\d+$/.test(pipelineName)) {
202
- pipelineID = pipelineName;
203
- const pipelineInfo = await getPipelineFunc(organizationId, pipelineID);
204
- name = pipelineInfo.name;
205
- } else {
206
- const pipelineInfo = await getPipelineInfoByName(pipelineName);
207
- pipelineID = pipelineInfo.id;
208
- name = pipelineInfo.name;
209
- }
210
- const runId = await createPipelineRunFunc(organizationId, pipelineID, {});
211
- const pipelineRunDetailUrl = `https://flow.aliyun.com/pipelines/${pipelineID}/builds/${runId}`;
212
- log(`流水线${name}[${pipelineID}]触发成功,流水线执行id: ${runId}, 流水线执行详情: ${pipelineRunDetailUrl}`);
265
+ const { pipelineInfo, runParams } = await getPipelineInfoDetail(pipelineName, opts.branch);
266
+ const runId = await createPipelineRunFunc(organizationId, pipelineInfo.id, runParams);
267
+ const pipelineRunDetailUrl = `https://flow.aliyun.com/pipelines/${pipelineInfo.id}/builds/${runId}`;
268
+ log(`流水线${pipelineInfo.name}[${pipelineInfo.id}]触发成功,流水线执行id: ${runId}, 流水线执行详情: ${pipelineRunDetailUrl}`);
213
269
  if (waitResult) {
214
- await waitPipelineRunFinish(pipelineID, runId);
270
+ await waitPipelineRunFinish(pipelineInfo.id, runId);
215
271
  }
216
272
  }
217
273
 
@@ -249,15 +305,10 @@ export function checkYunxiaoToken(config, mode) {
249
305
 
250
306
  async function runSinglePipeline(pipeline, index, total) {
251
307
  log(`开始处理流水线: ${pipeline.name}, 当前是第${index + 1}个, 总共${total}个`);
252
- let pipelineId = pipeline.id || "";
253
- if (!pipelineId) {
254
- // 获取流水线信息
255
- const pipelineInfo = await getPipelineInfoByName(pipeline.name);
256
- pipelineId = pipelineInfo.id;
257
- }
258
- log(`流水线${pipeline.name}的id: ${pipelineId}`);
308
+ const { pipelineInfo, runParams } = await getPipelineInfoDetail(pipeline.id || pipeline.name, pipeline.branch);
259
309
 
260
310
  // 处理分支强推的问题
311
+ // 将要废弃force2Branch这个配置
261
312
  const force2Branch = pipeline.force2Branch;
262
313
  if (force2Branch && force2Branch.src && force2Branch.dest) {
263
314
  let repo = pipeline.repo || "./";
@@ -284,13 +335,11 @@ async function runSinglePipeline(pipeline, index, total) {
284
335
  await git.raw("push", "origin", `${force2Branch.src}:${force2Branch.dest}`, "--force");
285
336
  log(`仓库${repo}强推${force2Branch.src}到${force2Branch.dest}完成`);
286
337
  }
287
- // TODO: PERF: 获取流水线信息后, 可以缓存下来, 避免每次都重新获取
288
- // TODO: 触发流水线时, 可以传入参数, 比如分支, 比如环境, 但是不知道为什么覆盖不了。这里先不传了
289
- const runId = await createPipelineRunFunc(organizationId, pipelineId, {});
290
- const piplineRunDetailUrl = `https://flow.aliyun.com/pipelines/${pipelineId}/builds/${runId}`;
291
- log(`流水线${pipeline.name}触发成功,流水线执行id: ${runId}, 流水线执行详情: ${piplineRunDetailUrl}`);
292
- if(pipeline.waitResult) {
293
- await waitPipelineRunFinish(pipelineId, runId);
338
+ const runId = await createPipelineRunFunc(organizationId, pipelineInfo.id, runParams);
339
+ const piplineRunDetailUrl = `https://flow.aliyun.com/pipelines/${pipelineInfo.id}/builds/${runId}`;
340
+ log(`流水线${pipelineInfo.name}触发成功,流水线执行id: ${runId}, 流水线执行详情: ${piplineRunDetailUrl}`);
341
+ if (pipeline.waitResult) {
342
+ await waitPipelineRunFinish(pipelineInfo.id, runId);
294
343
  }
295
344
  }
296
345
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taole/deploy-helper",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "脚本部署工具,用于将项目部署到测试环境或生产环境",
5
5
  "main": "index.mjs",
6
6
  "type": "module",