RushJS 自动化部署方案
在上一章节,我们搭建了基于Vue3的Monorepo项目,详情可点击 Vue3 + RushJS 前端Monorepo方案实践,这一篇文章将会逐步介绍自动化构建的方案。
1. 遇到的问题
- 自动化构建困难:像CI/CD,jenkins方案都难以满足多项目同时构建的需求
- 测试人员较为迷茫:该架构一旦实施,如果是由开发人员进行构建部署的话还好,毕竟我们知道哪些项目是需要进行构建的,然而当项目由测试人员进行主导测试的话,往往测试人员是需要合并他们想测试的分支进行测试的,现有的方案无非是缺少了可视化操作界面,以及无法达到测试的构建粒度。
2.方案思考
既然现有的方案已经无法满足现有的需求,那么就要重新写一套满足需求的自动化部署系统。
- 选用一个前端框架作为展示,这里我选用的是 Arco Design Pro(Vue3版本) 进行开发,原因也很简单,因为上一篇是搭建Vue3的项目,所以对应的中台也要选择相应的技术栈来降低心智负担。
- 我们一般都会把代码部署到公司私有的
gitlab
仓库中,那么这个时候,我们要想方设法的拿到gitlab
中所有的仓库信息,以及对应仓库下的分支信息。好在gitlab
确实是给了我们相关的API进行调用,方便我们后续做拓展。( 什么?英文文档看不懂?没有关系,gitlab
也有对应的中文文档站点供参考~)
3.方案实施 [前端部分]
-
请根据 Arco Design Pro(Vue3版本) 的快速入门文档,自行创建对应的项目,并成功运行。这里我创建的项目名为 monorepo-visual。
-
在
src -> router -> routes -> modules
中新建 automatedBuild.ts文件用于路由生命,以及在src -> views
中新增automatedBuild文件夹用于存放代码,如图所示: -
接着我们在 gitlab官网中创建自己的账号,然后将之前所创建的 my-monorepo 项目推送到仓库中,注意咱们创建的仓库一定得是
私有的
,因为一般公司的源码都会私有化,如下图所示:(注:如果你卡在了这一步,说明你的git知识还不过关,先去学习充分了再继续)
-
接着我们需要在用户个人中心里面申请
access_token
,因为我们的仓库是私有的,所以在访问gitlab API
的时候,是需要带上access_token
才能进行访问的,如图所示:接着拷贝下
access_token
等待后续使用,如图所示: -
接着我们根据
gitlab
的 项目API 、仓库API、分支API文档,在src -> api
目录下,新建automatedBuild.ts
接口文件,如下图所示:注意
getRepositoryTree
接口所带的参数为path=apps
,这是我们App所放置的目录,详见接口文档的参数解释。 -
在写对应接口的时候,我们发现少了接口的返回类型,但此时接口返回的参数数量实在是过多,这个时候我们可以借助vscode插件
JSON to TS
,把返回的json数据转为TypeScript所对应的类型,如图所示:然后我们把接口返回的JSON数据拷贝到
api -> automatedBuild.ts
文件中,按住Shift + Ctrl + Alt + V
组合键,便能生成对应的interface类型了,然后我们把类型进行导出即可。 -
接着我们在
src -> views -> automatedBuild -> index.vue
文件中写入以下代码:
<template>
<div class="container">
<Breadcrumb :items="['自动化构建', '工作台']" />
<a-table :columns="columns" :data="data.projectList">
<template #appList="{ record }">
<a-select
v-model="record.currentSelectAppList"
placeholder="请选择需要构建的项目"
multiple
:style="{ width: '320px' }"
>
<a-option
v-for="item in record.appList"
:key="item"
:label="item"
></a-option>
</a-select>
</template>
<template #branchList="{ record }">
<a-select
v-model="record.currentSelectbranch"
placeholder="请选择需要构建的分支"
:style="{ width: '320px' }"
value-key="name"
>
<a-option
v-for="item in record.branchList"
:key="item.name"
:label="item.name"
:value="item"
></a-option>
</a-select>
</template>
<template #opeartion="{ record }">
<a-button type="primary" status="success" @click="openModal(record)"
>开始构建</a-button
>
</template>
</a-table>
<a-modal
v-model:visible="data.visible"
@ok="startBuild"
@cancel="
data.visible = false;
data.modalData!.currentSelectbranch = {};
"
>
<template #title> 温馨提示 </template>
<div>
当前选择构建的项目为:{{
data.modalData?.currentSelectAppList?.join(',')
}}
</div>
<div>当前选择构建的分支信息如下:</div>
<div v-if="data.modalData?.currentSelectbranch?.name">
<a-collapse
:default-active-key="[data.modalData?.currentSelectbranch.name]"
>
<a-collapse-item
:key="data.modalData?.currentSelectbranch.name"
:header="data.modalData?.currentSelectbranch.name"
>
<div
>Hash值:{{ data.modalData?.currentSelectbranch.commit!.id }}</div
>
<div
>提交人员:{{
data.modalData?.currentSelectbranch.commit!.author_name
}}</div
>
<div
>提交时间:{{
dayjs(
data.modalData?.currentSelectbranch.commit!.committed_date
).format('YYYY-MM-DD HH:mm:ss')
}}</div
>
<div
>提交内容:{{
data.modalData?.currentSelectbranch.commit!.message
}}</div
>
</a-collapse-item>
</a-collapse>
</div>
</a-modal>
</div>
</template>
<script lang="ts" setup>
import {
getAllProjects,
getRepositoryTree,
getRepositoryBranches,
AllProjectItem,
Commit,
} from '@/api/automatedBuild';
import { Message, TableColumnData } from '@arco-design/web-vue';
import { onMounted, reactive } from 'vue';
import dayjs from 'dayjs';
interface ProjectItem {
id: number;
name: string;
currentSelectAppList?: string[];
appList?: string[];
currentSelectbranch?: { name?: string; commit?: Commit };
branchList?: { name: string; commit: Commit }[];
}
const columns: TableColumnData[] = [
{
title: '仓库ID',
dataIndex: 'id',
align: 'center',
},
{
title: '仓库名称',
dataIndex: 'name',
align: 'center',
},
{
title: '构建项目',
dataIndex: 'appList',
slotName: 'appList',
align: 'center',
},
{
title: '构建分支',
dataIndex: 'branchList',
slotName: 'branchList',
align: 'center',
},
{
title: '操作',
dataIndex: 'opeartion',
slotName: 'opeartion',
align: 'center',
},
];
const data: {
projectList: ProjectItem[];
visible: boolean;
modalData?: Required<
Pick<ProjectItem, 'currentSelectAppList' | 'currentSelectbranch'>
>;
} = reactive({ projectList: [], visible: false });
onMounted(async () => {
try {
const res = await getAllProjects();
res.forEach(async (item) => {
data.projectList.push({
id: item.id,
name: item.name,
currentSelectAppList: [],
appList: await handleRepositoryTree(item),
branchList: await handleRepositoryBranches(item),
currentSelectbranch: undefined,
});
});
} catch (e) {
Message.error(String(e));
}
});
const handleRepositoryTree = async (item: AllProjectItem) => {
try {
const nameList: string[] = [];
const res = await getRepositoryTree(item.id);
res.forEach((item) => {
nameList.push(item.name);
});
return nameList;
} catch (e) {
Message.error(String(e));
return [];
}
};
const handleRepositoryBranches = async (item: AllProjectItem) => {
try {
const branchList: { name: string; commit: Commit }[] = [];
const res = await getRepositoryBranches(item.id);
res.forEach((item) => {
branchList.push({
name: item.name,
commit: item.commit,
});
});
return branchList;
} catch (e) {
Message.error(String(e));
return [];
}
};
const openModal = (item: ProjectItem) => {
if (!item.currentSelectAppList?.length || !item.currentSelectbranch?.name) {
return Message.info('请先选择构建的项目/分支');
}
data.modalData = {
currentSelectAppList: item.currentSelectAppList,
currentSelectbranch: item.currentSelectbranch,
};
data.visible = true;
};
/** 请求后端接口 */
const startBuild = async () => {
try {
if (!data.modalData?.currentSelectAppList.length) return;
const params = {
currentSelectAppList: data.modalData.currentSelectAppList,
currentSelectbranch: data.modalData?.currentSelectbranch,
};
await startBuildAPI(params);
Message.success('构建成功');
} catch (e) {
Message.error('构建失败');
}
};
</script>
<script lang="ts">
export default {
name: 'AutomatedBuild',
};
</script>
<style scoped lang="less">
.container {
padding: 0 20px 20px;
}
</style>
生成的UI界面如图所示:
4. 方案实施[后端部分]
前面我们主要实现了UI方面的功能,接着我们可以简单的实现一下后端部分。先在原有的my-monorepo
项目中,新建server
文件夹,然后进行初始化后。命令如下:
cd my-monorepo
mkdir server
cd server
npm init -y
接下来新增app.js文件,并且修改package.json文件,如下图所示:
然后我们在
rush.json -> projects
中,新增刚才的项目路径,如下图所示:
接着我们执行一次 rush update
命令,再执行 rush add -p express
,将 express 框架安装到server
中,然后在app.js
文件中,写下以下代码:
const express = require('express')
const app = express()
const port = 3000
app.use(express.json())
app.post('/startBuild', (req, res) => {
console.log(req.body)
res.send({
code:200,
message:'成功'
})
})
app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
})
接着使用命令node app.js
启动我们的express服务。
前端方面,三部曲:定义接口 -> 使用接口 -> 配置Proxy
,如图所示:
通过以上的操作可以先测试下请求是否是正常的,接着我们继续修改app.js文件,如下所示:
const express = require('express')
const { execSync } = require("child_process");
const app = express()
const port = 3000
app.use(express.json())
app.post('/startBuild', (req, res) => {
try{
const body = req.body
const currentSelectbranchName = body.currentSelectbranch.name
execSync(`git checkout -B ${currentSelectbranchName}`)
execSync(`git pull origin ${currentSelectbranchName}`)
execSync('rush update')
for(let appName of body.currentSelectAppList){
console.log('appName: ', appName);
execSync(`rush build --to ${appName}`)
}
res.send({
code:200,
message:'构建成功'
})
}catch(e){
console.log('e: ', e);
res.send({
code:500,
message:'构建失败'
})
}
})
app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
})
这样,只需要通知对应的运维,把打包后的 dist 路径告诉他即可进行部署了。
注意:如果你的后端程序[比如用Node写后端],是放在Monorepo仓库中的,并且你在编码的过程中,也依赖于其他的Monorepo项目。这个时候你就需要用 rush deploy 去进行部署了。
项目的所有代码,已经上传到gitlab中了,仓库为:my-monorepo,monorepo-visual
转载自:https://juejin.cn/post/7185459711125389371