From 3f57c0fe699eb83f8fb89634f7945a460b245672 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint Date: Sun, 23 Aug 2026 19:39:02 -0400 Subject: [PATCH] fix: render runtime-access.sql into the service-dir build context (#70) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CLI resolves the recipe's "." context to the service directory but resolves the Dockerfile against the recipe output directory, so `codefly build service` builds with the service dir as context. buildRecipe rendered runtime-access.sql only into the recipe tree, so nothing sat at the service-dir root the Dockerfile COPYs it from and the build failed with `COPY runtime-access.sql ... "/runtime-access.sql": not found`. Render it into the service dir too, beside the already-committed migrations/, so the COPY resolves — mirroring how migrations/ lives in both the service dir and the recipe tree. Correct the buildRecipe doc comment, which still claimed "the context is the recipe tree itself" — the exact wrong model behind this regression and #68. Because the build now writes a generated runtime-access.sql into the consumer's service directory on every run, scaffold a .gitignore in the factory so it is not left as untracked noise beside the committed migrations/. Embed the factory tree with `all:` so the .gitignore dotfile is carried in (plain go:embed skips dotfiles and would drop it silently). Co-Authored-By: Claude Opus 4.8 --- build_test.go | 31 ++++++++++++++++++++++++++++--- builder.go | 30 ++++++++++++++++++++---------- templates/factory/.gitignore.tmpl | 4 ++++ 3 files changed, 52 insertions(+), 13 deletions(-) create mode 100644 templates/factory/.gitignore.tmpl diff --git a/build_test.go b/build_test.go index 6907c6e..055b1e4 100644 --- a/build_test.go +++ b/build_test.go @@ -82,9 +82,14 @@ func TestBuildEmitsRecipeToOutputDirectory(t *testing.T) { require.FileExists(t, filepath.Join(outputDirectory, "migrations", "1_create_table.up.sql")) require.FileExists(t, filepath.Join(outputDirectory, "migrations", "1_create_table.down.sql")) - // runtime-access.sql the Dockerfile COPYs must sit at the context root beside - // migrations/, not inside the builder/ recipe-metadata directory the CLI drops - // from the staged build context — otherwise the COPY fails to resolve. + // The CLI resolves the recipe's "." context to the service directory, so the + // runtime-access.sql the Dockerfile COPYs root-relative must sit at the service + // directory root beside the committed migrations/ — otherwise the COPY the CLI + // runs against that context fails to resolve. + require.FileExists(t, filepath.Join(builder.Location, "runtime-access.sql")) + + // It is also rendered into the recipe tree (never under builder/) so the emitted + // artifact stays a self-contained context a consumer can build directly. require.FileExists(t, filepath.Join(outputDirectory, "runtime-access.sql")) require.NoFileExists(t, filepath.Join(outputDirectory, "builder", "runtime-access.sql")) @@ -93,6 +98,26 @@ func TestBuildEmitsRecipeToOutputDirectory(t *testing.T) { require.NoError(t, services.VerifyDockerBuildPlan(outputDirectory, plan)) } +// TestFactoryScaffoldsGitignoreForGeneratedRuntimeAccess covers the file the +// build writes into the service directory on every run: Create must scaffold a +// .gitignore that keeps the generated runtime-access.sql out of version control, +// so a consumer's `codefly build service` does not leave untracked noise beside +// the committed migrations/. This also guards the `all:` embed — a plain +// //go:embed drops the .gitignore dotfile and the render would silently produce +// nothing. +func TestFactoryScaffoldsGitignoreForGeneratedRuntimeAccess(t *testing.T) { + ctx := context.Background() + builder := newBuildTestBuilder(t) + + require.NoError(t, builder.Templates(ctx, + create{DatabaseName: "test", TableName: "postgres"}, + services.WithFactory(factoryFS))) + + data, err := os.ReadFile(filepath.Join(builder.Location, ".gitignore")) + require.NoError(t, err, "Create must scaffold a .gitignore at the service root") + require.Contains(t, string(data), "/runtime-access.sql") +} + func TestBuildRecipeOmitsMigrationsWhenDisabled(t *testing.T) { ctx := context.Background() builder := newBuildTestBuilder(t) diff --git a/builder.go b/builder.go index 5d70aca..c68fc13 100644 --- a/builder.go +++ b/builder.go @@ -189,12 +189,14 @@ func (s *Builder) Build(ctx context.Context, req *builderv0.BuildRequest) (*buil return s.Builder.BuildResponse() } -// buildRecipe renders the bootstrap image's Dockerfile and build context into the -// caller-owned output directory and returns a reproducible build plan instead of -// running docker itself. The CLI builds the emitted recipe multi-arch and pushes -// a manifest list, so a consumer can rebuild the image without the agent -// toolchain. The context is the recipe tree itself: the rendered builder/ files -// plus the migrations the image applies at bootstrap. +// buildRecipe renders the bootstrap image's Dockerfile into the caller-owned +// output directory and returns a reproducible build plan instead of running +// docker itself. The CLI builds the emitted recipe multi-arch and pushes a +// manifest list, so a consumer can rebuild the image without the agent toolchain. +// The CLI resolves the recipe's "." context to the service directory (s.Location), +// not the recipe tree, so the files the Dockerfile COPYs must be staged there: +// migrations/ is already committed and runtime-access.sql is rendered in below. +// They are mirrored into the recipe tree so it also builds standalone. func (s *Builder) buildRecipe(ctx context.Context, outputDirectory string, img *resources.DockerImage, docker DockerTemplating) (*builderv0.BuildResponse, error) { // The plan inventories the whole output directory and the recipe context is // its root, so any pre-existing content the caller left here would be @@ -208,9 +210,15 @@ func (s *Builder) buildRecipe(ctx context.Context, outputDirectory string, img * return s.Builder.BuildError(err) } - // runtime-access.sql is a build-context file the Dockerfile COPYs, so it lives - // at the context root beside migrations/ — not inside builder/, which the CLI - // treats as recipe metadata and drops from the staged build context. + // The Dockerfile COPYs runtime-access.sql from the build context root. The CLI + // resolves the recipe's "." context to the service directory (s.Location) — not + // to the recipe tree — so render it there, beside the already-committed + // migrations/, for the build the CLI runs to resolve the COPY. Also render it + // into the recipe tree so the emitted artifact stays a self-contained context a + // consumer can build directly, mirroring how migrations/ lives in both places. + if err := s.renderRuntimeAccess(ctx, docker, s.Location); err != nil { + return s.Builder.BuildError(err) + } if err := s.renderRuntimeAccess(ctx, docker, outputDirectory); err != nil { return s.Builder.BuildError(err) } @@ -541,7 +549,9 @@ func (s *Builder) Communicate(stream builderv0.Builder_CommunicateServer) error return err } -//go:embed templates/factory +// all: so the scaffolded .gitignore (a dotfile go:embed skips by default) is +// carried into the factory tree and rendered into new services. +//go:embed all:templates/factory var factoryFS embed.FS //go:embed templates/builder diff --git a/templates/factory/.gitignore.tmpl b/templates/factory/.gitignore.tmpl new file mode 100644 index 0000000..b3f81f1 --- /dev/null +++ b/templates/factory/.gitignore.tmpl @@ -0,0 +1,4 @@ +# runtime-access.sql is re-rendered into the service directory on every +# `codefly build` — it is the bootstrap image's build context, generated and +# never hand-edited. Keep it out of version control. +/runtime-access.sql