Skip to content

fix(frontseat-plugin-gib): GH-199 image plugins claim the default export - #240

Open
jbadeau wants to merge 1 commit into
mainfrom
fix/199-image-default-export
Open

fix(frontseat-plugin-gib): GH-199 image plugins claim the default export#240
jbadeau wants to merge 1 commit into
mainfrom
fix/199-image-default-export

Conversation

@jbadeau

@jbadeau jbadeau commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Fixes #199. Fixes #131.

Both image plugins registered only a named "image" export, so combining an image build with any other plugin's named export on one entity tripped export_registry.go's "ambiguous primary export" error — the #131 reproducer.

  • apko always claims the default (unnamed) export alongside "image" — it has no artifact-consuming mode.
  • gib claims the default only when useArtifact is false: when consuming $(location :) the default export must keep resolving to the producing plugin's artifact, not gib's own image.

The gib test now encodes the contract both ways.

🤖 Generated with Claude Code

Both image plugins registered only a named "image" export, so an entity
combining an image build with any other plugin's named export (e.g. a uv
"source" export) hit "ambiguous primary export" — the #131 reproducer.
apko now always claims the default export alongside "image"; gib claims
it only when it is the artifact producer, because with useArtifact set
its $(location :) must keep resolving to the producing plugin's output.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
// named exports from other plugins stack instead of tripping the
// ambiguous-primary check (#131); "image" stays addressable by name.
packageExports := []*plugin.EntityExport{
{Patterns: []string{imageTarPath}},

@joaodinissf joaodinissf Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claiming the default unconditionally hard-fails any entity whose language plugin already owns it. export_registry.go:62 is a plain duplicate check:

if _, exists := r.exports[entityRef][name]; exists {
    return fmt.Errorf("duplicate export %q on entity %q", name, entityRef)
}

Three plugins already register Name: "":

  • maven/tasks.go:345 — unconditional for any non-pom packaging
  • go/task_build.go:106 — first main, name := ""
  • cargo/task_package.go:26 — library-only crate

The registry comment at line 33 states the contract: named exports stack, one plugin declares the default. No fixture pairs apko.yaml with a language manifest today, so this is latent — but apko/tasks.go declines to gate on an apko-typed facet specifically to support that layout, and it planned cleanly before.

A config-level signal is available if you want gib-style conditioning: apko reads local artifacts through contents.repositories file-path repos (@local /path, package referenced as name@local) and through contents.baseimage.image for a local OCI layout — both visible in apko.yaml.

Options: claim the default only when no peer has one, or keep image exports named-only and fix the plugins that emit only named secondaries — which is what #131 is about.

On gib: the gate narrows the window rather than closing it. A jib.yaml with no ${ARTIFACT} still gets an unnamed default and collides identically. It is also convention-only — ARTIFACT is a frontseat name, not a gib one; gib substitutes any ${name} and hard-fails with missing required template parameter when a used name is not supplied, so a jib.yaml that calls its artifact parameter something else both defeats the gate and breaks the build.

Minor: apko puts the default first in the slice, gib appends it last. Worth matching, since registration order decides which plugin trips first.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right that the mechanism needs rethinking, not patching — claiming the default just relocates the collision (apko unconditionally; gib's !useArtifact only narrows the window, and ARTIFACT is a frontseat-side convention a jib.yaml need not honor).

Rather than iterate on the default-export claim, we're going to remove the export registry and move to explicit, facet-qualified outputs (<entity>:<facet>:<export>, Bazel-label style), dropping the unnamed default entirely — which resolves this collision class structurally instead of by picking a winner. Wrote that up as #271 and linked the cluster (#131, #199, this PR, plus the output-modelling side #119/#145/#218).

So I'm going to park the mechanism rework on this PR rather than push a fix that #271 will delete. Options: hold #240 as superseded by #271, or land only the test that encodes the collision contract (currently it asserts slice length/names only) as a regression guard for the redesign. What's your preference?


Generated by Claude Code

@joaodinissf joaodinissf left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes — the apko task claims the default with no gate of any kind.

maven/tasks.go:345, go/task_build.go:106 and cargo/task_package.go:26 already register the unnamed default, and export_registry.go:62 treats a second one as duplicate export "". So an entity whose root holds both a language manifest and an apko.yaml fails at plan time. No fixture uses that layout today, so this is latent rather than currently breaking — but apko/tasks.go deliberately declines to gate on an apko-typed facet precisely so that layout is supported, and it planned cleanly before this change.

gib's !useArtifact gate narrows the window rather than closing it: a jib.yaml with no ${ARTIFACT} still gets an unnamed default and collides the same way.

Tests assert only slice length and names, so neither the collision nor the apko half is covered.

Fixes #131 is right in intent; the mechanism needs rethinking.

This review was published with assistance from Claude.

@joaodinissf

Copy link
Copy Markdown
Collaborator

Superseded is the right call — #271 subsumes this structurally rather than picking a winner, which is what the collision needed.

On the test: I'd skip it here. It would assert ExportRegistry.Register's behaviour, and #271 deletes that function, so it dies with its subject. Instead I've opened #274 against main — same scenario, landing independently of how this PR resolves:

  • an entity root holding a language manifest and an image manifest must plan
  • a plugin claiming the unnamed default when a language plugin already owns it must be rejected
  • and the ambiguity is cross-plugin: one plugin may stack its own named exports

It's green on main and red against this PR's change, with exactly duplicate export "". When #271 replaces the registry it will go red and have to be consciously ported — which is the point. A bullet in an issue can be skimmed; a red test cannot.

Two things worth knowing before you close this, both raised on #271: the migration inventory undercounts, and dropping the default is a breaking change for $(location …) that isn't currently scoped.

One piece of bookkeeping: Fixes #199. Fixes #131. is on this PR, and closing it un-merged closes neither — worth moving the linkage to #271.

No interim mitigation needed as far as I can tell: no directory in the tree pairs an apko.yaml or a jib.yaml with a language manifest, so nothing is hitting the collision today.

This comment was published with assistance from Claude.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants