feat(builder): every boot through the node-agent, every lost file counted, Steam cache
What a builder boot sees is now what a node does: - First boots (untouched, or -as-deployed with exactly a new server's settings) are deployed by the node-agent's BootTest in keep mode; the builder only watches the console and inspects what's left. The Go copy of the deploy (its own docker run, mounts and startup prefix) is gone. It had drifted: it gave the image's VOLUME folders their own volumes, which nodes don't, so saves there looked safe while every start loses them. - Files written outside /server and /data are counted whatever their type (it only counted config-like extensions, so .sav or .db saves went unseen), including the image's VOLUME folders; logs, pid and lock files and system folders are left out. - Boot tests ask the server to stay up holdSec (60 s, test.yaml can set it) after its ready line and to listen on every published container port (needs the node-agent's matching BootTest). - Steam apps are downloaded once per build into a gbr.cache volume and copied into each boot, so later boots only check the files (STEAM_CACHE=off). - RUNNER.md no longer suggests docker system prune, which deletes the gbr-boottest network when nothing is using it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e07025f03d
commit
04967a8a16
10 files changed
+424
-171
No files matched your search
+54
-26
@@ -35,21 +35,53 @@ func runBootTestPayload(args []string) error {
|
||||
fillBlank := fs.Bool("fill-blank-passwords", false, "also give blank password settings a throwaway value. Never for a "+
|
||||
"boot test: a real server doesn't get one. Only the AI builder uses it, to find out whether an empty setting is "+
|
||||
"why a draft fails")
|
||||
keep := fs.Bool("keep", false, "leave the container and its volume after the test, for inspection")
|
||||
fs.Parse(args)
|
||||
if fs.NArg() != 1 {
|
||||
return errors.New("boottest-payload needs a game id")
|
||||
}
|
||||
id := fs.Arg(0)
|
||||
dir := filepath.Join(*gamesDir, id)
|
||||
|
||||
g, err := loadMap(filepath.Join(dir, "game.yaml"))
|
||||
payload, err := bootTestPayload(*gamesDir, fs.Arg(0), *fillBlank)
|
||||
var missing *missingSecretError
|
||||
if errors.As(err, &missing) {
|
||||
fmt.Fprintln(os.Stderr, "skip: "+missing.Error())
|
||||
os.Exit(exitMissingSecret)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if *keep {
|
||||
payload["keepContainer"] = true
|
||||
}
|
||||
b, err := json.MarshalIndent(payload, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if *out == "" {
|
||||
_, err = os.Stdout.Write(b)
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(*out, b, 0o644)
|
||||
}
|
||||
|
||||
type missingSecretError struct{ game, secret, key string }
|
||||
|
||||
func (e *missingSecretError) Error() string {
|
||||
return fmt.Sprintf("%s needs secret %s for %s", e.game, e.secret, e.key)
|
||||
}
|
||||
|
||||
// bootTestPayload builds the DEPLOY_SERVER message the panel would send for a game, plus the boot-test
|
||||
// settings BootTest reads: ready and fail patterns, timeouts, how long the server must stay up after it is
|
||||
// ready, and the container ports it must then listen on.
|
||||
func bootTestPayload(gamesDir, id string, fillBlank bool) (map[string]any, error) {
|
||||
dir := filepath.Join(gamesDir, id)
|
||||
g, err := loadMap(filepath.Join(dir, "game.yaml"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
test := map[string]any{}
|
||||
if _, err := os.Stat(filepath.Join(dir, "test.yaml")); err == nil {
|
||||
if test, err = loadMap(filepath.Join(dir, "test.yaml")); err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,8 +100,7 @@ func runBootTestPayload(args []string) error {
|
||||
for key, name := range s {
|
||||
value, ok := os.LookupEnv(fmt.Sprint(name))
|
||||
if !ok || value == "" {
|
||||
fmt.Fprintf(os.Stderr, "skip: %s needs secret %s for %s\n", id, name, key)
|
||||
os.Exit(exitMissingSecret)
|
||||
return nil, &missingSecretError{game: id, secret: fmt.Sprint(name), key: key}
|
||||
}
|
||||
config[key] = value
|
||||
}
|
||||
@@ -79,7 +110,7 @@ func runBootTestPayload(args []string) error {
|
||||
// one: they passed CI and failed on the first real deploy.
|
||||
for key, p := range schemaProperties(g) {
|
||||
prop, _ := p.(map[string]any)
|
||||
if prop["x-generate"] == nil && (!*fillBlank || !passwordKey.MatchString(key)) {
|
||||
if prop["x-generate"] == nil && (!fillBlank || !passwordKey.MatchString(key)) {
|
||||
continue
|
||||
}
|
||||
// Only missing or empty text: ports, booleans and other values are never touched
|
||||
@@ -93,22 +124,26 @@ func runBootTestPayload(args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
timeout, settle := 900, 120
|
||||
timeout, settle, hold := 900, 120, 60
|
||||
if g["installMethod"] == "STEAMCMD" {
|
||||
timeout = 2400
|
||||
}
|
||||
if v, ok := test["timeoutSec"].(json.Number); ok {
|
||||
if n, err := v.Int64(); err == nil {
|
||||
timeout = int(n)
|
||||
for key, dst := range map[string]*int{"timeoutSec": &timeout, "settleSec": &settle, "holdSec": &hold} {
|
||||
if v, ok := test[key].(json.Number); ok {
|
||||
if n, err := v.Int64(); err == nil {
|
||||
*dst = int(n)
|
||||
}
|
||||
}
|
||||
}
|
||||
if v, ok := test["settleSec"].(json.Number); ok {
|
||||
if n, err := v.Int64(); err == nil {
|
||||
settle = int(n)
|
||||
// Once up, the server must listen on what nodes publish: the container side of every default port
|
||||
expectPorts := []any{}
|
||||
if ports, ok := g["defaultPorts"].([]any); ok {
|
||||
for _, p := range ports {
|
||||
expectPorts = append(expectPorts, fmt.Sprint(p))
|
||||
}
|
||||
}
|
||||
|
||||
payload := map[string]any{
|
||||
return map[string]any{
|
||||
"command": "DEPLOY_SERVER",
|
||||
"serverId": "boottest-" + id,
|
||||
"containerName": "boottest-" + id,
|
||||
@@ -130,16 +165,9 @@ func runBootTestPayload(args []string) error {
|
||||
"failPatterns": g["failPatterns"],
|
||||
"timeoutSec": timeout,
|
||||
"settleSec": settle,
|
||||
}
|
||||
b, err := json.MarshalIndent(payload, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if *out == "" {
|
||||
_, err = os.Stdout.Write(b)
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(*out, b, 0o644)
|
||||
"holdSec": hold,
|
||||
"expectPorts": expectPorts,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// resolvePlaceholders stands in for what the panel fills per server; saved values have no owner here.
|
||||
|
||||
Reference in new issue
Block a user