A module says what it is written in, and needs no Dockerfile
The bundle recipe: the one that both builds and packs. An archive packs a directory as it stands, so shipping compiled output meant compiling somewhere first — which meant a Dockerfile repeating the same incantation in every module. Two base arguments with no defaults, a working directory chosen so the SDK resolves upward, the compiler invoked by absolute path because the usual symlink is resolved away when the base image is assembled, a second stage, an environment variable naming the entrypoints. Most of the catalogue is unconverted and that is why; two conversions done in one session were each wrong twice with a working example open in the next window. A bundle says a language and a list of entrypoints. The mesh knows what the language implies. Anything a module could override there it would be writing a Dockerfile to override, so a toolchain is deliberately not configurable. Declared rather than inferred, both of them: guessing the language from which files are present makes a build depend on a directory listing, and guessing the entrypoints makes it change meaning when somebody adds a helper. A toolchain the mesh does not hold is refused before anything is compiled, naming what to build first — the same treatment a missing base already gets, because it is the same question and somebody can answer it. A language the mesh does not build is refused saying what would have worked, since the author is usually one word away. The list of languages is closed and adding to it is a decision. Every language is another implementation of the contracts every module shares, and those change rarely and cascade when they do (ADR 0039) — a mesh whose SDKs disagree about the envelope fails by ignoring messages rather than by failing to compile. Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const aBundle = `{"module":"greeter","version":"1",
|
||||
"build":{"artifacts":[
|
||||
{"name":"code","kind":"bundle","language":"typescript","entrypoints":["dist/index.js"]}]},
|
||||
"resources":[
|
||||
{"id":"files","type":"archive","path":"/opt/greeter","artifact":"code"}]}`
|
||||
|
||||
// compiling is a runner that behaves like a toolchain: when asked to compile, it leaves output
|
||||
// where the toolchain says output lands. Without this the pack step has nothing to pack, and the
|
||||
// test would be asserting on a failure rather than on a build.
|
||||
type compiling struct{ *recorded }
|
||||
|
||||
func (c compiling) run(ctx context.Context, dir, name string, args ...string) (string, error) {
|
||||
out, err := c.recorded.run(ctx, dir, name, args...)
|
||||
if name == "docker" && len(args) > 0 && args[0] == "run" {
|
||||
made := filepath.Join(dir, "dist")
|
||||
if err := os.MkdirAll(made, 0o755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(made, "index.js"), []byte("console.log(1)"), 0o644); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
return out, err
|
||||
}
|
||||
|
||||
// **A module says what it is written in, and needs no Dockerfile.** This is the whole point of the
|
||||
// bundle recipe: the same module previously needed a hand-written recipe repeating an incantation
|
||||
// that is easy to get wrong in ways that fail somewhere else.
|
||||
func TestABundleIsCompiledAndPackedWithNoDockerfile(t *testing.T) {
|
||||
r, workspace := aRepository(t, aBundle, map[string]string{"index.ts": "console.log(1)"})
|
||||
held := map[string]string{"mesh-tools/build": "registry.invalid/mesh-tools/build@sha256:" + strings.Repeat("b", 64)}
|
||||
|
||||
got, err := Build(context.Background(), compiling{r}.run, r,
|
||||
"https://forge.invalid/greeter.git", "", "", workspace, held)
|
||||
if err != nil {
|
||||
t.Fatalf("a module with a language and no Dockerfile did not build: %v", err)
|
||||
}
|
||||
|
||||
// Compiled in the toolchain the mesh chose, not in one the module named.
|
||||
var compiled string
|
||||
for _, line := range r.ran {
|
||||
if strings.HasPrefix(line, "docker run") {
|
||||
compiled = line
|
||||
}
|
||||
}
|
||||
if compiled == "" {
|
||||
t.Fatalf("nothing was compiled:\n%s", strings.Join(r.ran, "\n"))
|
||||
}
|
||||
if !strings.Contains(compiled, "mesh-tools/build@sha256:") {
|
||||
t.Fatalf("the compile did not run in the mesh's own toolchain: %s", compiled)
|
||||
}
|
||||
if strings.Contains(strings.Join(r.ran, "\n"), "docker build") {
|
||||
t.Fatalf("a bundle invoked a Dockerfile build, which is the thing it exists to avoid:\n%s",
|
||||
strings.Join(r.ran, "\n"))
|
||||
}
|
||||
|
||||
// And pinned by a digest of what came out, like any other artifact.
|
||||
digest, _ := got.Manifest.Resources[0]["digest"].(string)
|
||||
if !strings.HasPrefix(digest, "sha256:") {
|
||||
t.Fatalf("the bundle was not pinned: %v", got.Manifest.Resources[0])
|
||||
}
|
||||
}
|
||||
|
||||
// **Refused before anything is built, naming what to build first.** A base the mesh has not built
|
||||
// is not a compile that fails on its first line — it is a question somebody can answer, and saying
|
||||
// it early is the difference between a fixable message and one about a missing image.
|
||||
func TestABundleWhoseToolchainIsNotHeldIsRefusedFirst(t *testing.T) {
|
||||
r, workspace := aRepository(t, aBundle, map[string]string{"index.ts": "console.log(1)"})
|
||||
|
||||
_, err := Build(context.Background(), compiling{r}.run, r,
|
||||
"https://forge.invalid/greeter.git", "", "", workspace, nil)
|
||||
if err == nil {
|
||||
t.Fatal("a bundle was built with no toolchain to compile it in")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "mesh-tools") {
|
||||
t.Fatalf("the refusal does not name what has to be built first: %v", err)
|
||||
}
|
||||
for _, line := range r.ran {
|
||||
if strings.HasPrefix(line, "docker run") {
|
||||
t.Fatalf("a compile was attempted before the refusal: %s", line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A language the mesh does not build is refused the same way, and names what it can build.
|
||||
func TestABundleInAnUnknownLanguageIsRefused(t *testing.T) {
|
||||
manifest := strings.Replace(aBundle, `"language":"typescript"`, `"language":"cobol"`, 1)
|
||||
r, workspace := aRepository(t, manifest, map[string]string{"index.ts": "x"})
|
||||
|
||||
_, err := Build(context.Background(), compiling{r}.run, r,
|
||||
"https://forge.invalid/greeter.git", "", "", workspace,
|
||||
map[string]string{"mesh-tools/build": "registry.invalid/x@sha256:" + strings.Repeat("c", 64)})
|
||||
if err == nil {
|
||||
t.Fatal("a language nothing can compile was accepted")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "typescript") {
|
||||
t.Fatalf("the refusal does not say what would have worked: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user