diff --git a/changelog.md b/changelog.md index fc766c975b..e918424959 100644 --- a/changelog.md +++ b/changelog.md @@ -15,6 +15,7 @@ ### Fixes +- [#4886](https://github.com/ignite/cli/pull/4886) Fix chain scaffolding checks. - [#4889](https://github.com/ignite/cli/pull/4889) Plugin data race. ## [`v29.8.0`](https://github.com/ignite/cli/releases/tag/v29.8.0) diff --git a/ignite/services/plugin/plugin.go b/ignite/services/plugin/plugin.go index d612068b48..718df2268b 100644 --- a/ignite/services/plugin/plugin.go +++ b/ignite/services/plugin/plugin.go @@ -425,5 +425,8 @@ func (p *Plugin) outdatedBinary() bool { fmt.Printf("error while walking app source path %q\n", p.srcPath) return false } - return mostRecent.After(binaryTime) + // Rebuild when source files are newer OR have the same timestamp as the binary. + // In some environments (such as fresh CI checkouts), mtimes can be normalized + // to identical values, and strict "after" checks may incorrectly reuse stale binaries. + return !mostRecent.Before(binaryTime) } diff --git a/ignite/services/plugin/plugin_test.go b/ignite/services/plugin/plugin_test.go index cc9bbb98cf..99f2320241 100644 --- a/ignite/services/plugin/plugin_test.go +++ b/ignite/services/plugin/plugin_test.go @@ -541,6 +541,28 @@ func TestPluginClean(t *testing.T) { } } +func TestPluginOutdatedBinary(t *testing.T) { + t.Run("returns true when source and binary mtimes are equal", func(t *testing.T) { + tmp := t.TempDir() + srcFile := filepath.Join(tmp, "main.go") + binFile := filepath.Join(tmp, "app.ign") + + require.NoError(t, os.WriteFile(srcFile, []byte("package main\n"), 0o644)) + require.NoError(t, os.WriteFile(binFile, []byte("binary"), 0o755)) + + equalTime := time.Now().Add(-time.Minute).Truncate(time.Second) + require.NoError(t, os.Chtimes(srcFile, equalTime, equalTime)) + require.NoError(t, os.Chtimes(binFile, equalTime, equalTime)) + + p := Plugin{ + srcPath: tmp, + name: "app", + } + + require.True(t, p.outdatedBinary()) + }) +} + // scaffoldPlugin runs Scaffold and updates the go.mod so it uses the // current ignite/cli sources. func scaffoldPlugin(t *testing.T, dir, name string, sharedHost bool) string { diff --git a/ignite/services/scaffolder/component.go b/ignite/services/scaffolder/component.go index cb985be446..3f8f546193 100644 --- a/ignite/services/scaffolder/component.go +++ b/ignite/services/scaffolder/component.go @@ -106,6 +106,35 @@ func checkComponentCreated(appPath, moduleName string, compName multiformatname. return err } +// checkTypeProtoCreated checks if the proto type already exists in the module proto package. +func checkTypeProtoCreated( + ctx context.Context, + appPath, appName, protoDir, moduleName string, + compName multiformatname.Name, +) error { + path := filepath.Join(appPath, protoDir, appName, moduleName) + pkgs, err := protoanalysis.Parse(ctx, protoanalysis.NewCache(), path) + if err != nil { + return err + } + + for _, pkg := range pkgs { + for _, msg := range pkg.Messages { + if !strings.EqualFold(msg.Name, compName.PascalCase) { + continue + } + + return errors.Errorf("component %s with name %s is already created (type %s exists)", + componentType, + compName.Original, + msg.Name, + ) + } + } + + return nil +} + // checkCustomTypes returns error if one of the types is invalid. func checkCustomTypes(ctx context.Context, appPath, appName, protoDir, module string, fields []string) error { path := filepath.Join(appPath, protoDir, appName, module) diff --git a/ignite/services/scaffolder/component_test.go b/ignite/services/scaffolder/component_test.go index 117f49d014..791f2685f6 100644 --- a/ignite/services/scaffolder/component_test.go +++ b/ignite/services/scaffolder/component_test.go @@ -1,6 +1,9 @@ package scaffolder import ( + "context" + "os" + "path/filepath" "testing" "github.com/stretchr/testify/require" @@ -105,3 +108,42 @@ func TestContainsCustomTypes(t *testing.T) { }) } } + +func TestCheckTypeProtoCreated(t *testing.T) { + t.Run("should fail when proto type already exists", func(t *testing.T) { + tmp := t.TempDir() + protoFile := filepath.Join(tmp, "proto", "blog", "blog", "v1", "post.proto") + require.NoError(t, os.MkdirAll(filepath.Dir(protoFile), 0o755)) + + content := `syntax = "proto3"; +package blog.blog.v1; + +message Post {} +` + require.NoError(t, os.WriteFile(protoFile, []byte(content), 0o644)) + + name, err := multiformatname.NewName("post") + require.NoError(t, err) + + err = checkTypeProtoCreated(context.Background(), tmp, "blog", "proto", "blog", name) + require.EqualError(t, err, "component type with name post is already created (type Post exists)") + }) + + t.Run("should pass when proto type does not exist", func(t *testing.T) { + tmp := t.TempDir() + protoFile := filepath.Join(tmp, "proto", "blog", "blog", "v1", "comment.proto") + require.NoError(t, os.MkdirAll(filepath.Dir(protoFile), 0o755)) + + content := `syntax = "proto3"; +package blog.blog.v1; + +message Comment {} +` + require.NoError(t, os.WriteFile(protoFile, []byte(content), 0o644)) + + name, err := multiformatname.NewName("post") + require.NoError(t, err) + + require.NoError(t, checkTypeProtoCreated(context.Background(), tmp, "blog", "proto", "blog", name)) + }) +} diff --git a/ignite/services/scaffolder/type.go b/ignite/services/scaffolder/type.go index 046f47ded1..1a49ec8c3a 100644 --- a/ignite/services/scaffolder/type.go +++ b/ignite/services/scaffolder/type.go @@ -72,7 +72,11 @@ func SingletonType() AddTypeKind { // DryType only creates a type with a basic definition. func DryType() AddTypeKind { - return func(*addTypeOptions) {} + return func(o *addTypeOptions) { + // Dry type scaffolding only adds a proto type definition and never generates CRUD messages. + // Force this option so component validity checks don't treat existing Msg* types as conflicts. + o.withoutMessage = true + } } // TypeWithModule module to scaffold type into. @@ -140,6 +144,9 @@ func (s Scaffolder) AddType( if err := checkComponentValidity(s.appPath, moduleName, name, o.withoutMessage); err != nil { return err } + if err := checkTypeProtoCreated(ctx, s.appPath, s.modpath.Package, s.protoDir, moduleName, name); err != nil { + return err + } // Check and parse provided fields if err := checkCustomTypes(ctx, s.appPath, s.modpath.Package, s.protoDir, moduleName, o.fields); err != nil { diff --git a/ignite/services/scaffolder/type_test.go b/ignite/services/scaffolder/type_test.go index 8d6dbd2a2b..50362a4823 100644 --- a/ignite/services/scaffolder/type_test.go +++ b/ignite/services/scaffolder/type_test.go @@ -131,6 +131,18 @@ func TestParseTypeFields(t *testing.T) { }, }, }, + { + name: "dry type defaults to no message", + addKind: DryType(), + addOptions: []AddTypeOption{}, + expectedOptions: addTypeOptions{ + moduleName: testModuleName, + withoutMessage: true, + signer: testSigner, + }, + shouldError: false, + expectedFields: nil, + }, } for _, tc := range tests {