0%

关于使用 Node.js 来辅助进行 CI/CD 的一些想法

  由于使用到的 CI/CD 工具可能会更换,对应的学习成本也相应增加,但是 Node.js 其实可以帮助我们实现这些工具的大部分功能,包括操作文件、执行 cmd 等等。
  所以我们如果把大部分的打包或集成操作使用 Node.js 去实现,那么无论工具如何更换,我们只需学习如何使用该工具执行 npm 即可,从而大大降低迁移与学习成本。
  当然这只是我最近迁移时的一些解决方案与想法,如果有大佬指教一些其他的方式,那自然是更好啦哈哈哈~

Flutter 根据安卓版本打包 Demo

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
const fs = require('fs');
const nodeCmd = require('node-cmd');

let arguments = process.argv.slice(2); // 获取命令行传入参数
let targetBranch = arguments[0] || 11; // 需要打包的对应安卓版本
let shouldBuild = arguments[1] == undefined ? true : arguments[1] == 'true'; // 是否需要打包
let menusStation = arguments[2] == undefined ? 'changzhou' : arguments[2]; // 菜单地址
let filePath = arguments[3] || './pubspec.yaml'; // pubspec 配置文件位置
let buildFilePath = arguments[4] || './android/app/build.gradle'; // build.gradle 配置文件位置
let menusConfigPath = arguments[5] || './lib/configuration/menus.dart'; // menus.dart 配置文件位置

console.table({ arguments, targetBranch, shouldBuild, filePath, buildFilePath });

/// 读取对应的安卓目录配置文件
fs.readFile(buildFilePath, 'utf8', function (err, data) {
if (err) return console.log(`%c出错啦!${data}`, 'color:red;');

let result = data.replace(/minSdkVersion\s\d{2}/g, `minSdkVersion ${targetBranch == 11 ? 26 : 21}`);

console.log('正在修改安卓 build 配置文件......');

fs.writeFile(buildFilePath, result, 'utf8', function (err) {
if (err) return console.log(`%c出错啦!${data}`, 'color:red;');

setMenus();
});
});

/// 设置对应版本的菜单
function setMenus() {
fs.readFile(menusConfigPath, 'utf8', function (err, data) {
if (err) return console.log(`%c出错啦!${data}`, 'color:red;');

let result = data.replace(
/static\sList\smenuList\s=\sX_MENU_\w+\;/g,
`static List menuList = X_MENU_${menusStation};`
);

console.log(`正在修改菜单 build 配置文件 [X_MENU_${menusStation}]......`);

fs.writeFile(menusConfigPath, result, 'utf8', function (err) {
if (err) return console.log(`%c出错啦!${data}`, 'color:red;');

buildStart();
});
});
}

/// 开始打包
function buildStart() {
fs.readFile(filePath, 'utf8', function (err, data) {
if (err) return console.log(`%c出错啦!${data}`, 'color:red;');

let result = data.replace(/_android_\d{1,2}_scan/g, `_android_${targetBranch}_scan`); // 修改对呀安卓版本的 SDK 插件版本

console.log('正在修改 Flutter 配置文件......');

fs.writeFile(filePath, result, 'utf8', function (err) {
if (err) return console.log(`%c出错啦!${data}`, 'color:red;');

getFlutterPackages();
});
});
}

/// 获取 flutter 相关插件
function getFlutterPackages() {
console.log('执行下载命令 flutter pub get');

nodeCmd.run('flutter pub get', (err, data, stderr) => {
if (err) return console.log(`%c出错啦!${data}`, 'color:red;');

shouldBuild && buildFlutterApk();
});
}

/// 打包
function buildFlutterApk() {
console.log('打包中... flutter build apk --target-platform android-arm64');

nodeCmd.run('flutter build apk --target-platform android-arm64', (err, data, stderr) => {
if (err) return console.log(`%c出错啦!${data}`, 'color:red;');

console.log(`%c${data} %c成功,请检查 apk 文件!`, 'color:green;', 'color:chocolate;');
});
}



// 这样的话,我们只需要每次切换 CI/CD 工具时,学会使用 node 执行这个脚本即可。

其他

  • 以上脚本既可以用于本地打包,也可以设置在推送时自动运行。
  • 另外如果我们还要集成到服务端的不同目录,也可以使用 Node.js 去实现文件复制或者移动。
  • 如果需要在 commit 或者 push 前进行一些操作,我们还可以使用 package.json-scripts 定义一些钩子来实现。
