git post-receive 待驗證的代碼


使用 git post-receive 鈎子部署服務端代碼

  • 本站文章除注明轉載外,均為本站原創或者翻譯。
  • 本站文章歡迎各種形式的轉載,但請18歲以上的轉載者注明文章出處,尊重我的勞動,也尊重你的智商;
  • 本站部分原創和翻譯文章提供markdown格式源碼,歡迎使用文章源碼進行轉載;
  • 本博客采用 WPCMD 維護;
  • 本文標題:使用 git post-receive 鈎子部署服務端代碼
  • 本文鏈接:http://zengrong.net/post/2221.htm

在 git 中提交服務器源碼的時候,如果能夠直接更新到測試服務器,並且重啟服務使其生效,會節省懶惰的程序員們大量的時間。

git 的 Server-side hook (服務端鈎子/掛鈎)可以用來做件事。

本文以部署基於 OpenResty 的服務端程序為例來介紹我的做法。

技術信息

  • OS: CentOS 6.3
  • 服務器軟件: OpenResty
  • 開發語言: Lua

名詞解釋

  • 服務器: 服務器硬件 + OS
  • 服務端程序: OpenResty 在服務器中的進程
  • 服務端代碼: 部署在 OpenResty 中的 Lua 源程序

一、git 服務端鈎子類型

Pro git 中介紹了 git 鈎子的幾種類型,其中和服務端相關的有: 

  • pre-receive
    在客戶端推送時最先執行,可以用它來拒絕客戶端的推送。
  • update
    與 pre-receive 類似,但會在每個分支都執行一次。
  • post-receive
    在客戶端推送完成后執行。

根據我的需求,我選擇 post-receive 鈎子來做這件事。因為我不希望拒絕客戶端的推送(那樣程序員們可能不知道該怎么辦)。推送總是會成功的,只是 命令 不成功而已。碰到 命令 不成功的情況,客戶端只需要再次推送一個正確的 命令 即可。

關於 命令 的配置,后面會詳述。

二、git repostories

我建立了2個 git 倉庫來完成這個任務。分成2個倉庫的好處是,更新服務端代碼和控制服務端程序互不干擾。

在開發服務器上,我可以將 OpenResty 的 lua file 緩存關閉。這樣服務端代碼更新后會立刻生效,不必重啟服務端程序。

而如果服務端程序出現錯誤,只需要更新它的狀態(reopen/reload 等)即可,不必更新服務端代碼。

server.git

這個倉庫保存服務端邏輯代碼,客戶端的文件夾結構如下:

server
├── README.md
└── src
    └── main.lua

每次提交代碼的時候,在提交信息中可以包含特定的 命令 ,在推送這次提交時,git 服務端鈎子就會起作用,將提交的代碼更新到合適的地方。

如果提交信息中沒有特定的 命令 ,那么這就是一次普通的推送而已。

在本例中,鈎子所做的事情就是將 src/ 文件夾中的所有代碼更新到服務端程序中。

serverctrl.git

這個倉庫是空的,永遠不會有內容。通過在提交信息中包含特定的 命令,git 服務器鈎子會對我們的服務端程序進行給定的操作。

三、使用鈎子重啟服務端程序

OpenResty 的進程控制

使用 nginx 自己提供的 -s 參數來控制服務端程序:

nginx -s [stop|quit|reopen|reload] -p /path/to

不帶 -s 參數,就是 start 功能了:

nginx -p /path/to

命令

serverctrl 是個空項目,不可能有任何內容。因此我規定提交信息中直接寫操作命令即可。

要控制服務端程序重啟,只需要進行這樣的提交和推送:

git commit --allow-empty -m 'reopen' && git push origin zrong

執行 [start|stop|quit|reload] 也是一樣道理。

具體代碼

下面的代碼展示了 serverctrl 項目中 post-receive 鈎子的內容。鈎子可以用操作系統能夠識別的任意腳本語言來撰寫。這里我使用的是 Python (2.7和3.4通用)。

下面的代碼和注釋已經非常清楚了,我說一下幾點要注意的。

  • 在 callnginx 方法中,需要把 subprocess.check_output 方法的 stderr 參數設置為 STDOUT 。因為如果執行 nginx 出錯,出錯信息默認是寫入到 STDERR 中的,如果不進行這樣的修改,出錯時返回的輸入信息就是空的。
  • 需要把 nginx 程序以及 ‘/opt/openresty/nginx’ 整個文件夾和其下文件的 owner 設置為 git 用戶,否則鈎子將沒有權限啟動 nginx 進程。
  • post-receive 鈎子本身的 owner 也要設置成 git 用戶,並給予執行權限。
  • 如果已經有一個 -p 參數(prefix)相同的 nginx 進程在運行了,注意先將其結束。否則 git 用戶可能無權關閉這個進程。

#!/usr/bin/env python
import sys import os import subprocess # prefix 配置路徑 openresty = '/opt/openresty/nginx' # 執行文件路徑 nginx = openresty + '/sbin/nginx' # 能夠識別的信號 signals = ('start', 'stop', 'quit', 'reopen', 'reload') # 支持的分支(用戶) branches = ('master', 'allen', 'zrong', 'xiefei', 'zm') def getgitargs(*args): if args: return ['git', '--bare', '--git-dir', os.getcwd()] + list(args) return [] def callgit(*args): return subprocess.check_output(['env', '-i'] + getgitargs(*args), universal_newlines=True) def callnginx(action): args = [nginx, '-p' ,openresty] if action != 'start': args += ['-s', action] return subprocess.check_output(args, stderr=subprocess.STDOUT, universal_newlines=True) # 鈎子會將信息從 STDIN 寫入,將這些信息讀入變量 oldrev,newrev,refname = sys.stdin.readline().strip().split(' ') # 對我們的程序而言,只有 branch 名稱有用 branch = refname.split('/')[-1] print('oldref:%s, newrev:%s, refname:%s'%(oldrev, newrev, refname)) if not branch in branches: print('The branch "%s" is not in available list! ' 'The list is %s.'%(branch, str(branches))) exit(1) try: # 得到當前提供的 branch 下的最新提交信息 commitmsg = callgit('log', branch, '--oneline', '-1')[8:-1] print(branch+' '+commitmsg) if commitmsg in signals: callnginx(commitmsg) print('======= %s %s SUCCESS ======='%(branch, commitmsg)) else: print('The signal "%s" is not in available list! ' 'The list is %s.'%(commitmsg, str(signals))) except subprocess.CalledProcessError as err: print('======= %s %s %s ERROR ======='%(branch, commitmsg, err.output)) exit(1) exit(0) 

四、從服務器的 git bare repostory 中部署服務端代碼

在這個例子中,我假設 git 倉庫和測試服務器處於同一台服務器上。這也是小團隊開發比較普遍的情況。

服務端代碼的結構

服務端代碼位於 /opt/openresty/nginx/lua 這個文件夾中。在這個文件夾中有按照用戶名稱(分支名稱)建立的子文件夾,使用這種方式,可以讓多個開發者的服務端代碼互不干擾,獨立運行。

下面的文件夾結構展示了目前有兩個開發者 master 和 zrong 已經部署了自己的服務端代碼。

/opt/openresty/nginx/lua/
├── master
│   └── main.lua
└── zrong
    └── main.lua

程序員甚至可以選擇部署別人的代碼到自己的文件夾中。它只需要在執行命令的時候提供希望部署的用戶的分支名稱(或者干脆提供一個 git 的 commit sha1),就能達到這種效果。

命令

server 包含服務端代碼,命令需要區分是代碼內容推送還是代碼部署推送,或者二者兼有。

命令的設計如下:

若提交信息的第一行包含 UP 字樣,就代表是命令推送。如果同時還有其他的提交信息,可以換行寫。

如果只是普通的提交,那么直接寫就可以了。

假設當前的 git 庫位於 zrong 分支,而且當前的 git 倉庫是干凈的,下面是幾個例子:

Sample 1

將已經提交過的代碼推送到服務器,並更新服務端代碼:

git commit --allow-empty -m 'UP' && git push origin zrong

Sample 2

將 server 倉庫中的 allen 分支的內容更新到 /opt/openresty/nginx/lua/zrong :

git commit --allow-empty -m 'UP allen' && git push origin zrong

Sample 3

將 sha1 為 7ebbf4f3151e2dfd5bdcbe9fe276fc9b6afd25e7 的提交中的內容更新到 /opt/openresty/nginx/lua/zrong :

git commit --allow-empty -m 'UP 7ebbf4f' && git push origin zrong

導出一個 bare 倉庫中的內容

位於服務器上的 git 倉庫,通常是 bare 的,它沒有 work-tree ,不能直接通過復制的方式來更新服務端代碼。

但我們可以使用 git archive 命令先將倉庫中的代碼導出成一個包,然后再解壓這個包到合適的部署路徑即可實現服務端代碼更新。

下面的代碼做這樣幾件事:

  1. 找到 server.git 這個 bare 倉庫;
  2. 處理 zrong 分支;
  3. 將 src/ 文件夾打包成 src.tar 文件。

git --git-dir server.git archive -o src.tar zrong src/

如果只想要其中的一個文件,我們可以用 git show 把這個文件的內容輸出到一個文件:

git --git-dir server.git show zrong:src/main.lua > main.lua

具體代碼

下面的代碼展示了 server 項目中 post-receive 鈎子的內容。

#!/usr/bin/env python
import sys import re import os import subprocess import shutil luahome = '/opt/openresty/nginx/lua' # 使用正則獲取命令代碼和要處理的分支信息 actionfmt = r'^UP ?(\w+)?$' branches = ('master', 'allen', 'zrong', 'xiefei', 'zm') def getgitargs(*args): if args: return ['git', '--bare', '--git-dir', os.getcwd()] + list(args) return [] def callgit(*args): return subprocess.check_output(['env', '-i'] + getgitargs(*args), universal_newlines=True, stderr=subprocess.STDOUT) # 從提交信息中得到要處理的真正分支 def getref(branch): commitmsg = callgit('log', branch, '--oneline', '-1')[8:-1] matchobj = re.search(actionfmt, commitmsg) if matchobj: if matchobj.group(1): return matchobj.group(1) return branch return None # 將 server/src 備份到一個文件 def archive(refname): tarfile = '%s/%s.tar'%(luahome, refname) callgit('archive', '-o', tarfile, refname, 'src/') return tarfile # 解壓縮備份文件到正確的文件夾 def tarxf(tarfile, refname, branch): refdir = os.path.join(luahome, branch) if os.path.exists(refdir): shutil.rmtree(refdir) args = ['tar', 'xf', tarfile, '-C', luahome] output = subprocess.check_output(args, stderr=subprocess.STDOUT, universal_newlines=True) shutil.move(os.path.join(luahome, 'src/'), refdir) os.remove(tarfile) return output oldrev,newrev,refname = sys.stdin.readline().strip().split(' ') print('oldref:%s, newrev:%s, refname:%s'%(oldrev, newrev, refname)) branch = refname.split('/')[-1] if not branch in branches: print('The branch "%s" is not in available list! ' 'The list is %s.'%(branch, str(branches))) exit(1) try: refname = getref(branch) if refname: tarfile = archive(refname) succ = tarxf(tarfile, refname, branch) print('update [%s] by [%s] success.'%(branch, refname) ) print('======= update [%s] by [%s] SUCCESS ======='%(branch, refname)) except subprocess.CalledProcessError as err: print('update [%s] by [%s] error: %s'%(branch, refname, err.output)) print('======= update [%s] by [%s] ERROR ======='%(branch, refname)) exit(1) exit(0)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM