# 前言

  大多数的开发者都或多或少在GitHub上维护有项目,但是通常GitHub访问起来都很慢,或者无法响应。为了不能正常访问GitHub的用户,一般会将Gitee或其它平台托管作为镜像。

  我们通常只考虑维护在GitHub上的仓库就足够了,而对于其它镜像仓库,更多的是希望在GitHub更新的同时,都以静默的方式自动同步。

  因此以下将以Gitee作为镜像仓库,对比多种同步方式的利弊,跟随此文你将了解到。

  • 同步GitHubGitee代码仓库的多种方式
  • webhooks是什么
  • 什么是GitHub ActionsGitHub Actions可以做什么
  • GitHub Actions如何自动化部署Pages

# 同步

# 维护多个远端

  查看当前仓库关联的远程库。

# 推送多次

  删除origin,然后依次关联远程GitHubGitee仓库。

  本地关联的远程库名称大多数都是origin,此情况对于单个远端来说很适用。若关联有多个远端,名称最好还是要容易区分。

git remote rm origin
git remote add github https://github.com/xxx/repo.git
git remote add gitee https://gitee.com/xxx/repo.git

  查看关联的远程库。

  本地代码提交后,分别推送至两个远端。缺点也比较明显,即推送多次显得冗余。

git push github master
git push gitee master

# 添加 package.json 脚本命令

  可在package.json中合并两个命令,推送时只运行npm run push即可。实际看似简化了输入,却添加了与项目无关的scripts命令。

// package.json
{
  ...
  "scripts": {
    "push": "git push github master && git push gitee master"
  }
}

# 修改 Git 内部配置

  当前远端还是只有origin

  添加一条push的远端地址。

git remote set-url --add origin https://gitee.com/xxx/repo.git

  可以看到推送pushurl多了一条。

  因此fetch时将从GitHub拉取代码,push时将推送到GiteeGitHub两个远端。此方式相对可用,但是无法做到部分的自动化功能,例如测试、部署等。

# Gitee 同步按钮

  若仓库还未创建,可在头部选择导入仓库。

  若仓库已存在,可在仓库主页的管理菜单的功能设置下配置地址。

  两种方式都将在仓库主页创建同步按钮。

  注意强制同步将覆盖当前仓库。

# webhooks

  ;webhooks (opens new window)web钩子,是一个可以接收http/s请求(多为post)的URL。大多数情况下都是客户端调用api获取服务端提供的数据。而在webhooks中,服务端则将在特定事件时调用webhooks钩子。

  ;GitHub也提供了webhooks,当用户向仓库push推送(不单单是推送事件)代码时,GitHub将向配置的URL发送http/s请求,可用于发邮件、自动部署、备份镜像等等。

  代码仓库可访问 GitHub (opens new window)

# 准备工作

  根据以上特性,搭建一个express服务器,目的用于启动一个用于同步代码的服务端post接口。

  目录结构如下,app.js为入口文件,webhook-handler用于提供同步代码的核心功能。

├── webhook-handler
│   ├── index.js
│   ├── mirror.js
│   ├── shell.sh
│   ├── webhook.config.js
├── app.js
├── const.js
├── package.json
├── ...

  ;app.js中引入webhook-handler处理函数,注意GitHub触发webhooks传递的是json格式的数据,要用到express.json()中间件,继续往下看。

// app.js
const handler = require('./webhook-handler')

app.use(express.json())

app.post('/mirror', (req, res, next) => {
  handler( ... )
})

# 处理函数

  当GitHub调用mirror接口时,会将用户预设的秘钥secret和参数体json进行加密,加密后的序列会携带在请求头headerx-hub-signature (opens new window) 中。

  工具类函数encrypted,主要用于进行hmacsha1算法加密,参数secret为秘钥,sign为被用于加密的数据。

  函数isEqual,用于对比两字符串是否一致,但是注意判断相等 不建议 (opens new window)===,而应该使用 恒定时间 (opens new window) 字符串比较,有助于提高服务端的安全性。

