feat(build): summer plugin:update
- module installs move to the newest compatible registry release with one go get; submodules fetch tags, check out the release tag and require it in go.mod; go.work overrides and version pins are refreshed - unlisted submodules (git URL installs) update from their own tags and plugin.yaml; --offline restricts to that - without an ID every plugin that resolves is updated; in-app, unlisted and incompatible ones are reported; failure only when a resolved update cannot be carried out - same prompt rule as plugin:install
This commit is contained in:
@@ -44,6 +44,7 @@ func toolCommands() []bonfire.Command {
|
|||||||
addPluginCommand(),
|
addPluginCommand(),
|
||||||
pluginManifestCommand(),
|
pluginManifestCommand(),
|
||||||
pluginInstallCommand(),
|
pluginInstallCommand(),
|
||||||
|
pluginUpdateCommand(),
|
||||||
newCommand(),
|
newCommand(),
|
||||||
devCommand(),
|
devCommand(),
|
||||||
parityProxyCommand(),
|
parityProxyCommand(),
|
||||||
@@ -622,6 +623,76 @@ with a git URL. An empty plugins.registry_url disables registry calls.`,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func pluginUpdateCommand() bonfire.Command {
|
||||||
|
return bonfire.Command{
|
||||||
|
Name: "plugin:update",
|
||||||
|
Description: "Move installed plugins to their newest compatible release",
|
||||||
|
Long: `Updates one installed plugin, or every plugin in summer.yaml when no ID is
|
||||||
|
given, to the newest release whose framework minimum the application's
|
||||||
|
framework version meets. A plugin installed as a Go module moves with go get;
|
||||||
|
one installed as a git submodule fetches its tags, is checked out at the
|
||||||
|
release tag, and go.mod requires that tag. The go.work replaces the
|
||||||
|
application needs are refreshed, including version pins that follow the
|
||||||
|
framework requirement.
|
||||||
|
|
||||||
|
Releases come from the registry (plugins.registry_url). A submodule the
|
||||||
|
registry does not list, such as a plugin installed from a git URL, is updated
|
||||||
|
from its own tags and their plugin.yaml. A newer release that needs a newer
|
||||||
|
framework is reported, not installed. Plugins that cannot be updated (in-app
|
||||||
|
plugins, module installs the registry does not list, releases needing a
|
||||||
|
plugin the application lacks) are listed as not updated; the command fails
|
||||||
|
only when an update it resolved could not be carried out.
|
||||||
|
|
||||||
|
The updates are printed and confirmed with a prompt; --yes skips it, and a run
|
||||||
|
without a terminal needs --yes. --offline makes no registry call and updates
|
||||||
|
submodules from their own tags only. Run summer build afterwards.`,
|
||||||
|
Examples: []bonfire.Example{
|
||||||
|
{Command: "plugin:update acme.blog", Description: "Move acme.blog to its newest compatible release"},
|
||||||
|
{Command: "plugin:update --yes", Description: "Update every plugin that has a newer compatible release, without asking"},
|
||||||
|
{Command: "plugin:update --offline", Description: "Update submodule plugins from their own tags, without the registry"},
|
||||||
|
},
|
||||||
|
Args: []bonfire.Arg{{
|
||||||
|
Name: "plugin",
|
||||||
|
Description: "Plugin ID in vendor.name form (default: every plugin in summer.yaml)",
|
||||||
|
}},
|
||||||
|
Flags: []bonfire.Flag{
|
||||||
|
{Name: "yes", Description: "Update without asking for confirmation", Bare: true},
|
||||||
|
{Name: "offline", Description: "Make no registry call; update submodules from their own tags", Bare: true},
|
||||||
|
},
|
||||||
|
Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
|
||||||
|
id, _ := in.Argument("plugin")
|
||||||
|
dir, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
appDir, err := build.FindAppDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
offline := bonfire.FlagBool(in, "offline")
|
||||||
|
reg, err := registryFor(appDir, offline)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = build.Update(ctx, build.UpdateOptions{
|
||||||
|
StartDir: dir,
|
||||||
|
ID: id,
|
||||||
|
Yes: bonfire.FlagBool(in, "yes"),
|
||||||
|
Offline: offline,
|
||||||
|
Interactive: bonfire.Interactive(out),
|
||||||
|
Registry: reg,
|
||||||
|
Confirm: func(q string) (bool, error) { return out.Confirm(q, false) },
|
||||||
|
Out: out,
|
||||||
|
})
|
||||||
|
if errors.Is(err, build.ErrCancelled) {
|
||||||
|
out.Println("Nothing updated.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func newCommand() bonfire.Command {
|
func newCommand() bonfire.Command {
|
||||||
return bonfire.Command{
|
return bonfire.Command{
|
||||||
Name: "new",
|
Name: "new",
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ A shared plugin's own `go.mod` usually replaces the framework, and sometimes sib
|
|||||||
- when the module is itself in the workspace (another plugin installed as a submodule), `go.work` replaces that module at its required version with its directory, such as `replace git.example.com/acme/translate v0.1.0 => ./plugins/acme/translate` (Go refuses a replace of a workspace module at all versions);
|
- when the module is itself in the workspace (another plugin installed as a submodule), `go.work` replaces that module at its required version with its directory, such as `replace git.example.com/acme/translate v0.1.0 => ./plugins/acme/translate` (Go refuses a replace of a workspace module at all versions);
|
||||||
- otherwise `go.work` pins the module to the version the application requires, such as `replace git.golem15.com/golem15/summercms => git.golem15.com/golem15/summercms v0.6.0`.
|
- otherwise `go.work` pins the module to the version the application requires, such as `replace git.golem15.com/golem15/summercms => git.golem15.com/golem15/summercms v0.6.0`.
|
||||||
|
|
||||||
A pin of the last kind follows the application: when the framework requirement in `go.mod` moves, the next submodule install moves the pin to the new version. Replaces you wrote into `go.work` yourself are left alone.
|
A pin of the last kind follows the application: when the framework requirement in `go.mod` moves, the next `summer plugin:install` from source or `summer plugin:update` moves the pin to the new version. Replaces you wrote into `go.work` yourself are left alone.
|
||||||
|
|
||||||
## Replacing and forking a plugin
|
## Replacing and forking a plugin
|
||||||
|
|
||||||
|
|||||||
@@ -109,7 +109,8 @@ summer plugin:add plugins/acme/blog
|
|||||||
|---------|---------|
|
|---------|---------|
|
||||||
| `summer plugin:add` | Registers a local plugin directory: `summer.yaml`, a `require` and a local `replace` in `go.mod`, and `go.work`. |
|
| `summer plugin:add` | Registers a local plugin directory: `summer.yaml`, a `require` and a local `replace` in `go.mod`, and `go.work`. |
|
||||||
| `summer plugin:manifest` | Writes the plugin's `plugin.yaml` from its sources (`plugin.go`, `go.mod`, the README). `--pretend` prints it, `--check` exits 1 when the committed file is missing or stale, `--name` and `--description` override the README text. See [Publishing plugins](../plugins/marketplace.md). |
|
| `summer plugin:manifest` | Writes the plugin's `plugin.yaml` from its sources (`plugin.go`, `go.mod`, the README). `--pretend` prints it, `--check` exits 1 when the committed file is missing or stale, `--name` and `--description` override the README text. See [Publishing plugins](../plugins/marketplace.md). |
|
||||||
| `summer plugin:install` | Installs a plugin by ID (`acme.blog`, or pinned `acme.blog@v0.3.0`) from the registry as a Go module: the newest release the application's framework version meets, after printing it and asking (`--yes` skips the question). See [Installing plugins](../plugins/marketplace.md#installing-plugins). |
|
| `summer plugin:install` | Installs a plugin by ID (`acme.blog`, or pinned `acme.blog@v0.3.0`) from the registry as a Go module, the newest release the application's framework version meets, with the plugins it requires. `--source` installs them as git submodules, and a git URL installs a plugin from its repository as a submodule. It prints the additions and asks (`--yes` skips the question). See [Installing plugins](../plugins/marketplace.md#installing-plugins). |
|
||||||
|
| `summer plugin:update` | Moves one plugin, or every plugin, to its newest compatible release: `go get` for module installs, the release tag for submodules. Prints the updates and asks like `plugin:install`; `--offline` updates submodules from their own tags. See [Updating plugins](../plugins/marketplace.md#updating-plugins). |
|
||||||
|
|
||||||
## Generating plugin parts
|
## Generating plugin parts
|
||||||
|
|
||||||
|
|||||||
@@ -104,13 +104,13 @@ 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 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.
|
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; move it to a newer release with `summer plugin:update`.
|
||||||
|
|
||||||
### Required plugins
|
### 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 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.
|
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 tells you to run `summer plugin:update <id>` 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
|
### Confirmation
|
||||||
|
|
||||||
@@ -152,6 +152,21 @@ The plugin's required plugins are looked up in the registry by ID and installed
|
|||||||
|
|
||||||
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).
|
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).
|
||||||
|
|
||||||
|
### Updating plugins
|
||||||
|
|
||||||
|
`summer plugin:update` moves installed plugins to their newest release that the application's framework version allows:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
summer plugin:update acme.blog
|
||||||
|
summer plugin:update
|
||||||
|
summer plugin:update --yes
|
||||||
|
summer plugin:update --offline
|
||||||
|
```
|
||||||
|
|
||||||
|
With an ID it updates that plugin; without one it goes through every plugin in `summer.yaml`. A module install moves with `go get <module>@<version>`. A submodule install fetches the tags of its repository, is checked out at the release tag, and `go.mod` requires that tag; the `go.work` overrides are refreshed, so a version pin follows a moved framework requirement. A listed plugin's releases come from the registry. A submodule the registry does not list, such as a plugin installed from a git URL, is updated from its own tags, reading `plugin.yaml` at each tag for the framework minimum. `--offline` makes no registry call and updates submodules from their own tags only.
|
||||||
|
|
||||||
|
The command prints what it will move (`acme.blog v0.1.0 -> v0.1.1 (module)`) and asks for confirmation like `summer plugin:install`; `--yes` skips the question. A newer release that needs a newer framework is named but not installed. The plugins it leaves as they are come under "Not updated" with the reason: plugins that are part of the application's own repository, module installs the registry does not list, plugins already at their newest compatible release, and releases that require a plugin the application lacks or has at too old a version. The command exits with an error only when an update it resolved could not be carried out, such as a release the registry lists but the repository has no tag for. Run `summer build` afterwards.
|
||||||
|
|
||||||
### The registry setting
|
### The registry setting
|
||||||
|
|
||||||
The registry is `https://summercms.io` unless the application's `plugins.registry_url` setting names another one:
|
The registry is `https://summercms.io` unless the application's `plugins.registry_url` setting names another one:
|
||||||
|
|||||||
@@ -340,59 +340,15 @@ func (in *installer) installedSatisfies(id, min string) (bool, error) {
|
|||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// installedVersion returns the release of the module providing pkg: the
|
// installedVersion returns the installed release of the plugin package
|
||||||
// go.mod require, a version replace from go.mod or go.work, or the base
|
// pkg (see installedSource): the module version of a module install, the
|
||||||
// tag of `git describe` in a plugin directory that is its own repository
|
// base tag of git describe for a submodule, "" for an in-app plugin.
|
||||||
// (a submodule). It returns "" when the version cannot be told.
|
|
||||||
func installedVersion(ctx context.Context, appDir, pkg string) (string, error) {
|
func installedVersion(ctx context.Context, appDir, pkg string) (string, error) {
|
||||||
gomod, err := editJSON(ctx, appDir, "mod", "edit", "-json", filepath.Join(appDir, "go.mod"))
|
src, err := installedSource(ctx, appDir, pkg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("build: %w", err)
|
return "", err
|
||||||
}
|
}
|
||||||
mod, version := "", ""
|
return src.Version, nil
|
||||||
for _, r := range gomod.Require {
|
|
||||||
if (pkg == r.Path || strings.HasPrefix(pkg, r.Path+"/")) && len(r.Path) > len(mod) {
|
|
||||||
mod, version = r.Path, r.Version
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if mod == "" {
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
localDir := ""
|
|
||||||
apply := func(replaces []struct {
|
|
||||||
Old struct{ Path string }
|
|
||||||
New struct{ Path, Version string }
|
|
||||||
}, base string) {
|
|
||||||
for _, r := range replaces {
|
|
||||||
if r.Old.Path != mod {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if r.New.Version != "" {
|
|
||||||
version, localDir = r.New.Version, ""
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
localDir = r.New.Path
|
|
||||||
if !filepath.IsAbs(localDir) {
|
|
||||||
localDir = filepath.Join(base, localDir)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
apply(gomod.Replace, appDir)
|
|
||||||
if gw := findGoWork(appDir); gw != "" {
|
|
||||||
if work, err := editJSON(ctx, appDir, "work", "edit", "-json", gw); err == nil {
|
|
||||||
apply(work.Replace, filepath.Dir(gw))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if localDir == "" {
|
|
||||||
if kiosk.ValidVersion(version) {
|
|
||||||
return version, nil
|
|
||||||
}
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
if !isOwnRepository(ctx, localDir) {
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
return kiosk.BaseVersion(gitDescribe(ctx, localDir)), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func sortedKeys(m kiosk.Requires) []string {
|
func sortedKeys(m kiosk.Requires) []string {
|
||||||
|
|||||||
@@ -615,6 +615,230 @@ func TestInstallSmoke(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// plugin:update. tagRelease adds a tagged release to a fixture repo.
|
||||||
|
tagRelease := func(t *testing.T, repoURL, id, module string, rel fixtureRelease) {
|
||||||
|
t.Helper()
|
||||||
|
dir := strings.TrimPrefix(repoURL, "file://")
|
||||||
|
for name, body := range pluginRepoFiles(t, id, module, rel) {
|
||||||
|
write(t, filepath.Join(dir, name), body)
|
||||||
|
}
|
||||||
|
write(t, filepath.Join(dir, "CHANGES"), "release "+rel.Tag+"\n")
|
||||||
|
runTestGit(t, dir, "add", "-A")
|
||||||
|
runTestGit(t, dir, "commit", "--quiet", "-m", "release "+rel.Tag)
|
||||||
|
runTestGit(t, dir, "tag", rel.Tag)
|
||||||
|
}
|
||||||
|
requireOf := func(t *testing.T, app, module string) string {
|
||||||
|
t.Helper()
|
||||||
|
gomod, err := editJSON(t.Context(), app, "mod", "edit", "-json", filepath.Join(app, "go.mod"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, r := range gomod.Require {
|
||||||
|
if r.Path == module {
|
||||||
|
return r.Version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
// sourceApp is a git app with acme.blog v0.1.0 installed with --source.
|
||||||
|
sourceApp := func(t *testing.T) (app, blogRepo string, reg *fakeReg) {
|
||||||
|
t.Helper()
|
||||||
|
blogRepo = pluginRepo(t, "acme.blog", blogModule, []fixtureRelease{{Tag: "v0.1.0", Framework: "v0.5.0"}}, nil)
|
||||||
|
app = gitApp(t)
|
||||||
|
reg = fakeRegistry(t, map[string]fakePlugin{
|
||||||
|
"acme.blog": regPlugin("acme.blog", blogRepo, regVersion("v0.1.0", "v0.5.0", blogModule, blogModule, nil)),
|
||||||
|
})
|
||||||
|
plan, err := PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: "acme.blog", Source: true, Yes: true, Registry: reg.Client})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := ApplyInstall(t.Context(), plan, nil); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return app, blogRepo, reg
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("update", func(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("installs from a file:// module proxy")
|
||||||
|
}
|
||||||
|
app := helloCopy(t)
|
||||||
|
proxy := t.TempDir()
|
||||||
|
for _, v := range []string{"v0.1.0", "v0.1.1"} {
|
||||||
|
writeProxyModule(t, proxy, blogModule, v, fixturePluginFiles(blogModule, "blog", "acme.blog", "v0.5.0"))
|
||||||
|
}
|
||||||
|
offlineGoEnv(t, proxy)
|
||||||
|
reg := fakeRegistry(t, map[string]fakePlugin{"acme.blog": regPlugin("acme.blog", "", blogV1)})
|
||||||
|
plan, err := PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: "acme.blog", Yes: true, Registry: reg.Client})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := ApplyInstall(t.Context(), plan, nil); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
reg.set("acme.blog", regPlugin("acme.blog", "",
|
||||||
|
regVersion("v0.3.0", "v9.0.0", blogModule, blogModule, nil),
|
||||||
|
regVersion("v0.1.1", "v0.5.0", blogModule, blogModule, nil),
|
||||||
|
blogV1))
|
||||||
|
var out bytes.Buffer
|
||||||
|
if err := Update(t.Context(), UpdateOptions{StartDir: app, ID: "acme.blog", Yes: true, Registry: reg.Client, Out: &out}); err != nil {
|
||||||
|
t.Fatalf("update: %v\n%s", err, out.String())
|
||||||
|
}
|
||||||
|
if got := requireOf(t, app, blogModule); got != "v0.1.1" {
|
||||||
|
t.Fatalf("go.mod requires %s %s, want v0.1.1\n%s", blogModule, got, out.String())
|
||||||
|
}
|
||||||
|
if !strings.Contains(out.String(), "acme.blog v0.1.0 -> v0.1.1 (module)") {
|
||||||
|
t.Fatalf("update output:\n%s", out.String())
|
||||||
|
}
|
||||||
|
out.Reset()
|
||||||
|
if err := Update(t.Context(), UpdateOptions{StartDir: app, ID: "acme.blog", Yes: true, Registry: reg.Client, Out: &out}); err != nil {
|
||||||
|
t.Fatalf("second update: %v\n%s", err, out.String())
|
||||||
|
}
|
||||||
|
if got := requireOf(t, app, blogModule); got != "v0.1.1" {
|
||||||
|
t.Fatalf("second update moved to %s", got)
|
||||||
|
}
|
||||||
|
if !strings.Contains(out.String(), "v0.3.0 needs framework v9.0.0") {
|
||||||
|
t.Fatalf("second update does not report the incompatible release:\n%s", out.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("update_submodule", func(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("clones fixture repositories and builds the app")
|
||||||
|
}
|
||||||
|
gitTestEnv(t)
|
||||||
|
app, blogRepo, reg := sourceApp(t)
|
||||||
|
tagRelease(t, blogRepo, "acme.blog", blogModule, fixtureRelease{Tag: "v0.1.1", Framework: "v0.5.0"})
|
||||||
|
reg.set("acme.blog", regPlugin("acme.blog", blogRepo,
|
||||||
|
regVersion("v0.1.1", "v0.5.0", blogModule, blogModule, nil),
|
||||||
|
regVersion("v0.1.0", "v0.5.0", blogModule, blogModule, nil)))
|
||||||
|
var out bytes.Buffer
|
||||||
|
if err := Update(t.Context(), UpdateOptions{StartDir: app, ID: "acme.blog", Yes: true, Registry: reg.Client, Out: &out}); err != nil {
|
||||||
|
t.Fatalf("update: %v\n%s", err, out.String())
|
||||||
|
}
|
||||||
|
if got := gitOut(t, filepath.Join(app, "plugins", "acme", "blog"), "describe", "--tags"); got != "v0.1.1" {
|
||||||
|
t.Fatalf("submodule at %s, want v0.1.1\n%s", got, out.String())
|
||||||
|
}
|
||||||
|
if got := requireOf(t, app, blogModule); got != "v0.1.1" {
|
||||||
|
t.Fatalf("go.mod requires %s, want v0.1.1", got)
|
||||||
|
}
|
||||||
|
if !strings.Contains(out.String(), "acme.blog v0.1.0 -> v0.1.1 (submodule)") {
|
||||||
|
t.Fatalf("update output:\n%s", out.String())
|
||||||
|
}
|
||||||
|
buildApp(t, app)
|
||||||
|
|
||||||
|
// A go.work pin of a module to one of its own versions follows the
|
||||||
|
// app's require when that moves.
|
||||||
|
if err := runGo(t.Context(), app, "work", "edit", "-replace="+translateModule+"="+translateModule+"@v0.1.0"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := runGo(t.Context(), app, "mod", "edit", "-require="+translateModule+"@v0.2.0"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := refreshWorkOverrides(t.Context(), app, &out); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
work, _ := os.ReadFile(filepath.Join(app, "go.work"))
|
||||||
|
if !bytes.Contains(work, []byte(translateModule+" => "+translateModule+" v0.2.0")) {
|
||||||
|
t.Fatalf("pin not refreshed:\n%s", work)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("update_all", func(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("clones fixture repositories")
|
||||||
|
}
|
||||||
|
gitTestEnv(t)
|
||||||
|
app, blogRepo, reg := sourceApp(t)
|
||||||
|
// acme.translate from a git URL: not in the registry, updated from
|
||||||
|
// its own tags.
|
||||||
|
translateRepo := pluginRepo(t, "acme.translate", translateModule, []fixtureRelease{{Tag: "v0.1.0", Framework: "v0.5.0"}}, nil)
|
||||||
|
plan, err := PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: translateRepo, Yes: true, Registry: reg.Client})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := ApplyInstall(t.Context(), plan, nil); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// acme.ghost: a module install the registry does not list.
|
||||||
|
if err := appendManifestPlugins(filepath.Join(app, manifestName), []Plugin{{ID: "acme.ghost", Module: "example.com/acme/ghost"}}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := runGo(t.Context(), app, "mod", "edit", "-require=example.com/acme/ghost@v0.1.0"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
tagRelease(t, blogRepo, "acme.blog", blogModule, fixtureRelease{Tag: "v0.1.1", Framework: "v0.5.0"})
|
||||||
|
tagRelease(t, translateRepo, "acme.translate", translateModule, fixtureRelease{Tag: "v0.1.1", Framework: "v0.5.0"})
|
||||||
|
tagRelease(t, translateRepo, "acme.translate", translateModule, fixtureRelease{Tag: "v0.2.0", Framework: "v9.0.0"})
|
||||||
|
reg.set("acme.blog", regPlugin("acme.blog", blogRepo,
|
||||||
|
regVersion("v0.1.1", "v0.5.0", blogModule, blogModule, nil),
|
||||||
|
regVersion("v0.1.0", "v0.5.0", blogModule, blogModule, nil)))
|
||||||
|
|
||||||
|
var out bytes.Buffer
|
||||||
|
if err := Update(t.Context(), UpdateOptions{StartDir: app, Yes: true, Registry: reg.Client, Out: &out}); err != nil {
|
||||||
|
t.Fatalf("update all: %v\n%s", err, out.String())
|
||||||
|
}
|
||||||
|
for _, want := range []string{
|
||||||
|
"acme.blog v0.1.0 -> v0.1.1 (submodule)",
|
||||||
|
"acme.translate v0.1.0 -> v0.1.1 (submodule)",
|
||||||
|
"golem15.hello: ",
|
||||||
|
"acme.ghost: ",
|
||||||
|
"v0.2.0 needs framework v9.0.0",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(out.String(), want) {
|
||||||
|
t.Fatalf("update all output lacks %q:\n%s", want, out.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if got := gitOut(t, filepath.Join(app, "plugins", "acme", "translate"), "describe", "--tags"); got != "v0.1.1" {
|
||||||
|
t.Fatalf("translate at %s, want v0.1.1", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A release the registry lists but the repository lacks makes the
|
||||||
|
// run fail, naming the plugin; the other plugins are reported.
|
||||||
|
reg.set("acme.blog", regPlugin("acme.blog", blogRepo,
|
||||||
|
regVersion("v0.1.2", "v0.5.0", blogModule, blogModule, nil),
|
||||||
|
regVersion("v0.1.1", "v0.5.0", blogModule, blogModule, nil)))
|
||||||
|
out.Reset()
|
||||||
|
err = Update(t.Context(), UpdateOptions{StartDir: app, Yes: true, Registry: reg.Client, Out: &out})
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "acme.blog") {
|
||||||
|
t.Fatalf("failed update: err = %v\n%s", err, out.String())
|
||||||
|
}
|
||||||
|
if !strings.Contains(out.String(), "acme.ghost: ") {
|
||||||
|
t.Fatalf("skipped plugins not reported:\n%s", out.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("update_prompt", func(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("clones fixture repositories")
|
||||||
|
}
|
||||||
|
gitTestEnv(t)
|
||||||
|
app, blogRepo, reg := sourceApp(t)
|
||||||
|
tagRelease(t, blogRepo, "acme.blog", blogModule, fixtureRelease{Tag: "v0.1.1", Framework: "v0.5.0"})
|
||||||
|
reg.set("acme.blog", regPlugin("acme.blog", blogRepo,
|
||||||
|
regVersion("v0.1.1", "v0.5.0", blogModule, blogModule, nil),
|
||||||
|
regVersion("v0.1.0", "v0.5.0", blogModule, blogModule, nil)))
|
||||||
|
var out bytes.Buffer
|
||||||
|
err := Update(t.Context(), UpdateOptions{StartDir: app, ID: "acme.blog", Registry: reg.Client, Out: &out})
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "--yes") || !strings.Contains(out.String(), "acme.blog v0.1.0 -> v0.1.1") {
|
||||||
|
t.Fatalf("non-interactive update: err = %v\n%s", err, out.String())
|
||||||
|
}
|
||||||
|
err = Update(t.Context(), UpdateOptions{StartDir: app, ID: "acme.blog", Registry: reg.Client, Interactive: true,
|
||||||
|
Confirm: func(string) (bool, error) { return false, nil }})
|
||||||
|
if !errors.Is(err, ErrCancelled) {
|
||||||
|
t.Fatalf("declined update: err = %v", err)
|
||||||
|
}
|
||||||
|
if got := gitOut(t, filepath.Join(app, "plugins", "acme", "blog"), "describe", "--tags"); got != "v0.1.0" {
|
||||||
|
t.Fatalf("declined update moved the submodule to %s", got)
|
||||||
|
}
|
||||||
|
if got := requireOf(t, app, blogModule); got != "v0.1.0" {
|
||||||
|
t.Fatalf("declined update changed go.mod to %s", got)
|
||||||
|
}
|
||||||
|
if err := Update(t.Context(), UpdateOptions{StartDir: app, ID: "acme.nope", Yes: true, Registry: reg.Client}); err == nil || !strings.Contains(err.Error(), "summer.yaml") {
|
||||||
|
t.Fatalf("unknown ID: err = %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("append_manifest", func(t *testing.T) {
|
t.Run("append_manifest", func(t *testing.T) {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
path := filepath.Join(dir, manifestName)
|
path := filepath.Join(dir, manifestName)
|
||||||
|
|||||||
515
internal/build/update.go
Normal file
515
internal/build/update.go
Normal file
@@ -0,0 +1,515 @@
|
|||||||
|
package build
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.golem15.com/golem15/summercms/modules/bonfire"
|
||||||
|
"git.golem15.com/golem15/summercms/modules/kiosk"
|
||||||
|
)
|
||||||
|
|
||||||
|
// sourceLocal marks a plugin that is part of the application's own
|
||||||
|
// repository: it has no release to update.
|
||||||
|
const sourceLocal = "local"
|
||||||
|
|
||||||
|
// UpdateOptions are the inputs of summer plugin:update.
|
||||||
|
type UpdateOptions struct {
|
||||||
|
// StartDir is a directory inside the application.
|
||||||
|
StartDir string
|
||||||
|
// ID names one plugin; empty updates every plugin that resolves.
|
||||||
|
ID string
|
||||||
|
// Yes skips the confirmation prompt.
|
||||||
|
Yes bool
|
||||||
|
// Offline makes no registry call: only submodule installs are
|
||||||
|
// updated, from their own tags.
|
||||||
|
Offline bool
|
||||||
|
// Interactive reports that Confirm can ask a person.
|
||||||
|
Interactive bool
|
||||||
|
// Registry answers release lookups; nil when registry calls are
|
||||||
|
// disabled.
|
||||||
|
Registry Registry
|
||||||
|
// Confirm asks the question and returns the answer.
|
||||||
|
Confirm func(question string) (bool, error)
|
||||||
|
// Out receives the plan and progress lines; nil discards them.
|
||||||
|
Out io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
// pluginSource is how a summer.yaml plugin is installed.
|
||||||
|
type pluginSource struct {
|
||||||
|
// Kind is ModeModule, ModeSubmodule or sourceLocal.
|
||||||
|
Kind string
|
||||||
|
// Module is the module providing the package ("" for a package of the
|
||||||
|
// application module).
|
||||||
|
Module string
|
||||||
|
// Version is the module version (module), the base release tag of
|
||||||
|
// git describe (submodule) or "".
|
||||||
|
Version string
|
||||||
|
// Dir is the module root of a submodule or local plugin.
|
||||||
|
Dir string
|
||||||
|
}
|
||||||
|
|
||||||
|
// installedSource tells how the plugin package pkg is installed, from the
|
||||||
|
// app's go.mod and go.work without any network: the module that provides
|
||||||
|
// it (the longest go.mod require that prefixes it, or a go.work use
|
||||||
|
// module), whether a replace points it at a directory, and whether that
|
||||||
|
// directory is its own git repository (a submodule).
|
||||||
|
func installedSource(ctx context.Context, appDir, pkg string) (pluginSource, error) {
|
||||||
|
gomod, err := editJSON(ctx, appDir, "mod", "edit", "-json", filepath.Join(appDir, "go.mod"))
|
||||||
|
if err != nil {
|
||||||
|
return pluginSource{}, fmt.Errorf("build: %w", err)
|
||||||
|
}
|
||||||
|
var src pluginSource
|
||||||
|
for _, r := range gomod.Require {
|
||||||
|
if (pkg == r.Path || strings.HasPrefix(pkg, r.Path+"/")) && len(r.Path) > len(src.Module) {
|
||||||
|
src.Module, src.Version = r.Path, r.Version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var work goModJSON
|
||||||
|
workDir := ""
|
||||||
|
if gw := findGoWork(appDir); gw != "" {
|
||||||
|
if w, err := editJSON(ctx, appDir, "work", "edit", "-json", gw); err == nil {
|
||||||
|
work, workDir = w, filepath.Dir(gw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if src.Module == "" {
|
||||||
|
// A module only go.work uses, or a package of the app itself.
|
||||||
|
for _, u := range work.Use {
|
||||||
|
dir := u.DiskPath
|
||||||
|
if !filepath.IsAbs(dir) {
|
||||||
|
dir = filepath.Join(workDir, dir)
|
||||||
|
}
|
||||||
|
mod, err := readModulePath(filepath.Join(dir, "go.mod"))
|
||||||
|
if err != nil || samePath(dir, appDir) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (pkg == mod || strings.HasPrefix(pkg, mod+"/")) && len(mod) > len(src.Module) {
|
||||||
|
src.Module, src.Dir = mod, filepath.Clean(dir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if src.Module == "" {
|
||||||
|
return pluginSource{Kind: sourceLocal, Dir: appDir}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
apply := func(replaces []struct {
|
||||||
|
Old struct{ Path string }
|
||||||
|
New struct{ Path, Version string }
|
||||||
|
}, base string) {
|
||||||
|
for _, r := range replaces {
|
||||||
|
if r.Old.Path != src.Module {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if r.New.Version != "" {
|
||||||
|
src.Version, src.Dir = r.New.Version, ""
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
src.Dir = r.New.Path
|
||||||
|
if !filepath.IsAbs(src.Dir) {
|
||||||
|
src.Dir = filepath.Join(base, src.Dir)
|
||||||
|
}
|
||||||
|
src.Dir = filepath.Clean(src.Dir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
apply(gomod.Replace, appDir)
|
||||||
|
apply(work.Replace, workDir)
|
||||||
|
if src.Dir == "" {
|
||||||
|
src.Kind = ModeModule
|
||||||
|
if !kiosk.ValidVersion(src.Version) {
|
||||||
|
src.Version = ""
|
||||||
|
}
|
||||||
|
return src, nil
|
||||||
|
}
|
||||||
|
if !isOwnRepository(ctx, src.Dir) {
|
||||||
|
src.Kind, src.Version = sourceLocal, ""
|
||||||
|
return src, nil
|
||||||
|
}
|
||||||
|
src.Kind = ModeSubmodule
|
||||||
|
src.Version = kiosk.BaseVersion(gitDescribe(ctx, src.Dir))
|
||||||
|
return src, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateStep is one plugin plugin:update moves.
|
||||||
|
type updateStep struct {
|
||||||
|
ID, Package string
|
||||||
|
Src pluginSource
|
||||||
|
To kiosk.RegistryVersion
|
||||||
|
// Newer names a release above To that needs a newer framework.
|
||||||
|
Newer string
|
||||||
|
}
|
||||||
|
|
||||||
|
// updater carries the state of one Update run.
|
||||||
|
type updater struct {
|
||||||
|
ctx context.Context
|
||||||
|
opts UpdateOptions
|
||||||
|
out io.Writer
|
||||||
|
appDir string
|
||||||
|
fw string
|
||||||
|
|
||||||
|
steps []updateStep
|
||||||
|
notes []string // "id: why it is not updated"
|
||||||
|
failed []string // IDs whose update could not be resolved or applied
|
||||||
|
reasons map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update moves installed plugins to the newest release the application's
|
||||||
|
// framework version allows (D-10): a module install with go get, a
|
||||||
|
// submodule install by checking its repository out at the release tag and
|
||||||
|
// requiring that tag in go.mod, followed by the go.work overrides. A
|
||||||
|
// listed plugin's releases come from the registry; a submodule the
|
||||||
|
// registry does not list (a git URL install) is updated from its own tags
|
||||||
|
// and their plugin.yaml. Without opts.ID every plugin in summer.yaml that
|
||||||
|
// resolves is updated; in-app plugins, unlisted module installs and
|
||||||
|
// plugins whose newer releases need a newer framework are reported and
|
||||||
|
// skipped. The plan is printed and confirmed like plugin:install. The run
|
||||||
|
// fails when an update that resolved could not be carried out, or when
|
||||||
|
// the one plugin asked for cannot be updated.
|
||||||
|
func Update(ctx context.Context, opts UpdateOptions) error {
|
||||||
|
appDir, err := findAppDir(opts.StartDir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
m, err := LoadManifest(filepath.Join(appDir, manifestName))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
u := &updater{ctx: ctx, opts: opts, out: opts.Out, appDir: appDir, reasons: map[string]string{}}
|
||||||
|
if u.out == nil {
|
||||||
|
u.out = io.Discard
|
||||||
|
}
|
||||||
|
fw, err := EffectiveFramework(ctx, appDir)
|
||||||
|
if err != nil {
|
||||||
|
if fw == "" {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Fprintf(u.out, "warning: %v\n", err)
|
||||||
|
}
|
||||||
|
u.fw = fw
|
||||||
|
|
||||||
|
targets := m.Plugins
|
||||||
|
if opts.ID != "" {
|
||||||
|
targets = nil
|
||||||
|
for _, p := range m.Plugins {
|
||||||
|
if p.ID == opts.ID {
|
||||||
|
targets = []Plugin{p}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if targets == nil {
|
||||||
|
return bonfire.WithHint(fmt.Errorf("build: %s is not in summer.yaml", opts.ID),
|
||||||
|
"plugin:update moves plugins the application already has; add a new one with plugin:install.",
|
||||||
|
"{bin} plugin:install "+opts.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
installed := map[string]pluginSource{}
|
||||||
|
seenDir := map[string]string{}
|
||||||
|
for _, p := range targets {
|
||||||
|
src, err := installedSource(ctx, appDir, p.Module)
|
||||||
|
if err != nil {
|
||||||
|
u.fail(p.ID, err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
installed[p.ID] = src
|
||||||
|
switch src.Kind {
|
||||||
|
case sourceLocal:
|
||||||
|
u.note(p.ID, "part of the application's repository; it has no releases")
|
||||||
|
case ModeModule:
|
||||||
|
u.planModule(p, src)
|
||||||
|
case ModeSubmodule:
|
||||||
|
if first, ok := seenDir[src.Dir]; ok {
|
||||||
|
u.note(p.ID, "ships in the same repository as "+first+" and moves with it")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seenDir[src.Dir] = p.ID
|
||||||
|
u.planSubmodule(p, src)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
u.checkRequires(m, installed)
|
||||||
|
u.report()
|
||||||
|
|
||||||
|
if opts.ID != "" && len(u.steps) == 0 {
|
||||||
|
if why, ok := u.reasons[opts.ID]; ok && len(u.failed) > 0 {
|
||||||
|
return fmt.Errorf("build: cannot update %s: %s", opts.ID, why)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if len(u.steps) == 0 {
|
||||||
|
return u.failure()
|
||||||
|
}
|
||||||
|
n := len(u.steps)
|
||||||
|
noun := "plugin"
|
||||||
|
if n != 1 {
|
||||||
|
noun = "plugins"
|
||||||
|
}
|
||||||
|
if err := askToProceed(opts.Yes, opts.Interactive, opts.Confirm, fmt.Sprintf("Update %d %s?", n, noun)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := u.apply(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return u.failure()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *updater) note(id, why string) {
|
||||||
|
u.notes = append(u.notes, id+": "+why)
|
||||||
|
u.reasons[id] = why
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *updater) fail(id, why string) {
|
||||||
|
u.note(id, "failed: "+why)
|
||||||
|
u.failed = append(u.failed, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *updater) failure() error {
|
||||||
|
if len(u.failed) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("build: plugin:update could not update %s", strings.Join(u.failed, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
// registryReason says why the registry cannot be asked, or "".
|
||||||
|
func (u *updater) registryReason() string {
|
||||||
|
switch {
|
||||||
|
case u.opts.Offline:
|
||||||
|
return "--offline makes no registry calls"
|
||||||
|
case u.opts.Registry == nil:
|
||||||
|
return "the plugin registry is disabled (plugins.registry_url is empty)"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// pick chooses the newest release above have whose framework minimum the
|
||||||
|
// app satisfies, with a note naming a still newer release that needs a
|
||||||
|
// newer framework. When there is none it returns only a note: the newer
|
||||||
|
// release that needs a newer framework, or that have is the newest.
|
||||||
|
func (u *updater) pick(versions []kiosk.RegistryVersion, have string) (kiosk.RegistryVersion, string) {
|
||||||
|
base := have
|
||||||
|
if base == "" {
|
||||||
|
base = "v0.0.0"
|
||||||
|
}
|
||||||
|
var newer []kiosk.RegistryVersion
|
||||||
|
for _, v := range versions {
|
||||||
|
if kiosk.ValidVersion(v.Version) && kiosk.CompareVersions(v.Version, base) > 0 {
|
||||||
|
newer = append(newer, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if best, ok := kiosk.NewestCompatible(newer, u.fw); ok {
|
||||||
|
if n := newestOf(newer); kiosk.CompareVersions(n.Version, best.Version) > 0 {
|
||||||
|
return best, fmt.Sprintf("%s needs framework %s", n.Version, n.Framework)
|
||||||
|
}
|
||||||
|
return best, ""
|
||||||
|
}
|
||||||
|
if len(newer) > 0 {
|
||||||
|
n := newestOf(newer)
|
||||||
|
return kiosk.RegistryVersion{}, fmt.Sprintf("stays at %s; %s needs framework %s (this application builds against %s)", displayRef(have), n.Version, n.Framework, displayVersion(u.fw))
|
||||||
|
}
|
||||||
|
return kiosk.RegistryVersion{}, "up to date at " + displayRef(have)
|
||||||
|
}
|
||||||
|
|
||||||
|
// planModule plans a module install's update from the registry.
|
||||||
|
func (u *updater) planModule(p Plugin, src pluginSource) {
|
||||||
|
if why := u.registryReason(); why != "" {
|
||||||
|
u.note(p.ID, "installed as a Go module, and "+why)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
versions, err := u.opts.Registry.Versions(u.ctx, p.ID)
|
||||||
|
if errors.Is(err, kiosk.ErrNotFound) {
|
||||||
|
u.note(p.ID, "not in the registry")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
u.fail(p.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var same []kiosk.RegistryVersion
|
||||||
|
for _, v := range versions {
|
||||||
|
if v.Module == src.Module {
|
||||||
|
same = append(same, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
to, why := u.pick(same, src.Version)
|
||||||
|
if to.Version == "" {
|
||||||
|
u.note(p.ID, why)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
step := InstallStep{ID: p.ID, Version: to.Version, Module: to.Module, Package: to.Package, Mode: ModeModule}
|
||||||
|
if err := validateStep(step); err != nil {
|
||||||
|
u.fail(p.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
u.steps = append(u.steps, updateStep{ID: p.ID, Package: p.Module, Src: src, To: to, Newer: why})
|
||||||
|
}
|
||||||
|
|
||||||
|
// planSubmodule plans a submodule install's update: the registry's
|
||||||
|
// releases when it lists the plugin, otherwise the repository's own tags.
|
||||||
|
func (u *updater) planSubmodule(p Plugin, src pluginSource) {
|
||||||
|
if _, err := runCLIGit(u.ctx, src.Dir, u.opts.Interactive, "fetch", "--quiet", "--tags"); err != nil {
|
||||||
|
u.fail(p.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rel := strings.TrimPrefix(strings.TrimPrefix(p.Module, src.Module), "/")
|
||||||
|
var to kiosk.RegistryVersion
|
||||||
|
why := ""
|
||||||
|
fromRegistry := false
|
||||||
|
if u.registryReason() == "" {
|
||||||
|
versions, err := u.opts.Registry.Versions(u.ctx, p.ID)
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, kiosk.ErrNotFound):
|
||||||
|
case err != nil:
|
||||||
|
u.fail(p.ID, err.Error())
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
fromRegistry = true
|
||||||
|
var same []kiosk.RegistryVersion
|
||||||
|
for _, v := range versions {
|
||||||
|
if v.Module == src.Module {
|
||||||
|
same = append(same, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
to, why = u.pick(same, src.Version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !fromRegistry {
|
||||||
|
tags, err := releaseTags(u.ctx, src.Dir)
|
||||||
|
if err != nil {
|
||||||
|
u.fail(p.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var own []kiosk.RegistryVersion
|
||||||
|
for _, tag := range tags {
|
||||||
|
if src.Version != "" && kiosk.CompareVersions(tag, src.Version) <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
pm, err := manifestAt(u.ctx, src.Dir, tag, rel)
|
||||||
|
if err != nil || pm == nil || pm.ID != p.ID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
own = append(own, kiosk.RegistryVersion{Version: tag, Framework: pm.Framework, Module: pm.Module, Package: pm.Package, Requires: pm.Requires})
|
||||||
|
}
|
||||||
|
to, why = u.pick(own, src.Version)
|
||||||
|
}
|
||||||
|
if to.Version == "" {
|
||||||
|
u.note(p.ID, why)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err := runCLIGit(u.ctx, src.Dir, false, "rev-parse", "--verify", "--quiet", "refs/tags/"+to.Version+"^{commit}"); err != nil {
|
||||||
|
u.fail(p.ID, fmt.Sprintf("the registry lists %s, but its repository has no tag %s", to.Version, to.Version))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pm, err := manifestAt(u.ctx, src.Dir, to.Version, rel)
|
||||||
|
if err != nil {
|
||||||
|
u.fail(p.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if pm != nil && (pm.ID != p.ID || pm.Module != src.Module) {
|
||||||
|
u.fail(p.ID, fmt.Sprintf("%s at %s describes %s (%s); refused", kiosk.FileName, to.Version, pm.ID, pm.Module))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if pm != nil && to.Requires == nil {
|
||||||
|
to.Requires = pm.Requires
|
||||||
|
}
|
||||||
|
u.steps = append(u.steps, updateStep{ID: p.ID, Package: p.Module, Src: src, To: to, Newer: why})
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkRequires drops planned updates whose new release requires a plugin
|
||||||
|
// the application lacks, or at a version it will not have after this run.
|
||||||
|
func (u *updater) checkRequires(m Manifest, installed map[string]pluginSource) {
|
||||||
|
final := map[string]string{}
|
||||||
|
listed := map[string]string{}
|
||||||
|
for _, p := range m.Plugins {
|
||||||
|
listed[p.ID] = p.Module
|
||||||
|
}
|
||||||
|
for id, src := range installed {
|
||||||
|
final[id] = src.Version
|
||||||
|
}
|
||||||
|
for _, s := range u.steps {
|
||||||
|
final[s.ID] = s.To.Version
|
||||||
|
}
|
||||||
|
kept := u.steps[:0]
|
||||||
|
for _, s := range u.steps {
|
||||||
|
why := ""
|
||||||
|
for _, dep := range sortedKeys(s.To.Requires) {
|
||||||
|
min := s.To.Requires[dep]
|
||||||
|
if _, ok := listed[dep]; !ok {
|
||||||
|
why = fmt.Sprintf("%s requires %s, which is not installed; run summer plugin:install %s first", s.To.Version, dep, dep)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
have, known := final[dep]
|
||||||
|
if !known {
|
||||||
|
src, err := installedSource(u.ctx, u.appDir, listed[dep])
|
||||||
|
if err == nil {
|
||||||
|
have = src.Version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if have != "" && !kiosk.Satisfies(have, min) {
|
||||||
|
why = fmt.Sprintf("%s needs %s %s or newer, and the application has %s", s.To.Version, dep, min, have)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if why != "" {
|
||||||
|
u.note(s.ID, "stays at "+displayRef(s.Src.Version)+"; "+why)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
kept = append(kept, s)
|
||||||
|
}
|
||||||
|
u.steps = kept
|
||||||
|
}
|
||||||
|
|
||||||
|
// report prints the planned updates and the plugins left as they are.
|
||||||
|
func (u *updater) report() {
|
||||||
|
if n := len(u.steps); n > 0 {
|
||||||
|
noun := "plugin"
|
||||||
|
if n != 1 {
|
||||||
|
noun = "plugins"
|
||||||
|
}
|
||||||
|
fmt.Fprintf(u.out, "Updating %d %s in %s (framework %s):\n", n, noun, u.appDir, displayVersion(u.fw))
|
||||||
|
for _, s := range u.steps {
|
||||||
|
line := fmt.Sprintf(" %s %s -> %s (%s)", s.ID, displayRef(s.Src.Version), s.To.Version, s.Src.Kind)
|
||||||
|
if s.Newer != "" {
|
||||||
|
line += "; " + s.Newer
|
||||||
|
}
|
||||||
|
fmt.Fprintln(u.out, line)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Fprintln(u.out, "Nothing to update.")
|
||||||
|
}
|
||||||
|
if len(u.notes) > 0 {
|
||||||
|
fmt.Fprintln(u.out, "Not updated:")
|
||||||
|
for _, n := range u.notes {
|
||||||
|
fmt.Fprintf(u.out, " %s\n", n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// apply checks submodules out at their new tags, requires the tags in
|
||||||
|
// go.mod, runs one go get for the module updates and refreshes the
|
||||||
|
// go.work overrides.
|
||||||
|
func (u *updater) apply() error {
|
||||||
|
var getArgs []string
|
||||||
|
for _, s := range u.steps {
|
||||||
|
switch s.Src.Kind {
|
||||||
|
case ModeModule:
|
||||||
|
getArgs = append(getArgs, s.Src.Module+"@"+s.To.Version)
|
||||||
|
case ModeSubmodule:
|
||||||
|
if err := checkout(u.ctx, s.Src.Dir, s.To.Version); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := runGo(u.ctx, u.appDir, "mod", "edit", "-require="+s.Src.Module+"@"+s.To.Version); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Fprintf(u.out, "Checked out %s at %s\n", s.ID, s.To.Version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(getArgs) > 0 {
|
||||||
|
fmt.Fprintf(u.out, "go get %s\n", strings.Join(getArgs, " "))
|
||||||
|
if err := runGo(u.ctx, u.appDir, append([]string{"get", "--"}, getArgs...)...); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := refreshWorkOverrides(u.ctx, u.appDir, u.out); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Fprintln(u.out, "Updated; run summer build")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -146,7 +146,7 @@ func TestPluginManifest(t *testing.T) {
|
|||||||
|
|
||||||
| Key | Default | Meaning |
|
| Key | Default | Meaning |
|
||||||
|-----|---------|---------|
|
|-----|---------|---------|
|
||||||
| `plugins.registry_url` | `https://summercms.io` | Registry origin for `summer plugin:install` and the admin's Plugins screen (file `config/plugins.yaml`, env `SUMMER_PLUGINS__REGISTRY_URL`). It must be the final origin, because redirects are not followed. An empty value disables registry calls. |
|
| `plugins.registry_url` | `https://summercms.io` | Registry origin for `summer plugin:install`, `summer plugin:update` and the admin's Plugins screen (file `config/plugins.yaml`, env `SUMMER_PLUGINS__REGISTRY_URL`). It must be the final origin, because redirects are not followed. An empty value disables registry calls. |
|
||||||
|
|
||||||
## CLI commands
|
## CLI commands
|
||||||
|
|
||||||
@@ -154,6 +154,7 @@ func TestPluginManifest(t *testing.T) {
|
|||||||
|---------|---------|
|
|---------|---------|
|
||||||
| `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. |
|
| `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. |
|
||||||
| `summer plugin:install <id\|git-url>` | Installs a plugin; reads its releases through `kiosk.Client` and picks one with `kiosk.NewestCompatible`. See [Installing plugins](../../docs/plugins/marketplace.md#installing-plugins). |
|
| `summer plugin:install <id\|git-url>` | Installs a plugin; reads its releases through `kiosk.Client` and picks one with `kiosk.NewestCompatible`. See [Installing plugins](../../docs/plugins/marketplace.md#installing-plugins). |
|
||||||
|
| `summer plugin:update [id]` | Moves installed plugins to their newest compatible release, read through `kiosk.Client`. See [Updating plugins](../../docs/plugins/marketplace.md#updating-plugins). |
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user