- --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
14 KiB
title, description, section, order
| title | description | section | order |
|---|---|---|---|
| Publishing plugins | Describe a plugin with a generated plugin.yaml, install listed plugins with summer plugin:install, and read the registry API. | plugins | 50 |
Publishing plugins
A plugin that lives in its own repository can be listed on the SummerCMS marketplace. The listing reads one file from the repository, plugin.yaml, which the summer tool generates from the plugin's own sources. Nothing in it is typed by hand, so the ID, module path and version requirements the marketplace shows are the ones the code has.
This is the counterpart of WinterCMS's pluginDetails() in Plugin.php together with the composer.json a marketplace plugin carries.
The plugin.yaml manifest
plugin.yaml sits next to plugin.go, one per plugin package. A repository whose module holds a second plugin in a subdirectory has a second plugin.yaml in that subdirectory.
# Generated by summer plugin:manifest. Do not edit; rerun the command.
schema: 1
id: acme.blog
module: example.com/acme/sm-blog-plugin
package: example.com/acme/sm-blog-plugin
name: Blog
description: Posts, categories and a feed for SummerCMS applications.
framework: v0.6.0
requires:
acme.translate: v0.2.0
overrides:
name: Blog
| Field | Comes from |
|---|---|
schema |
The format version (kiosk.SchemaVersion). A reader that knows an older schema refuses the file and asks you to update summer. |
id |
What ID() in plugin.go returns: a string literal or a constant (of the same package, of another package of the module, or of a required module). The command reads the source and never runs it; any other expression, such as a function call or a variable, is an error naming ID(). |
module |
The module line of the nearest go.mod: what go get takes. |
package |
The plugin's import path: what summer.yaml lists. It differs from module for a plugin in a subdirectory. |
name |
The README's H1, or the --name override. |
description |
The first paragraph after the README's H1 (the one-sentence summary of the standard README structure), or the --description override. |
framework |
The version go.mod requires the framework (git.golem15.com/golem15/summercms) at: the oldest framework release the plugin works with. |
requires |
Each plugin ID Requires() returns (literals or constants, as for id), with the version go.mod requires its module at. A requirement at v0.0.0 is an error, because nobody can install that version; a pseudo-version is accepted with a warning. The value is empty for a plugin in the same module, and for one whose module is not a go.mod require. Left out when Requires() returns none. |
overrides |
The --name and --description values, kept across regenerations. Left out when neither is set. |
Versions are minimums in Go's sense (v0.6.0 means "v0.6.0 or newer"), never ranges: Go's minimal version selection picks the version, and a major version change moves the module path.
Generating and checking it
Run summer plugin:manifest in the plugin directory, or pass the directory:
summer plugin:manifest
summer plugin:manifest plugins/acme/blog
summer plugin:manifest --pretend
--pretend prints the file instead of writing it. Commit plugin.yaml, and rerun the command after changing what it reads: the README's H1 or summary, ID() or Requires(), or a go.mod require.
A README whose H1 is the plugin ID (# acme.blog) has no display name to offer, so the command stops and asks for one. Give it once; it is stored under overrides: and reused:
summer plugin:manifest --name="Blog"
summer plugin:manifest --description="Posts, categories and a feed."
summer plugin:manifest --name=""
The last line clears the name override, so the README's H1 is used again.
--check writes nothing. It regenerates the manifest in memory and exits with status 1, printing what it expected, when plugin.yaml is missing or differs from the committed file:
summer plugin:manifest --check
Run it before tagging a release, so the tag carries a manifest that matches its code.
Testing the manifest in your plugin
Add one test to the plugin, so go test fails as soon as plugin.yaml no longer matches the code. It imports git.golem15.com/golem15/summercms/modules/kiosk and passes the plugin directory, which is "." in a plugin's own package (the framework's copy below checks a fixture):
// TestPluginManifest is the test a plugin repository adds next to its
// plugin.go, with "." as the directory. Here it guards the committed
// manifest of the literal fixture.
func TestPluginManifest(t *testing.T) {
kiosk.CheckT(t, "testdata/literal")
}
kiosk.CheckT runs the same check as summer plugin:manifest --check and names the command to rerun. A nested plugin gets its own test in its own directory.
Installed plugin metadata
summer build reads the plugin.yaml of every plugin the application lists and bakes the name and description into the generated plugins.gen.go as PluginInfo, next to each plugin's version and the framework version (FrameworkVersion). At start-up the application publishes them as party.Installed, which the admin's Plugins screen reads. Go modules and workspaces explains where the versions come from.
Installing plugins
summer plugin:install adds a listed plugin to an application. Run it inside the application:
summer plugin:install acme.blog
summer plugin:install acme.blog@v0.3.0
summer plugin:install acme.blog --yes
The command asks the registry for the plugin's releases and picks the newest one whose framework minimum the application meets. The application's framework version is the effective one: the newest tag of a local framework checkout when go.mod or go.work replaces the framework with a directory, otherwise the version go.mod requires. @vX.Y.Z pins a release, which must meet the framework minimum too. When every release needs a newer framework, the command stops and names the framework version to move to; it never changes the framework requirement itself.
The plugin is installed as a Go module: one go get <module>@<version> in the application, then an entry with its ID and package in summer.yaml. The edit appends to the plugins: list and keeps the file's comments and blank lines. Run summer build afterwards: it generates the import, and the new require in go.mod loses its // indirect marker the next time you run go mod tidy. A plugin that is already in summer.yaml is not installed again.
Required plugins
A plugin's requires (from its plugin.yaml, as the registry lists it for the chosen release) are installed with it. For each required plugin the application does not have yet, the command picks the newest release that the framework version meets and that is at least the required minimum, then resolves that plugin's own requirements the same way. Required plugins come first in the list and in summer.yaml, and all module installs go through a single go get, so Go's minimal version selection sees the whole set. Two plugins that ship in the same module are installed from the same release of it.
A required plugin the application already has is not touched, but its version must be at least the minimum: the go.mod require for a module install, the tag git describe finds for a plugin checked out as a submodule. When it is older, the command stops and asks you to update that plugin first. A version it cannot read, such as that of a plugin kept in the application's own repository, is accepted with a warning. Requirements that form a cycle in the registry data are reported as an error.
Confirmation
Before changing anything, the command prints each plugin it is about to add, one line per plugin, with module or submodule as the mode:
Installing 2 plugins into /srv/acme (framework v0.6.2):
acme.translate@v0.2.1 (module)
acme.blog@v0.3.0 (module)
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:
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). 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:
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:
# config/plugins.yaml
registry_url: https://registry.example.com
The environment variable SUMMER_PLUGINS__REGISTRY_URL overrides the file. The value must be the final origin, because the client does not follow redirects. An empty value disables registry calls: installs by ID then fail, and the admin's Plugins screen shows only installed plugins. --offline disables registry calls for one run.
Every registry call goes through kiosk.Client, which uses fetchguard with a 10 second timeout and a 4 MiB limit on the answer. Module paths, package paths and versions from the registry are checked (a plain module path, a strict vX.Y.Z version) before any go command runs.
Registry API v1
summercms.io publishes the plugins it lists through a read-only JSON API. The marketplace pages, summer and the admin's Plugins screen read it. Its base path is /api/registry/v1 (kiosk.RegistryAPIPath), and the kiosk module holds its wire types, so every client and the server share one definition of the shape.
| Request | Answer |
|---|---|
GET /api/registry/v1/plugins?q=&category= |
{"data": [plugin, ...], "meta": {"total": N}} (kiosk.ListResponse). q searches ID, name and description; category filters. |
GET /api/registry/v1/plugins/{id} |
{"data": detail} (kiosk.DetailResponse). |
GET /api/registry/v1/plugins/{id}/versions |
{"data": [version, ...]}, newest first (kiosk.VersionsResponse). |
POST /api/registry/v1/ingest |
Authenticated with an API token that has the registry scope; body {"repository": "<https URL>"} (kiosk.IngestRequest). Answers 202 with {"data": {"repository": "...", "queued": true}} (kiosk.IngestResponse). |
A plugin in a list (kiosk.RegistryPlugin) has these fields:
| Field | Meaning |
|---|---|
id, name, description |
From the latest release's plugin.yaml. |
module, package |
What go get and summer.yaml take. |
repository |
The HTTPS clone URL. |
category, featured, icon_url |
Presentation fields edited on summercms.io. |
latest_version, latest_framework |
The newest tag and its framework minimum. |
updated_at |
When the listing last changed, RFC 3339. |
The detail (kiosk.RegistryPluginDetail) adds readme_markdown, readme_html, screenshots (a list of url and caption) and requires of the latest release. A version (kiosk.RegistryVersion) has version, framework, requires, module, package, commit and tagged_at.
Errors answer with {"error": {"code": "...", "message": "..."}} (kiosk.ErrorResponse): 404 with not_found for an unknown ID, 400 with invalid_query for a bad parameter.
Compatibility rules:
- Every answer is an envelope object (
data,metaorerror), never a bare array. - Clients ignore fields they do not know, and v1 changes only by adding fields. A change that removes or renames a field needs a
/api/registry/v2path. - The GET endpoints need no credentials, allow any origin (CORS) and answer with
Cache-Control: public, max-age=300and an ETag. - Versions are strict
vX.Y.Ztags (kiosk.ValidVersion) compared numerically (kiosk.CompareVersions). A client picks the release to install withkiosk.NewestCompatible: the newest one whose framework minimum the application's framework version satisfies (kiosk.Satisfies).