测试环境一键发布

news2025/1/10 19:11:26

背景

目前公司项目发布测试环境不够自动化,每次需要手动打包并且手动更新,影响开发效率

流程图

 

而且因为是本地手动发布,容易失误造成一些不必要的麻烦:

  • 远端代码有更新,忘记拉取代码

  • 快速发布,本地代码忘了提交,导致远端代码缺失

  • 误将本地的一些测试代码发布

...

因此,如果能够将操作脚本化,不仅可以提高开发效率,同时也能减少失误造成的影响。

一键发布

使用

  1. 先将deploy.js拷贝到需要发布项目的根目录

      
const { execSync } = require('child_process')
const fs = require('fs');
const readline = require('readline');
const packageJson = require('./package.json')
const Axios = require('axios').default;

const config = {
  container: 'simulated-trade-h5-test-container', // 容器名称
  buildScript: process.env.DEPLOY_ENV == 'prod' ? 'npm run build:pro' : 'npm run build:test', // 打包命令
  apiUrl: 'http://10.156.160.11:9000/api', // portainer接口地址
  env: process.env.DEPLOY_ENV || 'test', // ['local', 'test', 'prod'] 设置为local可通过本地代码进行部署,
}

const axios = Axios.create({
  baseURL: config.apiUrl,
})


// 远程仓库地址
const url = execSync('git config --get remote.origin.url')
const dirname = __dirname.split(/[\\/]/)
// 当前文件夹名称
const folder = dirname.slice(-1).toString()
// 当前分支
const branch = execSync("git branch --show-current")

// 创建副本仓库
process.chdir('../')
if (!fs.existsSync(`.deploy`)) {
  fs.mkdirSync('.deploy')
}
process.chdir('./.deploy')
if (!fs.existsSync(folder)) {
  execSync(`git clone ${url} ${folder}`, {stdio: 'inherit'})
}

// 创建缓存文件
if (!fs.existsSync(`.cache`)) {
  fs.writeFileSync('.cache', '')
}
const cacheFile = fs.readFileSync('.cache').toString()
const cache = {}
cacheFile.split('\n').forEach(i => {
  const arr = i.split('=')
  cache[arr[0]] = arr[1]
})


if (config.env == 'local') {
  console.log('本地仓库环境');
  process.chdir(`../${folder}`)
} else {
  console.log('远程仓库环境');
  process.chdir(folder)
  execSync(`git fetch --all && git reset --hard origin/${branch}`, {stdio: 'inherit'})
  console.log('安装依赖...');
  execSync('npm i', {stdio: 'inherit'})
}




axios.interceptors.request.use(config => {
  if (cache.token) {
    config.headers.Authorization = `Bearer ${cache.token}`
  }
  return config
})

// 检查token
const checkToken = async () => {
  const token = cache.token
  if (!token) {
    return false
  }
  try {
    await axios.get('/endpoints/3/docker/containers/json?all=1')
  } catch {
    return false
  }
  return true
}



