fix(34): CR-01 never move a pseudo-version install back to its base release
plugin:update dropped a go.mod require that was not a strict vX.Y.Z and treated it as v0.0.0, so a module at a commit after v0.4.0 (v0.4.1-0.<ts>-<hash>) was moved back to v0.4.0. kiosk.Newer now places a release against a release, a git describe string or a pseudo-version (by the tag it builds on, in Go's version order); plugin:update and the admin Plugins screen badge share it. A pseudo-version with no tag before its commit is left alone with a note naming go get <module>@<tag>.
This commit is contained in:
@@ -20,7 +20,7 @@ WinterCMS keeps this data in a plugin's `Plugin.php` `pluginDetails()` and in `c
|
||||
- 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.
|
||||
- Strict version rules: `kiosk.ValidVersion` (only `vX.Y.Z`), `kiosk.CompareVersions`, `kiosk.BaseVersion` for `git describe` output, `kiosk.Satisfies` for Go-style minimums, `kiosk.NewestCompatible` to pick the release an application can install, and `kiosk.Newer` with `kiosk.Comparable` to tell whether a release is an upgrade for an installed version (a release, a `git describe` string or a pseudo-version), so an update never goes back.
|
||||
- A registry client, `kiosk.Client`, for the summer tool and the admin: `kiosk.NewClient` builds it over a `fetchguard` client in trusted mode with a 4 MiB body cap, a 10 second timeout and no redirects, and `kiosk.RegistryURL` reads the registry origin from the `plugins.registry_url` config key (default `kiosk.DefaultRegistryURL`; an empty value disables registry calls).
|
||||
- `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`.
|
||||
|
||||
@@ -126,6 +126,8 @@ func TestPluginManifest(t *testing.T) {
|
||||
| `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.Newer` | Whether a release is an upgrade for an installed version: above a release or the base tag of a `git describe` string, and for a pseudo-version at or above the release after the tag it builds on (`v0.4.1` and later for `v0.4.1-0.<timestamp>-<hash>`, never `v0.4.0`). Nothing is newer than a version `kiosk.Comparable` refuses. |
|
||||
| `kiosk.Comparable` | Whether `kiosk.Newer` can place an installed version: a release, a `git describe` string, or a pseudo-version that builds on a tag (not `vX.0.0-<timestamp>-<hash>`, a bare hash or a pre-release). |
|
||||
| `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.DefaultRegistryURL` | `https://summercms.io`, the registry used when `plugins.registry_url` is not set. |
|
||||
|
||||
@@ -9,6 +9,10 @@ var (
|
||||
validVersionRe = regexp.MustCompile(`^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$`)
|
||||
describeRe = regexp.MustCompile(`^(v(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*))(?:-[0-9]+-g[0-9a-f]+)?$`)
|
||||
pseudoBaseRe = regexp.MustCompile(`^(v[0-9]+\.[0-9]+\.[0-9]+)-`)
|
||||
// pseudoNoBaseRe is a pseudo-version with no tag before its commit:
|
||||
// vX.0.0-<timestamp>-<hash>, without the "0." or "<pre>.0." of one
|
||||
// that builds on a release or a pre-release.
|
||||
pseudoNoBaseRe = regexp.MustCompile(`^v[0-9]+\.[0-9]+\.[0-9]+-[0-9]{14}-[0-9a-f]{12}$`)
|
||||
)
|
||||
|
||||
// ValidVersion reports whether v is a release version in the strict form
|
||||
@@ -65,6 +69,47 @@ func BaseVersion(describe string) string {
|
||||
return m[1]
|
||||
}
|
||||
|
||||
// Newer reports whether release is an upgrade for a plugin installed at
|
||||
// installed, so moving to it never goes back. installed may be a release
|
||||
// vX.Y.Z (newer: above it), a git describe string vX.Y.Z-N-g<hash>, a
|
||||
// commit after vX.Y.Z (newer: above vX.Y.Z), or a Go pseudo-version, a
|
||||
// commit after the tag it builds on: vX.Y.(Z+1)-0.<timestamp>-<hash>
|
||||
// after the release vX.Y.Z, vX.Y.Z-<pre>.0.<timestamp>-<hash> after the
|
||||
// pre-release vX.Y.Z-<pre>; newer is at or above vX.Y.(Z+1) and vX.Y.Z,
|
||||
// Go's own version order. Nothing is newer than an installed version
|
||||
// Comparable refuses (a pseudo-version with no tag before its commit, a
|
||||
// bare commit hash, a pre-release, ""). release must be a valid release.
|
||||
func Newer(release, installed string) bool {
|
||||
if !ValidVersion(release) {
|
||||
return false
|
||||
}
|
||||
if base := BaseVersion(installed); base != "" {
|
||||
return CompareVersions(release, base) > 0
|
||||
}
|
||||
if !isPseudoVersion(installed) || pseudoNoBaseRe.MatchString(installed) {
|
||||
return false
|
||||
}
|
||||
core := pseudoBaseRe.FindStringSubmatch(installed)
|
||||
if core == nil || !ValidVersion(core[1]) {
|
||||
return false
|
||||
}
|
||||
return CompareVersions(release, core[1]) >= 0
|
||||
}
|
||||
|
||||
// Comparable reports whether Newer can place installed among releases: a
|
||||
// release, a git describe string, or a pseudo-version that builds on a
|
||||
// release or a pre-release.
|
||||
func Comparable(installed string) bool {
|
||||
if BaseVersion(installed) != "" {
|
||||
return true
|
||||
}
|
||||
if !isPseudoVersion(installed) || pseudoNoBaseRe.MatchString(installed) {
|
||||
return false
|
||||
}
|
||||
core := pseudoBaseRe.FindStringSubmatch(installed)
|
||||
return core != nil && ValidVersion(core[1])
|
||||
}
|
||||
|
||||
// Satisfies reports whether the release have meets the minimum min. An
|
||||
// empty minimum is always met. A pseudo-version minimum such as
|
||||
// vX.Y.Z-0.<timestamp>-<hash> sorts just below vX.Y.Z, so it is met by
|
||||
|
||||
@@ -99,6 +99,46 @@ func TestVersions(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Newer", func(t *testing.T) {
|
||||
cases := []struct {
|
||||
release, installed string
|
||||
want bool
|
||||
}{
|
||||
{"v0.4.1", "v0.4.0", true}, {"v0.4.0", "v0.4.0", false}, {"v0.3.9", "v0.4.0", false},
|
||||
{"v0.4.1", "v0.4.0-3-gabc1234", true}, {"v0.4.0", "v0.4.0-3-gabc1234", false},
|
||||
// A commit after v0.4.0 (go get M@main): v0.4.0 and older are
|
||||
// downgrades; v0.4.1 is the next release in Go's order.
|
||||
{"v0.4.0", "v0.4.1-0.20261001120000-abcdef123456", false},
|
||||
{"v0.3.0", "v0.4.1-0.20261001120000-abcdef123456", false},
|
||||
{"v0.4.1", "v0.4.1-0.20261001120000-abcdef123456", true},
|
||||
{"v0.5.0", "v0.4.1-0.20261001120000-abcdef123456", true},
|
||||
// A commit after the pre-release v0.5.0-rc.1.
|
||||
{"v0.4.9", "v0.5.0-rc.1.0.20261001120000-abcdef123456", false},
|
||||
{"v0.5.0", "v0.5.0-rc.1.0.20261001120000-abcdef123456", true},
|
||||
// No tag before the commit, a bare hash, a pre-release, nothing:
|
||||
// never newer.
|
||||
{"v0.1.0", "v0.0.0-20261001120000-abcdef123456", false},
|
||||
{"v9.0.0", "abc1234", false}, {"v9.0.0", "v0.5.0-rc.1", false}, {"v9.0.0", "", false},
|
||||
{"main", "v0.4.0", false}, {"v0.5.0-rc.1", "v0.4.0", false},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := Newer(c.release, c.installed); got != c.want {
|
||||
t.Errorf("Newer(%q, %q) = %v, want %v", c.release, c.installed, got, c.want)
|
||||
}
|
||||
}
|
||||
for v, want := range map[string]bool{
|
||||
"v0.4.0": true, "v0.4.0-3-gabc1234": true,
|
||||
"v0.4.1-0.20261001120000-abcdef123456": true,
|
||||
"v0.5.0-rc.1.0.20261001120000-abcdef123456": true,
|
||||
"v0.0.0-20261001120000-abcdef123456": false,
|
||||
"abc1234": false, "v0.5.0-rc.1": false, "": false,
|
||||
} {
|
||||
if got := Comparable(v); got != want {
|
||||
t.Errorf("Comparable(%q) = %v, want %v", v, got, want)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("NewestCompatible", func(t *testing.T) {
|
||||
if _, ok := NewestCompatible(nil, "v0.6.0"); ok {
|
||||
t.Error("nil list found a version")
|
||||
|
||||
Reference in New Issue
Block a user