// webhook-handler/index.js
function handler(req, res, cb) {
  const sign = req.headers['x-hub-signature']
  const encrypted = encrypt(GITHUB_WEBHOOK_SECRET, req.body)
  ...
}

function encrypt(secret, sign) {
  return `sha1=${crypto.createHmac('sha1', secret).update(JSON.stringify(sign)).digest('hex')}`
}

function isEqual(value = '', other = '') {
  if (value.length !== other.length) return false

  return crypto.timingSafeEqual(Buffer.from(value), Buffer.from(other))
}

  思考一下,为什么GitHub会将用户预设的秘钥和参数体加密呢?

  秘钥加密可以理解,因为不能明文传递。

  以上提供的post接口,对于GitHub的任意仓库都能触发,别人的仓库是不是也可以呢。那么如何区分是否是我们的仓库触发了呢,秘钥就派上用场了。当服务端保存的静态秘钥与GitHub仓库预设的秘钥一致时,就能确定是我们的仓库了。

# 同步代码

  ;mirror.js用于启动子进程,运行shell脚本来同步代码。

  要明确的是,当我们向https://github.com/xxx/repo.git推送代码时,是无法登录验证权限的,而应当携带上用户名密码来推送,即推送到https://username:password@github.com/xxx/repo.git

// webhook-handler/mirror.js
const fullPath = getFullPath(DIST_REPO, GITEE_USERNAME, GITEE_PASSWORD)

function getFullPath(url, username, password) {
  const index = url.indexOf('//')
  const protocol = url.slice(0, index + 2)
  const path = url.slice(index + 2)

  return `${protocol}${username}:${password}@${path}`
}

  ;shell脚本中,接收两个参数,分别为源仓库和目标仓库地址,注意$1$2没有语义,可声明变量来保存。

// webhook-handler/mirror.js
const shPath = path.join(__dirname, 'shell.sh')
const command = `${shPath} ${SRC_REPO} ${fullPath}`

// webhook-handler/shell.sh
SRC_REPO=$1
DIST_REPO=$2
...

  有必要解释下shell.sh脚本的工作流程。

  • mkdir _temp ...:根目录下创建临时目录_temp,切换工作目录到_temp
  • git clone --mirror ...:镜像克隆,完全复制源仓库(包括分支、引用等等)
  • git remote set-url --push ...:将当前副本仓库的推送源地址修改为目标仓库
  • git push --mirror:镜像推送,完全推送到目标仓库(包括分支、引用等等)
  • cd ...:切换到初始的目录,删除临时仓库_temp
// webhook-handler/shell.sh
mkdir _temp && cd _temp

git clone --mirror "$SRC_REPO" && cd `basename "$SRC_REPO"`

git remote set-url --push origin "$DIST_REPO"

git push --mirror

cd ../../ && rm -rf _temp

# GitHub 添加 webhooks

  在GitHub仓库下,选中Settings功能的Webhooks菜单,单击Add webhook添加。

  输入你部署在服务器上的post接口,设置Secret秘钥,注意Content type选择为json,另外触发webhook的事件类型,默认只有推送事件,你也可以自定义选择。

  当你的仓库发生以上勾选的事件时,GitHub就会自动帮你调用部署在服务器上的mirror接口,进而镜像同步你的仓库代码。

  ;webhooks的方式相对来说比较可行,但是缺点也很明显,一方面必须额外的服务器(有node环境且安装了Git)支持用来部署接口。另一方面,单个仓库同步还是很便捷,但是如果说多个仓库要镜像同步呢?

  那我们的处理函数就要去判断请求接口的GitHub仓库来源,同时每多同步一组仓库,就要在服务端新增一组源仓库和目标仓库的地址。

提供一个 Gitee (opens new window) 官方的webhooks,用于GiteeGitHub双向同步的功能,但是注意目前尚处于内测期,只能 申请 (opens new window) 开通

# GitHub Actions

  ;GitHub Actions (opens new window) 即是一个免费的虚拟机,提供了三种可选的操作系统(Ubuntu LinuxMicrosoft WindowsmacOS),用以执行用户自定义的工作流程。

  那么何为工作流程呢?就是一个以.yml为后缀的文件(YAML语法),注意此文件要放置在代码仓库中的目录.github/workflows下才会生效。

注意对于每个工作流程,GitHub都会在预先配置好的全新虚拟机中执行

# Hello world

  我们就用GitHub Actions打印Hello world试试,不用太过复杂的例子。

  在GitHub仓库上选中Actions,单击set up a workflow yourself创建工作流程。

  然后在代码编辑器粘贴以下代码,以下为相关命令的含义,更多可 参考 (opens new window)

  • name: ...:工作流程的名称为Console hello world
  • on: ...:仓库发生推送push事件时执行
  • jobs:表示执行的一项或多项任务,当前仅有一个任务console
  • console:任务名为console
  • runs-on ...:任务console运行的虚拟机为最新的Ubuntu Linux环境
  • steps:任务console的运行步骤,当前仅有一个步骤
  • - run ...:运行命令echo Hello world
# .github/gitflows/main.yml
name: Console hello world

on: push

jobs:
  console:
    runs-on: ubuntu-latest
    steps:
      - run: echo Hello world

  点击Start Commit然后Commit new file提交。

  仓库目录下将生成main.yml文件,另外会创建一次提交记录Create main.yml

├── .github
│   ├── workflows
│   │   ├── main.yml
├── ...

  由于代码是在GitHub上提交的,相当于是本地代码推送push了一次,而脚本的执行条件就是push事件的发生,因此GitHub Actions将会触发。

  单击Create main.yml查看此次推送执行的工作流程,成功在虚拟机下打印出Hello world

# 准备工作

  先要保证本机环境有Git公钥和私钥。

  然后在Gitee用户SSH公钥中,添加标题并粘贴公钥保存。

  然后在GitHub的仓库repo下,Settings功能选择Actions,单击New repository secret添加秘钥。

  呐,粘贴你的私钥,准备工作就完成了。

# 添加脚本

  修改.github/workflow/main.yml

  根据刚才Hello world的例子,很容易知道当前工作流程的名称为Mirror to Gitee repo,仓库在推送push代码、删除delete分支或者创建create分支时,将执行此脚本。

  脚本包含一个任务,名为mirror,运行的虚拟机为最新的Ubuntu Linux环境。而此任务下又包含两个名为Config private keyClone repo and push的步骤。

# .github/gitflows/main.yml
name: Mirror to Gitee repo

on: [ push, delete, create ]

