Skip to content

Commit ff07003

Browse files
aledbfclaude
andcommitted
fix(dockerfile): unbound ARG shadowing ENV, and dockerfile:1 build-context support
Ported reference/src/test/dockerfileUtils.test.ts as an oracle (38 cases across ensureDockerfileHasFinalStageName / findBaseImage / findUserStatement / supportsBuildContexts). Two cases failed — both real bugs the oracle caught: - findUserStatement: an unbound `ARG X` (no value, no --build-arg) after an `ENV X=...` returned "" instead of the ENV value, because the ARG branch early-returned "" and shadowed the earlier ENV. TS treats an unbound ARG's value as undefined (its match predicate excludes it), so the search must fall through to the ENV. Skip an unbound, un-overridden ARG and keep scanning. - supportsBuildContexts: `# syntax=docker/dockerfile:1` reported false. TS uses semver.intersects(version, ">=1.4"), treating the partial tag "1" as the range 1.x (floating latest, which supports build contexts) — not the exact 1.0.0 that a plain semver parse produced. Replicate the range-intersection semantics (verified against the full 22-row version matrix from the reference test). The oracle's expected values are transcribed verbatim from the TS; no expectation was adjusted to pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 4490760 commit ff07003

2 files changed

Lines changed: 622 additions & 15 deletions

File tree

internal/docker/dockerfile.go

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@ package docker
22

33
import (
44
"regexp"
5+
"strconv"
56
"strings"
6-
7-
"github.com/Masterminds/semver/v3"
87
)
98

109
// Dockerfile represents a parsed Dockerfile.
@@ -261,16 +260,32 @@ func SupportsBuildContexts(df *Dockerfile) (supported bool, unknown bool) {
261260
if numVersion == "" {
262261
return true, false // "latest", "labs", no specific tag → assume yes
263262
}
263+
// TS uses semver.intersects(numVersion, ">=1.4"): a partial tag like "1" is a
264+
// RANGE (1.x, i.e. the floating latest 1.x), NOT the exact 1.0.0, so it
265+
// intersects >=1.4 and supports build contexts. Parsing "1" as 1.0.0 (exact)
266+
// would wrongly report false.
267+
return intersectsAtLeast14(numVersion), false
268+
}
264269

265-
constraint, err := semver.NewConstraint(">= 1.4")
266-
if err != nil {
267-
return false, false
268-
}
269-
v, err := semver.NewVersion(numVersion)
270-
if err != nil {
271-
return false, false
270+
// intersectsAtLeast14 reports whether the version range implied by a partial or
271+
// full semver tag overlaps [1.4.0, ∞) — replicating node-semver's
272+
// intersects(numVersion, ">=1.4"). A 1-part "N" spans [N, N+1); a 2-part "N.M"
273+
// spans [N.M, N.M+1); a 3-part tag is exact.
274+
func intersectsAtLeast14(numVersion string) bool {
275+
parts := strings.Split(numVersion, ".")
276+
atoi := func(s string) int { n, _ := strconv.Atoi(s); return n }
277+
major := atoi(parts[0])
278+
switch len(parts) {
279+
case 1:
280+
return major >= 1
281+
case 2:
282+
return major > 1 || (major == 1 && atoi(parts[1]) >= 4)
283+
default:
284+
if major != 1 {
285+
return major > 1
286+
}
287+
return atoi(parts[1]) >= 4
272288
}
273-
return constraint.Check(v), false
274289
}
275290

276291
// --- Variable replacement (matching TS dockerfileUtils.ts) ---
@@ -368,14 +383,17 @@ func findValue(df *Dockerfile, buildArgs, baseImageEnv map[string]string, variab
368383
return replaceVariables(df, buildArgs, baseImageEnv, instr.Value, stage, i)
369384
}
370385
if instr.Instruction == "ARG" && considerArg {
371-
val := instr.Value
372386
if override, ok := buildArgs[instr.Name]; ok {
373-
val = override
387+
return replaceVariables(df, buildArgs, baseImageEnv, override, stage, i)
374388
}
375-
if val != "" {
376-
return replaceVariables(df, buildArgs, baseImageEnv, val, stage, i)
389+
if instr.Value != "" {
390+
return replaceVariables(df, buildArgs, baseImageEnv, instr.Value, stage, i)
377391
}
378-
return ""
392+
// An unbound ARG (no value, no build-arg override) is NOT a
393+
// definition: TS treats its value as undefined, which its match
394+
// predicate excludes. Keep scanning so a preceding ENV of the same
395+
// name wins instead of being shadowed by an empty "".
396+
continue
379397
}
380398
}
381399

0 commit comments

Comments
 (0)