From fb6f27fab2d4fa3260631e7743bdb41014ec6254 Mon Sep 17 00:00:00 2001 From: David Zhao Date: Sat, 15 Aug 2026 23:22:03 -0500 Subject: [PATCH 1/2] Consolidate on go.yaml.in/yaml/v3 protocol pulled in two copies of the same yaml library: gopkg.in/yaml.v3 (auth, configutil, livekit/types) and go.yaml.in/yaml/v3 (transitively via buf.build/go/protoyaml). go.yaml.in is the project's new canonical home, so switch our own imports over and compile it once. RoomConfiguration, RoomEgress and RoomAgent needed care. Their custom UnmarshalYAML took a *yaml.Node, and a yaml decoder only recognizes that method if the Node belongs to its own package. Simply re-pointing the import would mean every caller still decoding with gopkg.in/yaml.v3 -- the server, egress, ingress, sip and cloud all do -- silently stops invoking the custom unmarshaller and falls back to reflective struct decoding of a protobuf message. No compile error; fields such as min_playout_delay just quietly come back zero. Instead these now use the func-based unmarshal signature, which names no types from any yaml package and so is honored by both decoders. Downstream repos keep working unchanged, whichever yaml package they import. Added TestUnmarshallRoomConfigurationBothYAMLPackages, which decodes the same document with both packages and asserts a populated message. Verified it fails (min_playout_delay 0 instead of 42) if the *yaml.Node signature is restored. gopkg.in/yaml.v3 stays in go.mod for that test only; it is no longer in the compiled dependency set. Co-Authored-By: Claude Opus 5 (1M context) --- auth/provider.go | 2 +- go.mod | 2 +- go.sum | 2 -- livekit/types.go | 55 ++++++++++++++++++++---------------- livekit/types_test.go | 48 ++++++++++++++++++++++++++++++- utils/configutil/observer.go | 2 +- 6 files changed, 80 insertions(+), 31 deletions(-) diff --git a/auth/provider.go b/auth/provider.go index 1503a44ab..dde8f2b73 100644 --- a/auth/provider.go +++ b/auth/provider.go @@ -17,7 +17,7 @@ package auth import ( "io" - "gopkg.in/yaml.v3" + "go.yaml.in/yaml/v3" ) type FileBasedKeyProvider struct { diff --git a/go.mod b/go.mod index 058aad339..3048626ca 100644 --- a/go.mod +++ b/go.mod @@ -40,6 +40,7 @@ require ( go.uber.org/multierr v1.11.0 go.uber.org/zap v1.28.0 go.uber.org/zap/exp v0.3.0 + go.yaml.in/yaml/v3 v3.0.4 golang.org/x/exp v0.0.0-20260603202125-055de637280b golang.org/x/mod v0.38.0 golang.org/x/sys v0.47.0 @@ -90,7 +91,6 @@ require ( go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 // indirect go.opentelemetry.io/otel/metric v1.44.0 // indirect go.opentelemetry.io/proto/otlp v1.10.0 // indirect - go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.53.0 // indirect golang.org/x/net v0.56.0 // indirect golang.org/x/sync v0.22.0 // indirect diff --git a/go.sum b/go.sum index c2d8c9f45..4330d4a19 100644 --- a/go.sum +++ b/go.sum @@ -87,8 +87,6 @@ github.com/lithammer/shortuuid/v4 v4.2.0 h1:LMFOzVB3996a7b8aBuEXxqOBflbfPQAiVzkI github.com/lithammer/shortuuid/v4 v4.2.0/go.mod h1:D5noHZ2oFw/YaKCfGy0YxyE7M0wMbezmMjPdhyEFe6Y= github.com/livekit/mageutil v0.0.0-20250511045019-0f1ff63f7731 h1:9x+U2HGLrSw5ATTo469PQPkqzdoU7be46ryiCDO3boc= github.com/livekit/mageutil v0.0.0-20250511045019-0f1ff63f7731/go.mod h1:Rs3MhFwutWhGwmY1VQsygw28z5bWcnEYmS1OG9OxjOQ= -github.com/livekit/psrpc v0.7.2 h1:6oZ+NODJ2pLyaT6VqDq1F4Qc/3TpDUSpyphj/P9MhQc= -github.com/livekit/psrpc v0.7.2/go.mod h1:rAI+m2+/cb4x9RXhLRtUx5ZwdfjjXOl4zi46IjEetaw= github.com/livekit/psrpc v0.7.3 h1:bekuZt/ZQzg8+/M8G6G5jq7bvV9fAKdPHSOZeTwrIIc= github.com/livekit/psrpc v0.7.3/go.mod h1:rAI+m2+/cb4x9RXhLRtUx5ZwdfjjXOl4zi46IjEetaw= github.com/mackerelio/go-osstat v0.2.8 h1:I2duicTaCGWoM53XwAwA9OIe1inu0xnVs8/pqOWWVr4= diff --git a/livekit/types.go b/livekit/types.go index 3904f2dfa..868ea520d 100644 --- a/livekit/types.go +++ b/livekit/types.go @@ -27,8 +27,8 @@ import ( "buf.build/go/protoyaml" "github.com/dennwc/iters" "go.opentelemetry.io/otel/attribute" + "go.yaml.in/yaml/v3" proto "google.golang.org/protobuf/proto" - "gopkg.in/yaml.v3" ) const ( @@ -139,48 +139,53 @@ type Guid interface { type GuidBlock [9]byte -func (r *RoomConfiguration) UnmarshalYAML(value *yaml.Node) error { - // Marshall the Node back to yaml to pass it to the protobuf specific unmarshaller - str, err := yaml.Marshal(value) - if err != nil { - return err - } - - return protoyaml.Unmarshal(str, r) +func (r *RoomConfiguration) UnmarshalYAML(unmarshal func(interface{}) error) error { + return unmarshalProto(unmarshal, r) } func (r *RoomConfiguration) MarshalYAML() (interface{}, error) { return marshalProto(r) } -func (r *RoomEgress) UnmarshalYAML(value *yaml.Node) error { - // Marshall the Node back to yaml to pass it to the protobuf specific unmarshaller - str, err := yaml.Marshal(value) - if err != nil { - return err - } - - return protoyaml.Unmarshal(str, r) +func (r *RoomEgress) UnmarshalYAML(unmarshal func(interface{}) error) error { + return unmarshalProto(unmarshal, r) } func (r *RoomEgress) MarshalYAML() (interface{}, error) { return marshalProto(r) } -func (r *RoomAgent) UnmarshalYAML(value *yaml.Node) error { - // Marshall the Node back to yaml to pass it to the protobuf specific unmarshaller - str, err := yaml.Marshal(value) - if err != nil { - return err - } - - return protoyaml.Unmarshal(str, r) +func (r *RoomAgent) UnmarshalYAML(unmarshal func(interface{}) error) error { + return unmarshalProto(unmarshal, r) } func (r *RoomAgent) MarshalYAML() (interface{}, error) { return marshalProto(r) } +// unmarshalProto decodes a yaml value into a protobuf message using the protobuf +// specific unmarshaller. +// +// The func-based unmarshal signature is deliberate: unlike UnmarshalYAML(*yaml.Node), +// it names no types from any yaml package, so decoders from both gopkg.in/yaml.v3 and +// go.yaml.in/yaml/v3 recognize it. Taking a *yaml.Node would bind these types to +// whichever package protocol imports, and callers using the other one would silently +// fall back to reflective struct decoding. +func unmarshalProto(unmarshal func(interface{}) error, o proto.Message) error { + var v interface{} + if err := unmarshal(&v); err != nil { + return err + } + + // Marshall the value back to yaml to pass it to the protobuf specific unmarshaller + str, err := yaml.Marshal(v) + if err != nil { + return err + } + + return protoyaml.Unmarshal(str, o) +} + func marshalProto(o proto.Message) (map[string]interface{}, error) { // Marshall the Node to yaml using the protobuf specific marshaller to ensure the proper field names are used str, err := protoyaml.MarshalOptions{UseProtoNames: true}.Marshal(o) diff --git a/livekit/types_test.go b/livekit/types_test.go index 729d6536a..254ad37d8 100644 --- a/livekit/types_test.go +++ b/livekit/types_test.go @@ -9,8 +9,9 @@ import ( "github.com/dennwc/iters" "github.com/stretchr/testify/require" + "go.yaml.in/yaml/v3" proto "google.golang.org/protobuf/proto" - "gopkg.in/yaml.v3" + gopkgyaml "gopkg.in/yaml.v3" ) func TestUnmarshallRoomConfiguration(t *testing.T) { @@ -44,6 +45,51 @@ a: } +// TestUnmarshallRoomConfigurationBothYAMLPackages guards the func-based UnmarshalYAML +// signature on RoomConfiguration/RoomEgress/RoomAgent. +// +// Callers decode protocol types with either gopkg.in/yaml.v3 or go.yaml.in/yaml/v3. +// A UnmarshalYAML(*yaml.Node) method is only recognized by the package that owns the +// Node type; the other package silently falls back to reflective struct decoding, +// which leaves proto fields such as min_playout_delay at their zero value rather than +// returning an error. Both decoders must therefore keep producing a populated message. +func TestUnmarshallRoomConfigurationBothYAMLPackages(t *testing.T) { + y := ` +name: room_name +egress: + room: + room_name: egress_room +agents: + - agent_name: ag + metadata: mm +min_playout_delay: 42 +` + + check := func(t *testing.T, rc *RoomConfiguration) { + t.Helper() + require.Equal(t, "room_name", rc.Name) + // zero here means the custom unmarshaller was skipped + require.Equal(t, uint32(42), rc.MinPlayoutDelay) + require.NotNil(t, rc.Egress) + require.Equal(t, "egress_room", rc.Egress.Room.RoomName) + require.Equal(t, 1, len(rc.Agents)) + require.Equal(t, "ag", rc.Agents[0].AgentName) + require.Equal(t, "mm", rc.Agents[0].Metadata) + } + + t.Run("go.yaml.in/yaml/v3", func(t *testing.T) { + var rc RoomConfiguration + require.NoError(t, yaml.Unmarshal([]byte(y), &rc)) + check(t, &rc) + }) + + t.Run("gopkg.in/yaml.v3", func(t *testing.T) { + var rc RoomConfiguration + require.NoError(t, gopkgyaml.Unmarshal([]byte(y), &rc)) + check(t, &rc) + }) +} + func TestMarshallRoomConfiguration(t *testing.T) { r := &RoomConfiguration{ Name: "name", diff --git a/utils/configutil/observer.go b/utils/configutil/observer.go index 6eb027a24..730423969 100644 --- a/utils/configutil/observer.go +++ b/utils/configutil/observer.go @@ -23,7 +23,7 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" "go.uber.org/atomic" - "gopkg.in/yaml.v3" + "go.yaml.in/yaml/v3" "github.com/livekit/protocol/logger" "github.com/livekit/protocol/utils/events" From a192275d9a7e7a0bf59a2d82402c9288b47a5ba4 Mon Sep 17 00:00:00 2001 From: David Zhao Date: Sun, 16 Aug 2026 09:58:35 -0500 Subject: [PATCH 2/2] Add changeset for consolidating go.yaml.in/yaml/v3 --- .changeset/rare-carrots-grab.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/rare-carrots-grab.md diff --git a/.changeset/rare-carrots-grab.md b/.changeset/rare-carrots-grab.md new file mode 100644 index 000000000..e2c208ea0 --- /dev/null +++ b/.changeset/rare-carrots-grab.md @@ -0,0 +1,5 @@ +--- + +--- + +Consolidate on go.yaml.in/yaml/v3