Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ jobs:
run: |
# Tolerate a leading "v" (e.g. v1.0.0) so the tag isn't doubled into "vv1.0.0".
VERSION="${VERSION#v}"
# Validate via the shared script (same semver check as the auto-updater).
go run ./cmd/scripts/semver "$VERSION"
# Validate via the shared script (same semver check as the auto-updater) and
# ensure it matches lib.SoftwareVersion.
go run ./cmd/scripts/semver --check-server-version "$VERSION"
echo "tag=v${VERSION}" >> "$GITHUB_OUTPUT"
echo "Version '$VERSION' is valid. Release tag: v${VERSION}"

Expand Down
7 changes: 3 additions & 4 deletions cmd/auto-update/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"time"

"github.com/canopy-network/canopy/cmd/cli"
"github.com/canopy-network/canopy/cmd/rpc"
"github.com/canopy-network/canopy/lib"
)

Expand Down Expand Up @@ -109,9 +108,9 @@ func main() {
binaryVersion := getSoftwareVersion(autoUpdaterEnabled, binPath)
if autoUpdaterEnabled {
logger.Infof("auto-update enabled (updater version: %s, canopy version: %s)",
rpc.SoftwareVersion, binaryVersion)
lib.SoftwareVersion, binaryVersion)
} else {
logger.Infof("auto-update disabled (canopy version: %s)", rpc.SoftwareVersion)
logger.Infof("auto-update disabled (canopy version: %s)", lib.SoftwareVersion)
}
// handle external shutdown signals
sigChan := make(chan os.Signal, 1)
Expand Down Expand Up @@ -275,7 +274,7 @@ func effectiveBinPath(paths ...string) string {

func getSoftwareVersion(autoUpdate bool, binPath string) string {
if !autoUpdate {
return rpc.SoftwareVersion
return lib.SoftwareVersion
}
// forward only --data-dir so the check targets the initialized directory and
// avoids passing newer flags an older installed binary may not recognize
Expand Down
4 changes: 2 additions & 2 deletions cmd/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ var versionCmd = &cobra.Command{
return nil
},
Run: func(cmd *cobra.Command, args []string) {
fmt.Println(rpc.SoftwareVersion)
fmt.Println(lib.SoftwareVersion)
},
}

Expand Down Expand Up @@ -175,7 +175,7 @@ func Start() {
l.Fatal(err.Error())
}
// initialize and start the metrics server
metrics := lib.NewMetricsServer(validatorKey.PublicKey().Address(), float64(config.ChainId), rpc.SoftwareVersion, config.MetricsConfig, l)
metrics := lib.NewMetricsServer(validatorKey.PublicKey().Address(), float64(config.ChainId), lib.SoftwareVersion, config.MetricsConfig, l)
// create a new database object from the config
db, err := store.New(config, metrics, l)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion cmd/rpc/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import (

// Version writes Canopy software's version information
func (s *Server) Version(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
write(w, SoftwareVersion, http.StatusOK)
write(w, lib.SoftwareVersion, http.StatusOK)
}

// Transaction submits a transaction
Expand Down
3 changes: 0 additions & 3 deletions cmd/rpc/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,6 @@ import (
const (
colon = ":"

// uses golang's semver naming convention
// https://pkg.go.dev/golang.org/x/mod/semver
SoftwareVersion = "v0.1.31+beta"
ContentType = "Content-MessageType"
ApplicationJSON = "application/json; charset=utf-8"

Expand Down
29 changes: 23 additions & 6 deletions cmd/scripts/semver/main.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
// Command semver validates that its argument is valid semver via
// lib.IsValidVersion (the same check the auto-updater uses), so the release
// GitHub Actions fail fast on malformed versions before a release is created.
// Usage: semver <version> (a leading "v" is optional; exit 0 means valid).
//
// Usage: semver [--check-server-version] <version> (a leading "v" is optional;
// exit 0 means valid). When --check-server-version is set (used by the main
// release workflow, not the plugin releases), it additionally asserts that
// <version> matches lib.SoftwareVersion, ignoring only the optional "v" prefix
// (build metadata such as "+beta" must match).
package main

import (
"flag"
"fmt"
"os"

Expand All @@ -18,13 +24,24 @@ func main() {
}
}

// run validates the single version argument.
// run validates the single version argument, optionally cross-checking it
// against lib.SoftwareVersion.
func run(args []string) error {
if len(args) != 1 || args[0] == "" {
return fmt.Errorf("usage: semver <version>")
fs := flag.NewFlagSet("semver", flag.ContinueOnError)
checkServerVersion := fs.Bool("check-server-version", false,
"also require the version to match lib.SoftwareVersion")
if err := fs.Parse(args); err != nil {
return err
}
if !lib.IsValidVersion(args[0]) {
return fmt.Errorf("invalid semantic version: %q (expected MAJOR.MINOR.PATCH, e.g. 1.2.3 or v1.2.3)", args[0])
if fs.NArg() != 1 || fs.Arg(0) == "" {
return fmt.Errorf("usage: semver [--check-server-version] <version>")
}
version := fs.Arg(0)
if !lib.IsValidVersion(version) {
return fmt.Errorf("invalid semantic version: %q (expected MAJOR.MINOR.PATCH, e.g. 1.2.3 or v1.2.3)", version)
}
if *checkServerVersion && !lib.MatchesSoftwareVersion(version) {
return fmt.Errorf("version mismatch: remember to update the version number in lib/version.go")
}
return nil
}
26 changes: 26 additions & 0 deletions cmd/scripts/semver/main_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package main

import (
"strings"
"testing"

"github.com/canopy-network/canopy/lib"
"github.com/stretchr/testify/require"
)

Expand All @@ -27,3 +29,27 @@ func TestRun(t *testing.T) {
require.Error(t, run([]string{"1.0.0", "2.0.0"}))
})
}

func TestRunCheckServerVersion(t *testing.T) {
// Strip only the optional "v"; build metadata like "+beta" is significant.
version := strings.TrimPrefix(lib.SoftwareVersion, "v")
t.Run("matching bare version", func(t *testing.T) {
require.NoError(t, run([]string{"--check-server-version", version}))
})
t.Run("matching v-prefixed version", func(t *testing.T) {
require.NoError(t, run([]string{"--check-server-version", "v" + version}))
})
t.Run("mismatched version", func(t *testing.T) {
require.Error(t, run([]string{"--check-server-version", "99.99.99"}))
})
t.Run("mismatched build metadata", func(t *testing.T) {
i := strings.IndexByte(version, '+')
if i < 0 {
t.Skip("server version has no build metadata to differ on")
}
require.Error(t, run([]string{"--check-server-version", version[:i]}))
})
t.Run("skips check when flag absent", func(t *testing.T) {
require.NoError(t, run([]string{"99.99.99"}))
})
}
15 changes: 0 additions & 15 deletions lib/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import (
"unsafe"

"github.com/canopy-network/canopy/lib/crypto"
"golang.org/x/mod/semver"
"google.golang.org/protobuf/encoding/protowire"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
Expand Down Expand Up @@ -1100,17 +1099,3 @@ func RandSlice(byteSize uint64) []byte {
rand.Read(value)
return value
}

// IsValidVersion reports whether version is valid semver, accepting an optional
// leading "v". Shared so the release script and auto-updater validate alike.
func IsValidVersion(version string) bool {
return semver.IsValid(normalizeVersion(version))
}

// normalizeVersion adds the leading "v" that golang.org/x/mod/semver requires.
func normalizeVersion(version string) string {
if version == "" || version[0] == 'v' {
return version
}
return "v" + version
}
43 changes: 0 additions & 43 deletions lib/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -637,46 +637,3 @@ func TestLoadCountedCallbackError(t *testing.T) {
page := NewPage(PageParams{PageNumber: 1, PerPage: 10}, TxResultsPageName)
require.Error(t, page.LoadCounted(25, &results, func(index int) ErrorI { return ErrInvalidArgument() }))
}

func TestIsValidVersion(t *testing.T) {
tests := []struct {
name string
input string
want bool
}{
// valid, bare (as entered in the manual release flow)
{"bare patch", "1.0.0", true},
{"bare large", "10.20.30", true},
{"bare prerelease", "1.0.0-rc.1", true},
{"bare prerelease zero", "1.2.3-0", true},
{"bare prerelease alnum", "1.2.3-alpha.1", true},
// valid, v-prefixed (as passed by old callers / full tags)
{"v patch", "v1.0.0", true},
{"v build metadata", "v1.2.3+build.5", true},
// semver.IsValid accepts partial versions; documented here so the
// behavior is intentional and does not surprise future readers.
{"v major only", "v1", true},
{"v major minor", "v1.2", true},
// invalid: the original bug that motivated this check
{"four segments bare", "1.0.4.5", false},
{"four segments v", "v1.0.4.5", false},
// invalid prerelease/build forms rejected by x/mod/semver
{"leading zero prerelease", "1.2.3-01", false},
{"empty prerelease identifiers", "1.2.3-..", false},
{"trailing dot prerelease", "1.2.3-alpha.", false},
// invalid: not versions at all
{"garbage", "foo", false},
{"empty", "", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.want, IsValidVersion(tt.input))
})
}
}

func TestNormalizeVersion(t *testing.T) {
require.Equal(t, "v1.2.3", normalizeVersion("1.2.3"))
require.Equal(t, "v1.2.3", normalizeVersion("v1.2.3"))
require.Equal(t, "", normalizeVersion(""))
}
32 changes: 32 additions & 0 deletions lib/version.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package lib

import (
"strings"

"golang.org/x/mod/semver"
)

// SoftwareVersion is the running Canopy software version. It follows golang's
// semver naming convention (https://pkg.go.dev/golang.org/x/mod/semver).
const SoftwareVersion = "v0.1.31+beta"

// IsValidVersion reports whether version is valid semver, accepting an optional
// leading "v". Shared so the release script and auto-updater validate alike.
func IsValidVersion(version string) bool {
return semver.IsValid(normalizeVersion(version))
}

// MatchesSoftwareVersion reports whether version refers to the running
// SoftwareVersion, accepting an optional leading "v" on either. Build metadata
// (e.g. "+beta") is significant.
func MatchesSoftwareVersion(version string) bool {
return strings.TrimPrefix(version, "v") == strings.TrimPrefix(SoftwareVersion, "v")
}

// normalizeVersion adds the leading "v" that golang.org/x/mod/semver requires.
func normalizeVersion(version string) string {
if version == "" || version[0] == 'v' {
return version
}
return "v" + version
}
62 changes: 62 additions & 0 deletions lib/version_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package lib

import (
"strings"
"testing"

"github.com/stretchr/testify/require"
)

func TestIsValidVersion(t *testing.T) {
tests := []struct {
name string
input string
want bool
}{
// valid, bare (as entered in the manual release flow)
{"bare patch", "1.0.0", true},
{"bare large", "10.20.30", true},
{"bare prerelease", "1.0.0-rc.1", true},
{"bare prerelease zero", "1.2.3-0", true},
{"bare prerelease alnum", "1.2.3-alpha.1", true},
// valid, v-prefixed (as passed by old callers / full tags)
{"v patch", "v1.0.0", true},
{"v build metadata", "v1.2.3+build.5", true},
// semver.IsValid accepts partial versions; documented here so the
// behavior is intentional and does not surprise future readers.
{"v major only", "v1", true},
{"v major minor", "v1.2", true},
// invalid: the original bug that motivated this check
{"four segments bare", "1.0.4.5", false},
{"four segments v", "v1.0.4.5", false},
// invalid prerelease/build forms rejected by x/mod/semver
{"leading zero prerelease", "1.2.3-01", false},
{"empty prerelease identifiers", "1.2.3-..", false},
{"trailing dot prerelease", "1.2.3-alpha.", false},
// invalid: not versions at all
{"garbage", "foo", false},
{"empty", "", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.want, IsValidVersion(tt.input))
})
}
}

func TestMatchesSoftwareVersion(t *testing.T) {
// Strip only the optional "v"; build metadata like "+beta" is significant.
version := strings.TrimPrefix(SoftwareVersion, "v")
require.True(t, MatchesSoftwareVersion(version))
require.True(t, MatchesSoftwareVersion("v"+version))
require.False(t, MatchesSoftwareVersion("99.99.99"))
if i := strings.IndexByte(version, '+'); i >= 0 {
require.False(t, MatchesSoftwareVersion(version[:i]))
}
}

func TestNormalizeVersion(t *testing.T) {
require.Equal(t, "v1.2.3", normalizeVersion("1.2.3"))
require.Equal(t, "v1.2.3", normalizeVersion("v1.2.3"))
require.Equal(t, "", normalizeVersion(""))
}
Loading