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
This commit is contained in:
Jakub Zych
2026-10-29 17:12:13 +01:00
parent 4916ee3d43
commit e8d3d724f5
15 changed files with 770 additions and 15 deletions

View File

@@ -26,6 +26,7 @@ Use cases:
- A process-wide, concurrency-safe registry filled by `party.Register` (nil plugins are ignored).
- `party.Activate` selects plugins by manifest ID and fails on an empty ID, a duplicate ID, an unregistered plugin, a missing requirement or a dependency cycle.
- Stable topological ordering: plugins without a dependency relation keep their manifest order.
- Build-time metadata: `party.Describe` publishes `party.Installed`, the plugin metadata (`party.Info`: ID, module, package, name, description, version and source) and the framework version that `summer build` baked into the application's `plugins.gen.go`. The generated `main` calls it before `party.Activate`.
- Activation sequence: records the ordered IDs on the `backpack.App`, publishes `party.Activated` (the ordered plugin list for commands that need plugin scopes at Run time), merges each `pact.HasConfig` tree into the app config under the plugin ID, runs every Register, publishes the translator ([phrasebook](../phrasebook/README.md)), the mailer ([postcard](../postcard/README.md)) and the menu item type registry ([signpost](../signpost/README.md), collected from every `pact.HasMenuItemTypes` plugin; a duplicate type key fails the activation), then registers each plugin's mail templates and runs its Boot.
## Usage
@@ -52,7 +53,7 @@ func init() {
}
```
The application activates the plugins it lists, in dependency order:
The application publishes its build-time metadata and activates the plugins it lists, in dependency order (`summer build` generates both lists):
```go
cfg, err := compass.Load("config")
@@ -60,6 +61,13 @@ if err != nil {
return err
}
app := backpack.New(cfg)
info := []party.Info{
{ID: "acme.user", Source: party.SourceLocal},
{ID: "acme.blog", Version: "v0.2.5", Source: party.SourceModule},
}
if err := party.Describe(app, info, "v0.6.0"); err != nil {
return err
}
plugins, err := party.Activate(app, []string{"acme.user", "acme.blog"})
if err != nil {
return err
@@ -73,6 +81,12 @@ if err != nil {
| `party.Plugin` | Interface every compiled plugin implements: ID, required plugin IDs, Register and Boot. |
| `party.Register` | Adds a plugin to the process-wide registry; called from the plugin's `init`. |
| `party.Activate` | Selects registered plugins by ID, orders them by `party.Plugin.Requires` and runs the config, Register, translation, mail, menu item type and Boot steps; returns the ordered plugins. |
| `party.Info` | One plugin's build-time metadata: `ID`, `Module`, `Package`, `Name`, `Description` (from its `plugin.yaml`), `Version` and `Source`. |
| `party.SourceModule` | Source of a plugin built from the Go module cache; its version is the module version. |
| `party.SourceSubmodule` | Source of a plugin directory that is its own git repository; its version is `git describe --tags` output. |
| `party.SourceLocal` | Source of a plugin inside the application repository; it has no version. |
| `party.Installed` | What the application was built with: `Framework` and `Plugins` in manifest order; look it up with `app.Lookup[party.Installed]()`. |
| `party.Describe` | Publishes `party.Installed` on the app; a nil app is an error. |
| `party.Activated` | The ordered `[]party.Plugin` published by `party.Activate`; commands look it up at Run time with `app.Lookup[party.Activated]()`. |
## Dependencies

56
modules/party/info.go Normal file
View File

@@ -0,0 +1,56 @@
package party
import (
"fmt"
"git.golem15.com/golem15/summercms/modules/backpack"
)
// Where an installed plugin's source came from at build time.
const (
// SourceModule is a plugin built from the Go module cache (go get).
SourceModule = "module"
// SourceSubmodule is a plugin directory that is its own git repository,
// such as a git submodule; its version is git describe output.
SourceSubmodule = "submodule"
// SourceLocal is a plugin directory inside the application repository;
// it has no version of its own.
SourceLocal = "local"
)
// Info is the metadata summer build bakes into plugins.gen.go for one
// plugin: its ID, Go module and package, the name and description from its
// plugin.yaml, its version and where it came from (SourceModule,
// SourceSubmodule or SourceLocal). A plugin without plugin.yaml has no
// name or description.
type Info struct {
ID string
Module string
Package string
Name string
Description string
Version string
Source string
}
// Installed is what the application was built with: the effective
// framework version and the plugin metadata in summer.yaml order. Describe
// publishes it; look it up with app.Lookup[party.Installed]().
type Installed struct {
Framework string
Plugins []Info
}
// Describe publishes the build-time plugin metadata and framework version
// as Installed. The generated main calls it before Activate.
func Describe(app *backpack.App, plugins []Info, framework string) error {
if app == nil {
return fmt.Errorf("party: app is nil")
}
list := make([]Info, len(plugins))
copy(list, plugins)
if err := app.Publish(Installed{Framework: framework, Plugins: list}); err != nil {
return fmt.Errorf("party: %w", err)
}
return nil
}