292 lines
6.7 KiB
Go
292 lines
6.7 KiB
Go
package main
|
||
|
||
import (
|
||
"encoding/json"
|
||
"flag"
|
||
"fmt"
|
||
"io"
|
||
"io/ioutil"
|
||
"log"
|
||
"net/http"
|
||
"os"
|
||
"os/exec"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
"regexp"
|
||
)
|
||
|
||
const (
|
||
path = "/"
|
||
healthCheckPath = "/health"
|
||
logDir = ".log/"
|
||
perm = 0770
|
||
)
|
||
|
||
var (
|
||
configs = []Config{}
|
||
configsLock = &sync.RWMutex{}
|
||
repositoryPath = func () string {
|
||
if os.Getenv("GOGS_REPOSITORY") != "" {
|
||
return os.Getenv("GOGS_REPOSITORY")
|
||
}
|
||
return os.Getenv("HOME") + "/repository/"
|
||
}()
|
||
logFile *os.File
|
||
)
|
||
|
||
func init() {
|
||
go func() {
|
||
for {
|
||
os.Mkdir(logDir, perm)
|
||
file := ".log/" + time.Now().Format("2006-01-02") + ".txt"
|
||
|
||
var err error
|
||
logFile, err = os.OpenFile(file, os.O_RDWR|os.O_CREATE|os.O_APPEND, perm)
|
||
if err != nil {
|
||
panic(err)
|
||
}
|
||
if os.Getenv("GOGS_REPOSITORY") != "" {
|
||
mw := io.MultiWriter(os.Stdout,logFile)
|
||
log.SetOutput(mw)
|
||
} else {
|
||
log.SetOutput(logFile)
|
||
}
|
||
time.Sleep(time.Minute)
|
||
}
|
||
}()
|
||
|
||
go func() {
|
||
for {
|
||
data, err := ioutil.ReadFile("config.json")
|
||
if err != nil {
|
||
return
|
||
}
|
||
|
||
configsLock.Lock()
|
||
if err := json.Unmarshal(data, &configs); err != nil {
|
||
log.Printf("Error unmarshalling configs: %v\n", err)
|
||
}
|
||
configsLock.Unlock()
|
||
time.Sleep(time.Second * 10)
|
||
}
|
||
}()
|
||
}
|
||
|
||
func getBranch(str string) (string) {
|
||
r, _ := regexp.Compile("refs/heads/(.*)")
|
||
matchArr := r.FindStringSubmatch(str)
|
||
if len(matchArr) > 0 {
|
||
return matchArr[len(matchArr)-1]
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func runCommand(script string, projectPath string, isFile bool) (error) {
|
||
log.Printf("Running script: %s\n", script)
|
||
var cmd *exec.Cmd
|
||
if (isFile) {
|
||
cmd = exec.Command(script)
|
||
} else {
|
||
cmd = exec.Command("/bin/bash", "-c", script)
|
||
}
|
||
cmd.Stdout = logFile
|
||
cmd.Stderr = logFile
|
||
cmd.Dir = projectPath
|
||
err := cmd.Run()
|
||
return err
|
||
}
|
||
|
||
func manageProj(projectPath string, payload GogsPayload) (error) {
|
||
// 如果没有这个文件夹就现clone下来
|
||
if _, err := os.Stat(projectPath); os.IsNotExist(err) {
|
||
log.Printf("%s is not exist, start clone", projectPath)
|
||
// 创建文件夹
|
||
os.Mkdir(projectPath, os.ModePerm)
|
||
// 给文件夹权限
|
||
os.Chmod(projectPath, os.ModePerm)
|
||
// clone 到指定文件夹
|
||
script := "git clone " + payload.Repository.SSHURL + " " + projectPath
|
||
runErr := runCommand(script, projectPath, false)
|
||
if runErr != nil {
|
||
return runErr
|
||
}
|
||
}
|
||
// 切换到指定的分支
|
||
branch := getBranch(payload.Ref)
|
||
script := "git checkout " + branch
|
||
runErr := runCommand(script, projectPath, false)
|
||
if runErr != nil {
|
||
return runErr
|
||
}
|
||
// 拉取代码
|
||
script = "git pull --rebase"
|
||
runErr = runCommand(script, projectPath, false)
|
||
if runErr != nil {
|
||
return runErr
|
||
}
|
||
// 切换到指定的commit
|
||
script = "git reset --hard " + payload.After
|
||
runErr = runCommand(script, projectPath, false)
|
||
if runErr != nil {
|
||
return runErr
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func manageDeploy(scriptPath string, projectPath string) (error) {
|
||
// 如果用户是Root就给部署文件添加执行权限
|
||
if os.Getenv("HOME") == "/root" {
|
||
log.Printf("Cur user is root, chmod u=rwx to %s", scriptPath)
|
||
script := "chmod u=rwx " + scriptPath
|
||
runErr := runCommand(script, projectPath, false)
|
||
if runErr != nil {
|
||
return runErr
|
||
}
|
||
}
|
||
// 开始执行脚本
|
||
runErr := runCommand(scriptPath, projectPath, true)
|
||
return runErr
|
||
}
|
||
|
||
func checkNeedDeploy(payload GogsPayload) (Config, bool) {
|
||
// 我们这个系统简单点,只要本次提交包含了部署信号,就直接部署最后一个commit
|
||
config := Config{
|
||
Repo: "",
|
||
Path: repositoryPath,
|
||
Script: "deploy.py",
|
||
Signal: "{D}",
|
||
Branch: "",
|
||
}
|
||
|
||
// 找到指定的项目,获取用户配置
|
||
for _, item := range configs {
|
||
if item.Repo == payload.Repository.FullName {
|
||
config.Repo = item.Repo
|
||
if item.Path != "" {
|
||
config.Path = item.Path
|
||
}
|
||
if item.Script != "" {
|
||
config.Script = item.Script
|
||
}
|
||
if item.Signal != "" {
|
||
config.Signal = item.Signal
|
||
}
|
||
if item.Branch != "" {
|
||
config.Branch = item.Branch
|
||
}
|
||
break
|
||
}
|
||
}
|
||
|
||
// 检查是否需要执行部署脚本,优先分支,然后是推送信息
|
||
|
||
// 分支匹配
|
||
if config.Branch != "" && config.Branch == getBranch(payload.Ref) {
|
||
return config, true
|
||
}
|
||
|
||
// 推送信息匹配
|
||
for _, commit := range payload.Commits {
|
||
if strings.Contains(commit.Message, config.Signal) {
|
||
return config, true
|
||
}
|
||
}
|
||
|
||
return config, false
|
||
}
|
||
|
||
|
||
func main() {
|
||
addr := flag.String("a", ":3001", "Address to listen on")
|
||
flag.Parse()
|
||
|
||
http.HandleFunc(healthCheckPath, func(w http.ResponseWriter, r *http.Request) {
|
||
w.Write([]byte("I'm alive"))
|
||
return
|
||
})
|
||
|
||
http.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
|
||
// 获取请求体
|
||
reqBody, err := ioutil.ReadAll(r.Body)
|
||
if err != nil {
|
||
log.Printf("Error reading request body: %v\n", err)
|
||
w.Write([]byte(fmt.Sprintf("Read body error: %s", err)))
|
||
return
|
||
}
|
||
|
||
// 解析payload
|
||
var payload GogsPayload
|
||
err = json.Unmarshal(reqBody, &payload)
|
||
if err != nil {
|
||
log.Printf("Error unmarshalling request body: %v\n", err)
|
||
w.Write([]byte(fmt.Sprintf("Unmarshal body error: %s", err)))
|
||
return
|
||
}
|
||
|
||
// 打印代码库名称以及推送者
|
||
log.Printf("Received: [Repository] %s [Sender] %s", payload.Repository.FullName, payload.Sender.Username)
|
||
|
||
// 检查是否需要部署
|
||
deployConfig, needDeploy := checkNeedDeploy(payload)
|
||
|
||
// 不需要部署就直接返回
|
||
if (!needDeploy) {
|
||
log.Print("No deploy commit found\n\n")
|
||
w.Write([]byte("No deploy commit found. Skip."))
|
||
return
|
||
}
|
||
|
||
// 准备部署,生成项目地址和脚本地址
|
||
projectPath := func() string {
|
||
hasPrefix := strings.HasPrefix(deployConfig.Path, "/")
|
||
hasSuffix := strings.HasSuffix(deployConfig.Path, "/")
|
||
base := ""
|
||
|
||
// 如果路径以/开头,则认为是绝对路径
|
||
if hasPrefix {
|
||
base = deployConfig.Path
|
||
} else {
|
||
base = repositoryPath + deployConfig.Path
|
||
}
|
||
|
||
repoFmt := ""
|
||
if hasSuffix {
|
||
repoFmt = "%s/"
|
||
} else {
|
||
repoFmt = "/%s/"
|
||
}
|
||
|
||
return fmt.Sprintf(base+repoFmt, payload.Repository.Name)
|
||
}()
|
||
scriptPath := projectPath + deployConfig.Script
|
||
|
||
// 处理项目,clone并切换到指定的分支和commit
|
||
err = manageProj(projectPath, payload)
|
||
if err != nil {
|
||
log.Printf("Error running manageProj script: %v\n", err)
|
||
w.Write([]byte(fmt.Sprintf("manageProj script error: %s", err)))
|
||
return
|
||
}
|
||
|
||
// 给脚本添加权限并执行脚本
|
||
err = manageDeploy(scriptPath, projectPath)
|
||
if err != nil {
|
||
log.Printf("Error running deploy script: %v\n", err)
|
||
w.Write([]byte(fmt.Sprintf("Deploy script error: %s", err)))
|
||
} else {
|
||
log.Print("Deploy finished\n\n")
|
||
w.Write([]byte("Deploy finished"))
|
||
}
|
||
|
||
return
|
||
})
|
||
|
||
log.Println("Webhook service started at " + *addr)
|
||
|
||
err := http.ListenAndServe(*addr, nil)
|
||
if err != nil {
|
||
log.Println("Error starting webhook service: " + err.Error())
|
||
}
|
||
}
|