Skip to content

Commit 345f2cb

Browse files
authored
fix: create tmp directory in working directory (#287)
<!-- 🚨 ATTENTION! 🚨 This PR template is REQUIRED. PRs not following this format will be closed without review. Requirements: - PR title must follow commit conventions: https://www.conventionalcommits.org/en/v1.0.0/ - Label your PR with the correct type (e.g., 🐛 Bug, ✨ Enhancement, 🧪 Test, etc.) - Provide clear and specific details in each section --> **Motivation:** <!-- Explain here the context, and why you're making that change. What is the problem you're trying to solve. --> As a devkit user that wants to upgrade to the latest template changes, I want any temp directory that is created to hold the correct permissions so that I do not need any `$TMPDIR` workarounds. **Modifications:** <!-- Describe the modifications you've done from a high level. What are the key technical decisions and why were they made? --> - Creates upgrade temp directory in cwd under `temp_internal` - Checks working tree is clean before upgrading - Adds `temp_internal` entry to `.gitignore` if missing and commits changes **Result:** <!-- *After your change, what will change. --> - Successful upgrades within the cwd **Testing:** <!-- *List testing procedures taken and useful artifacts. --> - Unit tests pass and manually tested against VRF **Open questions:** <!-- (optional) Any open questions or feedback on design desired? --> - ~~We should perform an upgrade to ensure the temp directory we use is present in the projects `.gitignore`. If the `.gitignore` does not have the appropriate entry, upgrades will fail with `Uncommitted changes found`~~
1 parent 171a770 commit 345f2cb

File tree

5 files changed

+163
-8
lines changed

5 files changed

+163
-8
lines changed

config/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ Thumbs.db
2929
# Release outputs
3030
temp_external/
3131

32+
# Internal upgrade directory
33+
temp_internal/
34+
3235
# DevKit context configurations (only devnet.yaml is indexed)
3336
config/contexts/**/*
3437
!config/contexts/devnet.yaml

pkg/commands/template/upgrade.go

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -145,18 +145,52 @@ func createUpgradeCommand(
145145
return fmt.Errorf("failed to get current working directory: %w", err)
146146
}
147147

148-
// Create temporary directory for cloning the template
149-
tempDir, err := os.MkdirTemp("", "devkit-template-upgrade-*")
148+
// Create a gitClient for upgrade process
149+
gitClient := template.NewGitClient()
150+
151+
// Check if working tree is clean before upgrading
152+
clean, err := gitClient.GitIsClean()
150153
if err != nil {
151-
return fmt.Errorf("failed to create temporary directory: %w", err)
154+
return fmt.Errorf("failed to check .git working tree state: %w", err)
155+
}
156+
if !clean {
157+
return fmt.Errorf("uncommitted changes found, please commit or stash them before upgrading")
158+
}
159+
160+
// Ensure .gitignore entry is present for tempInternal
161+
tempInternal := "temp_internal"
162+
if err := gitClient.EnsureGitignoreEntry(".gitignore", fmt.Sprintf("%s/", tempInternal)); err != nil {
163+
return fmt.Errorf("failed to add entry to .gitignore %s: %w", tempInternal, err)
164+
}
165+
166+
// Ensure parent exists
167+
tempParent := filepath.Join(absProjectPath, tempInternal)
168+
if err := os.MkdirAll(tempParent, 0o755); err != nil {
169+
return fmt.Errorf("failed to create %s: %w", tempParent, err)
152170
}
153-
defer os.RemoveAll(tempDir) // Clean up on exit
154171

155-
tempCacheDir, err := os.MkdirTemp("", "devkit-template-cache-*")
172+
// Create run-specific temp dir inside tempParent
173+
tempDir, err := os.MkdirTemp(tempParent, ".tmp-devkit-template-upgrade-*")
156174
if err != nil {
157-
return fmt.Errorf("failed to create temporary cache directory: %w", err)
175+
return fmt.Errorf("failed to create temporary directory: %w", err)
158176
}
159-
defer os.RemoveAll(tempCacheDir) // Clean up on exit
177+
178+
// Remove tempParent if it is empty after tempDir cleanup
179+
defer func() {
180+
err = os.RemoveAll(tempDir)
181+
if err != nil {
182+
logger.Warn("failed to remove %s: %v\n", tempDir, err)
183+
}
184+
entries, err := os.ReadDir(tempParent)
185+
if err != nil {
186+
logger.Warn("could not read %s: %v\n", tempParent, err)
187+
}
188+
if len(entries) == 0 {
189+
if err := os.Remove(tempParent); err != nil {
190+
logger.Warn("failed to remove %s: %v\n", tempParent, err)
191+
}
192+
}
193+
}()
160194

161195
logger.Info("Upgrading project template:")
162196
logger.Info(" Project: %s", projectName)
@@ -171,7 +205,7 @@ func createUpgradeCommand(
171205

172206
// Fetch main template
173207
fetcher := &template.GitFetcher{
174-
Client: template.NewGitClient(),
208+
Client: gitClient,
175209
Logger: *progresslogger.NewProgressLogger(
176210
logger,
177211
tracker,

pkg/commands/template/upgrade_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,11 @@ func TestUpgradeCommand(t *testing.T) {
103103
t.Fatalf("Failed to write config file: %v", err)
104104
}
105105

106+
// Create git repo in temp dir
107+
if err := testutils.TestGitInit(testProjectsDir); err != nil {
108+
t.Fatalf("Failed init git repo: %v", err)
109+
}
110+
106111
// Create mock template info getter
107112
mockTemplateInfoGetter := &MockTemplateInfoGetter{
108113
projectName: "template-upgrade-test",
@@ -137,6 +142,11 @@ func TestUpgradeCommand(t *testing.T) {
137142

138143
// Test upgrade command with version flag
139144
t.Run("Upgrade command with version", func(t *testing.T) {
145+
// Discard changes between runs
146+
if err := testutils.TestGitDiscardChanges(testProjectsDir); err != nil {
147+
t.Fatalf("Failed to clean working tree: %v", err)
148+
}
149+
140150
// Create a flag set and context with no-op logger
141151
set := flag.NewFlagSet("test", 0)
142152
set.String("version", "v0.0.4", "")
@@ -186,6 +196,11 @@ func TestUpgradeCommand(t *testing.T) {
186196

187197
// Test upgrade command without version flag
188198
t.Run("Upgrade command without version", func(t *testing.T) {
199+
// Discard changes between runs
200+
if err := testutils.TestGitDiscardChanges(testProjectsDir); err != nil {
201+
t.Fatalf("Failed to clean working tree: %v", err)
202+
}
203+
189204
// Create a flag set and context without version flag, with no-op logger
190205
set := flag.NewFlagSet("test", 0)
191206

@@ -233,6 +248,11 @@ func TestUpgradeCommand(t *testing.T) {
233248

234249
// Test upgrade command with incompatible to devkit version
235250
t.Run("Upgrade command with incompatible version", func(t *testing.T) {
251+
// Discard changes between runs
252+
if err := testutils.TestGitDiscardChanges(testProjectsDir); err != nil {
253+
t.Fatalf("Failed to clean working tree: %v", err)
254+
}
255+
236256
// Create a flag set and context with no-op logger
237257
set := flag.NewFlagSet("test", 0)
238258
set.String("version", "v0.0.5", "")
@@ -258,6 +278,11 @@ func TestUpgradeCommand(t *testing.T) {
258278

259279
// Test with missing config file
260280
t.Run("No config file", func(t *testing.T) {
281+
// Discard changes between runs
282+
if err := testutils.TestGitDiscardChanges(testProjectsDir); err != nil {
283+
t.Fatalf("Failed to clean working tree: %v", err)
284+
}
285+
261286
// Create a separate directory without a config file
262287
noConfigDir := filepath.Join(testProjectsDir, "no-config")
263288
err = os.MkdirAll(noConfigDir, 0755)

pkg/template/git_client.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package template
22

33
import (
44
"bufio"
5+
"bytes"
56
"context"
67
"fmt"
78
"io"
@@ -239,3 +240,61 @@ func (g *GitClient) ParseCloneOutput(r io.Reader, rep Reporter, dest string, ref
239240
}
240241
return nil
241242
}
243+
244+
func (g *GitClient) GitIsClean() (bool, error) {
245+
cmd := exec.Command("git", "status", "--porcelain")
246+
var out bytes.Buffer
247+
cmd.Stdout = &out
248+
cmd.Stderr = &out
249+
if err := cmd.Run(); err != nil {
250+
return false, fmt.Errorf("git status failed: %w\n%s", err, out.String())
251+
}
252+
return strings.TrimSpace(out.String()) == "", nil
253+
}
254+
255+
func (g *GitClient) EnsureGitignoreEntry(path, entry string) error {
256+
// Create .gitignore if missing
257+
f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0o644)
258+
if err != nil {
259+
return fmt.Errorf("open %s: %w", path, err)
260+
}
261+
defer f.Close()
262+
263+
// Check if entry exists
264+
scanner := bufio.NewScanner(f)
265+
for scanner.Scan() {
266+
if strings.TrimSpace(scanner.Text()) == entry {
267+
return nil // already present
268+
}
269+
}
270+
if err := scanner.Err(); err != nil {
271+
return fmt.Errorf("scan %s: %w", path, err)
272+
}
273+
274+
// Append entry
275+
if _, err := f.WriteString("\n" + entry + "\n"); err != nil {
276+
return fmt.Errorf("write %s: %w", path, err)
277+
}
278+
279+
if err := g.Commit(".gitignore", fmt.Sprintf("chore: ensure %s is in .gitignore", entry)); err != nil {
280+
return fmt.Errorf("failed to commit entry to .gitignore %s: %w", entry, err)
281+
}
282+
283+
return nil
284+
}
285+
286+
func (g *GitClient) Commit(file, msg string) error {
287+
cmds := [][]string{
288+
{"git", "add", file},
289+
{"git", "commit", "-m", msg},
290+
}
291+
292+
for _, args := range cmds {
293+
cmd := exec.Command(args[0], args[1:]...)
294+
out, err := cmd.CombinedOutput()
295+
if err != nil {
296+
return fmt.Errorf("%s failed: %v\n%s", strings.Join(args, " "), err, string(out))
297+
}
298+
}
299+
return nil
300+
}

pkg/testutils/utils.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"context"
66
"fmt"
77
"os"
8+
"os/exec"
89
"path/filepath"
910
"testing"
1011

@@ -249,3 +250,36 @@ func CaptureOutput(fn func()) (stdout string, stderr string) {
249250

250251
return stdout, stderr
251252
}
253+
254+
func TestGitCall(repoDir string, cmds [][]string) error {
255+
for _, args := range cmds {
256+
cmd := exec.Command(args[0], args[1:]...)
257+
cmd.Dir = repoDir
258+
out, err := cmd.CombinedOutput()
259+
if err != nil {
260+
return fmt.Errorf("git command %v failed: %v\n%s", args, err, out)
261+
}
262+
}
263+
return nil
264+
}
265+
266+
func TestGitInit(repoDir string) error {
267+
// Init git repo
268+
cmds := [][]string{
269+
{"git", "init"},
270+
{"git", "config", "user.name", "Test User"},
271+
{"git", "config", "user.email", "[email protected]"},
272+
{"git", "add", "."},
273+
{"git", "commit", "--allow-empty", "-m", "initial commit"},
274+
}
275+
return TestGitCall(repoDir, cmds)
276+
}
277+
278+
func TestGitDiscardChanges(repoDir string) error {
279+
// Discard changes in working tree
280+
cmds := [][]string{
281+
{"git", "reset", "--hard"},
282+
{"git", "clean", "-fdx"},
283+
}
284+
return TestGitCall(repoDir, cmds)
285+
}

0 commit comments

Comments
 (0)