mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-10-19 14:07:36 +02:00
The review found two failure paths that reported success for work the engine had not done. Rollback: it refused only when *non-protected* source files were modified, and then ran `git reset --hard` to the previous commit - which discards every local change to tracked files, not just the update's. Edits to roles/skills/tools (the update itself keeps those via its own stash) and edits made after the update were silently wiped, while Result said nothing: deployment.md's promise that "work done afterwards is not swept away" did not hold. Rollback now copies every protected file that differs right now into `.update-backup/rollback_<ts>/` before the reset, sets the copies back after it, re-applies hand deletions, and names what it kept in Result.KeptContent like an update does. Retry: Apply merges and writes the state file before it builds, so a failed (or toolchain-less) build leaves HEAD on the new commit with the old binary still in place. The retry then took the `!snap.UpdateAvailable` no-op branch, aligned the version and returned err=nil - the job was recorded as succeeded, the page said "update finished", the CLI printed that source and binary were in place, and the old executable kept running. The tree now carries a `.update-build-pending` marker, written the moment the merge lands (and when an adoption commits to its work tree) and cleared only after the new binary is in place. The no-op branch reads it: with a toolchain it compiles and reports the build (commits=0, binaryBuilt=true); without one it keeps refusing exactly like the first attempt, so "install go and click again" is now a real instruction. The console stops treating "nothing to pull" as "nothing to do" (the retry button stays live, the confirm dialog and the done toast say the build is what will happen), and the CLI's printResult no longer swallows the build-only result. Both paths are covered by regression tests that fail on the pre-fix tree: two rollback scenarios driven through real git (edit before and after the update, a hand deletion), and four retry scenarios driven through a scripted go on PATH (failed build, state-write failure, an adoption's failed build retried by the next update, and no toolchain then toolchain).
136 lines
4.3 KiB
Go
136 lines
4.3 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"cyberstrike-ai/internal/config"
|
|
"cyberstrike-ai/internal/update"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// The same one-click update the console page drives, for the two cases a page cannot
|
|
// serve: the process is not running at all, and somebody is ssh'd in with a keyboard.
|
|
// Keeping it in the binary rather than only in a shell script means one implementation of
|
|
// "fetch, fast-forward, rebuild, swap" instead of two that can disagree. The update
|
|
// source is read from the same config.yaml field the page saves to, so the keyboard and
|
|
// the button can never update from different repositories.
|
|
func updateCommandIfNeeded(configPath string, check, apply, rollback bool) (bool, int) {
|
|
if !check && !apply && !rollback {
|
|
return false, 0
|
|
}
|
|
root := filepath.Dir(configPath)
|
|
repo, err := config.LoadUpdateRepo(configPath)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "读取更新源失败: %v\n", err)
|
|
return true, 1
|
|
}
|
|
opts := update.Options{Root: root, Repo: repo}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Minute)
|
|
defer cancel()
|
|
|
|
source := strings.TrimSpace(repo)
|
|
if source == "" {
|
|
source = update.DefaultRepoURL + "(官方仓库默认)"
|
|
}
|
|
fmt.Printf("安装目录:%s\n", root)
|
|
fmt.Printf("更新源:%s\n", source)
|
|
switch {
|
|
case check:
|
|
snap, err := update.Check(ctx, opts)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "检查更新失败: %v\n", err)
|
|
return true, 1
|
|
}
|
|
printSnapshot(snap)
|
|
return true, 0
|
|
case apply:
|
|
res, err := update.Apply(ctx, opts, func(s update.Step) {
|
|
fmt.Printf("[%s] %s\n", s.Phase, s.Message)
|
|
})
|
|
if res != nil {
|
|
printResult(res)
|
|
}
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "更新未完成: %v\n", err)
|
|
return true, 1
|
|
}
|
|
fmt.Println("源码与二进制已就位;重启服务后新代码生效。")
|
|
return true, 0
|
|
case rollback:
|
|
res, err := update.Rollback(ctx, opts)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "回滚未完成: %v\n", err)
|
|
return true, 1
|
|
}
|
|
printResult(res)
|
|
return true, 0
|
|
}
|
|
return true, 0
|
|
}
|
|
|
|
func printSnapshot(s *update.Snapshot) {
|
|
if !s.Installed {
|
|
fmt.Printf("这个目录不是 git 工作树(%s),无法自动更新\n", s.Root)
|
|
return
|
|
}
|
|
target := s.TargetBranch
|
|
if target == "" {
|
|
target = "?"
|
|
}
|
|
fmt.Printf("本机分支 %s / 提交 %s (%s),更新源默认分支 %s\n", s.Branch, s.Commit, s.Subject, target)
|
|
if s.CheckError != "" {
|
|
fmt.Printf("检查失败:%s\n", s.CheckError)
|
|
return
|
|
}
|
|
fmt.Printf("更新源最新 %s (%s),落后 %d 个提交,本地领先 %d 个提交\n", s.RemoteCommit, s.RemoteSubject, s.Behind, s.Ahead)
|
|
for _, c := range s.Incoming {
|
|
fmt.Printf(" ○ %s %s\n", c.Commit, c.Subject)
|
|
}
|
|
if s.Behind > len(s.Incoming) {
|
|
fmt.Printf(" …(共 %d 个,只列出 %d 个)\n", s.Behind, len(s.Incoming))
|
|
}
|
|
if len(s.BlockingChanges) > 0 {
|
|
fmt.Printf("本机改过 %d 个源码文件,更新会拒绝执行:\n", len(s.BlockingChanges))
|
|
for _, c := range s.BlockingChanges {
|
|
fmt.Printf(" ! %s (%s)\n", c.Path, c.Status)
|
|
}
|
|
}
|
|
if !s.CanBuild {
|
|
fmt.Println("本机没有 Go 工具链:更新只搬源码,不会重编译")
|
|
}
|
|
if s.BuildPending {
|
|
fmt.Println("上一次更新留下了未完成的编译(源码已就位、二进制还是旧的):装好工具链后重跑更新即可补上")
|
|
}
|
|
if s.HasRollback {
|
|
fmt.Printf("可回滚到 %s\n", s.RollbackTo)
|
|
}
|
|
}
|
|
|
|
func printResult(r *update.Result) {
|
|
moved := r.Commits > 0 || r.ToCommit != r.FromCommit
|
|
if !moved && !r.BinaryBuilt {
|
|
fmt.Println("已经是最新,没有改动")
|
|
return
|
|
}
|
|
if moved {
|
|
fmt.Printf("%s → %s(%d 个提交,涉及 %d 个文件,用时 %s)\n", r.FromCommit, r.ToCommit, r.Commits, r.FilesTouched, r.Duration)
|
|
} else {
|
|
// commits == 0 but a binary was built: a retry that finished a build the previous
|
|
// attempt could not. Saying "已经是最新,没有改动" here would bury the one thing
|
|
// that did happen.
|
|
fmt.Printf("源码已经是最新(%s),本次补上了二进制\n", r.ToCommit)
|
|
}
|
|
if len(r.KeptContent) > 0 {
|
|
fmt.Printf("保留了 %d 个本机内容文件(roles/skills/tools 等,没有被上游覆盖):\n", len(r.KeptContent))
|
|
for _, p := range r.KeptContent {
|
|
fmt.Printf(" = %s\n", p)
|
|
}
|
|
}
|
|
if r.BinaryBuilt {
|
|
fmt.Printf("二进制已换新:%s(旧版本留在 %s 以便回滚)\n", r.BinaryPath, r.PrevBinary)
|
|
}
|
|
}
|