# 前言
大多数的开发者都或多或少在GitHub
上维护有项目,但是通常GitHub
访问起来都很慢,或者无法响应。为了不能正常访问GitHub
的用户,一般会将Gitee
或其它平台托管作为镜像。
我们通常只考虑维护在GitHub
上的仓库就足够了,而对于其它镜像仓库,更多的是希望在GitHub
更新的同时,都以静默的方式自动同步。
因此以下将以Gitee
作为镜像仓库,对比多种同步方式的利弊,跟随此文你将了解到。
- 同步
GitHub
和Gitee
代码仓库的多种方式 webhooks
是什么- 什么是
GitHub Actions
,GitHub Actions
可以做什么 GitHub Actions
如何自动化部署Pages
# 同步
# 维护多个远端
查看当前仓库关联的远程库。
# 推送多次
删除origin
,然后依次关联远程GitHub
和Gitee
仓库。
本地关联的远程库名称大多数都是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
可以看到推送push
的url
多了一条。
因此fetch
时将从GitHub
拉取代码,push
时将推送到Gitee
和GitHub
两个远端。此方式相对可用,但是无法做到部分的自动化功能,例如测试、部署等。
# 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
进行加密,加密后的序列会携带在请求头header
的 x-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
,用于Gitee
和GitHub
双向同步的功能,但是注意目前尚处于内测期,只能 申请 (opens new window) 开通
# GitHub Actions
;GitHub Actions (opens new window) 即是一个免费的虚拟机,提供了三种可选的操作系统(Ubuntu Linux
、Microsoft Windows
和macOS
),用以执行用户自定义的工作流程。
那么何为工作流程呢?就是一个以.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 key
和Clone 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
,并且自动部署GitHub
和Gitee
的Pages
。
此处用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_PASSWORD
为GitHub
仓库下添加的Gitee
平台密码,gitee-repo
和branch
表示对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
平台的vuepress
和username
仓库,GitHub
的vuepress
和username.github.io
仓库。
# 添加脚本
默认你的目录结构为以下,其中deploy-pages.yml
用于部署Pages
,mirror-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
目录下),推送到用户username
的username.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) 同步更新,欢迎关注😉~