From fa3bb7dae42d3d4cf4e4afa5cf30748aaf157a94 Mon Sep 17 00:00:00 2001 From: Siddharth Maddikayala Date: Thu, 5 Feb 2026 10:34:24 -0800 Subject: [PATCH 1/2] Add rollback integration to undo init on failure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When any step in the init flow fails, a deferred rollback now cleans up all scaffolded artifacts — frontend/backend directories are removed and the MongoDB seed collection is dropped. Firebase rollback is a no-op for now. Co-Authored-By: Claude Opus 4.6 --- internal/cli/rollback.go | 21 +++++++++++++++++++ internal/cli/root.go | 30 ++++++++++++++++++++-------- internal/fsutil/fsutil.go | 4 ++++ internal/stacks/express/express.go | 10 ++++++++++ internal/stacks/firebase/firebase.go | 4 ++++ internal/stacks/mongodb/mongodb.go | 27 +++++++++++++++++++++++++ internal/stacks/nextjs/nextjs.go | 10 ++++++++++ internal/stacks/types.go | 1 + 8 files changed, 99 insertions(+), 8 deletions(-) create mode 100644 internal/cli/rollback.go diff --git a/internal/cli/rollback.go b/internal/cli/rollback.go new file mode 100644 index 0000000..8574efc --- /dev/null +++ b/internal/cli/rollback.go @@ -0,0 +1,21 @@ +package cli + +import ( + "context" + "log" + + "github.com/b-jonathan/taco/internal/stacks" +) + +func rollbackStacks(ctx context.Context, opts *stacks.Options, ss ...stacks.Stack) { + for _, s := range ss { + if s == nil { + continue + } + log.Printf("Rolling back %s stack...", s.Name()) + + if err := s.Rollback(ctx, opts); err != nil { + log.Printf("rollback %s failed: %v", s.Name(), err) + } + } +} diff --git a/internal/cli/root.go b/internal/cli/root.go index c273f00..8527217 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -175,6 +175,19 @@ func initCmd() *cobra.Command { Port: 4000, } + // Rollback logic + rollbackNeeded := true + defer func() { + if !rollbackNeeded { + return + } + log.Println("Init failed, starting rollback...") + // use a fresh context for rollback so it isn't canceled + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + rollbackStacks(ctx, opts, frontend, backend, database, auth) + }() + // This is core core g, ctx := errgroup.WithContext(rootCtx) @@ -182,15 +195,15 @@ func initCmd() *cobra.Command { g.Go(func() error { return runSelected(ctx, "Frontend", frontend, opts, []string{"init", "generate"}) }) g.Go(func() error { return runSelected(ctx, "Backend", backend, opts, []string{"init", "generate"}) }) g.Go(func() error { return runSelected(ctx, "Database", database, opts, []string{"init", "seed"}) }) - g.Go(func() error { return runSelected(ctx, "Auth", auth, opts, []string{"init"}) }) + // g.Go(func() error { return runSelected(ctx, "Auth", auth, opts, []string{"init"}) }) if err := g.Wait(); err != nil { return err } - if err := runSelected(rootCtx, "Auth", auth, opts, []string{"generate"}); err != nil { - return err - } + // if err := runSelected(rootCtx, "Auth", auth, opts, []string{"generate"}); err != nil { + // return err + // } if err := runSelected(rootCtx, "Database", database, opts, []string{"generate"}); err != nil { return err @@ -201,9 +214,9 @@ func initCmd() *cobra.Command { if err := runSelected(ctx, "Frontend", frontend, opts, []string{"post"}); err != nil { return err } - if err := runSelected(ctx, "Auth", auth, opts, []string{"post"}); err != nil { - return err - } + // if err := runSelected(ctx, "Auth", auth, opts, []string{"post"}); err != nil { + // return err + // } return nil }) g.Go(func() error { @@ -215,7 +228,7 @@ func initCmd() *cobra.Command { } return nil }) - // g.Go(func() error { return runSelected(ctx, "Database", database, opts, []string{"post"}) }) + g.Go(func() error { return runSelected(ctx, "Database", database, opts, []string{"post"}) }) if err := g.Wait(); err != nil { return err @@ -254,6 +267,7 @@ func initCmd() *cobra.Command { // return err // } // log.Println("Pushed:", repo.GetHTMLURL()) + rollbackNeeded = false log.Println("Time Taken:", time.Since(start)) return nil }, diff --git a/internal/fsutil/fsutil.go b/internal/fsutil/fsutil.go index c7c5966..c01256a 100644 --- a/internal/fsutil/fsutil.go +++ b/internal/fsutil/fsutil.go @@ -76,6 +76,10 @@ func WithFileLock(path string, fn func() error) error { return fn() } +func RemoveDir(path string) error { + return os.RemoveAll(path) +} + func RenderTemplate(tmplPath string) ([]byte, error) { tmplPath = filepath.Join("internal", "stacks", "templates", tmplPath) tmpl, err := template.ParseFiles(tmplPath) diff --git a/internal/stacks/express/express.go b/internal/stacks/express/express.go index b97fd5d..2099f2d 100644 --- a/internal/stacks/express/express.go +++ b/internal/stacks/express/express.go @@ -164,3 +164,13 @@ func (express) Post(ctx context.Context, opts *Options) error { } return nil } + +func (express) Rollback(ctx context.Context, opts *Options) error { + backendDir := filepath.Join(opts.ProjectRoot, "backend") + + if err := fsutil.RemoveDir(backendDir); err != nil { + return fmt.Errorf("remove backend dir: %w", err) + } + + return nil +} diff --git a/internal/stacks/firebase/firebase.go b/internal/stacks/firebase/firebase.go index af9b718..41107f0 100644 --- a/internal/stacks/firebase/firebase.go +++ b/internal/stacks/firebase/firebase.go @@ -262,3 +262,7 @@ func (express) Post(ctx context.Context, opts *Options) error { fmt.Println("Firebase post-generation complete. Added Firebase ignores and credentials.") return nil } + +func (express) Rollback(ctx context.Context, opts *Options) error { + return nil +} diff --git a/internal/stacks/mongodb/mongodb.go b/internal/stacks/mongodb/mongodb.go index 183061b..9509703 100644 --- a/internal/stacks/mongodb/mongodb.go +++ b/internal/stacks/mongodb/mongodb.go @@ -2,6 +2,7 @@ package mongodb import ( "context" + "errors" "fmt" "os" "path/filepath" @@ -203,3 +204,29 @@ func (mongodb) Post(ctx context.Context, opts *Options) error { _ = fsutil.AppendUniqueLines(path, []string{content}) return nil } + +func (mongodb) Rollback(ctx context.Context, opts *Options) error { + if opts.DatabaseURI == "" { + return nil + } + client, err := mongo.Connect(ctx, options.Client().ApplyURI(opts.DatabaseURI)) + if err != nil { + return fmt.Errorf("connect mongo for rollback: %w", err) + } + defer func() { _ = client.Disconnect(ctx) }() + + db := client.Database(opts.AppName) + col := db.Collection("seed_test") + + if err := col.Drop(ctx); err != nil { + var cmdErr mongo.CommandError + // Code 26 = NamespaceNotFound (collection doesn't exist) — safe to ignore + if errors.As(err, &cmdErr) && cmdErr.Code == 26 { + return nil + } + return fmt.Errorf("drop seed_test collection: %w", err) + } + + fmt.Println("Rolled back MongoDB seed data (dropped collection seed_test)") + return nil +} diff --git a/internal/stacks/nextjs/nextjs.go b/internal/stacks/nextjs/nextjs.go index 874490d..950f2dc 100644 --- a/internal/stacks/nextjs/nextjs.go +++ b/internal/stacks/nextjs/nextjs.go @@ -155,3 +155,13 @@ func (nextjs) Post(ctx context.Context, opts *Options) error { } return nil } + +func (nextjs) Rollback(ctx context.Context, opts *Options) error { + frontendDir := filepath.Join(opts.ProjectRoot, "frontend") + + if err := fsutil.RemoveDir(frontendDir); err != nil { + return fmt.Errorf("remove frontend dir: %w", err) + } + + return nil +} diff --git a/internal/stacks/types.go b/internal/stacks/types.go index 40a0035..b5470ff 100644 --- a/internal/stacks/types.go +++ b/internal/stacks/types.go @@ -8,6 +8,7 @@ type Stack interface { Init(ctx context.Context, opts *Options) error Generate(ctx context.Context, opts *Options) error Post(ctx context.Context, opts *Options) error + Rollback(ctx context.Context, opts *Options) error } type Seeder interface { From fc66bf3d728b8018d189f07d59a3551f16e6e924 Mon Sep 17 00:00:00 2001 From: "ribellye@gmail.com" Date: Wed, 4 Mar 2026 20:45:50 -0800 Subject: [PATCH 2/2] patches --- internal/cli/rollback.go | 6 ++-- internal/cli/root.go | 54 ++++++---------------------- internal/stacks/firebase/firebase.go | 2 +- internal/stacks/mongodb/mongodb.go | 2 +- 4 files changed, 16 insertions(+), 48 deletions(-) diff --git a/internal/cli/rollback.go b/internal/cli/rollback.go index 8574efc..bdc7e9d 100644 --- a/internal/cli/rollback.go +++ b/internal/cli/rollback.go @@ -2,7 +2,7 @@ package cli import ( "context" - "log" + "fmt" "github.com/b-jonathan/taco/internal/stacks" ) @@ -12,10 +12,10 @@ func rollbackStacks(ctx context.Context, opts *stacks.Options, ss ...stacks.Stac if s == nil { continue } - log.Printf("Rolling back %s stack...", s.Name()) + fmt.Printf("Rolling back %s stack...\n", s.Name()) if err := s.Rollback(ctx, opts); err != nil { - log.Printf("rollback %s failed: %v", s.Name(), err) + fmt.Printf("rollback %s failed: %v\n", s.Name(), err) } } } diff --git a/internal/cli/root.go b/internal/cli/root.go index 77ea388..5f00fe5 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -211,11 +211,11 @@ func initCmd() *cobra.Command { if !rollbackNeeded { return } - log.Println("Init failed, starting rollback...") + fmt.Println("Init failed, starting rollback...") // use a fresh context for rollback so it isn't canceled - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + rbCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() - rollbackStacks(ctx, opts, frontend, backend, database, auth) + rollbackStacks(rbCtx, opts, frontend, backend, database, auth) }() // This is core core @@ -225,15 +225,15 @@ func initCmd() *cobra.Command { g.Go(func() error { return runSelected(ctx, "Frontend", frontend, opts, []string{"init", "generate"}) }) g.Go(func() error { return runSelected(ctx, "Backend", backend, opts, []string{"init", "generate"}) }) g.Go(func() error { return runSelected(ctx, "Database", database, opts, []string{"init", "seed"}) }) - // g.Go(func() error { return runSelected(ctx, "Auth", auth, opts, []string{"init"}) }) + g.Go(func() error { return runSelected(ctx, "Auth", auth, opts, []string{"init"}) }) if err := g.Wait(); err != nil { return err } - // if err := runSelected(rootCtx, "Auth", auth, opts, []string{"generate"}); err != nil { - // return err - // } + if err := runSelected(rootCtx, "Auth", auth, opts, []string{"generate"}); err != nil { + return err + } if err := runSelected(rootCtx, "Database", database, opts, []string{"generate"}); err != nil { return err @@ -244,9 +244,9 @@ func initCmd() *cobra.Command { if err := runSelected(ctx, "Frontend", frontend, opts, []string{"post"}); err != nil { return err } - // if err := runSelected(ctx, "Auth", auth, opts, []string{"post"}); err != nil { - // return err - // } + if err := runSelected(ctx, "Auth", auth, opts, []string{"post"}); err != nil { + return err + } return nil }) g.Go(func() error { @@ -291,42 +291,10 @@ func initCmd() *cobra.Command { return fmt.Errorf("git push failed: %w", err) } - // TODO: We're gonna have to add a functionality to "optionally" make github repo - // TODO: We're gonna have to add more gh functionality, more on the gh and git package (ci/cd stuff) - - // log.Println("Starting gh command") - // client := gh.MustFromContext(cmd.Context()) - // log.Println("GitHub client initialized") - // ctx, cancel := context.WithTimeout(cmd.Context(), 10*time.Second) - // defer cancel() - - // newRepo := &github.Repository{ - // Name: github.String(params.Name), - // Private: github.Bool(params.Private), - // Description: github.String(params.Description), - // } - - // repo, _, err := client.Repositories.Create(ctx, "", newRepo) - // if err != nil { - // return fmt.Errorf("create repo: %w", err) - // } - - // log.Println(cmd.OutOrStdout(), "Created:", repo.GetHTMLURL()) - // remoteURL := repo.GetSSHURL() - // if params.Remote == "https" { - // remoteURL = repo.GetCloneURL() - // } - // log.Println("Committing and Pushing to Github...") - // if err := git.InitAndPush(ctx, projectRoot, remoteURL, "chore: initial commit"); err != nil { - // _, err := client.Repositories.Delete(ctx, "", *newRepo.Name) - // return err - // } - // log.Println("Pushed:", repo.GetHTMLURL()) - rollbackNeeded = false - log.Println("Time Taken:", time.Since(start)) fmt.Println("Pushed:", repo.GetHTMLURL()) } + rollbackNeeded = false fmt.Println("Time Taken:", time.Since(start)) return nil }, diff --git a/internal/stacks/firebase/firebase.go b/internal/stacks/firebase/firebase.go index 640281a..d6ee02f 100644 --- a/internal/stacks/firebase/firebase.go +++ b/internal/stacks/firebase/firebase.go @@ -177,6 +177,6 @@ func (firebase) Post(ctx context.Context, opts *Options) error { return nil } -func (express) Rollback(ctx context.Context, opts *Options) error { +func (firebase) Rollback(ctx context.Context, opts *Options) error { return nil } diff --git a/internal/stacks/mongodb/mongodb.go b/internal/stacks/mongodb/mongodb.go index f3d6b18..7c28bb2 100644 --- a/internal/stacks/mongodb/mongodb.go +++ b/internal/stacks/mongodb/mongodb.go @@ -214,7 +214,7 @@ func (mongodb) Rollback(ctx context.Context, opts *Options) error { col := db.Collection("seed_test") if err := col.Drop(ctx); err != nil { - var cmdErr mongo.CommandError + var cmdErr *mongo.CommandError // Code 26 = NamespaceNotFound (collection doesn't exist) — safe to ignore if errors.As(err, &cmdErr) && cmdErr.Code == 26 { return nil