feat(build): plugin:install --source and git URLs as submodules

- --source clones each registry repository (https only) at the chosen tag,
  adds it as a submodule under plugins/<vendor>/<name>, absorbs its git
  dir and registers it with the tag as the go.mod require
- git URLs (https, ssh, user@host:path) are cloned with the user's git
  setup and never sent to the registry; the newest tag whose plugin.yaml
  framework minimum fits is checked out, else the default branch head
- go.work replace overrides for every local replace in workspace modules'
  go.mod files (app target, versioned workspace module, or version pin)
- GIT_ALLOW_PROTOCOL=https:ssh, '--' before URLs and paths; non-git apps
  are refused before cloning; plugin:add keeps require v0.0.0
This commit is contained in:
Jakub Zych
2026-10-29 18:39:22 +01:00
parent a462bf12c9
commit 8093587696
8 changed files with 1191 additions and 29 deletions

View File

@@ -93,7 +93,7 @@ build:
The generated `main` publishes both with `party.Describe` before activating the plugins, so the admin can show the installed plugins with `app.Lookup[party.Installed]()`. When the metadata cannot be resolved, `summer build` prints a warning and bakes the IDs only; it never fails a build. A submodule bump or a new framework tag therefore changes `plugins.gen.go`: commit the regenerated file with the bump.
`build.tags` lists Go build tags that `summer build` (and `summer dev`) passes to `go build` as `-tags=<tag>,<tag>`, printing them as `build tags: …`. Each tag must consist of lower-case letters, digits and underscores; a duplicate is ignored. Framework features that pull in large dependencies sit behind a tag, such as `summercms_s3` for S3 uploads buckets (see [Storage](../services/storage.md#s3-and-s3-compatible-buckets)). `summer plugin:add` keeps the list when it rewrites the manifest. A script that runs its own `go build` must pass the same tags.
`build.tags` lists Go build tags that `summer build` (and `summer dev`) passes to `go build` as `-tags=<tag>,<tag>`, printing them as `build tags: …`. Each tag must consist of lower-case letters, digits and underscores; a duplicate is ignored. Framework features that pull in large dependencies sit behind a tag, such as `summercms_s3` for S3 uploads buckets (see [Storage](../services/storage.md#s3-and-s3-compatible-buckets)). `summer plugin:add` and `summer plugin:install` append their entries to the `plugins:` list in place, so the rest of the file, comments and blank lines included, stays as it was. A script that runs its own `go build` must pass the same tags.
## Local plugins in a workspace
@@ -111,6 +111,18 @@ summer plugin:add plugins/acme/blog
summer build
```
## Plugins installed as submodules
`summer plugin:install <id> --source` and `summer plugin:install <git-url>` check a plugin's repository out at a release tag as a git submodule in `plugins/<vendor>/<name>` and register it like `summer plugin:add`, with one difference: the `require` in `go.mod` names the tag (`v0.3.1`), not `v0.0.0`, so a build with `GOWORK=off` and the version checks of later installs see the real release. See [Installing plugins](../plugins/marketplace.md#installing-plugins).
A shared plugin's own `go.mod` usually replaces the framework, and sometimes sibling plugins, with paths of its development layout, such as `replace git.golem15.com/golem15/summercms => ../summercms.go`. Inside the application's workspace those paths do not exist, and two workspace modules that replace one module with different paths make Go stop with "conflicting replacements". A `replace` in `go.work` wins over the replaces in the workspace modules' `go.mod` files, so the installer adds one for every directory replace it finds in a workspace module's `go.mod` that `go.work` does not replace yet:
- when the application's `go.mod` replaces the same module (a local framework checkout, for example), `go.work` gets the same target;
- when the module is itself in the workspace (another plugin installed as a submodule), `go.work` replaces that module at its required version with its directory, such as `replace git.example.com/acme/translate v0.1.0 => ./plugins/acme/translate` (Go refuses a replace of a workspace module at all versions);
- otherwise `go.work` pins the module to the version the application requires, such as `replace git.golem15.com/golem15/summercms => git.golem15.com/golem15/summercms v0.6.0`.
A pin of the last kind follows the application: when the framework requirement in `go.mod` moves, the next submodule install moves the pin to the new version. Replaces you wrote into `go.work` yourself are left alone.
## Replacing and forking a plugin
WinterCMS lets you replace a plugin by overriding its classes. In SummerCMS you fork the plugin's module and point the application at your fork with a `replace` directive. The plugin ID and the import path stay the same, so nothing else in the application changes:

View File

@@ -114,7 +114,7 @@ A required plugin the application already has is not touched, but its version mu
### Confirmation
Before changing anything, the command prints each plugin it is about to add, one line per plugin:
Before changing anything, the command prints each plugin it is about to add, one line per plugin, with `module` or `submodule` as the mode:
```text
Installing 2 plugins into /srv/acme (framework v0.6.2):
@@ -124,6 +124,34 @@ Installing 2 plugins into /srv/acme (framework v0.6.2):
Then it asks for confirmation, with "no" as the default. `--yes` skips the question. A run that cannot ask (no terminal, `-n` or `-q`) stops with an error naming `--yes` unless it is given. Declining changes nothing.
### Installing from source
`--source` installs the plugin, and every required plugin it brings, as git submodules instead of Go modules:
```sh
summer plugin:install acme.blog --source
```
Each repository (the `repository` the registry lists, which must be an `https` URL) is cloned, checked out at the chosen release tag and added as a submodule in `plugins/<vendor>/<name>`, named after the ID in the repository root's `plugin.yaml`. A plugin in a subdirectory of its repository lives inside that directory. The `plugin.yaml` in the checkout must describe the plugin the registry listed (same ID and module), or the install is refused. The plugin is then registered like `summer plugin:add` does it, with the tag as the version `go.mod` requires, and `go.work` gets the replace overrides the plugin's own `go.mod` needs to build inside the application (see [Plugins installed as submodules](../architecture/go-modules-and-workspaces.md#plugins-installed-as-submodules)). Commit `.gitmodules`, the submodule, `go.mod`, `go.work` and `summer.yaml` together.
A submodule needs a git repository: in an application that is not one, `--source` stops before cloning anything.
### Installing from a git URL
A plugin that is not listed, such as a private one, installs from its repository:
```sh
summer plugin:install https://git.example.com/acme/sm-blog-plugin.git
summer plugin:install git@git.example.com:acme/sm-blog-plugin.git
summer plugin:install ssh://git@git.example.com/acme/sm-blog-plugin.git
```
The repository is cloned with your own git setup (credential helpers, SSH keys, `insteadOf` rules) and never sent to the registry. The command reads `plugin.yaml` at the repository's release tags, newest first, and checks out the newest tag whose framework minimum the application meets. When no tag fits, it takes the default branch head and prints a warning. A git URL install is always a submodule install, with the same layout and `go.work` overrides as `--source`.
The plugin's required plugins are looked up in the registry by ID and installed as submodules too. With `--offline` no registry call is made, so every required plugin must already be in `summer.yaml`; a missing one stops the install and is named.
Only `https://`, `ssh://` and scp-like `user@host:path` addresses are accepted. Git runs with `GIT_ALLOW_PROTOCOL=https:ssh`, so `ext::` and `file::` transports, `file://` and plain `http://` are refused, as is an address that starts with `-` or carries a password (use a credential helper instead).
### The registry setting
The registry is `https://summercms.io` unless the application's `plugins.registry_url` setting names another one: