feat(build): summer plugin:install for registry plugins as Go modules

- kiosk.Client reads the registry API v1 over fetchguard (trusted mode,
  4 MiB, 10 s, no redirects); kiosk.RegistryURL reads plugins.registry_url
  (default https://summercms.io, empty disables)
- build.PlanInstall/ApplyInstall pick the newest release the effective
  framework satisfies (or a pin), validate registry strings, go get it and
  append it to summer.yaml
- summer.yaml edits (plugin:add and plugin:install) keep comments and
  blank lines
- TestInstallSmoke/module installs from a file:// GOPROXY and builds
This commit is contained in:
Jakub Zych
2026-10-29 18:22:01 +01:00
parent e7eda88ddd
commit 9b23e877a6
10 changed files with 1323 additions and 4 deletions

View File

@@ -14,6 +14,7 @@ import (
"git.golem15.com/golem15/summercms/internal/build"
"git.golem15.com/golem15/summercms/internal/dev"
"git.golem15.com/golem15/summercms/modules/bonfire"
"git.golem15.com/golem15/summercms/modules/compass"
"git.golem15.com/golem15/summercms/modules/diary"
"git.golem15.com/golem15/summercms/modules/kiosk"
"git.golem15.com/golem15/summercms/modules/lagoon"
@@ -42,6 +43,7 @@ func toolCommands() []bonfire.Command {
makeAdminControllerCommand(),
addPluginCommand(),
pluginManifestCommand(),
pluginInstallCommand(),
newCommand(),
devCommand(),
parityProxyCommand(),
@@ -508,6 +510,106 @@ plugin.yaml is missing or differs from what the command would write.`,
}
}
// registryFor opens the application's config and returns the registry
// client plugins.registry_url names, or nil when the setting is empty or
// the run is --offline.
func registryFor(appDir string, offline bool) (build.Registry, error) {
if offline {
return nil, nil
}
cfg, err := compass.Open(compass.Options{Dir: filepath.Join(appDir, "config")})
if err != nil {
return nil, err
}
base := kiosk.RegistryURL(cfg)
if base == "" {
return nil, nil
}
client, err := kiosk.NewClient(base, cfg)
if err != nil {
return nil, err
}
return client, nil
}
func pluginInstallCommand() bonfire.Command {
return bonfire.Command{
Name: "plugin:install",
Description: "Install a plugin from the registry or a git repository",
Long: `Adds a plugin the application does not have yet. The argument is a plugin ID
(vendor.name), an ID pinned to a release (vendor.name@vX.Y.Z), or a git URL
(https://, ssh:// or user@host:path).
For an ID, the registry (plugins.registry_url in the application's config,
https://summercms.io by default, SUMMER_PLUGINS__REGISTRY_URL overrides it)
lists the plugin's releases. The newest release whose framework minimum the
application's framework version meets is chosen; a pinned release must meet it
too. When only releases needing a newer framework exist, the command fails and
names the framework version to move to; it never changes the framework. The
plugin is installed as a Go module with go get and listed in summer.yaml.
--source installs it as a git submodule under plugins/<vendor>/<name> instead.
The plugins to add are printed as id@version (mode) and confirmed with a
prompt. --yes skips the prompt; a run without a terminal needs --yes. Run
summer build afterwards. --offline makes no registry call, so it works only
with a git URL. An empty plugins.registry_url disables registry calls.`,
Examples: []bonfire.Example{
{Command: "plugin:install acme.blog", Description: "Install the newest compatible release of acme.blog as a Go module"},
{Command: "plugin:install acme.blog@v0.3.0 --yes", Description: "Install release v0.3.0 without asking"},
{Command: "plugin:install acme.blog --source", Description: "Install it as a git submodule under plugins/acme/blog"},
{Command: "plugin:install git@git.example.com:acme/sm-blog-plugin.git", Description: "Install a private plugin from its repository, as a submodule"},
},
Args: []bonfire.Arg{{
Name: "plugin",
Description: "vendor.name, vendor.name@vX.Y.Z or a git URL",
Required: true,
}},
Flags: []bonfire.Flag{
{Name: "source", Description: "Install registry plugins as git submodules under plugins/<vendor>/<name>", Bare: true},
{Name: "yes", Description: "Install without asking for confirmation", Bare: true},
{Name: "offline", Description: "Make no registry call; dependencies must be installed already", Bare: true},
},
Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
arg, ok := in.Argument("plugin")
if !ok || arg == "" {
return fmt.Errorf("plugin:install requires a plugin ID or a git URL")
}
dir, err := os.Getwd()
if err != nil {
return err
}
appDir, err := build.FindAppDir(dir)
if err != nil {
return err
}
offline := bonfire.FlagBool(in, "offline")
reg, err := registryFor(appDir, offline)
if err != nil {
return err
}
plan, err := build.PlanInstall(ctx, build.InstallOptions{
StartDir: dir,
Arg: arg,
Source: bonfire.FlagBool(in, "source"),
Yes: bonfire.FlagBool(in, "yes"),
Offline: offline,
Registry: reg,
Interactive: bonfire.Interactive(out),
Confirm: func(q string) (bool, error) { return out.Confirm(q, false) },
Out: out,
})
if errors.Is(err, build.ErrCancelled) {
out.Println("Nothing installed.")
return nil
}
if err != nil {
return err
}
return build.ApplyInstall(ctx, plan, out)
},
}
}
func newCommand() bonfire.Command {
return bonfire.Command{
Name: "new",