From a7dcd10e2b301609c9fbc2ad7eb5fe68ce013a40 Mon Sep 17 00:00:00 2001 From: Clifford Tawiah Date: Tue, 10 Nov 2026 13:40:37 -0500 Subject: [PATCH] fix(sync): keep descriptor metadata out of the remote manifest The sync manifest API records the fingerprint and the version of each entry and rejects fingerprintSchema, referenceDigest, and adapterBehavior with "additional properties not allowed". The client no longer sends them in an upsert or reads them from a response. The sync.lock file keeps that metadata, so a change to it alone sends nothing to LaunchDarkly. Co-Authored-By: Claude Fable 5.1 --- internal/sync/api/manifest.go | 11 +++----- internal/sync/manifest/store.go | 25 +++++++---------- internal/sync/manifest/store_test.go | 41 ++++++++++++++++++---------- 3 files changed, 41 insertions(+), 36 deletions(-) diff --git a/internal/sync/api/manifest.go b/internal/sync/api/manifest.go index a80f21fa..8086b11f 100644 --- a/internal/sync/api/manifest.go +++ b/internal/sync/api/manifest.go @@ -18,15 +18,15 @@ type SyncManifest struct { Items []SyncManifestResource `json:"items"` } -// SyncManifestResource is one versioned baseline entry. +// SyncManifestResource is one versioned baseline entry. The remote manifest +// records the fingerprint and the version of each entry. The descriptor +// metadata of a linked resource, such as its reference digest, lives in the +// sync.lock file only, and the API rejects it. type SyncManifestResource struct { ResourceKind syncdomain.Kind `json:"resourceKind"` ResourceLookupKey string `json:"resourceLookupKey"` Fingerprint string `json:"fingerprint"` Version int `json:"version"` - FingerprintSchema string `json:"fingerprintSchema,omitempty"` - ReferenceDigest string `json:"referenceDigest,omitempty"` - AdapterBehavior string `json:"adapterBehavior,omitempty"` } // SyncManifestUpsert creates an entry, or updates the entry at Version. @@ -35,9 +35,6 @@ type SyncManifestUpsert struct { ResourceLookupKey string `json:"resourceLookupKey"` Fingerprint string `json:"fingerprint"` Version int `json:"version"` - FingerprintSchema string `json:"fingerprintSchema,omitempty"` - ReferenceDigest string `json:"referenceDigest,omitempty"` - AdapterBehavior string `json:"adapterBehavior,omitempty"` } // SyncManifestDeletion removes the entry at Version. diff --git a/internal/sync/manifest/store.go b/internal/sync/manifest/store.go index 0ebed1d4..701461d3 100644 --- a/internal/sync/manifest/store.go +++ b/internal/sync/manifest/store.go @@ -147,14 +147,11 @@ func (store Store) resourcesFromRemote(projectKey string, remote syncapi.SyncMan ) } resources = append(resources, Resource{ - ResourceKind: item.ResourceKind, - ProjectKey: projectKey, - LookupKey: item.ResourceLookupKey, - Fingerprint: item.Fingerprint, - Version: item.Version, - FingerprintSchema: item.FingerprintSchema, - ReferenceDigest: item.ReferenceDigest, - AdapterBehavior: item.AdapterBehavior, + ResourceKind: item.ResourceKind, + ProjectKey: projectKey, + LookupKey: item.ResourceLookupKey, + Fingerprint: item.Fingerprint, + Version: item.Version, }) } return resources, nil @@ -180,9 +177,6 @@ func changes(before, after []Resource) ([]syncapi.SyncManifestUpsert, []syncapi. ResourceLookupKey: resource.LookupKey, Fingerprint: resource.Fingerprint, Version: current.Version, - FingerprintSchema: resource.FingerprintSchema, - ReferenceDigest: resource.ReferenceDigest, - AdapterBehavior: resource.AdapterBehavior, }) } @@ -199,11 +193,12 @@ func changes(before, after []Resource) ([]syncapi.SyncManifestUpsert, []syncapi. return upserts, deletions } +// sameManifestContent reports whether two entries are the same to the remote +// manifest, which records only the fingerprint. The descriptor metadata of a +// linked resource stays in the sync.lock file, so a change to it alone sends +// nothing. func sameManifestContent(left, right Resource) bool { - return left.Fingerprint == right.Fingerprint && - left.FingerprintSchema == right.FingerprintSchema && - left.ReferenceDigest == right.ReferenceDigest && - left.AdapterBehavior == right.AdapterBehavior + return left.Fingerprint == right.Fingerprint } // changesApplied reports whether the remote manifest contains every change. diff --git a/internal/sync/manifest/store_test.go b/internal/sync/manifest/store_test.go index aad16e27..0017c87d 100644 --- a/internal/sync/manifest/store_test.go +++ b/internal/sync/manifest/store_test.go @@ -101,12 +101,7 @@ func TestStoreUpdatesWithRemoteVersions(t *testing.T) { client := &manifestClient{patchResponses: []syncapi.SyncManifest{{ Source: "git:example/repo", Items: []syncapi.SyncManifestResource{ - { - ResourceKind: syncdomain.KindVariation, ResourceLookupKey: "config/existing", - Fingerprint: fingerprint("c"), Version: 4, - FingerprintSchema: syncdomain.ProjectionFingerprintSchema, ReferenceDigest: fingerprint("e"), - AdapterBehavior: "markdown/1", - }, + {ResourceKind: syncdomain.KindVariation, ResourceLookupKey: "config/existing", Fingerprint: fingerprint("c"), Version: 4}, {ResourceKind: syncdomain.KindTool, ResourceLookupKey: "search", Fingerprint: fingerprint("d"), Version: 1}, }, }}} @@ -131,18 +126,36 @@ func TestStoreUpdatesWithRemoteVersions(t *testing.T) { require.Len(t, client.patches, 1) assert.Equal(t, []syncapi.SyncManifestUpsert{ {ResourceKind: syncdomain.KindTool, ResourceLookupKey: "search", Fingerprint: fingerprint("d"), Version: 0}, - { - ResourceKind: syncdomain.KindVariation, ResourceLookupKey: "config/existing", - Fingerprint: fingerprint("c"), Version: 3, - FingerprintSchema: syncdomain.ProjectionFingerprintSchema, ReferenceDigest: fingerprint("e"), - AdapterBehavior: "markdown/1", - }, - }, client.patches[0].upserts) + {ResourceKind: syncdomain.KindVariation, ResourceLookupKey: "config/existing", Fingerprint: fingerprint("c"), Version: 3}, + }, client.patches[0].upserts, "the upsert carries the fingerprint and the version, and no descriptor metadata") assert.Equal(t, []syncapi.SyncManifestDeletion{{ ResourceKind: syncdomain.KindSkill, ResourceLookupKey: "old", Version: 2, }}, client.patches[0].deletions) assert.Equal(t, 4, updated.Resources[1].Version) - assert.Equal(t, syncdomain.ProjectionFingerprintSchema, updated.Resources[1].FingerprintSchema) + assert.Empty(t, updated.Resources[1].FingerprintSchema, "the remote state has no descriptor metadata") +} + +// The remote manifest records only the fingerprint, so a change to the +// descriptor metadata of a linked resource with the same fingerprint sends +// nothing. The sync.lock file keeps that metadata. +func TestStoreSendsNothingForADescriptorMetadataChange(t *testing.T) { + client := &manifestClient{patchResponses: []syncapi.SyncManifest{{Source: "git:example/repo"}}} + store := NewStore(client, "git:example/repo") + previous := Manifest{Resources: []Resource{{ + ResourceKind: syncdomain.KindVariation, ProjectKey: "project", LookupKey: "config/linked", + Fingerprint: fingerprint("a"), Version: 3, + }}} + next := Manifest{Resources: []Resource{{ + ResourceKind: syncdomain.KindVariation, ProjectKey: "project", LookupKey: "config/linked", + Fingerprint: fingerprint("a"), Version: 3, + FingerprintSchema: syncdomain.ProjectionFingerprintSchema, ReferenceDigest: fingerprint("e"), + AdapterBehavior: "markdown/1", + }}} + + _, err := store.Update(previous, next) + + require.NoError(t, err) + assert.Empty(t, client.patches, "no patch for a change that the remote manifest cannot record") } func TestStoreReportsOptimisticConflict(t *testing.T) {