From 9230dea5d998f9d4e6e02d69b7ced9a27fcad671 Mon Sep 17 00:00:00 2001 From: Cliff Tawiah Date: Fri, 30 Oct 2026 14:31:32 -0400 Subject: [PATCH] feat(sync): prepare repository source links --- internal/sync/link/link.go | 81 +++++++++++++++++++++++++++++++++ internal/sync/link/link_test.go | 45 ++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 internal/sync/link/link.go create mode 100644 internal/sync/link/link_test.go diff --git a/internal/sync/link/link.go b/internal/sync/link/link.go new file mode 100644 index 00000000..cba56b40 --- /dev/null +++ b/internal/sync/link/link.go @@ -0,0 +1,81 @@ +// Package link validates and writes repository source descriptors. +package link + +import ( + "errors" + "os" + "path/filepath" + + syncdomain "github.com/launchdarkly/ldcli/internal/sync" + synclocal "github.com/launchdarkly/ldcli/internal/sync/local" + syncreference "github.com/launchdarkly/ldcli/internal/sync/reference" +) + +// Options are the explicit inputs for one repository source link. +type Options struct { + Store synclocal.Store + RepositoryRoot string + WorkingDirectory string + ID syncdomain.ResourceID + File string + Format string + Select string + Map map[string]string +} + +// Prepared is one validated descriptor and its compiled local resource. +type Prepared struct { + ID syncdomain.ResourceID + Reference syncdomain.Reference + Definition synclocal.SourceDefinition + Path string + Content []byte + Previous []byte +} + +// Prepare validates a repository source without writing its descriptor. +func Prepare(options Options) (Prepared, error) { + if err := options.Store.ValidateDescriptorTarget(options.ID); err != nil { + return Prepared{}, err + } + reference, err := synclocal.NewSourceReference( + options.RepositoryRoot, + options.WorkingDirectory, + options.File, + options.Format, + options.Select, + options.Map, + ) + if err != nil { + return Prepared{}, err + } + reference, err = syncreference.NormalizeReference(options.ID, reference) + if err != nil { + return Prepared{}, err + } + definition, err := synclocal.CompileSource(options.RepositoryRoot, options.ID, reference) + if err != nil { + return Prepared{}, err + } + path, err := synclocal.ReferencePath(options.ID) + if err != nil { + return Prepared{}, err + } + content, err := synclocal.RenderDescriptor(options.ID, reference) + if err != nil { + return Prepared{}, err + } + previous, readErr := os.ReadFile(filepath.Join(options.RepositoryRoot, filepath.FromSlash(path))) + if readErr != nil && !errors.Is(readErr, os.ErrNotExist) { + return Prepared{}, readErr + } + return Prepared{ + ID: options.ID, Reference: reference, Definition: definition, Path: path, Content: content, + Previous: previous, + }, nil +} + +// Write atomically publishes the prepared descriptor. +func (prepared Prepared) Write(store synclocal.Store) (string, error) { + return store.WriteDescriptor(prepared.ID, prepared.Reference) +} diff --git a/internal/sync/link/link_test.go b/internal/sync/link/link_test.go new file mode 100644 index 00000000..84e799bc --- /dev/null +++ b/internal/sync/link/link_test.go @@ -0,0 +1,45 @@ +package link + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + + syncdomain "github.com/launchdarkly/ldcli/internal/sync" + synclocal "github.com/launchdarkly/ldcli/internal/sync/local" +) + +func TestPrepareDoesNotWriteUntilExplicitWrite(t *testing.T) { + root := t.TempDir() + source := []byte(`from agents import Agent +support = Agent(instructions="Help with orders.") +`) + require.NoError(t, os.WriteFile(filepath.Join(root, "agents.py"), source, 0o644)) + id := syncdomain.VariationID("production", "support", "default") + + prepared, err := Prepare(Options{ + Store: synclocal.NewStore(root), + RepositoryRoot: root, + WorkingDirectory: root, + ID: id, + File: "agents.py", + Format: "python", + Select: `agents[id="support"]`, + Map: map[string]string{"prompt": "instructions"}, + }) + + require.NoError(t, err) + require.Equal(t, ".launchdarkly/production/configs/support/default.ref.yaml", prepared.Path) + require.NoFileExists(t, filepath.Join(root, filepath.FromSlash(prepared.Path))) + path, err := prepared.Write(synclocal.NewStore(root)) + require.NoError(t, err) + descriptor, err := os.ReadFile(filepath.Join(root, filepath.FromSlash(path))) + require.NoError(t, err) + require.Contains(t, string(descriptor), "formatVersion: 1") + require.Contains(t, string(descriptor), `select: agents[id="support"]`) + actual, err := os.ReadFile(filepath.Join(root, "agents.py")) + require.NoError(t, err) + require.Equal(t, source, actual) +}