開源⚡ auto-deploy-app自動化構建部署工具 前言 How to use Just do it 總結 後續規劃

前言

內部開發部署

當前開發部署流程中,主要藉助git-lab ci + docker compose實現,大致流程如下:

  1. 基於dev創建目標功能分支,完成功能實現和本地測試
  2. 測試穩定後,提交合並至dev分支,觸發dev對應runner,實現開發服務器部署更新
  3. dev分支測試通過後,提交合並至test分支,觸發test對應runner,實現測試服務器部署更新
  4. 測試完成,提交合並至prod分支(或master),觸發prod對應runner,實現生產服務器部署更新

Tips: 可通過tag管理不同runner

以上可應對多數場景,但對於以下情形仍有不足:

  • 依賴於git-lab,且服務器安裝git-lab-runner,簡單項目配置較繁瑣
  • 對於部分陳舊項目,運維部署較繁瑣
  • 無法在客戶服務器安裝git-lab-runner,此時手動部署、更新將產生大量重複勞動

爲何升級

針對上一版本(終端執行版本),存在以下痛點:

  • 顯示效果差 無法提供良好、直觀的展示效果
  • 功能高度耦合 沒有實現對 服務器、項目、配置等功能的解耦
  • 不支持快速修改 無法快速修改、調整項目配置
  • 不支持並行處理 無法支持項目的並行部署
  • 自由度低 僅對應前端項目,沒有提供更高的自由度

新版升級點

  • 提供可視化界面,操作便捷
  • 支持服務器、執行任務、任務實例的統一管理
  • 支持任務實例的快速修改、並行執行、重試、保存
  • 支持更加友好的信息展示(如:任務耗時統計、任務狀態記錄
  • 支持上傳文件、文件夾
  • 支持自定義本地編譯、清理命令
  • 支持遠端前置命令、後置命令批量順序執行
  • 支持僅執行遠端前置命令,用於觸發某些自動化腳本

How to use

下載並安裝

Download

查看使用幫助

  • 點擊查看使用幫助

創建任務並執行

  • 創建服務器(支持密碼、密鑰)


  • 點擊Create Task創建任務(本地編譯-->上傳文件夾-->編譯並啓動容器)
  • 任務結束後可保存


執行保存的任務實例

  • 選擇需要的任務點擊運行


Just do it

技術選型

鑑於上一版本(終端執行版本)的痛點,提供一個實時交互、直觀的用戶界面尤爲重要。

考慮到SSH連接、文件壓縮、上傳等操作,需要Node提供支持,而交互場景可通過瀏覽器環境實現。

因此不妨使用Electron來構建,並實現對跨平臺的支持(Windows、Mac OS/ Mac ARM OS)。

程序需持久化保存數據,這裏選用nedb數據庫實現。

Embedded persistent or in memory database for Node.js, nw.js, Electron and browsers, 100% JavaScript, no binary dependency. API is a subset of MongoDB's and it's plenty fast.

技術棧:Vue + Ant Design Vue + Electron + Node + nedb

功能設計

爲便於功能解耦,設計實現三個模塊:

  • 服務器(保存服務器連接信息)
  • 任務執行(連接服務器並完成相應命令或操作)
  • 任務實例(任務保存爲實例,便於再次快速運行)

各模塊功能統計如下:

任務執行模塊

這裏主要整理任務隊列的實現思路,對其他功能感興趣可在評論區進行討論😘。

任務隊列實現

任務隊列實現應保持邏輯簡潔、易擴展的設計思路

任務隊列需要支持任務的並行執行、重試、快速修改、刪除等功能,且保證各任務執行、相關操作等相互隔離。

考慮維護兩個任務隊列實現:

  • 待執行任務隊列 (新創建的任務需要添加至待執行隊列)
  • 執行中任務隊列 (從待執行隊列中取出任務,並依次加入執行中任務隊列,進行執行任務)

由於待執行任務隊列需保證任務添加的先後順序,且保存的數據爲任務執行的相關參數,則Array<object>可滿足以上需求。

考慮執行中任務隊列需要支持任務添加、刪除等操作,且對運行中的任務無強烈順序要求,這裏選用{ taskId: { status, logs ... } ... }數據結構實現。

因數據結構不同,這裏分別使用 List、Queue 命名兩個任務隊列

// store/modules/task.js
const state = {
  pendingTaskList: [],
  executingTaskQueue: {}
}

Executing Task頁面需根據添加至待執行任務隊列時間進行順序顯示,這裏使用lodash根據對象屬性排序後返回數組實現。

// store/task-mixin.js
const taskMixin = {
  computed: {
    ...mapState({
      pendingTaskList: state => state.task.pendingTaskList,
      executingTaskQueue: state => state.task.executingTaskQueue
    }),
    // executingTaskQueue sort by asc
    executingTaskList () {
      return _.orderBy(this.executingTaskQueue, ['lastExecutedTime'], ['asc'])
    }
  }
}

視圖無法及時更新

由於執行中任務隊列初始狀態沒有任何屬性,則添加新的執行任務時Vue無法立即完成對其視圖的響應式更新,這裏可參考深入響應式原理,實現對視圖響應式更新的控制。

// store/modules/task.js
const mutations = {
  ADD_EXECUTING_TASK_QUEUE (state, { taskId, task }) {
    state.executingTaskQueue = Object.assign({}, state.executingTaskQueue,
      { [taskId]: { ...task, status: 'running' } })
  },
}

任務實現

爲區分mixin中函數及後續功能維護便捷,mixin中函數均添加_前綴

該部分代碼較多,相關實現在之前的文章中有描述,這裏不在贅述。
可點擊task-mixin.js查看源碼。

// store/task-mixin.js
const taskMixin = {
  methods: {
    _connectServe () {},
    _runCommand () {},
    _compress () {},
    _uploadFile () {}
    // 省略...
  }
}

任務執行

任務執行流程按照用戶選擇依次執行:

  1. 提示任務執行開始執行,開始任務計時
  2. 執行服務器連接
  3. 是否存在遠端前置命令,存在則依次順序執行
  4. 是否開啓任務上傳,開啓則依次進入5、6、7,否則進進入8
  5. 是否存在本地編譯命令,存在則執行
  6. 根據上傳文件類型(文件、文件夾),是否開啓備份,上傳至發佈目錄
  7. 是否存在本地清理命令,存在則執行
  8. 是否存在遠端後置命令,存在則依次順序執行
  9. 計時結束,提示任務完成,若該任務爲已保存實例,則更新保存的上次執行狀態

Tip:

  • 每個流程完成後,會添加對應反饋信息至任務日誌中進行展示
  • 某流程發生異常,會中斷後續流程執行,並給出對應錯誤提示
  • 任務不會保存任務日誌信息,僅保存最後一次執行狀態與耗時
// views/home/TaskCenter.vue
export default {
  watch: {
    pendingTaskList: {
      handler (newVal, oldVal) {
        if (newVal.length > 0) {
          const task = JSON.parse(JSON.stringify(newVal[0]))
          const taskId = uuidv4().replace(/-/g, '')
          this._addExecutingTaskQueue(taskId, { ...task, taskId })
          this.handleTask(taskId, task)
          this._popPendingTaskList()
        }
      },
      immediate: true
    }
  },
  methods: {
    // 處理任務
    async handleTask (taskId, task) {
      const { name, server, preCommandList, isUpload } = task
      const startTime = new Date().getTime() // 計時開始
      let endTime = 0 // 計時結束
      this._addTaskLogByTaskId(taskId, '⚡開始執行任務...', 'primary')
      try {
        const ssh = new NodeSSH()
        // ssh connect
        await this._connectServe(ssh, server, taskId)
        // run post command in preCommandList
        if (preCommandList && preCommandList instanceof Array) {
          for (const { path, command } of preCommandList) {
            if (path && command) await this._runCommand(ssh, command, path, taskId)
          }
        }
        // is upload
        if (isUpload) {
          const { projectType, localPreCommand, projectPath, localPostCommand,
            releasePath, backup, postCommandList } = task
          // run local pre command
          if (localPreCommand) {
            const { path, command } = localPreCommand
            if (path && command) await this._runLocalCommand(command, path, taskId)
          }
          let deployDir = '' // 部署目錄
          let releaseDir = '' // 發佈目錄或文件
          let localFile = '' // 待上傳文件
          if (projectType === 'dir') {
            deployDir = releasePath.replace(new RegExp(/([/][^/]+)$/), '') || '/'
            releaseDir = releasePath.match(new RegExp(/([^/]+)$/))[1]
            // compress dir and upload file
            localFile = join(remote.app.getPath('userData'), '/' + 'dist.zip')
            if (projectPath) {
              await this._compress(projectPath, localFile, [], 'dist/', taskId)
            }
          } else {
            deployDir = releasePath
            releaseDir = projectPath.match(new RegExp(/([^/]+)$/))[1]
            localFile = projectPath
          }
          // backup check
          let checkFileType = projectType === 'dir' ? '-d' : '-f' // check file type
          if (backup) {
            this._addTaskLogByTaskId(taskId, '已開啓遠端備份', 'success')
            await this._runCommand(ssh,
              `
              if [ ${checkFileType} ${releaseDir} ];
              then mv ${releaseDir} ${releaseDir}_${dayjs().format('YYYY-MM-DD_HH:mm:ss')}
              fi
              `, deployDir, taskId)
          } else {
            this._addTaskLogByTaskId(taskId, '提醒:未開啓遠端備份', 'warning')
            await this._runCommand(ssh,
              `
              if [ ${checkFileType} ${releaseDir} ];
              then mv ${releaseDir} /tmp/${releaseDir}_${dayjs().format('YYYY-MM-DD_HH:mm:ss')}
              fi
              `, deployDir, taskId)
          }
          // upload file or dir (dir support unzip and clear)
          if (projectType === 'dir') {
            await this._uploadFile(ssh, localFile, deployDir + '/dist.zip', taskId)
            await this._runCommand(ssh, 'unzip dist.zip', deployDir, taskId)
            await this._runCommand(ssh, 'mv dist ' + releaseDir, deployDir, taskId)
            await this._runCommand(ssh, 'rm -f dist.zip', deployDir, taskId)
          } else {
            await this._uploadFile(ssh, localFile, deployDir + '/' + releaseDir, taskId)
          }
          // run local post command
          if (localPostCommand) {
            const { path, command } = localPostCommand
            if (path && command) await this._runLocalCommand(command, path, taskId)
          }
          // run post command in postCommandList
          if (postCommandList && postCommandList instanceof Array) {
            for (const { path, command } of postCommandList) {
              if (path && command) await this._runCommand(ssh, command, path, taskId)
            }
          }
        }
        this._addTaskLogByTaskId(taskId, `🎉恭喜,所有任務已執行完成,${name} 執行成功!`, 'success')
        // 計時結束
        endTime = new Date().getTime()
        const costTime = ((endTime - startTime) / 1000).toFixed(2)
        this._addTaskLogByTaskId(taskId, `總計耗時 ${costTime}s`, 'primary')
        this._changeTaskStatusAndCostTimeByTaskId(taskId, 'passed', costTime)
        // if task in deploy instance list finshed then update status
        if (task._id) this.editInstanceList({ ...task })
        // system notification
        const myNotification = new Notification('✔ Success', {
          body: `🎉恭喜,所有任務已執行完成,${name} 執行成功!`
        })
        console.log(myNotification)
      } catch (error) {
        this._addTaskLogByTaskId(taskId, `❌ ${name} 執行中發生錯誤,請修改後再次嘗試!`, 'error')
        // 計時結束
        endTime = new Date().getTime()
        const costTime = ((endTime - startTime) / 1000).toFixed(2)
        this._addTaskLogByTaskId(taskId, `總計耗時 ${costTime}s`, 'primary')
        this._changeTaskStatusAndCostTimeByTaskId(taskId, 'failed', costTime)
        console.log(error)
        // if task in deploy instance list finshed then update status
        if (task._id) this.editInstanceList({ ...task })
        // system notification
        const myNotification = new Notification('❌Error', {
          body: `🙃 ${name} 執行中發生錯誤,請修改後再次嘗試!`
        })
        console.log(myNotification)
      }
    }
  }
}

總結

此次使用electron終端執行版本的前端自動化部署工具進行了重構,實現了功能更強、更加快捷、自由的跨平臺應用

由於當前沒有Mac環境,無法對Mac端應用進行構建、測試,請諒解。歡迎大家對其編譯和測試,可通過github構建、測試。

🔔項目和文檔中仍有不足,歡迎指出,一起完善該項目。

🎉該項目已開源至 github,歡迎下載使用,後續會完善更多功能 🎉 源碼及項目說明

喜歡的話別忘記 star 哦😘,有疑問🧐歡迎提出 pr 和 issues ,積極交流。

後續規劃

待完善

  • 備份與共享
  • 項目版本及回滾支持
  • 跳板機支持

不足

  • 因當前遠端命令執行,使用非交互式shell,所以使用nohup&命令會導致該任務持續runing(沒有信號量返回)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章