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()) } }