jobs:
  mirror:
    runs-on: ubuntu-latest
    steps:
      - name: Config private key
        env:
          SSH_PRIVATE_KEY: ${{ secrets.GITEE_PRIVATE_KEY }}
        run: |
          mkdir -p ~/.ssh
          echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
          chmod 600 ~/.ssh/id_rsa
          echo "StrictHostKeyChecking no" >> ~/.ssh/config

      - name: Clone repo and push
        env:
          SRC_REPO: "https://github.com/xxx/repo.git"
          DIST_REPO: "git@gitee.com:xxx/repo.git"
        run: |
          git clone --mirror "$SRC_REPO"
          cd `basename "$SRC_REPO"`
          git remote set-url --push origin "$DIST_REPO"
          git push --mirror

  ;Config private key用于在虚拟机的用户目录中写入私钥。env下有环境变量SSH_PRIVATE_KEY,变量值由 secrets (opens new window) 上下文获取而来。

  有没有觉得GITEE_PRIVATE_KEY很眼熟呢?没错,就是刚才保存在仓库目录下的私钥。

  当用户向诸如git@github.com:xxx/xx.git的仓库推送代码时,遵循SSH传输协议。在推送前,Git将提交用户根目录下的私钥id_rsa到远端,远端则会将私钥和公钥(GitHub或者Gitee用户在服务端添加的公钥id_rsa.pub)对一起做验证,判别此私钥是否有推送权限。

  而我们已经将公钥保存在了Gitee远端服务器上,私钥保存在GitHub仓库内的GITEE_PRIVATE_KEY上,GitHub平台会将私钥传递在yml脚本中的secrets上下文内,然后脚本获取后,将其写在了虚拟机的用户根目录下。此时虚拟机要推送的话,它当然是有权限的。

  还是不太清楚的话,可以理解为。依托GitHub平台和yml脚本,我们本机的私钥将会被复制成为虚拟机的私钥。

  以下为Config private key写入私钥的过程。

  • mkdir -p ...:虚拟机根目录下创建.ssh文件夹。-p表示即使上级目录不存在,也要按目录层级自动创建
  • echo ...:将私钥写入.ssh文件夹下的id_rsa文件中
  • chmod ...:修改id_rsa的权限为600(仅所有者可读写)

  创建的id_rsa的访问权限0644过于开放,Git要求私钥文件不能被其他人访问。

  • echo ...:关闭初次连接服务器时的提示

  当第一次连接服务器时,例如push提交,Git将弹出公钥确认的提示,将导致自动化任务中断。

# 同步代码

  当我们向GitHub仓库推送代码时,GitHub Actions将自动运行仓库下的yml脚本。若任务和任务下的步骤都通过,则表示执行成功。

# actions

  现在来思考一个问题,如果要同步另外的GitHub仓库到Gitee呢?

  那么我们是不要复制以上代码,修改源和目的仓库地址呢?可以明确告诉你,不用。

  ;GitHub想到了一个很好的办法,开发者可以发布不同的工作流程到 官方市场 (opens new window),而用户可以引用别人的actions即可。同步GitHub仓库到Gitee的功能,很早就有团队写好发布了,缺陷相对也很少。刚才讲那么多命令,只是为了更好地帮助你理解GitHub Actions的工作原理。

  以下为 hub-mirror-action (opens new window) 配置参数。

  • src:源平台账户名
  • dst:目标平台账户名
  • dst_key:源仓库下保存的私钥
  • static_list:仅同步指定的仓库
  • force_update:强制同步

hub-mirror-action远远不止同步单个仓库,它可以将两个平台下的所有仓库都同步

# .github/gitflows/main.yml
name: Mirror to Gitee repo

on: [ push, delete, create ]

jobs:
  mirror:
    runs-on: ubuntu-latest
    steps:
      - uses: Yikun/hub-mirror-action@v1.2
        with:
          src: github/username
          dst: gitee/username
          dst_key: ${{ secrets.GITEE_PRIVATE_KEY }}
          static_list: 'repo'
          force_update: true

# 常见问题

# GitHub Actions 如何自动化部署 Pages

  一个很常见的场景,当我们将本地仓库代码推送至远端GitHub仓库时,要同步代码到Gitee,并且自动部署GitHubGiteePages

  此处用vuecli3脚手架作为示例,运行vue create app安装vue空脚手架,然后修改vue.config.js中的生产路径。

// vue.config.js
module.exports = {
  publicPath: './',
}

  根目录app下目录结构,main.yml用于部署Pages