// 发布
const deploy = async () => {
  try {
    const hasToken = await checkToken()
    if (!hasToken) {
      // 读取用户输入
      const rl = readline.createInterface({
        input: process.stdin,
        output: process.stdout
      });
      const q = (str) => new Promise(resolve => {
        rl.question(str, res => {
          resolve(res)
        })
      })
      let username = ''
      let password = ''
      let autoLogin = false
      console.log('登录portainer...');
      if (cache.username && cache.password) {
        const r = await q('自动登录(y/n)')
        if (!r || r === 'y') {
          username = cache.username
          password = cache.password
          autoLogin = true
        }
      }

      if (!autoLogin) {
        username = await q('请输入用户名:')
        password = await q('请输入密码:')
      }

      rl.close()
      
      // 登录portainer
      const { data } = await axios.post('/auth', {
        username,
        password
      })
      cache.token = data.jwt
      const obj = [
        `username=${username}`,
        `password=${password}`,
        `token=${data.jwt}`
      ]
      // 写入缓存
      fs.writeFileSync(`../${config.env == 'local' ? '.deploy/' : ''}.cache`, obj.join('\n'))
    }

    console.log('打包构建...');
    execSync(config.buildScript, {stdio: 'inherit'}) // 打包测试环境,执行命令以实际项目为准

    console.log('获取容器列表...');
    const { data } = await axios.get('/endpoints/3/docker/containers/json?all=1')
    const currentContainer = data.find(i => i.Names.includes(`/${config.container}`))
    if (!currentContainer) {
      console.log('未找到对应容器,请检查容器名称');
      return
    }

    console.log('获取容器配置...');
    const { data: currConfig } = await axios.get(`/endpoints/3/docker/containers/${currentContainer.Id}/json`)

    let imageName = currConfig.Config.Image.split(':')[0] + ":" + packageJson.version
    if (config.env == 'prod') {
      imageName = `harbor.saxofintech.com/online/westmoney/${packageJson.name}:${packageJson.version}` // 生产镜像
    }
    console.log(`镜像名称:${imageName}`);

    // 未自动上传镜像,上传一次
    if (!fs.existsSync(`./dist/dist.tar`)) {
      process.chdir('./dist')
      execSync('tar -cvf dist.tar *')
      console.log('生成压缩文件dist.tar...');
      await new Promise(r => setTimeout(() => r(), 3000))
      console.log('上传镜像...');
      const distTar = fs.readFileSync('./dist.tar')
      await axios.post(`/endpoints/3/docker/build?dockerfile=Dockerfile&t=${imageName}`, distTar, {
        headers: {
          'Content-Type': 'application/x-tar'
        }
      })
    }

    if (config.env == 'prod') {
      console.log('推送镜像...');
      await axios.post(`/endpoints/2/docker/images/${imageName.replace(/\//g, encodeURIComponent('/'))}/push`, {
        imageName,
      }, {
        headers: {
          'X-PortainerAgent-Target': 'wm-vm-h5-sit',
          'X-Registry-Auth': Buffer.from('{"registryId":1}').toString('base64')
        }
      })
      execSync(`echo ${imageName} | clip`),// windows下可用
      console.log('推送完成,已复制镜像名到剪贴板');
      return 
    }

    console.log('暂停当前容器...');
    await axios.post(`/endpoints/3/docker/containers/${currentContainer.Id}/stop`)
    console.log('重命名当前容器为old...');
    await axios.post(`/endpoints/3/docker/containers/${currentContainer.Id}/rename?name=${config.container}-old`)
    const params = {
      ...currConfig.Config,
      Image: imageName,
      HostConfig: currConfig.HostConfig,
      name: config.container,
    }
    // console.log(params);
    console.log('新建容器...');
    const { data: { Id, Portainer } } = await axios.post(`/endpoints/3/docker/containers/create?name=${config.container}`, params)
    console.log('配置容器权限...');
    await axios.put(`/resource_controls/${Portainer.ResourceControl.Id}`, {
      AdministratorsOnly: false,
      Public: false,
      Teams: [1], // 组先默认为1
      Users: []
    })
    console.log('启动容器...');
    await axios.post(`/endpoints/3/docker/containers/${Id}/start`,{})

    console.log('删除old容器...');
    await axios.delete(`/endpoints/3/docker/containers/${currentContainer.Id}?v=1&force=true`)
    console.log('发布完成!');

  } catch (err) {
    console.log(err);
  }
}

deploy()

    

2、修改容器名称及打包测试环境的命令

...
const config = {
  container: 'simulated-trade-h5-test-container', // 容器名称
  buildScript: process.env.DEPLOY_ENV == 'prod' ? 'npm run build:pro' : 'npm run build:test', // 打包命令
  apiUrl: 'http://10.156.160.11:9000/api', // portainer接口地址
  env: process.env.DEPLOY_ENV || 'test', // ['local', 'test', 'prod'] 设置为local可通过本地代码进行部署,
}

3、执行该js文件即可发布测试环境

node deploy.js

4、也可将执行命令加入npm script中,通过环境变量切换打包模式

// package.json
"scripts": {
    ...
    "deploy": "node deploy.js",
    "deploy:local": "cross-env DEPLOY_ENV=local node deploy.js",
    "deploy:prod": "cross-env DEPLOY_ENV=prod node deploy.js",
    ...
  },

DEPLOY_ENV=local: 打包本地代码发布测试环境

DEPLOY_ENV=test: 拉取远端代码打包测试环境

DEPLOY_ENV=prod: 打包生产并上传镜像(将镜像名发给运维发布)

流程图

 

实现原理

  • 首先需要创建一个单独的环境,防止被本地开发的代码影响。在当前项目的上级目录新建一个.deploy目录,用于发布指定项目。

  • 进到deploy目录拉取项目代码,并切换到当前分支

