Skip to content

Commit 655abb4

Browse files
committed
apps init: address code review findings for pre-rendered templates
- Check os.WriteFile error for package.json instead of discarding it - Check enc.Close() error for YAML encoder - Warn when --set values are used with pre-rendered templates - Fix stale comment about --features warning behavior - Add unit tests for isPreRenderedTemplate, replaceProjectName, setYAMLValue, setFirstAppName, and yamlMapLookup Co-authored-by: Isaac
1 parent 0250e4f commit 655abb4

2 files changed

Lines changed: 244 additions & 4 deletions

File tree

cmd/apps/init.go

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -684,7 +684,9 @@ func replaceProjectName(destDir, newName string) error {
684684
if err == nil {
685685
// Preserve trailing newline convention.
686686
out = append(out, '\n')
687-
_ = os.WriteFile(pkgPath, out, 0o644)
687+
if err := os.WriteFile(pkgPath, out, 0o644); err != nil {
688+
return fmt.Errorf("write package.json: %w", err)
689+
}
688690
}
689691
}
690692
}
@@ -710,7 +712,9 @@ func replaceProjectName(destDir, newName string) error {
710712
if err := enc.Encode(&doc); err != nil {
711713
return fmt.Errorf("encode %s: %w", bundleConfigFile, err)
712714
}
713-
enc.Close()
715+
if err := enc.Close(); err != nil {
716+
return fmt.Errorf("encode %s: %w", bundleConfigFile, err)
717+
}
714718

715719
return os.WriteFile(ymlPath, buf.Bytes(), 0o644)
716720
}
@@ -1217,12 +1221,18 @@ func runCreate(ctx context.Context, opts createOptions) error {
12171221
maps.Copy(resourceValues, setVals)
12181222
}
12191223

1224+
// Pre-rendered templates have no Go template placeholders, so --set
1225+
// values cannot be injected into the output files.
1226+
if preRendered && len(setVals) > 0 {
1227+
log.Warnf(ctx, "--set values are ignored for pre-rendered templates (resources are already configured in %s)", bundleConfigFile)
1228+
}
1229+
12201230
// Always include mandatory plugins regardless of user selection or flags.
12211231
selectedPlugins = appendUnique(selectedPlugins, m.GetMandatoryPluginNames()...)
12221232

