Skip to content

Commit 7ee8275

Browse files
committed
fix(publish): export app profiles as runtime metadata
1 parent 0cd0669 commit 7ee8275

7 files changed

Lines changed: 485 additions & 6 deletions

File tree

boot/deps/config/config.go

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,31 @@ const (
1818

1919
type ModuleConfig struct {
2020
ExcludeMeta map[string][]string `yaml:"exclude_meta,omitempty"`
21-
Organization string `yaml:"organization"`
22-
ModuleName string `yaml:"module"`
23-
Version string `yaml:"version"`
21+
Metadata map[string]any `yaml:"metadata,omitempty"`
22+
Runtime RuntimeConfig `yaml:"runtime,omitempty"`
23+
Repository string `yaml:"repository,omitempty"`
2424
Description string `yaml:"description,omitempty"`
2525
License string `yaml:"license,omitempty"`
26-
Repository string `yaml:"repository,omitempty"`
26+
Version string `yaml:"version"`
2727
Homepage string `yaml:"homepage,omitempty"`
28+
ModuleName string `yaml:"module"`
29+
Organization string `yaml:"organization"`
2830
Keywords []string `yaml:"keywords,omitempty"`
2931
Authors []string `yaml:"authors,omitempty"`
3032
Include []string `yaml:"include,omitempty"`
3133
Exclude []string `yaml:"exclude,omitempty"`
32-
Metadata map[string]any `yaml:"metadata,omitempty"`
3334
Embed []string `yaml:"embed,omitempty"`
3435
}
3536

37+
type RuntimeConfig struct {
38+
Profiles RuntimeProfilesConfig `yaml:"profiles,omitempty"`
39+
}
40+
41+
type RuntimeProfilesConfig struct {
42+
Publish *bool `yaml:"publish,omitempty"`
43+
Source string `yaml:"source,omitempty"`
44+
}
45+
3646
func Load(dir string) (*ModuleConfig, error) {
3747
path := filepath.Join(dir, DefaultConfigFile)
3848
return LoadFrom(path)

boot/deps/config/config_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,25 @@ metadata:
294294
assert.Equal(t, "debug", loggerMap["level"])
295295
}
296296

297+
func TestLoad_WithRuntimeProfilePublishConfig(t *testing.T) {
298+
dir := t.TempDir()
299+
content := `
300+
organization: myorg
301+
module: mymod
302+
runtime:
303+
profiles:
304+
publish: false
305+
source: config/profiles.yaml
306+
`
307+
require.NoError(t, os.WriteFile(filepath.Join(dir, DefaultConfigFile), []byte(content), 0644))
308+
309+
cfg, err := Load(dir)
310+
require.NoError(t, err)
311+
require.NotNil(t, cfg.Runtime.Profiles.Publish)
312+
assert.False(t, *cfg.Runtime.Profiles.Publish)
313+
assert.Equal(t, "config/profiles.yaml", cfg.Runtime.Profiles.Source)
314+
}
315+
297316
// --- EntryExcludes ---
298317

299318
func TestEntryExcludes(t *testing.T) {

cmd/wippy/cmd/publish.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,9 @@ func packModule(ctx context.Context, app *appinit.Context, cfg *config.ModuleCon
514514
}
515515
metadata[trimmed] = value
516516
}
517+
if err := addPublishedRuntimeProfileMetadata(metadata, srcDir, cfg.Runtime.Profiles); err != nil {
518+
return nil, NewPublishConfigError(err)
519+
}
517520

518521
packWriter := wapp.NewWriter()
519522

cmd/wippy/cmd/publish_profiles.go

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
// SPDX-License-Identifier: MPL-2.0
2+
3+
package cmd
4+
5+
import (
6+
"fmt"
7+
"path/filepath"
8+
"strings"
9+
10+
"github.com/wippyai/runtime/api/attrs"
11+
"github.com/wippyai/runtime/api/boot"
12+
"github.com/wippyai/runtime/boot/deps/config"
13+
"github.com/wippyai/runtime/cmd/internal/bootconfig"
14+
)
15+
16+
const (
17+
publishRuntimeMetadataKey = "runtime"
18+
publishRuntimeProfilesMetadataKey = "profiles"
19+
publishRuntimeVarsMetadataKey = "vars"
20+
)
21+
22+
func addPublishedRuntimeProfileMetadata(metadata attrs.Bag, configDir string, profileCfg config.RuntimeProfilesConfig) error {
23+
if profileCfg.Publish != nil && !*profileCfg.Publish {
24+
return nil
25+
}
26+
27+
source := strings.TrimSpace(profileCfg.Source)
28+
if source == "" {
29+
source = defaultConfigFile
30+
}
31+
if !filepath.IsAbs(source) {
32+
source = filepath.Join(configDir, source)
33+
}
34+
35+
cfg, err := bootconfig.Load(source)
36+
if err != nil {
37+
return fmt.Errorf("load runtime profile source %s: %w", source, err)
38+
}
39+
if cfg == nil {
40+
return nil
41+
}
42+
43+
profiles, err := runtimeProfilesFromConfig(cfg)
44+
if err != nil {
45+
return err
46+
}
47+
if len(profiles) == 0 {
48+
return nil
49+
}
50+
51+
if hasRuntimeProfilesMetadata(metadata) {
52+
return fmt.Errorf("runtime profiles are defined in both %s and wippy.yaml metadata; keep profile definitions in one layer", source)
53+
}
54+
55+
runtime, err := runtimeMetadataMap(metadata)
56+
if err != nil {
57+
return err
58+
}
59+
runtime[publishRuntimeProfilesMetadataKey] = profiles
60+
61+
vars := runtimeSectionFromConfig(cfg, publishRuntimeVarsMetadataKey)
62+
if len(vars) > 0 {
63+
if err := mergeRuntimeVarsMetadata(metadata, runtime, vars, source); err != nil {
64+
return err
65+
}
66+
}
67+
68+
return nil
69+
}
70+
71+
func runtimeProfilesFromConfig(cfg boot.Config) (map[string]any, error) {
72+
profiles := make(map[string]any)
73+
if cfg == nil {
74+
return profiles, nil
75+
}
76+
77+
for _, key := range cfg.Keys() {
78+
if !strings.HasPrefix(key, "profiles.") {
79+
continue
80+
}
81+
82+
rest := strings.TrimPrefix(key, "profiles.")
83+
profileName, rest, ok := strings.Cut(rest, ".")
84+
if !ok || profileName == "" || rest == "" {
85+
return nil, fmt.Errorf("invalid runtime profile key %q", key)
86+
}
87+
section, subkey, ok := strings.Cut(rest, ".")
88+
if !ok || section == "" || subkey == "" {
89+
return nil, fmt.Errorf("invalid runtime profile key %q", key)
90+
}
91+
92+
profileMap, ok := profiles[profileName].(map[string]any)
93+
if !ok {
94+
profileMap = make(map[string]any)
95+
profiles[profileName] = profileMap
96+
}
97+
98+
sectionMap, ok := profileMap[section].(map[string]any)
99+
if !ok {
100+
sectionMap = make(map[string]any)
101+
profileMap[section] = sectionMap
102+
}
103+
104+
value, _ := cfg.Get(key)
105+
sectionMap[subkey] = value
106+
}
107+
108+
return profiles, nil
109+
}
110+
111+
func runtimeSectionFromConfig(cfg boot.Config, section string) map[string]any {
112+
values := make(map[string]any)
113+
if cfg == nil {
114+
return values
115+
}
116+
117+
prefix := section + "."
118+
for _, key := range cfg.Keys() {
119+
if !strings.HasPrefix(key, prefix) {
120+
continue
121+
}
122+
value, _ := cfg.Get(key)
123+
values[strings.TrimPrefix(key, prefix)] = value
124+
}
125+
return values
126+
}
127+
128+
func runtimeMetadataMap(metadata attrs.Bag) (map[string]any, error) {
129+
if metadata == nil {
130+
return nil, fmt.Errorf("metadata bag is nil")
131+
}
132+
133+
raw, exists := metadata[publishRuntimeMetadataKey]
134+
if !exists {
135+
runtime := make(map[string]any)
136+
metadata[publishRuntimeMetadataKey] = runtime
137+
return runtime, nil
138+
}
139+
140+
switch typed := raw.(type) {
141+
case map[string]any:
142+
return typed, nil
143+
case attrs.Bag:
144+
runtime := map[string]any(typed)
145+
metadata[publishRuntimeMetadataKey] = runtime
146+
return runtime, nil
147+
default:
148+
return nil, fmt.Errorf("wippy.yaml metadata.runtime must be a map when publishing runtime profiles")
149+
}
150+
}
151+
152+
func hasRuntimeProfilesMetadata(metadata attrs.Bag) bool {
153+
return hasNestedRuntimeMetadata(metadata, publishRuntimeProfilesMetadataKey)
154+
}
155+
156+
func mergeRuntimeVarsMetadata(metadata attrs.Bag, runtime map[string]any, vars map[string]any, source string) error {
157+
existing := make(map[string]any)
158+
if raw, exists := runtime[publishRuntimeVarsMetadataKey]; exists {
159+
switch typed := raw.(type) {
160+
case map[string]any:
161+
existing = typed
162+
case attrs.Bag:
163+
existing = map[string]any(typed)
164+
default:
165+
return fmt.Errorf("wippy.yaml metadata.runtime.vars must be a map when publishing runtime profiles")
166+
}
167+
}
168+
169+
dottedPrefix := publishRuntimeMetadataKey + "." + publishRuntimeVarsMetadataKey + "."
170+
dottedVars := make(map[string]struct{})
171+
for key := range metadata {
172+
if strings.HasPrefix(key, dottedPrefix) {
173+
dottedVars[strings.TrimPrefix(key, dottedPrefix)] = struct{}{}
174+
}
175+
}
176+
177+
for key, value := range vars {
178+
if _, exists := existing[key]; exists {
179+
return fmt.Errorf("runtime var %q is defined in both %s and wippy.yaml metadata", key, source)
180+
}
181+
if _, exists := dottedVars[key]; exists {
182+
return fmt.Errorf("runtime var %q is defined in both %s and wippy.yaml metadata", key, source)
183+
}
184+
existing[key] = value
185+
}
186+
187+
runtime[publishRuntimeVarsMetadataKey] = existing
188+
return nil
189+
}
190+
191+
func hasNestedRuntimeMetadata(metadata attrs.Bag, nestedKey string) bool {
192+
if metadata == nil {
193+
return false
194+
}
195+
196+
dotted := publishRuntimeMetadataKey + "." + nestedKey
197+
for key := range metadata {
198+
if key == dotted || strings.HasPrefix(key, dotted+".") {
199+
return true
200+
}
201+
}
202+
203+
raw, exists := metadata[publishRuntimeMetadataKey]
204+
if !exists {
205+
return false
206+
}
207+
switch typed := raw.(type) {
208+
case map[string]any:
209+
_, exists = typed[nestedKey]
210+
return exists
211+
case attrs.Bag:
212+
_, exists = typed[nestedKey]
213+
return exists
214+
default:
215+
return false
216+
}
217+
}

0 commit comments

Comments
 (0)