├── .github
│   ├── workflows
│   │   ├── main.yml
├── node_modules
├── src
│   ├── App.vue
│   ├── main.js
│   ├── ...
├── ...
├── vue.config.js

  部署GitHub Pages相对容易,GitHub Pages关联的分支有更新时将自动部署。

  而Gitee Pages相对麻烦,只能手动更新部署。第三方gitee-pages-action内部实际是利用Gitee用户名和密码登录至平台,调用更新接口的方式来实现的自动部署。

  以下为各个actions的功能及参数的作用。

  • checkout (opens new window):检出当前仓库。你可以想象成在虚拟机内克隆了当前仓库,然后就可以运行package.json中的scripts命令,例如npm run build等。
  • setup-node (opens new window):虚拟机安装node环境。其中node-version用于指定node版本
  • actions-gh-pages (opens new window):部署GitHub Pages页面。将npm run build命令构建出的dist目录,创建为新分支page用于部署。force_orphan表示page分支只生成一次提交记录,full_commit_message为提交说明,allow_empty_commit表示即使dist文件没有更新,也要重新提交
  • hub-mirror-action (opens new window):镜像同步仓库
  • gitee-pages-action (opens new window):部署Gitee Pages页面。其中GITEE_PASSWORDGitHub仓库下添加的Gitee平台密码,gitee-repobranch表示对Gitee下的repo仓库的page分支进行部署
# .github/gitflows/main.yml
name: Mirror to Gitee repo and deploy pages

on:
  push:
    branches:
      - master

jobs:
  deploy-github:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Setup node
        uses: actions/setup-node@v3
        with:
          node-version: '16.14.0'

      - name: Depend install and build
        run: |
          npm install
          npm run build

      - name: Deploy GitHub pages
        uses: peaceiris/actions-gh-pages@v3
        with:
          deploy_key: ${{ secrets.GITEE_PRIVATE_KEY }}
          publish_branch: page
          publish_dir: dist
          allow_empty_commit: true
          force_orphan: true
          full_commit_message: 'feat: deploy pages'

  deploy-gitee:
    needs: deploy-github
    runs-on: ubuntu-latest
    steps:
      - name: Mirror to Gitee repo
        uses: Yikun/hub-mirror-action@v1.2
        with:
          src: github/username
          dst: gitee/username
          dst_key: ${{ secrets.GITEE_PRIVATE_KEY }}
          static_list: 'repo'
          force_update: true

      - name: Deploy Gitee pages
        uses: yanglbme/gitee-pages-action@v1.4.0
        with:
          gitee-username: username
          gitee-password: ${{ secrets.GITEE_PASSWORD }}
          gitee-repo: repo
          branch: page

  因此main.yml脚本的工作流程也非常清晰了,任务deploy-github用于检出仓库后构建代码,部署至GitHub对应仓库的page分支。任务deploy-gitee用于同步GitHub仓库至Gitee,然后在Gitee平台,对仓库的page分支再单独部署。

  注意任务deploy-gitee一定是在deploy-github后运行的,否则仓库同步将无法达到预期。needs (opens new window) 表示当前任务要等待指定任务完成后才能执行。

# VuePress 如何多仓库同步和部署

  ;VuePress (opens new window) 也有仓库的同步和部署Pages的场景,但是相对来说有很大的差异性,主要原因在于部署Pages的代码位于另外的仓库下,而不是当前仓库的page分支。

# 准备工作

  你的GitHub应该包括两个仓库,一个用于保存VuePress的源码,一个用于静态博客,保存VuePress打包后的代码,另外Gitee仓库也是同理。

  可能你会问,用一个命名分支(例如page)保存打包后的代码,在部署页面时选择此分支不是更好,还能节省一个仓库。

  为什么会这样做呢?

  在Gitee平台中,如果你的用户名为username,当你的仓库名和用户名一致时,就会触发一个 隐藏特性 (opens new window),即访问地址https://username.gitee.io,就能访问你部署在username仓库的静态页面。

一般的Gitee仓库,部署为静态Pages后,访问地址都为二级目录。例如repo仓库,部署后的地址为http://username.gitee.io/repo

  而在GitHub平台中,如果你的用户名为username,当你的仓库名为username.github.io时,也会触发一个隐藏特性,即访问地址https://username.github.io,就能访问部署在username.github.io仓库的静态页面,否则也会是二级目录的形式。

GitHub平台,用户名和仓库名一致时,还会有另外的隐藏特性

  为什么会创建两个仓库,而不是以分支的方式就不用我多说了吧。为了触发平台的隐藏特性,让你的个人主页地址更加简洁,容易记忆。

  因此我们实际要有四个仓库,Gitee平台的vuepressusername仓库,GitHubvuepressusername.github.io仓库。

# 添加脚本

  默认你的目录结构为以下,其中deploy-pages.yml用于部署Pagesmirror-repo.yml用于同步VuePress源码仓库。

├── .github
│   ├── workflows
│   │   ├── deploy-pages.yml
│   │   ├── mirror-repo.yml
├── node_modules
├── docs
│   ├── .vuepress
│   │   ├── config.js
│   ├── README.md
├── package.json
├── .gitignore

  ;package.json添加scripts命令。

// package.json
{
  ...
  "scripts": {
    "dev": "vuepress dev docs --temp .temp",
    "build": "vuepress build docs",
  },
}

  然后新增mirror-repo.yml脚本,作用很简单,即将Github平台用户username的仓库vuepress,强制同步到Gitee平台用户username的仓库vuepress

# .github/workflows/mirror-repo.yml
name: Mirror to Gitee repo

on:
  push:
    branches:
      - master

jobs:
  mirror:
    runs-on: ubuntu-latest
    steps:
      - uses: Yikun/hub-mirror-action@v1.2
        with:
          src: github/username
          dst: gitee/username
          dst_key: ${{ secrets.GITEE_PRIVATE_KEY }}
          static_list: 'vuepress'
          force_update: true

  最后新增deploy-pages.yml脚本,以下为各步骤作用。

  • Setup node:签出当前仓库并安装node环境
  • Depend install and build:安装依赖并打包代码
  • Deploy GitHub pages:将打包后的代码(位于docs/.vuepress/dist目录下),推送到用户usernameusername.github.io仓库
  • Mirror to Gitee repo:同步GitHub平台的仓库username.github.io代码,至Gitee平台的username仓库。其中mappings参数表示仓库名不同时的映射
  • Deploy Gitee pages:将Gitee平台的用户username下的username仓库,其中的master分支代码部署为Pages
# .github/workflows/mirror-repo.yml
name: Deploy pages

on:
  push:
    branches:
      - master

jobs:
  deploy-github:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Setup node
        uses: actions/setup-node@v3
        with:
          node-version: '16.14.0'

      - name: Depend install and build
        run: |
          npm install
          npm run build
          
      - name: Deploy GitHub pages
        uses: peaceiris/actions-gh-pages@v3
        with:
          deploy_key: ${{ secrets.GITEE_PRIVATE_KEY }}
          external_repository: username/username.github.io
          publish_branch: master
          publish_dir: docs/.vuepress/dist
          allow_empty_commit: true
          force_orphan: true
          full_commit_message: 'feat: deploy pages'

  deploy-gitee:
    needs: deploy-github
    runs-on: ubuntu-latest
    steps:
      - name: Mirror to Gitee repo
        uses: Yikun/hub-mirror-action@v1.2
        with:
          src: github/username
          dst: gitee/username
          dst_key: ${{ secrets.GITEE_PRIVATE_KEY }}
          mappings: "username.github.io=>username"
          static_list: 'username.github.io'
          force_update: true

      - name: Deploy Gitee pages
        uses: yanglbme/gitee-pages-action@v1.4.0
        with:
          gitee-username: username
          gitee-password: ${{ secrets.GITEE_PASSWORD }}
          gitee-repo: username
          branch: master

# 🎉 写在最后

🍻伙伴们,如果你已经看到了这里,觉得这篇文章有帮助到你的话不妨点赞👍或 Star (opens new window) ✨支持一下哦!

手动码字,如有错误,欢迎在评论区指正💬~

你的支持就是我更新的最大动力💪~

GitHub (opens new window) / Gitee (opens new window)GitHub Pages (opens new window)掘金 (opens new window)CSDN (opens new window) 同步更新,欢迎关注😉~

最后更新时间: 6/13/2022, 8:34:24 PM