feat(builder): SteamCMD first for Linux servers, and fall back to the next image
A draft built on a ready-made image whose files all live outside /server (nodes keep only /server and /data) went round three times and ended as WIP, although research had also verified the game's Steam app, which SteamCMD installs straight into /server. The builder never tried it. - research reads each Steam app's OS list. When it ships a Linux server, cm2network/steamcmd goes first in the variant's images and that variant is recommended. Windows-only servers keep images with Proton or Wine first. The research prompt says the same. - build tries the variant's images in order: when no draft passes with one, the next image gets a fresh start. The first draft that boots wins; if none does, the PR gets the draft that got furthest, and its summary lists each image tried and how it went. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
274ed68cc5
commit
9f6e26bc0c
3 files changed
+196
-61
No files matched your search
+79
-23
@@ -27,6 +27,7 @@ type buildResult struct {
|
||||
Verified []string `json:"verified"`
|
||||
Problems []string `json:"problems"`
|
||||
Summary string `json:"summary"`
|
||||
Tried []string `json:"tried,omitempty"` // each image tried, and how it went
|
||||
}
|
||||
|
||||
const defaultAgentImage = "www.gitea.gbreed.com/gbreed-public/gbr-node-agent:latest"
|
||||
@@ -93,26 +94,81 @@ type buildOptions struct {
|
||||
agentImage string
|
||||
}
|
||||
|
||||
// build tries the variant's images in order (research puts SteamCMD first when the game ships a Linux
|
||||
// server) and keeps the first draft that boots. A draft that can't be made to work with one image is set
|
||||
// aside and the next image gets a fresh start; if none works, the draft that got furthest is returned.
|
||||
func build(o buildOptions) (*buildResult, error) {
|
||||
if len(o.variant.Images) == 0 {
|
||||
return nil, errors.New("the variant has no image to build from")
|
||||
}
|
||||
res := &buildResult{GameID: o.id, Dir: filepath.Join(o.gamesDir, o.id)}
|
||||
res.Verified = append(res.Verified, o.variant.Verified...)
|
||||
|
||||
// 1. First boot of each candidate image until one runs as a non-root user
|
||||
var report *bootReport
|
||||
var candidates []string
|
||||
for _, image := range o.variant.Images {
|
||||
steam := isSteamCMDImage(image) && o.variant.SteamAppID != ""
|
||||
if isSteamCMDImage(image) && !steam {
|
||||
if isSteamCMDImage(image) && o.variant.SteamAppID == "" {
|
||||
continue // the SteamCMD image alone runs nothing
|
||||
}
|
||||
res.Image = image
|
||||
if steam && o.variant.StartupHint == "" {
|
||||
fmt.Printf("[build] no startup command known for Steam app %s; drafting without a first boot\n", o.variant.SteamAppID)
|
||||
report = nil
|
||||
break
|
||||
candidates = append(candidates, image)
|
||||
}
|
||||
if len(candidates) == 0 {
|
||||
return nil, errors.New("the variant has no image to build from")
|
||||
}
|
||||
dir := filepath.Join(o.gamesDir, o.id)
|
||||
setAside := filepath.Join("firstboot-out", o.id+"-best-draft")
|
||||
var best *buildResult
|
||||
var tried []string
|
||||
for i, image := range candidates {
|
||||
if i > 0 {
|
||||
fmt.Printf("[build] trying the next image: %s\n", image)
|
||||
}
|
||||
res, err := buildWithImage(o, image)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
tried = append(tried, attemptNote(res))
|
||||
if res.Passed {
|
||||
res.Tried = tried
|
||||
res.Summary = buildSummary(o, res)
|
||||
return res, nil
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(dir, "game.yaml")); err == nil {
|
||||
// This one got a draft written: keep it in case no later image does better
|
||||
os.RemoveAll(setAside)
|
||||
if err := os.Rename(dir, setAside); err != nil {
|
||||
return res, err
|
||||
}
|
||||
best = res
|
||||
}
|
||||
}
|
||||
if best == nil {
|
||||
return &buildResult{GameID: o.id, Dir: dir, Tried: tried, Problems: []string{"no image of this variant could be drafted"}},
|
||||
errors.New("no image of this variant could be drafted: " + strings.Join(tried, "; "))
|
||||
}
|
||||
if err := os.Rename(setAside, dir); err != nil {
|
||||
return best, err
|
||||
}
|
||||
best.Tried = tried
|
||||
best.Summary = buildSummary(o, best)
|
||||
return best, nil
|
||||
}
|
||||
|
||||
func attemptNote(res *buildResult) string {
|
||||
switch {
|
||||
case res.Passed:
|
||||
return res.Image + ": booted"
|
||||
case len(res.Problems) > 0:
|
||||
return res.Image + ": " + firstLine(res.Problems[0])
|
||||
}
|
||||
return res.Image + ": did not boot"
|
||||
}
|
||||
|
||||
// buildWithImage drafts the template for one image: first boot, draft, then boots of the draft with
|
||||
// failures going back to the model.
|
||||
func buildWithImage(o buildOptions, image string) (*buildResult, error) {
|
||||
res := &buildResult{GameID: o.id, Dir: filepath.Join(o.gamesDir, o.id), Image: image}
|
||||
res.Verified = append(res.Verified, o.variant.Verified...)
|
||||
steam := isSteamCMDImage(image)
|
||||
|
||||
// 1. First boot of the image untouched: its log, files and settings are the draft's facts
|
||||
var report *bootReport
|
||||
if steam && o.variant.StartupHint == "" {
|
||||
fmt.Printf("[build] no startup command known for Steam app %s; drafting without a first boot\n", o.variant.SteamAppID)
|
||||
} else {
|
||||
fmt.Printf("[build] first boot of %s\n", image)
|
||||
r, err := firstBoot(o.id+"-probe", image, o.variant.SteamAppID, steam, o.variant.StartupHint)
|
||||
if err != nil {
|
||||
@@ -120,18 +176,12 @@ func build(o buildOptions) (*buildResult, error) {
|
||||
}
|
||||
if why := imageUnusable(r); why != "" {
|
||||
fmt.Printf("[build] %s can't be used: %s\n", image, why)
|
||||
res.Problems = append(res.Problems, why)
|
||||
res.Image = ""
|
||||
continue
|
||||
res.Problems = []string{why}
|
||||
return res, nil
|
||||
}
|
||||
report = r
|
||||
res.Verified = append(res.Verified, fmt.Sprintf("first boot of %s: %s", image, r.Outcome))
|
||||
break
|
||||
}
|
||||
if res.Image == "" {
|
||||
return res, errors.New("no image of this variant runs as a non-root user")
|
||||
}
|
||||
res.Problems = nil
|
||||
|
||||
// 2. Draft until validation and the image checks pass
|
||||
steamApp := ""
|
||||
@@ -411,6 +461,12 @@ func buildSummary(o buildOptions, res *buildResult) string {
|
||||
fmt.Fprintf(&b, ", Steam app %s", o.variant.SteamAppID)
|
||||
}
|
||||
fmt.Fprintf(&b, "\n- Model rounds: %d\n", res.Rounds)
|
||||
if len(res.Tried) > 1 {
|
||||
b.WriteString("\n**Images tried, in order**\n")
|
||||
for _, t := range res.Tried {
|
||||
fmt.Fprintf(&b, "- %s\n", firstLine(t))
|
||||
}
|
||||
}
|
||||
if len(res.Verified) > 0 {
|
||||
b.WriteString("\n**Checked for real**\n")
|
||||
for _, v := range res.Verified {
|
||||
|
||||
Reference in new issue
Block a user