1
2
3
4
5
6
7
8
9
10
11
12
prepublish: 在包发布之前运行,也会在 npm install 安装到本地时运行。
publish,postpublish: 包被发布之后运行
preinstall: 包被安装前运行
install,postinstall: 包被安装后运行
preuninstall,uninstall: 包被卸载前运行
postuninstall: 包被卸载后运行
preversion: bump 包版本前运行
postversion: bump 包版本后运行
pretest,test,posttest: 通过 npm test 命令运行
prestop,stop,poststop: 通过 npm stop 命令运行
prestart,start,poststart: 通过 npm start 命令运行
prerestart,restart,postrestart: 通过 npm restart 运行

需要复制指定目录提交到某个仓库的 Demo

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
const path = require('path');
const fs = require('fs');
const isLocalPublish = process.argv[2] === 'true';
const targetDir = process.argv[3] ?? './test';
const pkg = require(path.resolve('package.json'));
const nodeCmd = require('node-cmd');

// git clone url[请设置带 token 的地址,或者先设置 SSH。]
// 将对外目录 git 仓库拉取到本地

/**
* 判断目录是否存在,不存在则创建目录。
*/
const isDirExist = (path) => {
// fs.access(path, function (err) {
// if (err) {
// // 目录不存在时创建目录
// fs.mkdirSync(path);
// }
// });
if (!fs.existsSync(path)) {
fs.mkdirSync(path);
}
};

/*
* 复制目录、子目录,及其中的文件。
* @param src {String} 要复制的目录
* @param target {String} 复制到目标目录
*/
const copyDir = (err, src, target) => {
if (err) {
console.log({ 'copyDir error': err });
return;
}

fs.readdir(src, function (err, paths) {
if (err) {
console.log({ 'copyDir error': err });
return;
}

paths.forEach(function (path) {
let _src = src + '/' + path;
let _target = target + '/' + path;
fs.stat(_src, function (err, stat) {
if (err) {
console.log({ 'copyDir error': err });
return;
}

// 判断是文件还是目录
if (stat.isFile()) {
fs.writeFileSync(_target, fs.readFileSync(_src));
} else if (stat.isDirectory()) {
// 当是目录是,递归复制。
isDirExist(_target);
copyDir(null, _src, _target);
}
});
});
});
};

/**
* 复制文件
* @param {*} src
* @param {*} target
*/
const copyFile = (src, target) => {
try {
fs.writeFileSync(target, fs.readFileSync(src));
} catch (e) {
console.log({ 'copyFile error': e });
try {
fs.createReadStream(src).pipe(fs.createWriteStream(target)); // 大文件复制
} catch (err) {
console.log({ 'copyBigFile error': err });
}
}
};

/**
* 获取发布的所有对外文件
* @param {*} targetDir
*/
const getSubmitFiles = (targetDir) => {
isDirExist(targetDir);

['dir1', 'dir2', 'dir3'].forEach(function (srcDir) {
let copyTargetDir = `${targetDir}/${srcDir}`;
isDirExist(copyTargetDir);
copyDir(null, srcDir, copyTargetDir);
});

['package.json', 'README.md', 'README.en.md'].forEach(function (file) {
copyFile(file, `${targetDir}/${file}`);
});

publishPackage();
};

/**
* 发布包
*/
const publishPackage = () => {
const dateObj = new Date();
const versionNo = `${dateObj.getMilliseconds()}${dateObj.getSeconds()}${dateObj.getMinutes()}${dateObj.getHours()}${dateObj.getDate()}${
dateObj.getMonth() + 1
}${dateObj.getFullYear()}`;
const versionId = `(${pkg.version}) ${versionNo}`;

nodeCmd.run(
`cd ${targetDir} && git checkout main && git config --global user.name "doubleam" && git config --global user.email "admin@biugle.cn" && git status && git add -A . && git commit -m "Auto publish, Version ${versionId}." && git push origin main -f`,
(err, data, stderr) => {
if (err) return console.log(`%c 提交出错啦!${data}`, 'color:red;');

if (isLocalPublish) {
// 需要有 .npmrc 文件,才可自动登录并执行 npm publish。或者设置 CI/CD 专用 token。 https://docs.npmjs.com/using-private-packages-in-a-ci-cd-workflow
nodeCmd.run('npm publish', (err, data, stderr) => {
if (err) return console.log(`%c 发布版本出错啦!${data}`, 'color:red;');

console.log(data);
nodeCmd.run(`cd ../ && rimraf ${targetDir}`, (err, data, stderr) => {
if (err) return console.log(`%c 删除文档出错啦!${data}`, 'color:red;');

console.log(data);
});
});
}
}
);
};

getSubmitFiles(targetDir);

其他

  • 适用于我们部分源码不方便公开的情况,我们可以设置一个目录来暴露生产包,但是保留源码的私有性。
  • 为了简化命令参数,我们可以预先写好放到 package.json-scripts 中去,方便直接使用 npm run xxx 执行。
  • 以上内容仅供参考 (0.0)
bulb