Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion api/v1alpha1/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ type Promotion struct {
}

type EnvironmentSpec struct {
// Organization is the org this environment belongs to. This feature helps us define org-local content.
Organization string `yaml:"organization,omitempty" json:"organization,omitempty"`

// Order controls catalog ordering (lower values first).
// When two environments share the same Order, they are sorted alphabetically by Name.
Order int `yaml:"order,omitempty" json:"order,omitempty"`
Expand Down Expand Up @@ -94,7 +97,8 @@ func (e Environment) Validate(validChartRefs []string) error {
errs = append(errs, fmt.Errorf("unknown ref: %s", ref))
}
}
return xerr.MultiErrOrderedFrom("",
return xerr.MultiErrOrderedFrom(
"",
internal.ValidateAgainstSchema(schemas.Environment, e.File.Tree),
xerr.MultiErrOrderedFrom("validating chart references", errs...),
)
Expand Down
5 changes: 3 additions & 2 deletions api/v1alpha1/schemas.cue
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ package v1alpha1
kind: "Environment"
metadata!: #metadata
spec: {
order?: number
organization?: string
order?: number
promotion?: {
allowAutoMerge?: bool
fromPullRequests?: bool
Expand Down Expand Up @@ -98,6 +99,6 @@ package v1alpha1
name!: string
annotations?: [string]: string
labels?: [string]: string
relativePath?: string
relativePath?: string
absolutePath?: string
}
6 changes: 4 additions & 2 deletions internal/release/cross/release.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,14 @@ func (r *Release) ComputePromotedFile(sourceEnv, targetEnv *v1alpha1.Environment
return nil
}

sameOrg := sourceEnv.Spec.Organization == targetEnv.Spec.Organization

// Do we have an existing target release?
var promotedFile *yml.File
var err error
if targetRelease != nil && targetRelease.File != nil {
// Promote source release to existing target
mergedTree := yml.Merge(targetRelease.File.Tree, sourceRelease.File.Tree)
mergedTree := yml.Merge(targetRelease.File.Tree, sourceRelease.File.Tree, sameOrg)
promotedFile, err = targetRelease.File.CopyWithNewTree(mergedTree)
if err != nil {
return fmt.Errorf("creating in-memory copy of target file using merged result: %w", err)
Expand All @@ -56,7 +58,7 @@ func (r *Release) ComputePromotedFile(sourceEnv, targetEnv *v1alpha1.Environment
}

// Promote source release to empty target
promoted := yml.Merge(nil, sourceRelease.File.Tree)
promoted := yml.Merge(nil, sourceRelease.File.Tree, sameOrg)
targetPath := filepath.Join(targetEnv.Dir, relativePath)

promotedFile, err = yml.NewFileFromTree(targetPath, sourceRelease.File.Indent, promoted)
Expand Down
55 changes: 54 additions & 1 deletion internal/release/promote/test/promotion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ package promote_test
import (
"fmt"
"io"
"maps"
"slices"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"

"github.com/nestoca/joy/api/v1alpha1"
"github.com/nestoca/joy/internal/git/pr"
Expand Down Expand Up @@ -268,6 +271,56 @@ func TestPromotion(t *testing.T) {
pullRequestTemplate: simplePullRequestTemplate,
expectedPromoted: true,
},
{
name: "org-local",
opts: newOpts(),
setup: func(args setupArgs) func(t *testing.T) {
args.opts.SourceEnv.Spec.Organization = "bar"
args.opts.TargetEnv.Spec.Organization = "foo"

args.opts.Catalog.Releases.Items = args.opts.Catalog.Releases.Items[:1]
args.opts.Catalog.Releases.Items[0].Releases[sourceEnvIndex] = newRelease(
"release1",
`spec: { key: value, potato: !org-local mashed }`,
sourceEnvName,
)

args.opts.Catalog.Releases.Items[0].Releases[targetEnvIndex] = newRelease("release1", `spec: {}`, sourceEnvName)

args.promptProvider.SelectReleasesFunc = func(list cross.ReleaseList, maxColumnWidth int) (cross.ReleaseList, error) {
return list, nil
}
args.promptProvider.SelectPromotionActionFunc = func() (string, error) {
return promote.CreatePR, nil
}

args.yamlWriter.WriteFileFunc = func(file *yml.File) error {
return nil
}

args.prProvider.CreateFunc = func(createParams pr.CreateParams) (string, error) {
return "https://github.com/owner/repo/pull/123", nil
}

setupDefaultMockInfoProvider(args.infoProvider)

return func(t *testing.T) {
calls := args.yamlWriter.WriteFileCalls()
require.Len(t, calls, 1)

var resource struct {
Spec map[string]any `yaml:"spec"`
}
require.NoError(t, yaml.Unmarshal(calls[0].File.Yaml, &resource))
require.Equal(t, []string{"key"}, slices.Collect(maps.Keys(resource.Spec)))
}
},
commitTemplate: simpleCommitTemplate,
pullRequestTemplate: simplePullRequestTemplate,
pullRequestLinkTemplate: "",
expectedErrorMessage: "",
expectedPromoted: true,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
Expand Down Expand Up @@ -374,7 +427,7 @@ metadata:
name, specYaml)
file, err := yml.NewFile("/dummy/environments/"+envName+"/releases/release.yaml", []byte(yaml))
if err != nil {
panic(err)
panic(fmt.Errorf("%v: %s", err, yaml))
}
return file
}
Expand Down
28 changes: 22 additions & 6 deletions internal/yml/merge.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"gopkg.in/yaml.v3"
)

func Merge(dst, src *yaml.Node) *yaml.Node {
func Merge(dst, src *yaml.Node, sameOrg bool) *yaml.Node {
dst, src = Clone(dst), Clone(src)

doc := func() *yaml.Node {
Expand All @@ -23,6 +23,10 @@ func Merge(dst, src *yaml.Node) *yaml.Node {
src = markLockedValuesAsTodo(src, false)
src = purgeLocalContent(src)

if !sameOrg {
src = purgeOrgLocalContent(src)
}

result := merge(dst, src)
if result == nil {
result = &yaml.Node{Kind: yaml.MappingNode}
Expand Down Expand Up @@ -162,6 +166,10 @@ func IsLocal(node *yaml.Node) bool {
return node != nil && node.Tag == "!local"
}

func IsOrgLocal(node *yaml.Node) bool {
return node != nil && node.Tag == "!org-local"
}

type KeyValuePair struct {
Key *yaml.Node
Value *yaml.Node
Expand Down Expand Up @@ -216,7 +224,15 @@ func firstNonNil(nodes ...*yaml.Node) *yaml.Node {
}

func purgeLocalContent(node *yaml.Node) *yaml.Node {
if node == nil || IsLocal(node) {
return purgeNodes(node, IsLocal)
}

func purgeOrgLocalContent(node *yaml.Node) *yaml.Node {
return purgeNodes(node, IsOrgLocal)
}

func purgeNodes(node *yaml.Node, predicate func(*yaml.Node) bool) *yaml.Node {
if node == nil || predicate(node) {
return nil
}

Expand All @@ -226,17 +242,17 @@ func purgeLocalContent(node *yaml.Node) *yaml.Node {
switch node.Kind {
case yaml.MappingNode:
for i := 0; i < len(node.Content); i += 2 {
if IsLocal(node.Content[i+1]) {
if predicate(node.Content[i+1]) {
continue
}
copy.Content = append(copy.Content, node.Content[i], purgeLocalContent(node.Content[i+1]))
copy.Content = append(copy.Content, node.Content[i], purgeNodes(node.Content[i+1], predicate))
}
case yaml.SequenceNode:
for _, item := range node.Content {
if IsLocal(item) {
if predicate(item) {
continue
}
copy.Content = append(copy.Content, purgeLocalContent(item))
copy.Content = append(copy.Content, purgeNodes(item, predicate))
}
}

Expand Down
27 changes: 22 additions & 5 deletions internal/yml/merge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,7 @@ func testMerge(t *testing.T, testcase MergeCase) {
}

// Merge
result := Merge(dst, src)
result := Merge(dst, src, false)

// Marshal the result with custom indentation
var buf bytes.Buffer
Expand Down Expand Up @@ -450,7 +450,7 @@ e:
k: TODO
`
// Merge YAML nodes
mergedNode := Merge(nil, &sourceNode)
mergedNode := Merge(nil, &sourceNode, false)

// Marshal the merged result
var buf bytes.Buffer
Expand Down Expand Up @@ -525,7 +525,7 @@ func TestTodoMerge(t *testing.T) {
var dst yaml.Node
require.NoError(t, yaml.Unmarshal([]byte(tc.Dst), &dst))

result, err := yaml.Marshal(Merge(&dst, &src).Content[0])
result, err := yaml.Marshal(Merge(&dst, &src, false).Content[0])
require.NoError(t, err)

result = bytes.TrimSpace(result)
Expand Down Expand Up @@ -751,7 +751,7 @@ func TestYmlMerge(t *testing.T) {
require.NoError(t, yaml.Unmarshal([]byte(tc.Src), &src))
require.NoError(t, yaml.Unmarshal([]byte(tc.Dst), &dst))

actual, err := yaml.Marshal(Merge(&dst, &src))
actual, err := yaml.Marshal(Merge(&dst, &src, false))
require.NoError(t, err)

actual = bytes.TrimSpace(actual)
Expand All @@ -761,11 +761,28 @@ func TestYmlMerge(t *testing.T) {
var result yaml.Node
require.NoError(t, yaml.Unmarshal(actual, &result))

check, err := yaml.Marshal(Merge(&result, &src))
check, err := yaml.Marshal(Merge(&result, &src, false))
require.NoError(t, err)

check = bytes.TrimSpace(check)
require.Equal(t, actual, check, "failed idempotency check")
})
}
}

func TestOrgLocalMerge(t *testing.T) {
var src, dst yaml.Node

require.NoError(t, yaml.Unmarshal([]byte(`{
apple: bottom,
jeans: !org-local boots,
}`), &src))

require.NoError(t, yaml.Unmarshal([]byte(`{}`), &dst))

result := Merge(&dst, &src, true)
require.Equal(t, "boots", FindNodeValueOrDefault(result, "jeans", "not-found"))

result = Merge(&dst, &src, false)
require.Equal(t, "not-found", FindNodeValueOrDefault(result, "jeans", "not-found"))
}
2 changes: 1 addition & 1 deletion internal/yml/tags.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ var StandardTags = []string{
"!!merge",
}

var CustomTags = []string{"!lock", "!local"}
var CustomTags = []string{"!lock", "!local", "!org-local"}

var KnownTags = append(StandardTags, CustomTags...)
Loading