// 远程仓库地址
const url = execSync('git config --get remote.origin.url')
const dirname = __dirname.split(/[\\/]/)
// 当前文件夹名称
const folder = dirname.slice(-1).toString()
// 当前分支
const branch = execSync("git branch --show-current")

// 创建副本仓库
process.chdir('../')
if (!fs.existsSync(`.deploy`)) {
  fs.mkdirSync('.deploy')
}
process.chdir('./.deploy')
if (!fs.existsSync(folder)) {
  execSync(`git clone ${url} ${folder}`, {stdio: 'inherit'})
}
...
if (config.env == 'local') {
  console.log('本地仓库环境');
  process.chdir(`../${folder}`)
} else {
  console.log('远程仓库环境');
  process.chdir(folder)
  execSync(`git fetch --all && git reset --hard origin/${branch}`, {stdio: 'inherit'})
  console.log('安装依赖...');
  execSync('npm i', {stdio: 'inherit'})
}
  • 登陆portainer, 同时对登录信息进行缓存,将用户名密码及token缓存到.cache文件中

  • const checkToken = async () => {
      const token = cache.token
      if (!token) {
        return false
      }
      try {
        await axios.get('/endpoints/3/docker/containers/json?all=1')
      } catch {
        return false
      }
      return true
    }
    
    ...
    const hasToken = await checkToken()
    if (!hasToken) {
      // 读取用户输入
      const rl = readline.createInterface({
        input: process.stdin,
        output: process.stdout
      });
      const q = (str) => new Promise(resolve => {
        rl.question(str, res => {
          resolve(res)
        })
      })
      let username = ''
      let password = ''
      let autoLogin = false
      console.log('登录portainer...');
      if (cache.username && cache.password) {
        const r = await q('自动登录(y/n)')
        if (!r || r === 'y') {
          username = cache.username
          password = cache.password
          autoLogin = true
        }
      }
    
      if (!autoLogin) {
        username = await q('请输入用户名:')
        password = await q('请输入密码:')
      }
    
      rl.close()
      
      // 登录portainer
      const { data } = await axios.post('/auth', {
        username,
        password
      })
      cache.token = data.jwt
      const obj = [
        `username=${username}`,
        `password=${password}`,
        `token=${data.jwt}`
      ]
      // 写入缓存
      fs.writeFileSync(`../${config.env == 'local' ? '.deploy/' : ''}.cache`, obj.join('\n'))
    }

  • 执行打包

console.log('打包构建...');
execSync(config.buildScript, {stdio: 'inherit'}) // 打包测试环境,执行命令以实际项目为准
  • 调用portainer容器相关接口,完成发布

console.log('获取容器列表...');
const { data } = await axios.get('/endpoints/3/docker/containers/json?all=1')
const currentContainer = data.find(i => i.Names.includes(`/${config.container}`))
if (!currentContainer) {
  console.log('未找到对应容器,请检查容器名称');
  return
}

console.log('获取容器配置...');
const { data: currConfig } = await axios.get(`/endpoints/3/docker/containers/${currentContainer.Id}/json`)

let imageName = currConfig.Config.Image.split(':')[0] + ":" + packageJson.version
if (config.env == 'prod') {
  imageName = `harbor.saxofintech.com/online/westmoney/${packageJson.name}:${packageJson.version}` // 生产镜像
}
console.log(`镜像名称:${imageName}`);

// 未自动上传镜像,上传一次
if (!fs.existsSync(`./dist/dist.tar`)) {
  process.chdir('./dist')
  execSync('tar -cvf dist.tar *')
  console.log('生成压缩文件dist.tar...');
  await new Promise(r => setTimeout(() => r(), 3000))
  console.log('上传镜像...');
  const distTar = fs.readFileSync('./dist.tar')
  await axios.post(`/endpoints/3/docker/build?dockerfile=Dockerfile&t=${imageName}`, distTar, {
    headers: {
      'Content-Type': 'application/x-tar'
    }
  })
}

if (config.env == 'prod') {
  console.log('推送镜像...');
  await axios.post(`/endpoints/2/docker/images/${imageName.replace(/\//g, encodeURIComponent('/'))}/push`, {
    imageName,
  }, {
    headers: {
      'X-PortainerAgent-Target': 'wm-vm-h5-sit',
      'X-Registry-Auth': Buffer.from('{"registryId":1}').toString('base64')
    }
  })
  execSync(`echo ${imageName} | clip`),// windows下可用
  console.log('推送完成,已复制镜像名到剪贴板');
  return 
}

