@taole/deploy-helper 1.0.0-beta.1 → 1.0.0-beta.3

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/deploy.mjs CHANGED
@@ -1,7 +1,9 @@
1
+ import { join } from "path";
2
+ import { simpleGit } from 'simple-git';
3
+ import fs from 'fs';
1
4
  import { hasChangeMe } from './util.mjs';
2
5
  import { runPipeline, checkYunxiaoToken } from './lib/pipelineApi.mjs';
3
6
  import { log } from './lib/util.mjs';
4
- import { simpleGit } from 'simple-git';
5
7
  import { getScpClient, TEST_SERVER_HOST } from './util.mjs';
6
8
 
7
9
 
package/deploy2.mjs CHANGED
@@ -1,6 +1,274 @@
1
- export default class Deployer2 {
1
+ import { join, dirname } from "path";
2
+ import { simpleGit } from 'simple-git';
3
+ import { homedir } from 'node:os';
4
+ import fs from 'fs';
5
+ import { hasChangeMe } from './util.mjs';
6
+ import { runPipeline, checkYunxiaoToken } from './lib/pipelineApi.mjs';
7
+ import { log } from './lib/util.mjs';
8
+ import { getScpClient, TEST_SERVER_HOST } from './util.mjs';
9
+
10
+
11
+ function repoUrl2https(url) {
12
+ return url.replace("git@codeup.aliyun.com:", "https://codeup.aliyun.com/");
13
+ }
14
+
15
+ // https://codeup.aliyun.com/5ec8bb7bd1d1abe63b55cd33/PHP/Static.git
16
+ const repoMap = {
17
+ "Static": "git@codeup.aliyun.com:5ec8bb7bd1d1abe63b55cd33/PHP/Static.git",
18
+ "Static_2025": "git@codeup.aliyun.com:5ec8bb7bd1d1abe63b55cd33/PC_Web/Static_2025.git",
19
+ "OfficialSite": "git@codeup.aliyun.com:5ec8bb7bd1d1abe63b55cd33/PHP/OfficialSite.git",
20
+ "events": "git@codeup.aliyun.com:5ec8bb7bd1d1abe63b55cd33/PHP/events.git",
21
+ }
22
+
23
+ function getDeployWorkDir() {
24
+ const workDir = join(homedir(), ".deploy-helper-workspace");
25
+ if (!fs.existsSync(workDir)) {
26
+ fs.mkdirSync(workDir, { recursive: true });
27
+ }
28
+ return workDir;
29
+ }
30
+
31
+
32
+
33
+ /**
34
+ * @typedef {Object} PubProjectConfig
35
+ * @property {string} dest 项目名称
36
+ * @property {{[src: string]: string}} files 项目文件路径映射
37
+ * @property {string} [branch] 项目分支
38
+ * @property {string} [commit] 项目提交信息
39
+ */
40
+
41
+
42
+ async function initPubProject(workDir, config, packageJson, isAssets = false) {
43
+ const deployWorkDir = getDeployWorkDir();
44
+
45
+ // 获取目标项目
46
+ let dest = config.dest;
47
+ if (dest.indexOf("/") !== -1) {
48
+ dest = dest.split("/").pop();
49
+ }
50
+ const supportedRepoNames = Object.keys(repoMap);
51
+ if (!supportedRepoNames.includes(dest)) {
52
+ throw new Error(`${dest}不是支持的项目, 请确认`);
53
+ }
54
+ let repoUrl = repoMap[dest];
55
+ let repoUrls = [repoUrl, repoUrl2https(repoUrl)];
56
+ // 默认使用ssh, 如果从某处获知当前用户使用的是https, 那么先尝试https方式的拉取
57
+ const useSSH = false;
58
+ if (useSSH) {
59
+ repoUrls = repoUrls.reverse();
60
+ }
61
+
62
+ const projectName = `${dest}_${packageJson.name}_${packageJson.version}_${Date.now()}`;
63
+ const projectDir = join(deployWorkDir, projectName);
64
+
65
+
66
+ // 检查实际的构建产物是否存在, 并且获取需要sparse-checkout的目录
67
+ let dests = Object.keys(config.files).map(src => {
68
+ const i = config.files[src];
69
+ const fullPath = join(workDir, src);
70
+ if (fs.existsSync(fullPath)) {
71
+ const isDir = fs.statSync(fullPath).isDirectory();
72
+ if (isDir) {
73
+ return i;
74
+ } else {
75
+ const subs = i.split("/");
76
+ if (subs.length === 1) {
77
+ return "";
78
+ }
79
+ subs.pop();
80
+ return subs.join("/");
81
+ }
82
+ } else {
83
+ throw new Error(`${fullPath}不存在,请确认构建产物是否正确`);
84
+ }
85
+ }).filter(Boolean);
86
+ dests = [...new Set(dests)];
87
+
88
+
89
+ let git = simpleGit(deployWorkDir);
90
+
91
+ if (isAssets && dests.length === 0) {
92
+ throw new Error(`【警告】构建产物assets部分一般需要指定子目录, 请检查部署配置是否有误`);
93
+ }
94
+
95
+ for (let i = 0; i < repoUrls.length; i++) {
96
+ try {
97
+ log(`try clone ${repoUrls[i]}`);
98
+ const gitCmdStr = `clone --branch master --depth=1 --filter=blob:none --no-checkout ${repoUrls[i]} ${projectDir}`;
99
+ await git.raw(gitCmdStr.split(' '));
100
+ break;
101
+ } catch (error) {
102
+ log(`clone ${repoUrls[i]} failed: ${error}`);
103
+ }
104
+ }
105
+ if (!fs.existsSync(projectDir)) {
106
+ throw new Error(`clone ${repoUrls[i]} failed, please find the author for help`);
107
+ }
108
+ git = simpleGit(projectDir);
109
+ if (dests.length > 0) {
110
+ log(`git sparse-checkout init --cone`);
111
+ await git.raw("sparse-checkout init --cone".split(' '));
112
+ //TODO: 进一步容错, 如果有包含关系的, 去掉被包含的
113
+ for (const dest of dests) {
114
+ const cmds = ['sparse-checkout', 'set', dest];
115
+ log(`git ${cmds.join(' ')}`);
116
+ await git.raw(cmds);
117
+ }
118
+ log(`git checkout master`);
119
+ await git.checkout('master');
120
+ log(`${dest} partial clone done`)
121
+ } else {
122
+ log(`git checkout master`);
123
+ await git.checkout('master');
124
+ log(`${dest} clone done`)
125
+ }
126
+ return { git, projectDir, destReopName: dest };
127
+ }
128
+
129
+ /**
130
+ * 将构建产物复制到项目的对应位置
131
+ * @param {string} workDir
132
+ * @param {PubProjectConfig} config
133
+ * @param {string} targetProjectDir
134
+ */
135
+ function cpFiles(workDir, config, targetProjectDir) {
136
+ // log(`cpFiles: ${workDir} -> ${targetProjectDir}`);
137
+ let assetsFilesCount = 0;
138
+ const assetsFiles = config.files;
139
+ for (const [src, dest] of Object.entries(assetsFiles)) {
140
+ const srcPath = join(workDir, src);
141
+ if (!fs.existsSync(srcPath)) {
142
+ throw new Error(`${srcPath}不存在,请确认构建产物是否正确`);
143
+ }
144
+ const destPath = join(targetProjectDir, dest);
145
+ if (fs.statSync(srcPath).isDirectory()) {
146
+ fs.cpSync(srcPath, destPath, { recursive: true });
147
+ } else {
148
+ fs.copyFileSync(srcPath, destPath);
149
+ }
150
+ log(`assets copy: ${srcPath} -> ${destPath}`);
151
+ assetsFilesCount++;
152
+ }
153
+ log(`assets copy done. ${assetsFilesCount} files copied.`);
154
+ }
155
+
156
+
157
+ /**
158
+ * 提交和推送
159
+ * @param {*} git
160
+ * @param {PubProjectConfig} config
161
+ * @param {string} testReopName
162
+ */
163
+ async function commitAndPush(git, config, testReopName, isAssets = false) {
164
+ await git.add(".");
165
+ const status = await git.status();
166
+ if (!status.isClean()) {
167
+ const assetsCommit = `${config.commit || "feat:部署资源文件"} by deploy-helper`;
168
+ const assetsCommitResult = await git.commit(assetsCommit);
169
+ log(`${testReopName} commit: ${assetsCommit}`);
170
+ log(`${testReopName} commit: ${JSON.stringify(assetsCommitResult.summary)}`);
171
+ return true;
172
+ } else {
173
+ log(`${testReopName} ${isAssets ? "静态资源" : "入口文件"}未发现修改内容,跳过提交`);
174
+ return false;
175
+ }
176
+ }
177
+
178
+ export default class Deployer2 {
2
179
  async deploy(workDir, mode, config) {
180
+
181
+ log(`将使用新版部署方式`);
182
+
3
183
  // check
4
- // run
184
+ // 检查配置中是否存在默认值(CHANGE_ME)
185
+ const result = hasChangeMe(JSON.stringify(config));
186
+ if (result) {
187
+ throw new Error(`配置中存在默认值(CHANGE_ME), 请先修改配置`);
188
+ }
189
+
190
+ const needHandleAssets = config.assets && config.assets.dest && Object.keys(config.assets.files || {}).length > 0;
191
+ const needHandleEntry = config.entry && config.entry.dest && (mode === 'prod' || (mode === 'test' && !config.entry.onlyProd));
192
+ const entryTestBranch = (config.entry && config.entry.testBranch) || "test";
193
+ const entryProdBranch = (config.entry && config.entry.prodBranch) || "master";
194
+ const entryTargetBranch = mode === 'prod' ? entryProdBranch : entryTestBranch;
195
+
196
+ // 检查流水线配置
197
+ checkYunxiaoToken(config, mode);
198
+ const gitsNeedPush = [];
199
+ const gitsToHandle = [];
200
+ const dirsToDelete = [];
201
+ if (needHandleAssets) {
202
+ gitsToHandle.push({
203
+ subConfig: config.assets,
204
+ isAssets: true,
205
+ branch: "master",
206
+ });
207
+ }
208
+ if (needHandleEntry) {
209
+ gitsToHandle.push({
210
+ subConfig: config.entry,
211
+ isAssets: false,
212
+ branch: entryTargetBranch,
213
+ });
214
+ }
215
+ for (const { subConfig, isAssets } of gitsToHandle) {
216
+ const { git, projectDir, destReopName } = await initPubProject(workDir, subConfig, config.packageJson, isAssets);
217
+ cpFiles(workDir, subConfig, projectDir);
218
+ const canPush = await commitAndPush(git, subConfig, destReopName, isAssets);
219
+ if (canPush) {
220
+ gitsNeedPush.push({ git, destReopName, projectDir });
221
+ }
222
+ dirsToDelete.push(projectDir);
223
+ }
224
+ for (const { git, destReopName } of gitsNeedPush) {
225
+ let isPushed = false;
226
+ try {
227
+ await git.push();
228
+ isPushed = true;
229
+ } catch (error) {
230
+ log(`${destReopName} push failed, try again.`);
231
+ }
232
+ if(!isPushed) {
233
+ try {
234
+ await git.push();
235
+ } catch (error) {
236
+ log(`${destReopName} push failed, please find the author for help.`);
237
+ throw error;
238
+ }
239
+ }
240
+ log(`${destReopName} push done.`);
241
+ }
242
+ for (const dir of dirsToDelete) {
243
+ try {
244
+ fs.rmSync(dir, { recursive: true });
245
+ } catch (error) {
246
+ // ignore
247
+ log(`${dir} delete failed, 请稍后手动删除`);
248
+ console.error(error);
249
+ console.error(error.stack);
250
+ }
251
+ }
252
+
253
+ try {
254
+ // 8 测试环境将entry scp至测试环境
255
+ if (mode === 'test' && config.testSync) {
256
+ const syncTestFiles = config.testSync;
257
+ const scpClient = await getScpClient();
258
+ for (const [src, dest] of Object.entries(syncTestFiles)) {
259
+ const srcPath = join(workDir, src);
260
+ const destPath = "/home/web/website/tuwan_www/templets/static/play/" + dest;
261
+ log(`scp: ${srcPath} -> ${TEST_SERVER_HOST}:${destPath}`);
262
+ await scpClient.uploadFile(srcPath, destPath)
263
+ }
264
+ }
265
+ } catch (error) {
266
+ log(`scp error: ${error}`);
267
+ if (assetsFilesCount > 0) {
268
+ log(`不过请放心,构建产物的assets已经处理完成,只要手动处理index.html文件即可。`);
269
+ }
270
+ }
271
+ // 处理触发流水线的任务
272
+ await runPipeline(config, mode);
5
273
  }
6
274
  }
package/index.mjs CHANGED
@@ -2,16 +2,16 @@
2
2
 
3
3
  import fs from 'fs';
4
4
  import { join, basename, dirname } from "path";
5
- import { runPipeline, checkYunxiaoToken, triggerPipeline } from './lib/pipelineApi.mjs';
5
+ import { triggerPipeline } from './lib/pipelineApi.mjs';
6
6
  import { setDebug, log } from './lib/util.mjs';
7
- import path from 'path';
8
7
  import { fileURLToPath } from 'url';
9
8
  import Deployer from './deploy.mjs';
9
+ import Deployer2 from './deploy2.mjs';
10
10
  import { getScpClient, TEST_SERVER_HOST } from './util.mjs';
11
11
 
12
12
 
13
13
  const __filename = fileURLToPath(import.meta.url);
14
- const __dirname = path.dirname(__filename);
14
+ const __dirname = dirname(__filename);
15
15
 
16
16
 
17
17
  /**
@@ -20,12 +20,20 @@ const __dirname = path.dirname(__filename);
20
20
  */
21
21
  async function loadConfig() {
22
22
  const configJsonPath = join(process.cwd(), "deploy.config.json");
23
+ let configJson = null;
23
24
  if (fs.existsSync(configJsonPath)) {
24
- const configJson = JSON.parse(fs.readFileSync(configJsonPath, "utf-8"));
25
- return configJson;
25
+ configJson = JSON.parse(fs.readFileSync(configJsonPath, "utf-8"));
26
26
  }
27
- log(`deploy.config.json不存在, 请先创建`);
28
- return null;
27
+ if(!configJson){
28
+ log(`deploy.config.json不存在, 请先创建`);
29
+ return null;
30
+ }
31
+ const repoPackageJsonPath = join(process.cwd(), "package.json");
32
+ if (fs.existsSync(repoPackageJsonPath)) {
33
+ const repoPackageJson = JSON.parse(fs.readFileSync(repoPackageJsonPath, "utf-8"));
34
+ configJson.packageJson = repoPackageJson;
35
+ }
36
+ return configJson;
29
37
  }
30
38
 
31
39
  async function initConfigJson() {
@@ -78,7 +86,10 @@ async function getVersion() {
78
86
  }
79
87
 
80
88
 
81
- function getDeployer() {
89
+ function getDeployer(config) {
90
+ if(config.useDeploy2 === true){
91
+ return new Deployer2();
92
+ }
82
93
  return new Deployer();
83
94
  }
84
95
 
@@ -90,7 +101,6 @@ async function main() {
90
101
  setDebug(true);
91
102
 
92
103
 
93
- // await devTest();
94
104
  // log(`process.argv: ${process.argv[2]} ${process.argv[3]} ${process.argv[4]}`);
95
105
 
96
106
  // return;
@@ -178,9 +188,8 @@ async function main() {
178
188
  const mode = process.argv[2] === 'prod' ? 'prod' : 'test'; // 部署环境
179
189
  log(`deploy mode: ${mode}`);
180
190
  log(`workDir: ${workDir}`);
181
- const deployer = getDeployer();
182
-
183
191
  const config = await loadConfig();
192
+ const deployer = getDeployer(config);
184
193
  await deployer.deploy(workDir, mode, config);
185
194
  log(`deploy-helper deploy done.`);
186
195
  process.exit(0);
@@ -354,7 +354,8 @@ export async function runPipeline(config, mode) {
354
354
  }
355
355
  const token = getYunxiaoToken(pipelineConfig);
356
356
  if (!token) {
357
- log(`未设置云效token, 此次流水线未自动执行: ${JSON.stringify(pipelineConfig)}`);
357
+ log(`未设置云效token, 此次流水线未自动执行: ${JSON.stringify(pipelineConfig)}, 请移步网页版${`https://flow.aliyun.com/pipelines/${pipelineConfig.id}`}手动执行`);
358
+ return;
358
359
  }
359
360
  process.env.YUNXIAO_ACCESS_TOKEN = token;
360
361
 
package/package.json CHANGED
@@ -1,12 +1,9 @@
1
1
  {
2
2
  "name": "@taole/deploy-helper",
3
- "version": "1.0.0-beta.1",
3
+ "version": "1.0.0-beta.3",
4
4
  "description": "脚本部署工具,用于将项目部署到测试环境或生产环境",
5
5
  "main": "index.mjs",
6
6
  "type": "module",
7
- "scripts": {
8
- "test": "node index.mjs"
9
- },
10
7
  "bin": {
11
8
  "deploy-helper": "index.mjs",
12
9
  "dh": "index.mjs"