fix(bootstrap): ship deploy/paper in the embedded game-stack tar

This commit is contained in:
flyemoji committed 2026-08-04 16:34:09 +09:00
1 parent 587f183191
commit 3f2b28d0ec
2 files changed
+76 -5

No files matched your search

+6 -5
View File
@@ -13,8 +13,8 @@ var bootstrapScript string
//go:embed deploy/crd/*.yaml //go:embed deploy/crd/*.yaml
var bootstrapAssets embed.FS var bootstrapAssets embed.FS
// gameStackAssets carries everything deploy/bootstrap.sh needs to build the two // gameStackAssets carries everything deploy/bootstrap.sh needs to build the three
// always-on game images (login limbo + lobby) and the Velocity plugin, for the TUI // game images (login limbo, lobby, plain Paper) and the Velocity plugin, for the TUI
// install path — which pipes the embedded bootstrap.sh into bash and therefore has // install path — which pipes the embedded bootstrap.sh into bash and therefore has
// NO source checkout on disk to build from. // NO source checkout on disk to build from.
// //
@@ -26,6 +26,7 @@ var bootstrapAssets embed.FS
// //
//go:embed deploy/limbo/Dockerfile deploy/limbo/entrypoint.sh //go:embed deploy/limbo/Dockerfile deploy/limbo/entrypoint.sh
//go:embed deploy/lobby/Dockerfile deploy/lobby/entrypoint.sh //go:embed deploy/lobby/Dockerfile deploy/lobby/entrypoint.sh
//go:embed deploy/paper/Dockerfile deploy/paper/entrypoint.sh
//go:embed plugins/limbo/build.gradle plugins/limbo/settings.gradle plugins/limbo/src //go:embed plugins/limbo/build.gradle plugins/limbo/settings.gradle plugins/limbo/src
//go:embed plugins/paper/build.gradle plugins/paper/settings.gradle plugins/paper/src //go:embed plugins/paper/build.gradle plugins/paper/settings.gradle plugins/paper/src
//go:embed plugins/velocity/build.gradle plugins/velocity/settings.gradle plugins/velocity/src //go:embed plugins/velocity/build.gradle plugins/velocity/settings.gradle plugins/velocity/src
@@ -46,9 +47,9 @@ func GameStackTar(w io.Writer) error {
if err != nil { if err != nil {
return err return err
} }
// Mode 0644 for everything: entrypoint.sh is invoked as `sh <file>` by both // Mode 0644 for everything: entrypoint.sh is invoked as `sh <file>` by all
// Dockerfiles precisely because the +x bit does not survive a Windows checkout, // three Dockerfiles precisely because the +x bit does not survive a Windows
// so nothing here needs to be executable. // checkout, so nothing here needs to be executable.
if err := tw.WriteHeader(&tar.Header{ if err := tw.WriteHeader(&tar.Header{
Name: path, Name: path,
Mode: 0o644, Mode: 0o644,
+70
View File
@@ -1,6 +1,8 @@
package felis package felis
import ( import (
"io/fs"
"regexp"
"strings" "strings"
"testing" "testing"
) )
@@ -104,6 +106,74 @@ func TestBootstrapPinsViaBlockConnectionsOff(t *testing.T) {
} }
} }
// The embed list and the images bootstrap.sh builds are two lists nobody reconciles.
// deploy/paper shipped an image build without ever being added to gameStackAssets, and
// nothing said so: a checkout on disk satisfies the build either way, and the tar is
// only the build context on the path that has no checkout — `curl | bash`, where the
// third `docker build -f` then names a file that was never unpacked. So derive the
// inputs from the script and from each Dockerfile's own COPY lines instead of restating
// them here; a fourth image inherits the check for free.
func TestGameStackTarCarriesEveryBuildInput(t *testing.T) {
// Matches the path only when GAME_STACK_DIR is followed by one, which skips the
// build-context arguments (`"$GAME_STACK_DIR"`, `"${GAME_STACK_DIR}:/src:z"`) and
// the glob for gradle's output, none of which are inputs this tar has to carry.
found := regexp.MustCompile(`\$\{GAME_STACK_DIR\}/(\S+?)"`).FindAllStringSubmatch(BootstrapScript(), -1)
var paths []string
seen := map[string]bool{}
for _, m := range found {
if !seen[m[1]] {
seen[m[1]] = true
paths = append(paths, m[1])
}
}
// Guards the regex itself: a rewrite of how bootstrap.sh spells the build context
// would otherwise turn this test into an unconditional pass. It has to come before
// the loop — a missing file in there is fatal, and a floor placed after it would
// never be reached to say that the regex, not the tar, is what went wrong.
if len(paths) < 3 {
t.Fatalf("only %d game-stack path(s) resolved out of bootstrap.sh; the limbo, "+
"lobby and paper Dockerfiles are all built from ${GAME_STACK_DIR}", len(paths))
}
for _, path := range paths {
requireEmbedded(t, path)
// A Dockerfile that arrives without the files it COPYs fails just as late and
// just as far from here; the deploy/paper gap was missing its entrypoint too.
for _, src := range copySources(t, path) {
requireEmbedded(t, src)
}
}
}
// copySources lists the build-context paths a Dockerfile COPYs in, skipping the
// --from=<stage> copies, whose sources are produced by an earlier stage rather than
// unpacked from the tar.
func copySources(t *testing.T, dockerfile string) []string {
t.Helper()
var out []string
// Continuations are joined first: a COPY split across lines would otherwise be two
// fragments, neither of them starting with COPY followed by a source, and its
// source would slip past unchecked.
body := strings.ReplaceAll(readGameStackFile(t, dockerfile), "\\\n", " ")
for line := range strings.SplitSeq(body, "\n") {
f := strings.Fields(line)
if len(f) < 2 || f[0] != "COPY" || strings.HasPrefix(f[1], "--") {
continue
}
out = append(out, strings.TrimSuffix(f[1], "/"))
}
return out
}
// fs.Stat rather than ReadFile: half of these are directories (`COPY plugins/shared/`),
// and embed.FS answers for those too.
func requireEmbedded(t *testing.T, path string) {
t.Helper()
if _, err := fs.Stat(gameStackAssets, path); err != nil {
t.Errorf("%s is a game-stack build input but is not in gameStackAssets; an "+
"install with no source checkout dies on it: %v", path, err)
}
}
func readGameStackFile(t *testing.T, name string) string { func readGameStackFile(t *testing.T, name string) string {
t.Helper() t.Helper()
b, err := gameStackAssets.ReadFile(name) b, err := gameStackAssets.ReadFile(name)