console.log('暂停当前容器...');
await axios.post(`/endpoints/3/docker/containers/${currentContainer.Id}/stop`)
console.log('重命名当前容器为old...');
await axios.post(`/endpoints/3/docker/containers/${currentContainer.Id}/rename?name=${config.container}-old`)
const params = {
  ...currConfig.Config,
  Image: imageName,
  HostConfig: currConfig.HostConfig,
  name: config.container,
}
// console.log(params);
console.log('新建容器...');
const { data: { Id, Portainer } } = await axios.post(`/endpoints/3/docker/containers/create?name=${config.container}`, params)
console.log('配置容器权限...');
await axios.put(`/resource_controls/${Portainer.ResourceControl.Id}`, {
  AdministratorsOnly: false,
  Public: false,
  Teams: [1], // 组先默认为1
  Users: []
})
console.log('启动容器...');
await axios.post(`/endpoints/3/docker/containers/${Id}/start`,{})

console.log('删除old容器...');
await axios.delete(`/endpoints/3/docker/containers/${currentContainer.Id}?v=1&force=true`)
console.log('发布完成!');

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/602469.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Redis7实战加面试题-高阶篇(Redis为什么快?高性能设计之epoll和IO多路复用深度解析)

多路复用要解决的问题 并发多客户端连接,在多路复用之前最简单和典型的方案:同步阻塞网络IO模型 这种模式的特点就是用一个进程来处理一个网络连接(一个用户请求),比如一段典型的示例代码如下。 直接调用 recv 函数从一个 socket 上读取数据…

Nginx:Nginx优化与防盗链

Nginx:Nginx优化与防盗链 一、配置Nginx隐藏版本号二、修改Nginx用户与组三、配置Nginx网页缓存时间3.1 修改配置文件3.2 页面缓存验证 四、配置Nginx实现连接超时五、更改Nginx运行进程数六 、配置Nginx实现网页压缩功能七、配置Nginx实现防盗链 一、配置Nginx隐藏…

[数据集][目标检测]目标检测数据集蜜蜂4073张1类别VOC格式

出品方:未来自主研究中心(FIRC) 数据集格式:Pascal VOC格式(不包含分割路径的txt文件和yolo格式的txt文件,仅仅包含jpg图片和对应的xml) 图片数量(jpg文件个数):7398 标注数量(xml文件个数):7398 标注类别数&#xff…

如何利用宝塔面板快速搭建Wordpress网站?

本章教程,主要介绍一下,如何利用宝塔面板快速搭建Wordpress网站。 目录 一、 前置条件 二、 打开宝塔面板 三、解析域名 四、安装界面 五、主题安装 六、网站预览 一、 前置条件 需要准备一台Linux服务器,系统版本使用centos 7.X。 使用…

这两个PPT素材网站很多大佬都在用

01 OfficePLUS是一款基于Microsoft Office的专业辅助工具,能够为用户提供更加高效、便捷的办公体验。该工具针对多个版本的Office软件进行优化和定制,提供了丰富的功能和实用的设计资源,如PPT模板、Word模板、Excel模板等。 OfficePLUS的主…

除了ChatGPT,还有哪些好用的AI工具?(建议收藏)

目前市面上的AI工具非常多,本文对他们进行了汇总整理,大家可以按需下载或者使用哦~ 一、AI对话聊天工具 1、New Bing(AI对话搜索) https://cn.bing.com 有点像ChatGPT版的必应,可以帮你搜索问题、回答问题、写文案、写…

简单分析jsjiami.v6加密逻辑

