From 9a7d1326eecd03e12c880c5195e36bfab78bc86a Mon Sep 17 00:00:00 2001 From: Grezle <4310551+grezle@users.noreply.github.com> Date: Tue, 30 Sep 2025 16:27:36 +0100 Subject: [PATCH 1/4] fix: create tmp directory in working directory --- pkg/commands/template/upgrade.go | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/pkg/commands/template/upgrade.go b/pkg/commands/template/upgrade.go index 8f87631a..0c0af169 100644 --- a/pkg/commands/template/upgrade.go +++ b/pkg/commands/template/upgrade.go @@ -145,18 +145,34 @@ func createUpgradeCommand( return fmt.Errorf("failed to get current working directory: %w", err) } - // Create temporary directory for cloning the template - tempDir, err := os.MkdirTemp("", "devkit-template-upgrade-*") - if err != nil { - return fmt.Errorf("failed to create temporary directory: %w", err) + // Ensure parent exists + tempParent := filepath.Join(absProjectPath, "temp_external") + if err := os.MkdirAll(tempParent, 0o755); err != nil { + return fmt.Errorf("failed to create %s: %w", tempParent, err) } - defer os.RemoveAll(tempDir) // Clean up on exit - tempCacheDir, err := os.MkdirTemp("", "devkit-template-cache-*") + // Create run-specific temp dir inside tempParent + tempDir, err := os.MkdirTemp(tempParent, ".tmp-devkit-template-upgrade-*") if err != nil { - return fmt.Errorf("failed to create temporary cache directory: %w", err) + return fmt.Errorf("failed to create temporary directory: %w", err) } - defer os.RemoveAll(tempCacheDir) // Clean up on exit + + // Remove tempParent if it is empty after tempDir cleanup + defer func() { + err = os.RemoveAll(tempDir) + if err != nil { + logger.Warn("failed to remove %s: %v\n", tempDir, err) + } + entries, err := os.ReadDir(tempParent) + if err != nil { + logger.Warn("could not read %s: %v\n", tempParent, err) + } + if len(entries) == 0 { + if err := os.Remove(tempParent); err != nil { + logger.Warn("failed to remove %s: %v\n", tempParent, err) + } + } + }() logger.Info("Upgrading project template:") logger.Info(" Project: %s", projectName) From d8c17e98e4290b4d6983355d2ce29ad338315e7a Mon Sep 17 00:00:00 2001 From: Grezle <4310551+grezle@users.noreply.github.com> Date: Tue, 30 Sep 2025 17:14:59 +0100 Subject: [PATCH 2/4] fix: add .gitignore entry if missing --- pkg/commands/template/upgrade.go | 16 ++++++++++-- pkg/template/git_client.go | 42 ++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/pkg/commands/template/upgrade.go b/pkg/commands/template/upgrade.go index 0c0af169..f21b94c4 100644 --- a/pkg/commands/template/upgrade.go +++ b/pkg/commands/template/upgrade.go @@ -145,8 +145,20 @@ func createUpgradeCommand( return fmt.Errorf("failed to get current working directory: %w", err) } + // Create a gitClient for upgrade process + gitClient := template.NewGitClient() + + // Ensure .gitignore entry is present for tempExternal + tempExternal := "temp_external" + if err := gitClient.EnsureGitignoreEntry(".gitignore", tempExternal); err != nil { + return fmt.Errorf("failed to add entry to .gitignore %s: %w", tempExternal, err) + } + if err := gitClient.Commit(".gitignore", fmt.Sprintf("chore: ensure %s is in .gitignore", tempExternal)); err != nil { + return fmt.Errorf("failed to commit entry to .gitignore %s: %w", tempExternal, err) + } + // Ensure parent exists - tempParent := filepath.Join(absProjectPath, "temp_external") + tempParent := filepath.Join(absProjectPath, tempExternal) if err := os.MkdirAll(tempParent, 0o755); err != nil { return fmt.Errorf("failed to create %s: %w", tempParent, err) } @@ -187,7 +199,7 @@ func createUpgradeCommand( // Fetch main template fetcher := &template.GitFetcher{ - Client: template.NewGitClient(), + Client: gitClient, Logger: *progresslogger.NewProgressLogger( logger, tracker, diff --git a/pkg/template/git_client.go b/pkg/template/git_client.go index 02c11257..49df4270 100644 --- a/pkg/template/git_client.go +++ b/pkg/template/git_client.go @@ -239,3 +239,45 @@ func (g *GitClient) ParseCloneOutput(r io.Reader, rep Reporter, dest string, ref } return nil } + +func (g *GitClient) EnsureGitignoreEntry(path, entry string) error { + // Create .gitignore if missing + f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0o644) + if err != nil { + return fmt.Errorf("open %s: %w", path, err) + } + defer f.Close() + + // Check if entry exists + scanner := bufio.NewScanner(f) + for scanner.Scan() { + if strings.TrimSpace(scanner.Text()) == entry { + return nil // already present + } + } + if err := scanner.Err(); err != nil { + return fmt.Errorf("scan %s: %w", path, err) + } + + // Append entry + if _, err := f.WriteString("\n" + entry + "\n"); err != nil { + return fmt.Errorf("write %s: %w", path, err) + } + return nil +} + +func (g *GitClient) Commit(file, msg string) error { + cmds := [][]string{ + {"git", "add", file}, + {"git", "commit", "-m", msg}, + } + + for _, args := range cmds { + cmd := exec.Command(args[0], args[1:]...) + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("%s failed: %v\n%s", strings.Join(args, " "), err, string(out)) + } + } + return nil +} From 7a85ea68474ccc78450d3228991e9b0d139a6272 Mon Sep 17 00:00:00 2001 From: Grezle <4310551+grezle@users.noreply.github.com> Date: Tue, 30 Sep 2025 17:26:37 +0100 Subject: [PATCH 3/4] fix: check working tree is clean before upgrading --- pkg/commands/template/upgrade.go | 12 +++++++--- pkg/commands/template/upgrade_test.go | 25 ++++++++++++++++++++ pkg/template/git_client.go | 17 ++++++++++++++ pkg/testutils/utils.go | 34 +++++++++++++++++++++++++++ 4 files changed, 85 insertions(+), 3 deletions(-) diff --git a/pkg/commands/template/upgrade.go b/pkg/commands/template/upgrade.go index f21b94c4..fe0c5a21 100644 --- a/pkg/commands/template/upgrade.go +++ b/pkg/commands/template/upgrade.go @@ -148,14 +148,20 @@ func createUpgradeCommand( // Create a gitClient for upgrade process gitClient := template.NewGitClient() + // Check if working tree is clean before upgrading + clean, err := gitClient.GitIsClean() + if err != nil { + return fmt.Errorf("failed to check .git working tree state: %w", err) + } + if !clean { + return fmt.Errorf("uncommitted changes found, please commit or stash them before upgrading") + } + // Ensure .gitignore entry is present for tempExternal tempExternal := "temp_external" if err := gitClient.EnsureGitignoreEntry(".gitignore", tempExternal); err != nil { return fmt.Errorf("failed to add entry to .gitignore %s: %w", tempExternal, err) } - if err := gitClient.Commit(".gitignore", fmt.Sprintf("chore: ensure %s is in .gitignore", tempExternal)); err != nil { - return fmt.Errorf("failed to commit entry to .gitignore %s: %w", tempExternal, err) - } // Ensure parent exists tempParent := filepath.Join(absProjectPath, tempExternal) diff --git a/pkg/commands/template/upgrade_test.go b/pkg/commands/template/upgrade_test.go index 6c6e1277..a1ded1c3 100644 --- a/pkg/commands/template/upgrade_test.go +++ b/pkg/commands/template/upgrade_test.go @@ -103,6 +103,11 @@ func TestUpgradeCommand(t *testing.T) { t.Fatalf("Failed to write config file: %v", err) } + // Create git repo in temp dir + if err := testutils.TestGitInit(testProjectsDir); err != nil { + t.Fatalf("Failed init git repo: %v", err) + } + // Create mock template info getter mockTemplateInfoGetter := &MockTemplateInfoGetter{ projectName: "template-upgrade-test", @@ -137,6 +142,11 @@ func TestUpgradeCommand(t *testing.T) { // Test upgrade command with version flag t.Run("Upgrade command with version", func(t *testing.T) { + // Discard changes between runs + if err := testutils.TestGitDiscardChanges(testProjectsDir); err != nil { + t.Fatalf("Failed to clean working tree: %v", err) + } + // Create a flag set and context with no-op logger set := flag.NewFlagSet("test", 0) set.String("version", "v0.0.4", "") @@ -186,6 +196,11 @@ func TestUpgradeCommand(t *testing.T) { // Test upgrade command without version flag t.Run("Upgrade command without version", func(t *testing.T) { + // Discard changes between runs + if err := testutils.TestGitDiscardChanges(testProjectsDir); err != nil { + t.Fatalf("Failed to clean working tree: %v", err) + } + // Create a flag set and context without version flag, with no-op logger set := flag.NewFlagSet("test", 0) @@ -233,6 +248,11 @@ func TestUpgradeCommand(t *testing.T) { // Test upgrade command with incompatible to devkit version t.Run("Upgrade command with incompatible version", func(t *testing.T) { + // Discard changes between runs + if err := testutils.TestGitDiscardChanges(testProjectsDir); err != nil { + t.Fatalf("Failed to clean working tree: %v", err) + } + // Create a flag set and context with no-op logger set := flag.NewFlagSet("test", 0) set.String("version", "v0.0.5", "") @@ -258,6 +278,11 @@ func TestUpgradeCommand(t *testing.T) { // Test with missing config file t.Run("No config file", func(t *testing.T) { + // Discard changes between runs + if err := testutils.TestGitDiscardChanges(testProjectsDir); err != nil { + t.Fatalf("Failed to clean working tree: %v", err) + } + // Create a separate directory without a config file noConfigDir := filepath.Join(testProjectsDir, "no-config") err = os.MkdirAll(noConfigDir, 0755) diff --git a/pkg/template/git_client.go b/pkg/template/git_client.go index 49df4270..c877aa1f 100644 --- a/pkg/template/git_client.go +++ b/pkg/template/git_client.go @@ -2,6 +2,7 @@ package template import ( "bufio" + "bytes" "context" "fmt" "io" @@ -240,6 +241,17 @@ func (g *GitClient) ParseCloneOutput(r io.Reader, rep Reporter, dest string, ref return nil } +func (g *GitClient) GitIsClean() (bool, error) { + cmd := exec.Command("git", "status", "--porcelain") + var out bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &out + if err := cmd.Run(); err != nil { + return false, fmt.Errorf("git status failed: %w\n%s", err, out.String()) + } + return strings.TrimSpace(out.String()) == "", nil +} + func (g *GitClient) EnsureGitignoreEntry(path, entry string) error { // Create .gitignore if missing f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0o644) @@ -263,6 +275,11 @@ func (g *GitClient) EnsureGitignoreEntry(path, entry string) error { if _, err := f.WriteString("\n" + entry + "\n"); err != nil { return fmt.Errorf("write %s: %w", path, err) } + + if err := g.Commit(".gitignore", fmt.Sprintf("chore: ensure %s is in .gitignore", entry)); err != nil { + return fmt.Errorf("failed to commit entry to .gitignore %s: %w", entry, err) + } + return nil } diff --git a/pkg/testutils/utils.go b/pkg/testutils/utils.go index d1a3e3fd..d6c9f801 100644 --- a/pkg/testutils/utils.go +++ b/pkg/testutils/utils.go @@ -5,6 +5,7 @@ import ( "context" "fmt" "os" + "os/exec" "path/filepath" "testing" @@ -249,3 +250,36 @@ func CaptureOutput(fn func()) (stdout string, stderr string) { return stdout, stderr } + +func TestGitCall(repoDir string, cmds [][]string) error { + for _, args := range cmds { + cmd := exec.Command(args[0], args[1:]...) + cmd.Dir = repoDir + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("git command %v failed: %v\n%s", args, err, out) + } + } + return nil +} + +func TestGitInit(repoDir string) error { + // Init git repo + cmds := [][]string{ + {"git", "init"}, + {"git", "config", "user.name", "Test User"}, + {"git", "config", "user.email", "test@example.com"}, + {"git", "add", "."}, + {"git", "commit", "--allow-empty", "-m", "initial commit"}, + } + return TestGitCall(repoDir, cmds) +} + +func TestGitDiscardChanges(repoDir string) error { + // Discard changes in working tree + cmds := [][]string{ + {"git", "reset", "--hard"}, + {"git", "clean", "-fdx"}, + } + return TestGitCall(repoDir, cmds) +} From 2bcad18dd73bac466e778ac24e9e121ea994115d Mon Sep 17 00:00:00 2001 From: Grezle <4310551+grezle@users.noreply.github.com> Date: Tue, 30 Sep 2025 17:58:56 +0100 Subject: [PATCH 4/4] fix: use temp_internal as temp upgrade directory --- config/.gitignore | 3 +++ pkg/commands/template/upgrade.go | 10 +++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/config/.gitignore b/config/.gitignore index 444bb5af..64879bc6 100644 --- a/config/.gitignore +++ b/config/.gitignore @@ -29,6 +29,9 @@ Thumbs.db # Release outputs temp_external/ +# Internal upgrade directory +temp_internal/ + # DevKit context configurations (only devnet.yaml is indexed) config/contexts/**/* !config/contexts/devnet.yaml diff --git a/pkg/commands/template/upgrade.go b/pkg/commands/template/upgrade.go index fe0c5a21..87df3aeb 100644 --- a/pkg/commands/template/upgrade.go +++ b/pkg/commands/template/upgrade.go @@ -157,14 +157,14 @@ func createUpgradeCommand( return fmt.Errorf("uncommitted changes found, please commit or stash them before upgrading") } - // Ensure .gitignore entry is present for tempExternal - tempExternal := "temp_external" - if err := gitClient.EnsureGitignoreEntry(".gitignore", tempExternal); err != nil { - return fmt.Errorf("failed to add entry to .gitignore %s: %w", tempExternal, err) + // Ensure .gitignore entry is present for tempInternal + tempInternal := "temp_internal" + if err := gitClient.EnsureGitignoreEntry(".gitignore", fmt.Sprintf("%s/", tempInternal)); err != nil { + return fmt.Errorf("failed to add entry to .gitignore %s: %w", tempInternal, err) } // Ensure parent exists - tempParent := filepath.Join(absProjectPath, tempExternal) + tempParent := filepath.Join(absProjectPath, tempInternal) if err := os.MkdirAll(tempParent, 0o755); err != nil { return fmt.Errorf("failed to create %s: %w", tempParent, err) }