Files
summercms/modules/kiosk/README.md
Jakub Zych 4916ee3d43 feat(kiosk): registry v1 wire types and version rules
- RegistryAPIPath, RegistryPlugin, RegistryPluginDetail, RegistryVersion, envelopes, errors, ingest types
- ValidVersion, CompareVersions, BaseVersion, Satisfies, NewestCompatible
- docs: Registry API v1 reference in plugins/marketplace.md
2026-10-29 17:05:41 +01:00

10 KiB

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

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:

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:

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

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.