Files
git-hook/runner.go
T
2026-08-18 11:25:47 +08:00

122 lines
3.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"bufio"
"context"
"errors"
"fmt"
"io"
"log"
"os"
"os/exec"
"strings"
"sync"
"time"
)
// scriptTimeout 脚本执行超时上限(15 分钟)
const scriptTimeout = 60 * time.Minute
// shortCommit 安全地取 commit 短 ID,避免空值/短值切片越界 panic
func shortCommit(commitID string) string {
if len(commitID) > 7 {
return commitID[:7]
}
if commitID == "" {
return "unknown"
}
return commitID
}
// streamLog 逐行读取并实时打印脚本输出,直到 r 关闭
func streamLog(r io.Reader, tag string) {
br := bufio.NewReader(r)
for {
line, err := br.ReadString('\n')
if line != "" {
log.Printf("[SCRIPT %s] %s", tag, strings.TrimRight(line, "\r\n"))
}
if err != nil {
return
}
}
}
// notifyResult 发送执行结果通知
func notifyResult(ok bool, repoName, ref, commitID string, cost time.Duration, reason string) {
state := "成功"
if !ok {
state = "失败"
}
title := fmt.Sprintf("%s更新%s", repoName, state)
body := fmt.Sprintf("分支: %s\nCommit: %s\n耗时: %s\n原因: %s\n服务器已接收到 Push,更新%s",
ref, shortCommit(commitID), cost.Round(time.Second), reason, state)
if err := SendNotification(pushURL, globalConfig.DeviceKeys, title, body); err != nil {
log.Printf("[WARNING] 推送通知失败: %v", err)
return
}
log.Println("[INFO] 推送通知发送成功")
}
// runScript 异步执行 Shell 脚本,输出实时流式打印,超时 15 分钟
func runScript(scriptPath, repoName, ref, commitID string) {
start := time.Now()
tag := fmt.Sprintf("%s:%s", repoName, ref)
log.Printf("[INFO] 开始执行脚本: %s (仓库: %s, 分支: %s) 超时上限 %s",
scriptPath, repoName, ref, scriptTimeout)
ctx, cancel := context.WithTimeout(context.Background(), scriptTimeout)
defer cancel()
cmd := exec.CommandContext(ctx, "bash", scriptPath)
// 超时/取消后最多再给 10 秒让进程优雅退出;超时则强制 SIGKILL
cmd.WaitDelay = 10 * time.Second
// 传入环境变量,脚本内可读取
cmd.Env = append(os.Environ(),
fmt.Sprintf("GIT_REPO_NAME=%s", repoName),
fmt.Sprintf("GIT_REF=%s", ref),
fmt.Sprintf("GIT_COMMIT_ID=%s", commitID),
)
// stdout / stderr 合并到同一个管道,保持原有交错顺序
pr, pw := io.Pipe()
cmd.Stdout = pw
cmd.Stderr = pw
if err := cmd.Start(); err != nil {
pw.Close()
pr.Close()
log.Printf("[ERROR] 脚本启动失败 [%s]: %v", scriptPath, err)
notifyResult(false, repoName, ref, commitID, time.Since(start), fmt.Sprintf("启动失败: %v", err))
return
}
var wg sync.WaitGroup
wg.Go(func() {
streamLog(pr, tag)
})
err := cmd.Wait() // 进程退出 + 内部 copy goroutine 结束
pw.Close() // 关闭写端,读端收到 EOF
wg.Wait() // 等日志全部刷完再往下走
cost := time.Since(start)
// 先判断超时(ctx.Err() 在超时/取消时非 nil
switch {
case errors.Is(ctx.Err(), context.DeadlineExceeded):
log.Printf("[TIMEOUT] 脚本执行超时 [%s] 已运行 %s 上限 %s", scriptPath, cost.Round(time.Second), scriptTimeout)
notifyResult(false, repoName, ref, commitID, cost, "执行超时,已强制终止")
return
case err != nil:
log.Printf("[ERROR] 脚本执行失败 [%s] 耗时 %s: %v", scriptPath, cost.Round(time.Millisecond), err)
notifyResult(false, repoName, ref, commitID, cost, err.Error())
return
}
log.Printf("[SUCCESS] 脚本执行成功 [%s] 耗时 %s", scriptPath, cost.Round(time.Millisecond))
notifyResult(true, repoName, ref, commitID, cost, "执行成功")
}