fix(34): CR-02 install plugins of an installed module at its version

plugin:install pinned only modules chosen in the same plan, so a second
plugin package in a module the application already required was
installed at the newest release, moving the plugin already there, and
with --source the repository was cloned a second time. PlanInstall now
seeds every module of summer.yaml: a plugin in one installs at the
installed version with no go get or clone (only its summer.yaml entry),
and is refused with a plugin:update hint when that version has no
release of it or is not a release. A git URL whose repository is already
a submodule, or whose module the application has, is refused.
This commit is contained in:
Jakub Zych
2026-10-29 22:01:30 +01:00
parent 60551485ed
commit 9b72439533
5 changed files with 264 additions and 3 deletions

View File

@@ -230,6 +230,11 @@ func (in *installer) planGitURL(rawURL string) error {
if err := requireGitRepo(in.ctx, in.appDir); err != nil {
return err
}
if dir := submoduleWithURL(in.ctx, in.appDir, rawURL); dir != "" {
return bonfire.WithHint(fmt.Errorf("build: %s is already checked out at %s", rawURL, dir),
"plugin:install does not clone a repository the application already has as a submodule. Update the plugins in it with plugin:update.",
"{bin} plugin:update")
}
fmt.Fprintf(in.out, "Cloning %s\n", rawURL)
tmp, err := cloneInto(in.ctx, in.appDir, rawURL, in.opts.Interactive)
if err != nil {
@@ -306,6 +311,11 @@ func (in *installer) planGitURL(rawURL string) error {
if err := validateStep(step); err != nil {
return err
}
if im, ok := in.installed[step.Module]; ok {
return bonfire.WithHint(fmt.Errorf("build: %s is module %s, which the application already has (for %s)", rawURL, step.Module, im.by),
"plugin:install does not move plugins the application already has, and a second checkout of one module breaks the workspace. Update the module with plugin:update.",
"{bin} plugin:update "+im.by)
}
if _, err := submoduleTarget(in.ctx, in.appDir, tmp, step.ID, rawURL); err != nil {
return err
}
@@ -320,6 +330,31 @@ func (in *installer) planGitURL(rawURL string) error {
return nil
}
// submoduleWithURL returns the path of the application's submodule whose
// .gitmodules URL is rawURL (ignoring a trailing slash or .git), or "".
func submoduleWithURL(ctx context.Context, appDir, rawURL string) string {
out, err := gitOutput(ctx, appDir, "config", "--file", ".gitmodules", "--get-regexp", `^submodule\..*\.url$`)
if err != nil {
return ""
}
norm := func(u string) string {
return strings.TrimSuffix(strings.TrimRight(u, "/"), ".git")
}
want := norm(rawURL)
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
key, val, ok := strings.Cut(line, " ")
if !ok || norm(strings.TrimSpace(val)) != want {
continue
}
name := strings.TrimSuffix(strings.TrimPrefix(key, "submodule."), ".url")
if p, err := gitOutput(ctx, appDir, "config", "--file", ".gitmodules", "submodule."+name+".path"); err == nil {
return strings.TrimSpace(string(p))
}
return name
}
return ""
}
// readCheckoutManifest reads <dir>/<rel>/plugin.yaml from a work tree; nil
// when it does not exist.
func readCheckoutManifest(dir, rel string) (*kiosk.Manifest, error) {