ci: boot-test changed games before publishing
Only games whose folder changed are boot-tested (against the previous
main commit, or against main on branches; manual runs pick games). Each
one is deployed by the node-agent's real deploy code on a dedicated
game-test runner, in a network that can reach the internet but not the
LAN. Publishing to the panel runs only if they all booted.
- boottest-payload builds the DEPLOY_SERVER message the panel would send
- readyPattern/failPatterns in game.yaml; readyExample in test.yaml is
checked against the pattern during validation
- secrets in test.yaml come from CI; a game missing one is skipped
- Minecraft gets a ready pattern; Dragonwilds needs DRAGONWILDS_OWNER_ID
- RUNNER.md + scripts/runner: runner, isolated network, systemd unit
Also fixes newline values ("\n" separators/footers) that yaml.v3 wrote
as "" on import, which would have broken ARK and Dragonwilds config
files once synced, and adds a round-trip test for the importer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5fb7736be2
commit
e9ff989246
21 files changed
+585
-18
No files matched your search
+46
-4
@@ -8,6 +8,7 @@ import (
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
@@ -21,7 +22,7 @@ var gameFields = []string{
|
||||
"installMethod", "installConfig",
|
||||
"defaultConfig", "configSchema", "configFilePath", "configFileFormat", "configFormatOptions",
|
||||
"agnosticCommandConfig", "clusterConfig",
|
||||
"logFilePath", "backupPaths", "maxBackupSizeMb",
|
||||
"logFilePath", "readyPattern", "failPatterns", "backupPaths", "maxBackupSizeMb",
|
||||
"publicIpConfigKey", "maxPlayersConfigKey", "mapConfigKey",
|
||||
"aiCoverPrompt",
|
||||
}
|
||||
@@ -82,11 +83,11 @@ func writeGame(path string, g map[string]any) error {
|
||||
if !ok || isEmpty(v) {
|
||||
continue
|
||||
}
|
||||
var val yaml.Node
|
||||
if err := val.Encode(normalize(v)); err != nil {
|
||||
val, err := toNode(normalize(v))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
doc.Content = append(doc.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: f}, &val)
|
||||
doc.Content = append(doc.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: f}, val)
|
||||
}
|
||||
return writeYAML(path, gameHeader, doc)
|
||||
}
|
||||
@@ -128,6 +129,47 @@ func writeYAML(path, header string, doc *yaml.Node) error {
|
||||
return os.WriteFile(path, []byte(b.String()), 0o644)
|
||||
}
|
||||
|
||||
// toNode builds the YAML node tree by hand because yaml.v3's Node.Encode turns a string made only of
|
||||
// whitespace, like a "\n" config separator, into "" and the value is silently lost.
|
||||
func toNode(v any) (*yaml.Node, error) {
|
||||
switch x := v.(type) {
|
||||
case map[string]any:
|
||||
n := &yaml.Node{Kind: yaml.MappingNode}
|
||||
keys := make([]string, 0, len(x))
|
||||
for k := range x {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
val, err := toNode(x[k])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
n.Content = append(n.Content, &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: k}, val)
|
||||
}
|
||||
return n, nil
|
||||
case []any:
|
||||
n := &yaml.Node{Kind: yaml.SequenceNode}
|
||||
for _, e := range x {
|
||||
val, err := toNode(e)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
n.Content = append(n.Content, val)
|
||||
}
|
||||
return n, nil
|
||||
case string:
|
||||
if x != "" && strings.TrimSpace(x) == "" {
|
||||
return &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: x, Style: yaml.DoubleQuotedStyle}, nil
|
||||
}
|
||||
}
|
||||
n := &yaml.Node{}
|
||||
if err := n.Encode(v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// normalize turns whole-number floats from encoding/json back into ints so YAML shows 25565, not 25565.0.
|
||||
func normalize(v any) any {
|
||||
switch x := v.(type) {
|
||||
|
||||
Reference in new issue
Block a user