- RegistryAPIPath, RegistryPlugin, RegistryPluginDetail, RegistryVersion, envelopes, errors, ingest types - ValidVersion, CompareVersions, BaseVersion, Satisfies, NewestCompatible - docs: Registry API v1 reference in plugins/marketplace.md
138 lines
10 KiB
Markdown
138 lines
10 KiB
Markdown
# kiosk
|
|
|
|
Plugin manifests and the plugin registry contract: generates and checks each plugin's plugin.yaml from its own sources.
|
|
|
|
`import "git.golem15.com/golem15/summercms/modules/kiosk"`
|
|
|
|
**Name:** A beach kiosk where every plugin on the shelf carries its label; the label is printed from what is in the box, never written by hand.
|
|
|
|
## Overview
|
|
|
|
A plugin describes itself in `plugin.yaml`, committed next to its `plugin.go`. The file is generated: `summer plugin:manifest` runs `kiosk.Write`, which reads the plugin's sources and never runs its code. The ID and the required plugins come from what `ID()` and `Requires()` in `plugin.go` return, the module path and the version minimums from `go.mod`, the package import path from the directory, and the name and description from the README's H1 and the paragraph after it (the standard README structure). The registry on summercms.io reads the same file from every tagged release, and `summer build` bakes its name and description into the application.
|
|
|
|
WinterCMS keeps this data in a plugin's `Plugin.php` `pluginDetails()` and in `composer.json`; here it is derived once, so the copy in the repository cannot drift from the code.
|
|
|
|
## Features
|
|
|
|
- One `plugin.yaml` per plugin package directory, so a repository with a nested plugin carries two files.
|
|
- `kiosk.ReadIdentity` reads `ID()` and `Requires()` statically with `go/parser`: a string literal, a constant of the same package, of another package of the module, or of a package in a module `go.mod` requires. It never builds or runs plugin code, and `summer plugin:add` reads plugin IDs with it too.
|
|
- A schema version (`kiosk.SchemaVersion`) and a fixed header line (`kiosk.Header`); a manifest with a newer schema is refused with a hint to update summer.
|
|
- Go-style minimums: `framework` is the `go.mod` require of `kiosk.FrameworkModule`, and each required plugin gets the version its module is required at. A requirement at `v0.0.0` is an error (not a released version); a pseudo-version is accepted with a warning.
|
|
- Name and description overrides (`--name`, `--description`) stored in an `overrides:` block that regeneration keeps.
|
|
- The registry API v1 contract in Go: `kiosk.RegistryAPIPath` and the wire types (`kiosk.ListResponse`, `kiosk.DetailResponse`, `kiosk.VersionsResponse`, `kiosk.ErrorResponse`, `kiosk.IngestRequest`, `kiosk.IngestResponse`). The registry server encodes with them and the summer tool and the admin decode with them, so the JSON shape exists once.
|
|
- Strict version rules: `kiosk.ValidVersion` (only `vX.Y.Z`), `kiosk.CompareVersions`, `kiosk.BaseVersion` for `git describe` output, `kiosk.Satisfies` for Go-style minimums and `kiosk.NewestCompatible` to pick the release an application can install.
|
|
- `kiosk.Check` regenerates in memory and returns a `kiosk.StaleError` when the committed file is missing or differs, which `summer plugin:manifest --check` turns into exit status 1, and `kiosk.CheckT` turns into a failing test in the plugin's own `go test`.
|
|
|
|
## Usage
|
|
|
|
```go
|
|
path, warnings, err := kiosk.Write(ctx, "plugins/acme/blog", kiosk.DeriveOptions{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, w := range warnings {
|
|
log.Println(w)
|
|
}
|
|
fmt.Println("wrote", path)
|
|
|
|
if err := kiosk.Check(ctx, "plugins/acme/blog"); err != nil {
|
|
var stale *kiosk.StaleError
|
|
if errors.As(err, &stale) {
|
|
fmt.Printf("expected:\n%s", stale.Want)
|
|
}
|
|
return err
|
|
}
|
|
```
|
|
|
|
Picking the newest release of a plugin that an application on framework v0.6.2 can install:
|
|
|
|
```go
|
|
var versions kiosk.VersionsResponse
|
|
if err := json.Unmarshal(body, &versions); err != nil {
|
|
return err
|
|
}
|
|
v, ok := kiosk.NewestCompatible(versions.Data, "v0.6.2")
|
|
if !ok {
|
|
return errors.New("every release needs a newer framework")
|
|
}
|
|
fmt.Println("go get", v.Module+"@"+v.Version)
|
|
```
|
|
|
|
A plugin repository keeps its manifest current with one test:
|
|
|
|
```go
|
|
func TestPluginManifest(t *testing.T) {
|
|
kiosk.CheckT(t, ".")
|
|
}
|
|
```
|
|
|
|
## API reference
|
|
|
|
| Identifier | Description |
|
|
|------------|-------------|
|
|
| `kiosk.FileName` | The manifest file name, `plugin.yaml`. |
|
|
| `kiosk.SchemaVersion` | The newest schema this package reads, and the one it writes. |
|
|
| `kiosk.FrameworkModule` | The framework module path; its `go.mod` require is the plugin's framework minimum. |
|
|
| `kiosk.Header` | The comment line every generated `plugin.yaml` starts with. |
|
|
| `kiosk.Manifest` | One `plugin.yaml`: `Schema`, `ID`, `Module`, `Package`, `Name`, `Description`, `Framework`, `Requires`, `Overrides`, written in that order. |
|
|
| `kiosk.Requires` | Required plugin ID to minimum version; marshals sorted by ID. An empty minimum means the plugin ships in the same module or its module is not a `go.mod` require. |
|
|
| `kiosk.Requires.MarshalYAML` | Emits the map as a mapping sorted by ID. |
|
|
| `kiosk.Overrides` | The persisted `--name` and `--description` values. |
|
|
| `kiosk.Marshal` | The `plugin.yaml` bytes: the header line, then the fields. |
|
|
| `kiosk.Parse` | Reads `plugin.yaml` bytes; unknown fields are ignored, a newer schema is an error. |
|
|
| `kiosk.DeriveOptions` | Override changes for `kiosk.Derive`: nil keeps the persisted value, a pointer to `""` clears it. |
|
|
| `kiosk.Derive` | Builds the manifest of a plugin directory from its sources and returns it with warnings. |
|
|
| `kiosk.Generate` | `kiosk.Derive` plus `kiosk.Marshal`: the bytes `kiosk.Write` would write. |
|
|
| `kiosk.Write` | Generates and writes `<dir>/plugin.yaml` and returns its path. |
|
|
| `kiosk.Check` | Returns a `kiosk.StaleError` when `<dir>/plugin.yaml` is missing or differs from a fresh generation. |
|
|
| `kiosk.StaleError.Error` | Names the path, and for a stale file the first differing line. |
|
|
| `kiosk.TB` | The part of `testing.TB` that `kiosk.CheckT` calls (`Helper`, `Fatalf`); kiosk does not import `testing`. |
|
|
| `kiosk.CheckT` | Fails the test with the `kiosk.Check` error and the command to rerun when the manifest is missing or stale. |
|
|
| `kiosk.Validate` | Checks a manifest: schema, ID and required IDs, module and package paths (no leading `-`), name, description, and `vX.Y.Z` or pseudo-version minimums. `kiosk.Parse` and `kiosk.Generate` call it. |
|
|
| `kiosk.Requires.MarshalJSON` | Encodes the map as a JSON object, `{}` when empty, so the registry never sends `null` for requirements. |
|
|
| `kiosk.RegistryAPIPath` | `/api/registry/v1`, the base path of the registry API. |
|
|
| `kiosk.RegistryPlugin` | One listed plugin: `id`, `name`, `description`, `module`, `package`, `repository`, `category`, `featured`, `icon_url`, `latest_version`, `latest_framework`, `updated_at` (RFC 3339). |
|
|
| `kiosk.RegistryPluginDetail` | `kiosk.RegistryPlugin` plus `readme_markdown`, `readme_html`, `screenshots` and the `requires` of the latest version. |
|
|
| `kiosk.RegistryScreenshot` | One screenshot: `url`, `caption`. |
|
|
| `kiosk.RegistryVersion` | One tagged release: `version`, `framework`, `requires`, `module`, `package`, `commit`, `tagged_at`. |
|
|
| `kiosk.ListResponse` | `{"data": [...], "meta": {"total": N}}` for the plugin list. |
|
|
| `kiosk.ListMeta` | The `meta` object of a list: `total`. |
|
|
| `kiosk.DetailResponse` | `{"data": {...}}` for one plugin. |
|
|
| `kiosk.VersionsResponse` | `{"data": [...]}` for a plugin's releases, newest first. |
|
|
| `kiosk.ErrorResponse` | `{"error": {"code", "message"}}`, the body of every error. |
|
|
| `kiosk.RegistryError` | A stable `code` and a `message`. |
|
|
| `kiosk.RegistryErrorNotFound` | The `not_found` code of a 404. |
|
|
| `kiosk.RegistryErrorInvalidQuery` | The `invalid_query` code of a 400. |
|
|
| `kiosk.IngestRequest` | The body of the authenticated ingest request: `repository`. |
|
|
| `kiosk.IngestResponse` | The 202 answer to an ingest request: `{"data": {"repository", "queued"}}`. |
|
|
| `kiosk.IngestResult` | The `data` of an ingest answer. |
|
|
| `kiosk.ValidVersion` | Reports whether a version is a strict `vX.Y.Z` release (no pre-release, build metadata or leading zeros). |
|
|
| `kiosk.CompareVersions` | Compares two valid versions numerically per component: `v0.10.0` sorts after `v0.9.9`. |
|
|
| `kiosk.BaseVersion` | The `vX.Y.Z` tag a `git describe --tags` string starts from (`v0.3.1` for `v0.3.1-4-gabc1234`), or `""`. |
|
|
| `kiosk.Satisfies` | Whether a release meets a minimum; an empty minimum is always met, and a pseudo-version minimum is met by its base release and later. |
|
|
| `kiosk.NewestCompatible` | The newest release whose framework minimum a framework version satisfies. |
|
|
| `kiosk.Identity` | The plugin ID and the required plugin IDs. |
|
|
| `kiosk.ReadIdentity` | Reads what `ID()` and `Requires()` return from the sources of a directory, resolving constants across packages and modules without running code. Any other expression is an error naming the method. |
|
|
| `kiosk.StaleError` | `Path`, `Missing`, and the generated (`Want`) and committed (`Got`) bytes; its message names the first differing line. |
|
|
|
|
## CLI commands
|
|
|
|
| Command | Purpose |
|
|
|---------|---------|
|
|
| `summer plugin:manifest [dir]` | Writes `plugin.yaml` for the plugin in `dir` (default: the current directory). `--pretend` prints it instead, `--check` fails when the committed file is missing or stale, `--name` and `--description` set or (with an empty value) clear the overrides. |
|
|
|
|
## Dependencies
|
|
|
|
- SummerCMS modules: none.
|
|
- Third-party: `github.com/goccy/go-yaml` (manifest YAML), `github.com/yuin/goldmark` (README headings and paragraphs).
|
|
- Standard library: `bytes`, `context`, `encoding/json`, `errors`, `fmt`, `go/ast`, `go/parser`, `go/token`, `go/types` (expression printing only), `os`, `os/exec`, `path/filepath`, `regexp`, `slices`, `sort`, `strconv`, `strings`, `time`.
|
|
- Tools: the `go` command (`go mod edit -json`, `go list -m -json`, and `go mod download -json` for a required module not in the module cache), run without a shell and with workspaces off.
|
|
|
|
## Testing
|
|
|
|
```sh
|
|
go test ./modules/kiosk/...
|
|
```
|
|
|
|
`TestManifestSmoke` copies the fixture plugins in `testdata/` to a temporary directory and generates, checks and edits their manifests: a literal-ID plugin, a plugin whose identity is spread over constants of three packages and two modules, a nested plugin, the error cases, overrides, `kiosk.CheckT` and `kiosk.Validate`, the registry JSON (golden bytes, unknown fields ignored) and the version rules. Every fixture module replaces the framework with the stub in `testdata/framework`, so the tests run the local `go` command but need no network and no database.
|