From dbc5ee8eebf1c4b9773df32fc75bbe5d7ac1a722 Mon Sep 17 00:00:00 2001 From: Tmwakalasya Date: Sat, 10 Oct 2026 23:53:19 -0400 Subject: [PATCH] fix(buildkite): handle null build metadata without panicking --- platform/buildkite/client.go | 11 ++++++----- platform/buildkite/client_test.go | 26 +++++++++++++++++++++----- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/platform/buildkite/client.go b/platform/buildkite/client.go index a37f1f842..6088613e0 100644 --- a/platform/buildkite/client.go +++ b/platform/buildkite/client.go @@ -208,11 +208,9 @@ func ParseState(raw string) State { } // DecodeMetadataEnv recovers a JSON-encoded map[string]string from env[key]. -// Buildkite echoes env vars back on the build object, so a caller that seeded -// a JSON-encoded map at Trigger time can recover it here at Status time -// without any local state. Returns an empty non-nil map when the key is -// absent. When the value is present but cannot be decoded, returns a nil map -// and the decode error for the caller to log or act on as it sees fit. +// Returns a non-nil map on success, including when the value is absent, +// empty, or JSON null. A value that cannot be decoded returns a nil map +// and the decode error. func DecodeMetadataEnv(env map[string]string, key string) (map[string]string, error) { meta := make(map[string]string) raw, ok := env[key] @@ -222,5 +220,8 @@ func DecodeMetadataEnv(env map[string]string, key string) (map[string]string, er if err := json.Unmarshal([]byte(raw), &meta); err != nil { return nil, fmt.Errorf("decode metadata env %q: %w", key, err) } + if meta == nil { + meta = make(map[string]string) + } return meta, nil } diff --git a/platform/buildkite/client_test.go b/platform/buildkite/client_test.go index aee60df86..28f87f208 100644 --- a/platform/buildkite/client_test.go +++ b/platform/buildkite/client_test.go @@ -221,11 +221,27 @@ func TestDecodeMetadataEnv_PresentAndValid(t *testing.T) { assert.Equal(t, map[string]string{"requester": "alice", "ticket": "SQ-42"}, got) } -func TestDecodeMetadataEnv_Absent_ReturnsEmptyMap(t *testing.T) { - got, err := DecodeMetadataEnv(map[string]string{}, "SQ_METADATA") - require.NoError(t, err) - assert.Empty(t, got) - assert.NotNil(t, got) +func TestDecodeMetadataEnv_Empty_ReturnsWritableMap(t *testing.T) { + tests := []struct { + name string + env map[string]string + }{ + {"absent", nil}, + {"empty string", map[string]string{"SQ_METADATA": ""}}, + {"empty object", map[string]string{"SQ_METADATA": "{}"}}, + {"null", map[string]string{"SQ_METADATA": "null"}}, + {"null with whitespace", map[string]string{"SQ_METADATA": " \nnull\t"}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := DecodeMetadataEnv(tt.env, "SQ_METADATA") + require.NoError(t, err) + require.NotNil(t, got) + assert.Empty(t, got) + got["url"] = "https://buildkite.com/test-org/my-pipeline/builds/42" + assert.Len(t, got, 1) + }) + } } func TestDecodeMetadataEnv_Malformed_ReturnsError(t *testing.T) {