buildDocker: fail fast with a specific message when the repo doesn't match the fallback's assumed layout

Every fallback template (go/node/python/java) assumes one fixed repo
layout — a root go.mod, a root package.json, requirements.txt, a root
pom.xml. Without this, a repo shaped any other way failed inside
`docker build`'s COPY step with a raw "file not found" error that never
said which file the fallback expected or why, several minutes into a
build someone was told needs no Dockerfile at all (testing1 hit exactly
this: no go.mod, "COPY failed: file not found in build context: go.mod").

validateRepoStructure checks for the one file each language's template
actually requires before rendering it, and errors with the file name,
what it's for, and the escape hatch (bring your own Dockerfile, which
always wins over this fallback regardless of layout).

This does not make the fallback dynamic — it is still one fixed layout
per language. It only turns a silent wrong assumption into a message
that says what the assumption was. Real detection across arbitrary repo
layouts is Cloud Native Buildpacks' job, tracked as its own step in
toolshed's docs/PRODUCT-ARCHITECTURE.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wajog7nELA3i8JWTjxYGHF
This commit is contained in:
Mukul Sharma
2026-09-05 22:08:13 +05:30
co-authored by Claude Opus 5
parent f448f204e2
commit 43ca78e83e
+34
View File
@@ -98,8 +98,42 @@ def renderDockerfile(Map config) {
def (templateFile, defaultVersion) = entry def (templateFile, defaultVersion) = entry
def resolvedVersion = version ?: defaultVersion def resolvedVersion = version ?: defaultVersion
validateRepoStructure(lang)
log.info("buildDocker: no Dockerfile in repo — rendering ${templateFile} for ${lang} ${resolvedVersion}") log.info("buildDocker: no Dockerfile in repo — rendering ${templateFile} for ${lang} ${resolvedVersion}")
def constructObj = new constructTemplate() def constructObj = new constructTemplate()
constructObj.renderTemplate([version: resolvedVersion], templateFile, 'Dockerfile') constructObj.renderTemplate([version: resolvedVersion], templateFile, 'Dockerfile')
sh 'cat Dockerfile' sh 'cat Dockerfile'
} }
// Each fallback template assumes one specific, canonical repo layout — it is
// a fixed Dockerfile per language, not a detector across the many layouts a
// real repo might actually use (a subdirectory build, Gradle instead of
// Maven, a different entrypoint name). That is a real limitation, not just
// this check's — see docs/PRODUCT-ARCHITECTURE.md's buildpacks item for the
// actual fix. Until then, this at least turns a missing file into a specific
// message naming the file and the fix, in place of a raw `docker build`
// COPY failure that never says what the fallback expected in the first
// place, several minutes into a build someone was told needs no Dockerfile.
//
// Deliberately only checks for a file's existence, never its contents (e.g.
// not whether requirements.txt's app matches gunicorn's `app:app` target) —
// that would need language-aware parsing this stage has no business doing,
// and a wrong guess would be a worse failure than no check at all.
def validateRepoStructure(String lang) {
def required = [
go : ['go.mod', 'a Go module — run `go mod init <module-name>` at the repo root'],
node : ['package.json', 'an npm project — run `npm init` at the repo root, with a "start" script'],
python: ['requirements.txt', 'your dependencies, and expects the app itself as `app:app` (Flask/FastAPI-style) for gunicorn'],
java : ['pom.xml', 'a Maven project — Gradle repos need their own Dockerfile for now'],
maven : ['pom.xml', 'a Maven project — Gradle repos need their own Dockerfile for now'],
]
def check = required[lang]
if (!check) {
return // php has no required file — composer.json is used only if present
}
def (file, help) = check
if (!fileExists(file)) {
error("buildDocker: no ${file} found at the repo root. The '${lang}' fallback build expects ${help}. If your repository has a different layout, add your own Dockerfile instead — that always takes priority over this fallback and can build however you like.")
}
}