Gokins
一个用 Go 编写的轻量级自动化构建系统,零依赖、单文件部署,可作为个人或小团队的 Jenkins 替代方案。
特性一览
| 特性 | 说明 |
|---|---|
| 零数据库依赖 | 数据全量存于本地 jobs.json / history.json,无需 MySQL / Redis |
| 单文件部署 | 通过 go:embed 将前端打包进二进制,丢到服务器直接运行 |
| 跨平台执行 | Linux 自动使用 bash,Windows 自动生成 .bat 运行 |
| 实时日志 | 强制注入 PYTHONUNBUFFERED=1,结合 I/O 管道实现毫秒级日志上屏 |
| Jenkins 兼容 | 自动转译 H * * * * 轮询语法,修复 Python dict 格式问题 |
| 参数化构建 | 支持 String(文本)和 Choice(下拉框)动态传参 |
| 生命周期控制 | 手动触发、强制中断(Kill 进程树)、暂停 / 恢复定时器 |
| 崩溃自愈 | 服务重启时自动将滞留的 RUNNING 构建标记为 ABORTED,终止空跑计时 |
| 无感部署 | Graceful Shutdown + 部署脚本,等待当前构建完成后再切换,零中断 |
| 现代化 UI | TailwindCSS + Vue3,毛玻璃卡片 + Mac 终端风格控制台 |
快速开始
环境要求:Go 1.18+
# 克隆项目
git clone https://github.com/yourname/gokins && cd gokins
# 安装依赖
go mod tidy
# 启动(访问 http://localhost:8080)
go run main.go
编译为单一二进制
# Linux
GOOS=linux GOARCH=amd64 go build -o gokins main.go
# Windows
GOOS=windows GOARCH=amd64 go build -o gokins.exe main.go
将 gokins 文件上传到服务器,执行 ./gokins 即可,无需任何额外依赖。
认证
Gokins 支持账号密码认证,凭据通过环境变量配置。
| 环境变量 | 默认值 | 说明 |
|---|---|---|
GOKINS_USER |
admin |
登录用户名 |
GOKINS_PASS |
123456 |
登录密码 |
Basic Auth 方式(推荐用于脚本调用)
curl -u "admin:yourpassword" http://localhost:8080/api/jobs
前端 fetch 写法
const token = localStorage.getItem('gokins_auth')
const res = await fetch('/api/jobs', {
headers: { 'Authorization': `Basic ${token}` }
})
API 文档
所有接口均需携带认证 Header(见上方认证章节)。
触发任务
POST /api/run
{
"job_name": "充电",
"params": {
"charge_times": "40",
"index": "mi13"
}
}
curl -X POST http://localhost:8080/api/run \
-u "admin:yourpassword" \
-H "Content-Type: application/json" \
-d '{"job_name": "充电", "params": {"charge_times": "40"}}'
获取任务列表
GET /api/jobs
返回所有任务的名称、运行状态、上次执行时间。
获取任务详情与历史
GET /api/job/detail?name={job_name}
返回任务配置及最近构建记录。
查看构建日志
GET /api/logs?name={job_name}&build_id={build_id}
实时日志流(SSE)
GET /api/logs/stream?name={job_name}&build_id={build_id}
前端通过 EventSource 订阅,300ms 增量推送,任务结束后自动发送 event: done 关闭连接。
强制中断任务
GET /api/stop?name={job_name}
向目标进程树发送 SIGTERM,3 秒后强制 SIGKILL。
暂停 / 恢复定时器
GET /api/pause?name={job_name}
导出全量配置
GET /api/export
批量导入配置
POST /api/import
目录结构
gokins/
├── main.go # 启动入口(含 Graceful Shutdown)
├── models/
│ └── job.go # 数据模型(Job、Parameter、BuildRecord)
├── store/
│ └── store.go # 持久化层(读写 jobs.json / history.json)
├── scheduler/
│ └── cron.go # Cron 解析与调度引擎
├── runner/
│ ├── runner.go # 进程执行器(环境变量注入 + 实时日志落盘)
│ └── recover.go # 重启自愈:清理滞留 RUNNING 构建
├── api/
│ ├── router.go # RESTful HTTP 路由
│ ├── sse.go # SSE 实时日志推送
│ ├── webhook.go # Git Webhook 触发
│ └── auth.go # Basic Auth 中间件
└── ui/
├── ui.go # go:embed 静态资源入口
├── index.html # Vue3 SPA
└── assets/
├── style.css
└── auth.js
重启自愈机制
服务因任何原因重启(崩溃、更新、手动重启),启动时会自动扫描 history.json,将所有状态为 RUNNING 的构建记录:
- 状态标记为
ABORTED,写入终止时间 - 在对应日志文件末尾追加终止说明,供前端展示
[Recover] Job=停止任务 Build=#4 RUNNING → ABORTED
[Recover] 共清理 1 个滞留构建
前端下次轮询后,#4 自动变为 🛑 状态,计时停止,终端显示终止原因。
关键文件:runner/recover.go
func RecoverStaleBuilds() {
jobs := store.GetJobs()
now := time.Now().Unix()
for _, job := range jobs {
for _, record := range store.GetHistory(job.JobName) {
if record.Status != "RUNNING" {
continue
}
// 追加日志说明
if f, err := os.OpenFile(record.LogFile, os.O_APPEND|os.O_WRONLY, 0644); err == nil {
f.WriteString(fmt.Sprintf("\n\n=== ⚠️ 服务已重启,构建 #%d 被强制终止 (ABORTED) ===\n", record.BuildID))
f.Close()
}
store.UpdateHistoryStatus(job.JobName, record.BuildID, "ABORTED", now)
}
}
}
main.go 中的调用顺序(顺序严格不可换):
runner.Init()
store.Init() // ← 必须先加载数据
runner.RecoverStaleBuilds() // ← 再清理滞留构建
scheduler.Init()
scheduler.Reload()
无感部署
目标:上传新二进制 → 等待当前正在跑的构建全部完成 → 新进程接管,全程无中断。
原理
旧进程收到 SIGTERM
└─ HTTP Server 停止接受新请求
└─ 等待所有 in-flight HTTP 请求完成(最多 30s)
└─ 等待所有正在运行的 Build 完成(最多 10min)
└─ 进程退出
systemd 自动拉起新进程
└─ RecoverStaleBuilds() 扫描(超时的 Build 会被 ABORTED)
└─ 新进程开始服务
第一步:为 main.go 增加 Graceful Shutdown
替换原来的 http.ListenAndServe 为以下写法:
import (
"context"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
// ... 初始化代码不变 ...
srv := &http.Server{
Addr: ":" + port,
Handler: authedHandler,
}
// 后台启动 HTTP Server
go func() {
log.Printf("Gokins 正在监听端口 :%s\n", port)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("服务器异常退出: %v", err)
}
}()
// 监听系统信号
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
sig := <-quit
log.Printf("收到信号 %v,开始优雅退出...", sig)
// 1. 停止接受新 HTTP 请求(30s 超时)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Printf("HTTP Server 强制关闭: %v", err)
}
// 2. 等待所有 Build 完成(最多 10 分钟)
runner.WaitAllBuilds(10 * time.Minute)
log.Println("Gokins 已安全退出")
}
第二步:在 runner/runner.go 中增加 WaitAllBuilds
// WaitAllBuilds 等待所有正在运行的构建完成,最多等待 timeout
func WaitAllBuilds(timeout time.Duration) {
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
mutex.RLock()
count := len(cancelFunc)
mutex.RUnlock()
if count == 0 {
return
}
log.Printf("[Shutdown] 等待 %d 个构建完成...", count)
time.Sleep(3 * time.Second)
}
log.Println("[Shutdown] 超时,强制退出")
}
第三步:配置 systemd
/etc/systemd/system/gokins.service
[Unit]
Description=Gokins CI Server
After=network.target
[Service]
Type=simple
User=yys
WorkingDirectory=/home/yys/gokins-app
ExecStart=/home/yys/gokins-app/gokins
Restart=always
RestartSec=3
# 发送 SIGTERM 后最多等待 15 分钟再强制 SIGKILL
TimeoutStopSec=900
Environment=GOKINS_USER=admin
Environment=GOKINS_PASS=yourpassword
[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl enable gokins
systemctl start gokins
第四步:一键部署脚本
在本地或 CI 机器上执行 deploy.sh:
#!/bin/bash
set -e
REMOTE="yys@your-server"
REMOTE_DIR="/home/yys/gokins-app"
BINARY="gokins"
echo "📦 编译新版本..."
GOOS=linux GOARCH=amd64 go build -o $BINARY main.go
echo "🚀 上传到服务器..."
scp $BINARY $REMOTE:$REMOTE_DIR/gokins.new
echo "🔄 原子替换二进制..."
# mv 是原子操作,替换过程中旧进程仍使用旧文件(inode 不变)
ssh $REMOTE "mv $REMOTE_DIR/gokins.new $REMOTE_DIR/gokins"
echo "⏳ 通知旧进程优雅退出(等待构建完成)..."
# systemctl reload 发送 SIGHUP;如需完全重启用 restart
ssh $REMOTE "systemctl restart gokins"
echo "✅ 部署完成!"
为什么
mv是安全的? Linux 中mv替换文件只是改变目录项指向的 inode,正在运行的旧进程持有原 inode 的文件描述符,不受影响。新进程启动时才读取新 inode,两者互不干扰。
部署时序图
本地 服务器(旧进程) 服务器(新进程)
│ │ │
├─ go build ──────────────────────►│ │
├─ scp gokins.new ────────────────►│ │
├─ mv gokins.new → gokins ────────►│(inode 替换,旧进程不感知)
├─ systemctl restart ─────────────►│ │
│ 收到 SIGTERM │
│ 停止接受请求 │
│ 等待 Build 完成... │
│ 安全退出 ──────────────────────►│
│ systemd 拉起新进程
│ RecoverStaleBuilds()
│ 开始服务 ✅
常见问题
Q:Windows 上运行 cd /home/... 路径报错?
Gokins 不修改脚本内容。从 Jenkins(Linux)导出的脚本在 Windows 上运行时,Linux 路径会失效,请确保脚本与运行环境匹配。
Q:重启后某个构建一直显示 RUNNING 计时怎么办?
检查 main.go 中的初始化顺序,必须是 store.Init() → runner.RecoverStaleBuilds(),顺序不能颠倒。
Q:日志会无限增大吗?
日志存放在 logs/ 目录,构建历史保留最近 1000 条(history.json)。物理日志文件可通过以下命令定期清理:
# 删除 30 天前的日志
find /home/yys/gokins-app/logs -name "*.log" -mtime +30 -delete
Q:从 Jenkins 迁移需要注意什么?
Gokins 自动处理:H * * * * Cron 语法转标准格式、Python dict 中的单引号 / None / True 等非标准值。其余内容导入后通过 UI 验证即可。
Q:部署期间正在运行的 Build 会被中断吗?
不会。systemctl restart 会发送 SIGTERM,进程会等待所有 Build 完成后再退出(最多 10 分钟),超时才强制结束。超时被终止的 Build 下次启动时会被 RecoverStaleBuilds 标记为 ABORTED。
评论区(0 条)
发表评论⏳ 加载编辑器…