Files
summercms/docs/plugins/marketplace.md
Jakub Zych e8d3d724f5 feat(build): bake plugin metadata and the framework version into plugins.gen.go
- party.Info, party.Installed, party.Describe (additive; Plugin and Activate unchanged)
- summer build resolves plugin dirs with one go list, reads plugin.yaml, versions per D-07 (module version, git describe without --dirty, none for in-app plugins)
- build.EffectiveFramework: newest tag of a locally replaced or used framework checkout, else the go.mod require
- generated main calls party.Describe(app, PluginInfo, FrameworkVersion) before Activate; metadata failures warn and fall back to IDs
- summer new goldens and examples/hello regenerated
2026-10-29 17:12:13 +01:00

127 lines
8.0 KiB
Markdown

---
title: Publishing plugins
description: Describe a plugin for the marketplace with a generated plugin.yaml, keep it current with summer plugin:manifest --check, and read the registry API.
section: plugins
order: 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.
```yaml
# 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:
```sh
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:
```sh
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:
```sh
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):
```go src=modules/kiosk/checkt_test.go#TestPluginManifest
// 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](../architecture/go-modules-and-workspaces.md#the-summeryaml-manifest) explains where the versions come from.
## 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`, `meta` or `error`), 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/v2` path.
- The GET endpoints need no credentials, allow any origin (CORS) and answer with `Cache-Control: public, max-age=300` and an ETag.
- Versions are strict `vX.Y.Z` tags (`kiosk.ValidVersion`) compared numerically (`kiosk.CompareVersions`). A client picks the release to install with `kiosk.NewestCompatible`: the newest one whose framework minimum the application's framework version satisfies (`kiosk.Satisfies`).