使用git钩子自动部署服务

首先声明一下,我们公司的开发流程比较严(fan)谨(suo),真实环境并没有像本文这样自动部署的做法。
我们要达到的效果是:我们本地开发好的代码,推送到服务器后,服务器自动部署最新版本的代码。

这里介绍两种方式:

  • 裸仓库方式
  • 非裸仓库方式

    裸仓库方式

    这也是比较推荐的方式,不太有副作用,尤其需要多人共享时。
    那什么是裸仓库呢?
    就是没有工作目录的仓库, 不能执行有关git的任何命令,只记录历史提交信息,只能通过clone它的仓库对它进行push操作对它进行更新。裸仓库一般用来做多人代码交换的中转站,它只有这么些目录:
    branches config description HEAD hooks info objects refs
    接下来,仅以一个简单的node服务为例,演示整个过程。
    ps: 本地和服务器都要安装git和node,否则没法玩~

    1.本地创建仓库并开发

    终端cd到你喜欢的一个新目录(这里以githook-test为例),然后:
    1
    git init

创建index.js,使用你喜欢的编辑器,输入以下代码(摘自node官网,改了下host):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const http = require('http');
const hostname = '0.0.0.0';
const port = 3000;
const server = http.createServer((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('Hello World\n');
});
server.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`);
});

本地跑一下试试看:

1
node index.js

然后用浏览器访问http://locaohost:3000/,看到了Hello World
嗯,很好。
提交代码:

1
2
git add .
git commit -m 'init'

2.创建远程裸仓库

ssh登陆到服务器,cd到你喜欢的目录(比如~/git_projects),创建一个新仓库目录:

1
mkdir githook-test.git && cd $_

裸仓库一般都命名为xxx.git,然后初始化git:

1
git init --bare

注意,这里的bare参数很重要,就是创建裸仓库的意思

3.推送代码到服务器

配置本地仓库的remote,比如你的服务器host为vps.xx.com,登陆用户为git,刚刚创建的远程仓库路径为~/git_projects/githook-test.git,那么就在本地仓库执行:

1
git remote add origin git@vps.xx.com:~/git_projects/githook-test.git

最好配置服务器免密登陆,把你的公钥塞到服务器的~/.ssh/authorized_keys文件中即可。
第一次推送:

1
git push -u origin master

如果提示信息的最后两行是:

1
2
* [new branch] master -> master
Branch master set up to track remote branch master from origin.

就说明推送成功了。

4.创建远程非裸仓库

由于裸仓库没有工作目录,没法执行真正的代码,所以需要另一个目录(非裸仓库)拉取实际代码,通过clone的方式拉取即可,cd到你喜欢的目录,比如~/www,执行:

1
git clone ~/git_projects/githook-test.git

5.在服务器上启动服务

由于服务器的服务进程需要在后台执行,我们借助pm2这个神器帮我们管理node服务.首先全局安装pm2:

1
npm install pm2 -g

然后cd到真正运行服务的仓库:

1
cd ~/www/githook-test

启动服务:

1
pm2 start index.js --name githook-test

name参数用来给当前的服务起一个名字标识,用于后续给指定的服务执行操作,比如重启,停止等.
打开浏览器,访问这个服务,比如,你的服务器host是vps.xx.com,那么就访问http://vps.xx.com:3000.
如果也看到了Hello World,就说明服务器上的服务已经跑起来了~

6.配置git钩子

这里才是重点~
git钩子可以理解为一个触发器,当仓库发生指定事件,可以触发执行指定的shell脚本。我们这里需要的是post-receive钩子。
在服务器的仓库中创建钩子文件:

1
vi ~/git_projects/githook-test.git/hooks/post-receive

粘贴如下脚本:

1
2
3
4
5
6
7
8
9
#!/bin/sh
unset GIT_DIR
cd ~/www/githook-test
echo 'Pulling code:'
git pull origin master
echo 'Restarting server:'
pm2 restart githook-test
echo 'Deployment Done'
exit 0

保存并退出
其中,unset GIT_DIR是必须的,和Makefile一样,在每一行,目录都是固定的,需要解除环境变量GIT_DIR环境变量才能自由移动当前位置,这东西坑了我好一会儿~
感谢https://argcv.com/articles/2078.c 这篇文章~
现在这个钩子只是个普通文件,需要给它加上可执行权限:

1
chmod +x ~/git_projects/githook-test.git/hooks/post-receive

7.测试自动部署

修改本地的index.js,比如把res.end('Hello World\n');这句改成res.end('Hello World. I Love Git!\n');
然后提交,并推送:

1
git commit -a -m 'edit' && git push

你会在本地终端看到类似如下的信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Counting objects: 3, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 269 bytes | 0 bytes/s, done.
Total 3 (delta 1), reused 0 (delta 0)
remote: Pulling code:
remote: From <和谐内容>/git_projects/githook-test
remote: * branch master -> FETCH_HEAD
remote: cd2b660..50118a9 master -> origin/master
remote: Updating cd2b660..50118a9
remote: Fast-forward
remote: index.js | 2 +-
remote: 1 file changed, 1 insertion(+), 1 deletion(-)
remote: Restarting server:
remote: [PM2] Applying action restartProcessId on app [githook-test](ids: 1)
remote: [PM2] [githook-test](1) ✓
remote: ┌──────────────┬────┬──────┬───────┬────────┬─────────┬────────┬─────────────┬──────────┐
remote: │ App name │ id │ mode │ pid │ status │ restart │ uptime │ memory │ watching │
remote: ├──────────────┼────┼──────┼───────┼────────┼─────────┼────────┼─────────────┼──────────┤
remote: │ githook-test │ 1 │ fork │ 17297 │ online │ 4 │ 0s │ 11.789 MB │ disabled │
remote: └──────────────┴────┴──────┴───────┴────────┴─────────┴────────┴─────────────┴──────────┘
remote: Use `pm2 show <id|name>` to get more details about an app
remote: Deployment Done
To <和谐内容>@<和谐内容>:~/git_projects/githook-test.git
cd2b660..50118a9 master -> master

然后,再次打开浏览器访问服务器上的服务,如果能看到Hello World. I Love Git! 说明自动部署已经成功了!

非裸仓库方式

这种方式,不需要创建裸仓库,只需创建非裸仓库,感觉会节省点磁盘空间~
具体操与裸仓库方式大体一致:
裸仓库方式中的 2.创建远程裸仓库步骤,更改为:

1
cd ~/www && mkdir githook-test && cd &_ && git init

由于默认情况下,git不允许push代码到非裸仓库,我们需要对这个非裸仓库进行一些设置:

1
git config receive.denyCurrentBranch ignore

3.推送代码到服务器步骤中,由于不存在裸仓库了,我们直接push代码到非裸仓库,我们本地仓库的远程仓库地址也要修改一下:

1
git remote add origin git@vps.xx.com:~/www/githook-test

如果远程仓库不执行git config receive.denyCurrentBranch ignore的话,直接推送会看到如下错误信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
remote: error: refusing to update checked out branch: refs/heads/master
remote: error: By default, updating the current branch in a non-bare repository
remote: error: is denied, because it will make the index and work tree inconsistent
remote: error: with what you pushed, and will require 'git reset --hard' to match
remote: error: the work tree to HEAD.
remote: error:
remote: error: You can set 'receive.denyCurrentBranch' configuration variable to
remote: error: 'ignore' or 'warn' in the remote repository to allow pushing into
remote: error: its current branch; however, this is not recommended unless you
remote: error: arranged to update its work tree to match what you pushed in some
remote: error: other way.
remote: error:
remote: error: To squelch this message and still keep the default behaviour, set
remote: error: 'receive.denyCurrentBranch' configuration variable to 'refuse'.

跳过4.创建远程非裸仓库 步骤.
即使可以向远程非裸仓库push,远程仓库的工作目录也不会随之改变,需要通过钩子,将工作目录更新,所以6.配置git钩子步骤中,钩子的脚本也要改一下:

1
2
3
4
5
6
7
8
9
#!/bin/sh
unset GIT_DIR
cd ~/www/githook-test
echo 'Checkout working copy:'
git checkout -f
echo 'Restarting server:'
pm2 restart githook-test
echo 'Deployment Done'
exit 0

再次测试,发现也可以自动部署啦!
从git的默认行为来看,非裸仓库的方式,并不推荐,尤其是生产环境,一定要慎用。
在一些不重要的小应用里使用还是不错的~

——-update:

其实还有一种更简单的方式,即在裸仓库的基础上,把post-receive的内容改为:

1
2
3
4
5
6
7
#!/bin/sh
echo 'Checkout code:'
GIT_WORK_TREE=~/www/githook-test git checkout -f
echo 'Restarting server:'
pm2 restart githook-test
echo 'Deployment Done'
exit 0

参考: http://mikeeverhart.net/2013/01/using-git-to-deploy-code/