Skip to content

Commit ae99f50

Browse files
[CAP-9513] add .gitignore and .env.example to workflows init scaffold (#358)
GitOrigin-RevId: 37b4df9a3d0a44dc87a3c6ad5932491d75011d24
1 parent 698d214 commit ae99f50

2 files changed

Lines changed: 133 additions & 7 deletions

File tree

pkg/workflows/scaffold/scaffold.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,18 @@ func Scaffold(opts Options) (*Result, error) {
306306
return nil, err
307307
}
308308

309+
// Generate .gitignore if the template didn't provide one
310+
createdFiles, err = writeIfNotExists(opts.Dir, ".gitignore", gitignoreContent(opts.Language), createdFiles)
311+
if err != nil {
312+
return nil, fmt.Errorf("failed to create .gitignore: %w", err)
313+
}
314+
315+
// Generate .env.example if the template didn't provide one
316+
createdFiles, err = writeIfNotExists(opts.Dir, ".env.example", envExampleContent(), createdFiles)
317+
if err != nil {
318+
return nil, fmt.Errorf("failed to create .env.example: %w", err)
319+
}
320+
309321
// Support the old "additionalNextSteps" key as a fallback
310322
nextSteps := meta.NextSteps
311323
if len(nextSteps) == 0 && len(meta.LegacyNextSteps) > 0 {
@@ -364,6 +376,41 @@ func copyFile(src, dst string) error {
364376
return err
365377
}
366378

379+
// writeIfNotExists writes content to a file in dir only if it doesn't already
380+
// exist (e.g. provided by the template). If written, the filename is appended
381+
// to files and the updated slice is returned.
382+
func writeIfNotExists(dir, name, content string, files []string) ([]string, error) {
383+
destPath := filepath.Join(dir, name)
384+
if _, err := os.Stat(destPath); err == nil {
385+
return files, nil // already exists from template
386+
}
387+
if err := os.WriteFile(destPath, []byte(content), 0o644); err != nil {
388+
return files, err
389+
}
390+
return append(files, name), nil
391+
}
392+
393+
// gitignoreContent returns the .gitignore contents for the given language.
394+
func gitignoreContent(lang Language) string {
395+
var b strings.Builder
396+
b.WriteString("dist/\n")
397+
b.WriteString(".env\n")
398+
b.WriteString(".DS_Store\n")
399+
switch lang {
400+
case Python:
401+
b.WriteString(".venv/\n")
402+
b.WriteString("__pycache__/\n")
403+
case TypeScript:
404+
b.WriteString("node_modules/\n")
405+
}
406+
return b.String()
407+
}
408+
409+
// envExampleContent returns the .env.example file contents.
410+
func envExampleContent() string {
411+
return "RENDER_API_KEY=\n"
412+
}
413+
367414
// InitGitRepo initializes a new git repository in dir.
368415
func InitGitRepo(dir string) error {
369416
_, err := git.PlainInitWithOptions(dir, &git.PlainInitOptions{

pkg/workflows/scaffold/scaffold_test.go

Lines changed: 86 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -194,10 +194,10 @@ nextSteps:
194194
assert.Equal(t, "render-workflows main:app", result.RenderStartCommand)
195195
require.Len(t, result.NextSteps, 1)
196196

197-
expectedFiles := []string{"README.md", "main.py", "requirements.txt"}
197+
expectedFiles := []string{"README.md", "main.py", "requirements.txt", ".gitignore", ".env.example"}
198198
require.Len(t, result.Files, len(expectedFiles))
199-
for i, f := range expectedFiles {
200-
assert.Equal(t, f, result.Files[i])
199+
for _, f := range expectedFiles {
200+
assert.Contains(t, result.Files, f)
201201

202202
info, err := os.Stat(filepath.Join(outDir, f))
203203
require.NoError(t, err, "expected file %s to exist", f)
@@ -229,11 +229,10 @@ startCommand: npx tsx src/index.ts
229229
assert.Equal(t, TypeScript, result.Language)
230230
assert.Equal(t, "npx tsx src/index.ts", result.StartCommand)
231231

232-
// WalkDir produces sorted order: README.md, package.json, src/index.ts, tsconfig.json
233-
expectedFiles := []string{"README.md", "package.json", "src/index.ts", "tsconfig.json"}
232+
expectedFiles := []string{"README.md", "package.json", "src/index.ts", "tsconfig.json", ".gitignore", ".env.example"}
234233
require.Len(t, result.Files, len(expectedFiles))
235-
for i, f := range expectedFiles {
236-
assert.Equal(t, f, result.Files[i])
234+
for _, f := range expectedFiles {
235+
assert.Contains(t, result.Files, f)
237236

238237
info, err := os.Stat(filepath.Join(outDir, f))
239238
require.NoError(t, err, "expected file %s to exist", f)
@@ -327,6 +326,86 @@ buildCommand: test-install
327326
assert.Equal(t, expectedContent, string(got))
328327
}
329328

329+
func TestScaffoldCreatesGitignore(t *testing.T) {
330+
tests := []struct {
331+
name string
332+
lang Language
333+
wantIn []string
334+
}{
335+
{"Python", Python, []string{"dist/", ".env", ".venv/", "__pycache__/"}},
336+
{"TypeScript", TypeScript, []string{"dist/", ".env", "node_modules/"}},
337+
}
338+
for _, tt := range tests {
339+
t.Run(tt.name, func(t *testing.T) {
340+
meta := "name: Test\nbuildCommand: test\n"
341+
repoDir := setupFakeRepo(t, "test", meta, map[string]string{
342+
"app.txt": "hello\n",
343+
})
344+
345+
outDir := filepath.Join(t.TempDir(), "workflows")
346+
_, err := Scaffold(Options{
347+
Language: tt.lang,
348+
TemplateName: "test",
349+
Dir: outDir,
350+
RepoDir: repoDir,
351+
})
352+
require.NoError(t, err)
353+
354+
got, err := os.ReadFile(filepath.Join(outDir, ".gitignore"))
355+
require.NoError(t, err)
356+
for _, entry := range tt.wantIn {
357+
assert.Contains(t, string(got), entry)
358+
}
359+
})
360+
}
361+
}
362+
363+
func TestScaffoldCreatesEnvExample(t *testing.T) {
364+
meta := "name: Test\nbuildCommand: test\n"
365+
repoDir := setupFakeRepo(t, "test", meta, map[string]string{
366+
"app.txt": "hello\n",
367+
})
368+
369+
outDir := filepath.Join(t.TempDir(), "workflows")
370+
_, err := Scaffold(Options{
371+
Language: Python,
372+
TemplateName: "test",
373+
Dir: outDir,
374+
RepoDir: repoDir,
375+
})
376+
require.NoError(t, err)
377+
378+
got, err := os.ReadFile(filepath.Join(outDir, ".env.example"))
379+
require.NoError(t, err)
380+
assert.Contains(t, string(got), "RENDER_API_KEY=")
381+
}
382+
383+
func TestScaffoldDoesNotOverwriteTemplateFiles(t *testing.T) {
384+
meta := "name: Test\nbuildCommand: test\n"
385+
repoDir := setupFakeRepo(t, "test", meta, map[string]string{
386+
"app.txt": "hello\n",
387+
".gitignore": "custom-ignore\n",
388+
".env.example": "CUSTOM_VAR=foo\n",
389+
})
390+
391+
outDir := filepath.Join(t.TempDir(), "workflows")
392+
_, err := Scaffold(Options{
393+
Language: Python,
394+
TemplateName: "test",
395+
Dir: outDir,
396+
RepoDir: repoDir,
397+
})
398+
require.NoError(t, err)
399+
400+
gotGitignore, err := os.ReadFile(filepath.Join(outDir, ".gitignore"))
401+
require.NoError(t, err)
402+
assert.Equal(t, "custom-ignore\n", string(gotGitignore))
403+
404+
gotEnv, err := os.ReadFile(filepath.Join(outDir, ".env.example"))
405+
require.NoError(t, err)
406+
assert.Equal(t, "CUSTOM_VAR=foo\n", string(gotEnv))
407+
}
408+
330409
func TestInitGitRepoUsesMainAsDefaultBranch(t *testing.T) {
331410
dir := t.TempDir()
332411

0 commit comments

Comments
 (0)