Skip to content

Commit 6ef25cd

Browse files
committed
feat(config): implement loading of .forge.yaml for app-level configuration
- Added functionality to load .forge.yaml file to configure app settings such as port and build output. - Enhanced the newApp function to read configuration before server initialization, ensuring proper port setup. - Updated DevPlugin to utilize app-level configuration for port settings, prioritizing explicit flags and environment variables. This update improves the flexibility and configurability of the application.
1 parent 04ffee4 commit 6ef25cd

2 files changed

Lines changed: 139 additions & 9 deletions

File tree

app_impl.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"net/http"
88
"os"
99
"os/signal"
10+
"path/filepath"
1011
"runtime"
1112
"sync"
1213
"syscall"
@@ -19,6 +20,7 @@ import (
1920
metricsinternal "github.com/xraph/forge/internal/metrics"
2021
"github.com/xraph/forge/internal/shared"
2122
"github.com/xraph/vessel"
23+
"gopkg.in/yaml.v3"
2224
)
2325

2426
// app implements the App interface.
@@ -195,6 +197,10 @@ func newApp(config AppConfig) *app {
195197
}
196198
}
197199

200+
// Load .forge.yaml for app-level configuration (port, build settings, etc.)
201+
// This runs BEFORE server initialization to ensure port is configured
202+
config = loadForgeYAMLConfig(config, logger)
203+
198204
// Create health manager with full config support
199205
var healthManager HealthManager
200206

@@ -1303,3 +1309,103 @@ func (a *app) handleHealthReady(ctx Context) error {
13031309
"message": "one or more services unhealthy",
13041310
})
13051311
}
1312+
1313+
// forgeYAMLConfig represents the app-level .forge.yaml configuration
1314+
// This is loaded at runtime to provide app-specific settings like port.
1315+
type forgeYAMLConfig struct {
1316+
App struct {
1317+
Name string `yaml:"name"`
1318+
Type string `yaml:"type"`
1319+
Version string `yaml:"version"`
1320+
} `yaml:"app"`
1321+
Dev struct {
1322+
Port int `yaml:"port"`
1323+
Host string `yaml:"host"`
1324+
} `yaml:"dev"`
1325+
Build struct {
1326+
Output string `yaml:"output"`
1327+
} `yaml:"build"`
1328+
}
1329+
1330+
// loadForgeYAMLConfig searches for and loads .forge.yaml to configure runtime settings.
1331+
// This ensures the app can read its configuration when run directly (not through forge dev).
1332+
func loadForgeYAMLConfig(config AppConfig, logger Logger) AppConfig {
1333+
// Search for .forge.yaml starting from current directory
1334+
dir, err := os.Getwd()
1335+
if err != nil {
1336+
return config
1337+
}
1338+
1339+
var forgeConfigPath string
1340+
maxDepth := 5
1341+
1342+
// Search up the directory tree
1343+
for i := 0; i < maxDepth; i++ {
1344+
yamlPath := filepath.Join(dir, ".forge.yaml")
1345+
ymlPath := filepath.Join(dir, ".forge.yml")
1346+
1347+
if _, err := os.Stat(yamlPath); err == nil {
1348+
forgeConfigPath = yamlPath
1349+
break
1350+
}
1351+
1352+
if _, err := os.Stat(ymlPath); err == nil {
1353+
forgeConfigPath = ymlPath
1354+
break
1355+
}
1356+
1357+
// Move up one directory
1358+
parent := filepath.Dir(dir)
1359+
if parent == dir {
1360+
// Reached root
1361+
break
1362+
}
1363+
dir = parent
1364+
}
1365+
1366+
// If no .forge.yaml found, return original config
1367+
if forgeConfigPath == "" {
1368+
return config
1369+
}
1370+
1371+
// Read and parse .forge.yaml
1372+
data, err := os.ReadFile(forgeConfigPath)
1373+
if err != nil {
1374+
if logger != nil {
1375+
logger.Debug("failed to read .forge.yaml", F("path", forgeConfigPath), F("error", err.Error()))
1376+
}
1377+
return config
1378+
}
1379+
1380+
var forgeConfig forgeYAMLConfig
1381+
if err := yaml.Unmarshal(data, &forgeConfig); err != nil {
1382+
if logger != nil {
1383+
logger.Debug("failed to parse .forge.yaml", F("path", forgeConfigPath), F("error", err.Error()))
1384+
}
1385+
return config
1386+
}
1387+
1388+
// Apply configuration from .forge.yaml
1389+
// Priority: Explicit AppConfig > .forge.yaml > Defaults
1390+
1391+
// Set HTTPAddress from dev.port if:
1392+
// 1. HTTPAddress is still the default ":8080"
1393+
// 2. dev.port is set in .forge.yaml
1394+
// 3. PORT environment variable is not set (env vars take precedence)
1395+
if config.HTTPAddress == ":8080" && forgeConfig.Dev.Port > 0 && os.Getenv("PORT") == "" {
1396+
config.HTTPAddress = fmt.Sprintf(":%d", forgeConfig.Dev.Port)
1397+
if logger != nil {
1398+
logger.Info("loaded port from .forge.yaml", F("port", forgeConfig.Dev.Port), F("path", forgeConfigPath))
1399+
}
1400+
}
1401+
1402+
// If PORT env var is set, use it (highest priority for port)
1403+
if portEnv := os.Getenv("PORT"); portEnv != "" {
1404+
config.HTTPAddress = ":" + portEnv
1405+
if logger != nil {
1406+
logger.Info("using port from environment variable", F("port", portEnv))
1407+
}
1408+
}
1409+
1410+
return config
1411+
}

cmd/forge/plugins/dev.go

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,15 @@ func (p *DevPlugin) runDev(ctx cli.CommandContext) error {
113113
return err
114114
}
115115

116-
// Set port if specified
116+
// Set port: use --port flag if provided, otherwise use app config, otherwise let app decide
117117
if port > 0 {
118+
// Explicit flag takes precedence
118119
os.Setenv("PORT", strconv.Itoa(port))
120+
} else if app.AppConfig != nil && app.AppConfig.Dev.GetPort() > 0 {
121+
// Use port from app's .forge.yaml
122+
appPort := app.AppConfig.Dev.GetPort()
123+
os.Setenv("PORT", strconv.Itoa(appPort))
124+
ctx.Info(fmt.Sprintf("Using port %d from app configuration", appPort))
119125
}
120126

121127
if watch {
@@ -210,17 +216,20 @@ func (p *DevPlugin) buildDev(ctx cli.CommandContext) error {
210216

211217
// AppInfo represents a discoverable app.
212218
type AppInfo struct {
213-
Name string
214-
Path string
215-
Type string
219+
Name string
220+
Path string
221+
Type string
222+
AppConfig *config.AppConfig // App-level .forge.yaml configuration
216223
}
217224

218225
func (p *DevPlugin) discoverApps() ([]AppInfo, error) {
219226
var apps []AppInfo
220227

221228
if p.config.IsSingleModule() {
222229
// For single-module, scan cmd directory
223-
cmdDir := filepath.Join(p.config.RootDir, p.config.Project.GetStructure().Cmd)
230+
structure := p.config.Project.GetStructure()
231+
cmdDir := filepath.Join(p.config.RootDir, structure.Cmd)
232+
appsDir := filepath.Join(p.config.RootDir, structure.Apps)
224233

225234
entries, err := os.ReadDir(cmdDir)
226235
if err != nil {
@@ -235,11 +244,19 @@ func (p *DevPlugin) discoverApps() ([]AppInfo, error) {
235244
if entry.IsDir() {
236245
mainPath := filepath.Join(cmdDir, entry.Name(), "main.go")
237246
if _, err := os.Stat(mainPath); err == nil {
238-
apps = append(apps, AppInfo{
247+
appInfo := AppInfo{
239248
Name: entry.Name(),
240249
Path: filepath.Join(cmdDir, entry.Name()),
241250
Type: "app",
242-
})
251+
}
252+
253+
// Try to load app-level .forge.yaml from apps/{appname}/
254+
appConfigDir := filepath.Join(appsDir, entry.Name())
255+
if appConfig, err := config.LoadAppConfig(appConfigDir); err == nil {
256+
appInfo.AppConfig = appConfig
257+
}
258+
259+
apps = append(apps, appInfo)
243260
}
244261
}
245262
}
@@ -263,11 +280,18 @@ func (p *DevPlugin) discoverApps() ([]AppInfo, error) {
263280
cmdDir := filepath.Join(appDir, "cmd")
264281

265282
if _, err := os.Stat(cmdDir); err == nil {
266-
apps = append(apps, AppInfo{
283+
appInfo := AppInfo{
267284
Name: entry.Name(),
268285
Path: cmdDir,
269286
Type: "app",
270-
})
287+
}
288+
289+
// Try to load app-level .forge.yaml from apps/{appname}/
290+
if appConfig, err := config.LoadAppConfig(appDir); err == nil {
291+
appInfo.AppConfig = appConfig
292+
}
293+
294+
apps = append(apps, appInfo)
271295
}
272296
}
273297
}

0 commit comments

Comments
 (0)