代码案例: var _0xodRjsjiami.com.v6,_0xodR_function(){return[‮_0xodR],_0x4311[_0xodR,TMOw51jPALDkw,w6LDqcOhw7PCqQ,I8OkP0zDs8KF,fyjCv8OB,CMOdEk5b,OnthccOZHXXDoA,UnDCrcKvw61VAsKNw7TCoRN8wpkhFVzCgA40woV6wrTCtMOXbXo,CMOiwohCXsKhw4PDq8KKw5MbPMOnWVjC…

基于LayUI+Servlet的权限管理系统的设计

权限管理是所有后台系统的都会涉及的一个重要组成部分,主要目的是对不同的人访问资源进行权限的控制,避免因权限控制缺失或操作不当引发的风险问题,如操作错误,隐私数据泄露等问题。本系统基于JSPServletJDBCLayUI的技术&#xff…

Java并发体系-第三阶段-JUC并发包-[2]-CompleableFuture,SynchronousQueue

Phaser工具(有时间就了解一下,下面还有队列) 简介 java7中引入了一种新的可重复使用的同步屏障,称为移相器Phaser。Phaser拥有与CyclicBarrier和CountDownLatch类似的功能. 但是这个类提供了更加灵活的应用。CountDownLatch和CyclicBarrier都是只适用于固定数量的…

(5)NUC980先来点个灯如何设置开启自启动

嵌入式Linux开机启动过程: 可以分为以下几个步骤: CPU复位:开机时,CPU会执行复位操作,将内存的内容清空,寄存器的初始值复位。 ROM启动:CPU会从ROM中读取启动程序,将其加载到内存中…

【阅读笔记】时间序列之TPA-LSTM(含Pytorch代码实现)

本文作为自己阅读论文后的总结和思考,不涉及论文翻译和模型解读,适合大家阅读完论文后交流想法,关于论文翻译可以查看参考文献。论文地址:https://arxiv.org/abs/1809.04206 TPA-LSTM 一. 全文总结二. 研究方法三. 结论四. 创新点…

C++ 学习 ::【基础篇:13】:C++ 类的基本成员函数:类类型成员的初始化与构造函数问题

本系列 C 相关文章 仅为笔者学习笔记记录,用自己的理解记录学习!C 学习系列将分为三个阶段:基础篇、STL 篇、高阶数据结构与算法篇,相关重点内容如下: 基础篇:类与对象(涉及C的三大特性等&#…

科技云报道:数字化时代,企业终端安全防护该“上新”了!

科技云报道原创。 随着云计算、大数据、物联网等创新技术的加速落地,企业原有的网络边界被打破,各种终端设备如:笔记本电脑、台式机、平板电脑、智能手机、物联网终端等成为了新的安全边界。 在此背景下,想确保企业高效办公的灵活…

罗德与施瓦茨FSWP26相位噪声分析仪

罗德与施瓦茨FSWP26 相位噪声分析仪和VCO测试仪 FSWP相位噪声分析仪和VCO测试仪结合极低噪声内源和互相关技术,实现了相位噪声测量的超高灵敏度。因此,即便测量高度稳定的源 (例如在雷达应用中的源) 也只需几秒钟。脉冲信号测量、附加相位噪声 (包括脉冲…

【敲敲云】免费的零代码产品 — 应用用户角色与权限

之前的文章我们介绍了应用创建与设置,应用的使用离不开权限设置,毕竟不是每个人都可以查看所有的工作表,下面我们再来看一下应用的角色与权限。 一、应用用户 应用用户: 结合应用角色,可设置哪些用户可以访问哪些应用或…

Numpy---ndarray的特点、效率

1. Numpy Numpy(Numerical Python)是一个开源的 Python 科学计算库,用于快速处理任意维度的数组。 Numpy 支持常见的数组和矩阵操作。对于同样的数值计算任务,使用 Numpy 比直接使用 Python 要简洁的多。 Numpy 使用 ndarray 对…

Redis五大数据结构的底层实现

一)String类型:可以使用object encoding name就可以查看字符串的编码 SDS,flags的值不同,那么len和alloc所表示的值的数据范围也不同,所以flags的只是为了标识SDS头的总大小; alloc和len刚开始进行申请内存空间的时候都是相同的 S…

Vue.js 中的父子组件通信方式

Vue.js 中的父子组件通信方式 在 Vue.js 中,组件是构建应用程序的基本单元。当我们在应用程序中使用组件时,组件之间的通信是非常重要的。在 Vue.js 中,父子组件通信是最常见的组件通信方式之一。在本文中,我们将讨论 Vue.js 中的…

原神3.2服务端PC端架设教程

安装教程 安装 MongoDB,不会安装建议使用宝塔面板 sudo apt updatesudo apt install mongodb-orgsudo systemctl enable --now mongod 直接启动.如果失败看看数据库启动了没有.端口一致 ./start.sh 安装配置JDK17 双击 OpenJDK17U-jdk_x64_windows_hotspot_1…

4年经验,面试二十多家公司后的整理....

先说一下自己的个人情况,普通二本计算机专业毕业,懂python,会写脚本,会selenium,会性能,然而离职后到今天都没有收到一份offer!一直在待业中,从离职第一天就开始准备简历&#xff0c…