12231233
// Warn when --features adds plugins that the pre-rendered template
1224-
// cannot inject (they'll appear in .env but not in databricks.yml,
1225-
// server.ts, or app.yaml).
1234+
// cannot inject (its databricks.yml, server.ts, and app.yaml are
1235+
// already finalised).
12261236
if preRendered && opts.pluginsChanged {
12271237
mandatoryNames := m.GetMandatoryPluginNames()
12281238
mandatory := make(map[string]bool, len(mandatoryNames))

cmd/apps/init_test.go

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

33
import (
44
"bytes"
5+
"encoding/json"
56
"errors"
67
"io"
78
"io/fs"
@@ -18,6 +19,7 @@ import (
1819
"github.com/spf13/cobra"
1920
"github.com/stretchr/testify/assert"
2021
"github.com/stretchr/testify/require"
22+
"go.yaml.in/yaml/v3"
2123
)
2224

2325
func TestIsTextFile(t *testing.T) {
@@ -1148,3 +1150,231 @@ func TestRunCreate_NameDotAndOutputDirAreMutuallyExclusive(t *testing.T) {
11481150
require.Error(t, err)
11491151
assert.ErrorIs(t, err, prompt.ErrNameDotWithOutputDir)
11501152
}
1153+
1154+
func TestIsPreRenderedTemplate(t *testing.T) {
1155+
tests := []struct {
1156+
name string
1157+
setup func(dir string)
1158+
expected bool
1159+
}{
1160+
{
1161+
name: "normal template with project_name dir",
1162+
setup: func(dir string) {
1163+
require.NoError(t, os.MkdirAll(filepath.Join(dir, "{{.project_name}}"), 0o755))
1164+
require.NoError(t, os.WriteFile(filepath.Join(dir, bundleConfigFile), []byte("bundle:\n name: myapp\n"), 0o644))
1165+
},
1166+
expected: false,
1167+
},
1168+
{
1169+
name: "pre-rendered template",
1170+
setup: func(dir string) {
1171+
require.NoError(t, os.WriteFile(filepath.Join(dir, bundleConfigFile), []byte("bundle:\n name: myapp\n"), 0o644))
1172+
},
1173+
expected: true,
1174+
},
1175+
{
1176+
name: "no databricks.yml",
1177+
setup: func(dir string) {
1178+
require.NoError(t, os.WriteFile(filepath.Join(dir, "README.md"), []byte("hello"), 0o644))
1179+
},
1180+
expected: false,
1181+
},
1182+
{
1183+
name: "databricks.yml with Go template syntax",
1184+
setup: func(dir string) {
1185+
require.NoError(t, os.WriteFile(filepath.Join(dir, bundleConfigFile), []byte("bundle:\n name: {{.project_name}}\n"), 0o644))
1186+
},
1187+
expected: false,
1188+
},
1189+
{
1190+
name: "unreadable directory",
1191+
setup: func(_ string) {
1192+
// dir is replaced with a nonexistent path below.
1193+
},
1194+
expected: false,
1195+
},
1196+
}
1197+
1198+
for _, tt := range tests {
1199+
t.Run(tt.name, func(t *testing.T) {
1200+
dir := t.TempDir()
1201+
if tt.name == "unreadable directory" {
1202+
dir = filepath.Join(dir, "nonexistent")
1203+
} else {
1204+
tt.setup(dir)
1205+
}
1206+
assert.Equal(t, tt.expected, isPreRenderedTemplate(dir))
1207+
})
1208+
}
1209+
}
1210+
1211+
func TestReplaceProjectName(t *testing.T) {
1212+
tests := []struct {
1213+
name string
1214+
yml string
1215+
pkgJSON string // empty means no package.json
1216+
newName string
1217+
wantBundleName string
1218+
wantAppName string
1219+
wantPkgName string
1220+
wantErr bool
1221+
}{
1222+
{
1223+
name: "bundle and app name replaced",
1224+
yml: "bundle:\n name: old-app\nresources:\n apps:\n old-app:\n name: \"old-app\"\n",
1225+
newName: "new-app",
1226+
wantBundleName: "new-app",
1227+
wantAppName: "new-app",
1228+
},
1229+
{
1230+
name: "bundle name only, no resources",
1231+
yml: "bundle:\n name: old-app\n",
1232+
newName: "new-app",
1233+
wantBundleName: "new-app",
1234+
},
1235+
{
1236+
name: "no bundle.name, only app name",
1237+
yml: "resources:\n apps:\n myapp:\n name: \"myapp\"\n",
1238+
newName: "new-app",
1239+
wantAppName: "new-app",
1240+
},
1241+
{
1242+
name: "package.json round-trip",
1243+
yml: "bundle:\n name: old-app\n",
1244+
pkgJSON: `{"name":"old-app","version":"1.0.0"}`,
1245+
newName: "new-app",
1246+
wantBundleName: "new-app",
1247+
wantPkgName: "new-app",
1248+
},
1249+
{
1250+
name: "no package.json",
1251+
yml: "bundle:\n name: old-app\n",
1252+
newName: "new-app",
1253+
wantBundleName: "new-app",
1254+
},
1255+
}
1256+
1257+
for _, tt := range tests {
1258+
t.Run(tt.name, func(t *testing.T) {
1259+
dir := t.TempDir()
1260+
1261+
require.NoError(t, os.WriteFile(filepath.Join(dir, bundleConfigFile), []byte(tt.yml), 0o644))
1262+
1263+
if tt.pkgJSON != "" {
1264+
require.NoError(t, os.WriteFile(filepath.Join(dir, "package.json"), []byte(tt.pkgJSON), 0o644))
1265+
}
1266+
1267+
err := replaceProjectName(dir, tt.newName)
1268+
if tt.wantErr {
1269+
require.Error(t, err)
1270+
return
1271+
}
1272+
require.NoError(t, err)
1273+
1274+
// Verify databricks.yml via yaml.Node to avoid format sensitivity.
1275+
ymlData, err := os.ReadFile(filepath.Join(dir, bundleConfigFile))
1276+
require.NoError(t, err)
1277+
1278+
var doc yaml.Node
1279+
require.NoError(t, yaml.Unmarshal(ymlData, &doc))
1280+
1281+
if tt.wantBundleName != "" {
1282+
bundleName := yamlMapLookup(yamlMapLookup(doc.Content[0], "bundle"), "name")
1283+
require.NotNil(t, bundleName, "bundle.name not found in output")
1284+
assert.Equal(t, tt.wantBundleName, bundleName.Value)
1285+
}
1286+
1287+
if tt.wantAppName != "" {
1288+
appsNode := yamlMapLookup(yamlMapLookup(doc.Content[0], "resources"), "apps")
1289+
require.NotNil(t, appsNode)
1290+
require.True(t, appsNode.Kind == yaml.MappingNode && len(appsNode.Content) >= 2)
1291+
appName := yamlMapLookup(appsNode.Content[1], "name")
1292+
require.NotNil(t, appName, "resources.apps.*.name not found in output")
1293+
assert.Equal(t, tt.wantAppName, appName.Value)
1294+
}
1295+
1296+
if tt.wantPkgName != "" {
1297+
pkgData, err := os.ReadFile(filepath.Join(dir, "package.json"))
1298+
require.NoError(t, err)
1299+
var pkg map[string]any
1300+
require.NoError(t, json.Unmarshal(pkgData, &pkg))
1301+
assert.Equal(t, tt.wantPkgName, pkg["name"])
1302+
}
1303+
})
1304+
}
1305+
}
1306+
1307+
func TestReplaceProjectNameNoDatabricksYml(t *testing.T) {
1308+
dir := t.TempDir()
1309+
err := replaceProjectName(dir, "new-app")
1310+
require.Error(t, err)
1311+
}
1312+
1313+
func TestSetYAMLValue(t *testing.T) {
1314+
input := "top:\n nested:\n leaf: old\n"
1315+
var doc yaml.Node
1316+
require.NoError(t, yaml.Unmarshal([]byte(input), &doc))
1317+
1318+
setYAMLValue(&doc, []string{"top", "nested", "leaf"}, "new")
1319+
1320+
val := yamlMapLookup(yamlMapLookup(yamlMapLookup(doc.Content[0], "top"), "nested"), "leaf")
1321+
require.NotNil(t, val)
1322+
assert.Equal(t, "new", val.Value)
1323+
}
1324+
1325+
func TestSetYAMLValueMissingKey(t *testing.T) {
1326+
input := "top:\n nested:\n leaf: old\n"
1327+
var doc yaml.Node
1328+
require.NoError(t, yaml.Unmarshal([]byte(input), &doc))
1329+
1330+
// Should not panic or modify the tree.
1331+
setYAMLValue(&doc, []string{"top", "nonexistent", "leaf"}, "new")
1332+
1333+
val := yamlMapLookup(yamlMapLookup(yamlMapLookup(doc.Content[0], "top"), "nested"), "leaf")
1334+
require.NotNil(t, val)
1335+
assert.Equal(t, "old", val.Value)
1336+
}
1337+
1338+
func TestYamlMapLookup(t *testing.T) {
1339+
input := "a: 1\nb: 2\n"
1340+
var doc yaml.Node
1341+
require.NoError(t, yaml.Unmarshal([]byte(input), &doc))
1342+
1343+
root := doc.Content[0]
1344+
assert.NotNil(t, yamlMapLookup(root, "a"))
1345+
assert.Equal(t, "1", yamlMapLookup(root, "a").Value)
1346+
assert.Nil(t, yamlMapLookup(root, "missing"))
1347+
assert.Nil(t, yamlMapLookup(nil, "a"))
1348+
}
1349+
1350+
func TestSetFirstAppName(t *testing.T) {
1351+
input := "resources:\n apps:\n myapp:\n name: \"old-name\"\n description: keep\n"
1352+
var doc yaml.Node
1353+
require.NoError(t, yaml.Unmarshal([]byte(input), &doc))
1354+
1355+
setFirstAppName(&doc, "new-name")
1356+
1357+
appsNode := yamlMapLookup(yamlMapLookup(doc.Content[0], "resources"), "apps")
1358+
require.NotNil(t, appsNode)
1359+
appName := yamlMapLookup(appsNode.Content[1], "name")
1360+
require.NotNil(t, appName)
1361+
assert.Equal(t, "new-name", appName.Value)
1362+
1363+
// Verify description is untouched.
1364+
desc := yamlMapLookup(appsNode.Content[1], "description")
1365+
require.NotNil(t, desc)
1366+
assert.Equal(t, "keep", desc.Value)
1367+
}
1368+
1369+
func TestSetFirstAppNameNoResources(t *testing.T) {
1370+
input := "bundle:\n name: myapp\n"
1371+
var doc yaml.Node
1372+
require.NoError(t, yaml.Unmarshal([]byte(input), &doc))
1373+
1374+
// Should not panic when resources.apps is absent.
1375+
setFirstAppName(&doc, "new-name")
1376+
1377+
bundleName := yamlMapLookup(yamlMapLookup(doc.Content[0], "bundle"), "name")
1378+
require.NotNil(t, bundleName)
1379+
assert.Equal(t, "myapp", bundleName.Value)
1380+
}

0 commit comments

Comments
 (0)