Skip to content

Commit 9b8872a

Browse files
authored
Merge pull request #72 from thand-io/fix-yaml2json-on
fixed parsing of on: keys for yaml2json conversions
2 parents d3f2ab6 + d915a74 commit 9b8872a

5 files changed

Lines changed: 216 additions & 50 deletions

File tree

.github/workflows/test-and-build.yml

Lines changed: 38 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,19 @@ jobs:
215215
runs-on: ubuntu-latest
216216
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
217217
needs: [auto-tag, build]
218+
strategy:
219+
matrix:
220+
include:
221+
- goos: linux
222+
goarch: amd64
223+
- goos: linux
224+
goarch: arm64
225+
- goos: darwin
226+
goarch: amd64
227+
- goos: darwin
228+
goarch: arm64
229+
- goos: windows
230+
goarch: amd64
218231

219232
steps:
220233
- name: Checkout code
@@ -235,50 +248,41 @@ jobs:
235248
echo "version=$VERSION" >> $GITHUB_OUTPUT
236249
echo "commit=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
237250
238-
- name: Build for multiple platforms
251+
- name: Build for ${{ matrix.goos }}/${{ matrix.goarch }}
239252
run: |
240253
VERSION=${{ steps.version.outputs.version }}
241254
COMMIT=${{ steps.version.outputs.commit }}
255+
GOOS=${{ matrix.goos }}
256+
GOARCH=${{ matrix.goarch }}
242257
243258
# Create dist directory
244259
mkdir -p dist
245260
246-
# Build for different platforms and create archives
247-
platforms=(
248-
"linux/amd64"
249-
"linux/arm64"
250-
"darwin/amd64"
251-
"darwin/arm64"
252-
"windows/amd64"
253-
)
261+
# Set output name
262+
output_name="agent"
263+
if [ "$GOOS" = "windows" ]; then
264+
output_name="agent.exe"
265+
fi
266+
267+
echo "Building for $GOOS/$GOARCH..."
268+
GOEXPERIMENT=jsonv2 CGO_ENABLED=0 GOOS=$GOOS GOARCH=$GOARCH go build -a -installsuffix cgo \
269+
-ldflags "-X github.com/thand-io/agent/internal/common.Version=$VERSION -X github.com/thand-io/agent/internal/common.GitCommit=$COMMIT" \
270+
-o "dist/$output_name" .
271+
272+
# Create archive
273+
if [ "$GOOS" = "windows" ]; then
274+
cd dist && zip "agent-${GOOS}-${GOARCH}.zip" "$output_name" && cd ..
275+
else
276+
cd dist && tar -czf "agent-${GOOS}-${GOARCH}.tar.gz" "$output_name" && cd ..
277+
fi
254278
255-
for platform in "${platforms[@]}"; do
256-
IFS='/' read -r os arch <<< "$platform"
257-
output_name="agent"
258-
if [ "$os" = "windows" ]; then
259-
output_name="agent.exe"
260-
fi
261-
262-
echo "Building for $os/$arch..."
263-
GOEXPERIMENT=jsonv2 CGO_ENABLED=0 GOOS=$os GOARCH=$arch go build -a -installsuffix cgo \
264-
-ldflags "-X github.com/thand-io/agent/internal/common.Version=$VERSION -X github.com/thand-io/agent/internal/common.GitCommit=$COMMIT" \
265-
-o "dist/$output_name" .
266-
267-
# Create archive
268-
if [ "$os" = "windows" ]; then
269-
cd dist && zip "agent-${os}-${arch}.zip" "$output_name" && cd ..
270-
else
271-
cd dist && tar -czf "agent-${os}-${arch}.tar.gz" "$output_name" && cd ..
272-
fi
273-
274-
# Also keep the binary for direct download
275-
mv "dist/$output_name" "dist/agent-${os}-${arch}$([ "$os" = "windows" ] && echo ".exe" || echo "")"
276-
done
279+
# Also keep the binary for direct download
280+
mv "dist/$output_name" "dist/agent-${GOOS}-${GOARCH}$([ "$GOOS" = "windows" ] && echo ".exe" || echo "")"
277281
278282
- name: Upload build artifacts
279283
uses: actions/upload-artifact@v4
280284
with:
281-
name: binaries-${{ steps.version.outputs.version }}
285+
name: binaries-${{ matrix.goos }}-${{ matrix.goarch }}-${{ steps.version.outputs.version }}
282286
path: dist/
283287

284288
docker-release:
@@ -302,8 +306,9 @@ jobs:
302306
- name: Download build artifacts
303307
uses: actions/download-artifact@v4
304308
with:
305-
name: binaries-${{ steps.version.outputs.version }}
309+
pattern: binaries-*-${{ steps.version.outputs.version }}
306310
path: dist/
311+
merge-multiple: true
307312

308313
- name: Set up Docker Buildx
309314
uses: docker/setup-buildx-action@v3

go.mod

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,6 @@ require (
7676
k8s.io/api v0.34.1
7777
k8s.io/apimachinery v0.34.1
7878
k8s.io/client-go v0.34.1
79-
sigs.k8s.io/yaml v1.6.0
8079
)
8180

8281
// removed replace github.com/moby/moby => github.com/docker/docker (not needed for v24)
@@ -153,7 +152,7 @@ require (
153152
github.com/felixge/httpsnoop v1.0.4 // indirect
154153
github.com/fsnotify/fsnotify v1.9.0 // indirect
155154
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
156-
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
155+
github.com/gabriel-vasile/mimetype v1.4.11 // indirect
157156
github.com/gin-contrib/sse v1.1.0 // indirect
158157
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
159158
github.com/go-logr/logr v1.4.3 // indirect
@@ -315,9 +314,9 @@ require (
315314
golang.org/x/term v0.36.0 // indirect
316315
golang.org/x/time v0.14.0 // indirect
317316
golang.org/x/tools v0.38.0 // indirect
318-
google.golang.org/genproto v0.0.0-20251022142026-3a174f9686a8 // indirect
319-
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect
320-
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect
317+
google.golang.org/genproto v0.0.0-20251029180050-ab9386a59fda // indirect
318+
google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda // indirect
319+
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda // indirect
321320
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
322321
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
323322
gopkg.in/inf.v0 v0.9.1 // indirect
@@ -327,4 +326,5 @@ require (
327326
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
328327
sigs.k8s.io/randfill v1.0.0 // indirect
329328
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
329+
sigs.k8s.io/yaml v1.6.0 // indirect
330330
)

go.sum

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -238,8 +238,8 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S
238238
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
239239
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
240240
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
241-
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
242-
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
241+
github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik=
242+
github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
243243
github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ=
244244
github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE=
245245
github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY=
@@ -763,12 +763,12 @@ google.golang.org/api v0.254.0 h1:jl3XrGj7lRjnlUvZAbAdhINTLbsg5dbjmR90+pTQvt4=
763763
google.golang.org/api v0.254.0/go.mod h1:5BkSURm3D9kAqjGvBNgf0EcbX6Rnrf6UArKkwBzAyqQ=
764764
google.golang.org/genai v1.32.0 h1:kku/m3kWOncjnw8EIa2sgmrPLhaxFHaP+uqOq5ZckvI=
765765
google.golang.org/genai v1.32.0/go.mod h1:7pAilaICJlQBonjKKJNhftDFv3SREhZcTe9F6nRcjbg=
766-
google.golang.org/genproto v0.0.0-20251022142026-3a174f9686a8 h1:a12a2/BiVRxRWIqBbfqoSK6tgq8cyUgMnEI81QlPge0=
767-
google.golang.org/genproto v0.0.0-20251022142026-3a174f9686a8/go.mod h1:1Ic78BnpzY8OaTCmzxJDP4qC9INZPbGZl+54RKjtyeI=
768-
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4=
769-
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
770-
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk=
771-
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
766+
google.golang.org/genproto v0.0.0-20251029180050-ab9386a59fda h1:fQ3VVQ11pb84nu0o/8wD6oZq13Q6+HK30P+9GSRlrqk=
767+
google.golang.org/genproto v0.0.0-20251029180050-ab9386a59fda/go.mod h1:1Ic78BnpzY8OaTCmzxJDP4qC9INZPbGZl+54RKjtyeI=
768+
google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda h1:+2XxjfsAu6vqFxwGBRcHiMaDCuZiqXGDUDVWVtrFAnE=
769+
google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
770+
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda h1:i/Q+bfisr7gq6feoJnS/DlpdwEL4ihp41fvRiM3Ork0=
771+
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
772772
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
773773
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
774774
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=

internal/config/common.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import (
1313
"github.com/serverlessworkflow/sdk-go/v3/model"
1414
"github.com/sirupsen/logrus"
1515
"github.com/thand-io/agent/internal/common"
16-
yamlSig "sigs.k8s.io/yaml"
16+
"gopkg.in/yaml.v3"
1717
)
1818

1919
// loadDataFromSource loads data from either a file path or URL
@@ -189,12 +189,19 @@ func readData[T WorkflowDefinitions | RoleDefinitions | ProviderDefinitions](
189189
// If JSON we can unmarshal directly
190190
logrus.Debugln("Data format detected: JSON")
191191
} else {
192-
// If YAML we need to convert to JSON after
193-
if yamlData, err := yamlSig.YAMLToJSON(data); err != nil {
192+
// If YAML we need to convert to JSON after. Have to use json
193+
// as the DSL serverless workflow SDK expects JSON
194+
var yamlData any
195+
if err := yaml.Unmarshal(data, &yamlData); err != nil {
196+
logrus.WithError(err).Errorln("Failed to unmarshal YAML")
197+
return nil, err
198+
}
199+
200+
if jsonData, err := json.Marshal(yamlData); err != nil {
194201
logrus.WithError(err).Errorln("Failed to convert YAML to JSON")
195202
return nil, err
196203
} else {
197-
data = yamlData
204+
data = jsonData
198205
}
199206
}
200207

internal/config/common_test.go

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package config
2+
3+
import (
4+
"encoding/json"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
// TestReadData_WorkflowStructure tests readData with realistic workflow structure
12+
func TestReadData_WorkflowStructure(t *testing.T) {
13+
yamlInput := `version: "1.0"
14+
workflows:
15+
deploy-workflow:
16+
name: "Deploy Application"
17+
description: "Workflow that triggers on push events"
18+
enabled: true`
19+
20+
var definition WorkflowDefinitions
21+
result, err := readData([]byte(yamlInput), definition)
22+
require.NoError(t, err)
23+
require.NotNil(t, result)
24+
25+
// Verify the result has the expected structure
26+
assert.Equal(t, "1.0", result.Version)
27+
assert.NotNil(t, result.Workflows)
28+
29+
workflow, exists := result.Workflows["deploy-workflow"]
30+
assert.True(t, exists, "workflow should exist")
31+
assert.Equal(t, "Deploy Application", workflow.Name)
32+
assert.Equal(t, "Workflow that triggers on push events", workflow.Description)
33+
assert.True(t, workflow.Enabled)
34+
}
35+
36+
// TestReadData_YAMLWithProblematicKeysDirectValidation tests the core YAML parsing fix
37+
func TestReadData_YAMLWithProblematicKeysDirectValidation(t *testing.T) {
38+
// Test serverless workflow YAML that uses 'on' in schedule - this would be broken with sigs.k8s.io/yaml
39+
yamlInput := `version: "1.0"
40+
workflows:
41+
scheduled-workflow:
42+
name: "Scheduled Workflow"
43+
description: "Workflow with schedule trigger using on event consumption"
44+
enabled: true
45+
workflow:
46+
document:
47+
dsl: "1.0.1"
48+
namespace: "test"
49+
name: "scheduled-workflow"
50+
version: "1.0.0"
51+
schedule:
52+
on:
53+
any:
54+
- with:
55+
type: com.example.cron.trigger
56+
data:
57+
schedule: "0 0 * * *"
58+
- with:
59+
type: com.example.event.trigger
60+
data:
61+
enabled: true
62+
cron: "0 0 * * *"
63+
do:
64+
- log:
65+
call: http
66+
with:
67+
method: POST
68+
url: "https://api.example.com/log"
69+
body:
70+
message: "Scheduled workflow executed"`
71+
72+
var definition WorkflowDefinitions
73+
result, err := readData([]byte(yamlInput), definition)
74+
require.NoError(t, err)
75+
require.NotNil(t, result)
76+
77+
// The key test: marshal back to JSON and verify 'on' is preserved as a key
78+
jsonBytes, err := json.Marshal(result)
79+
require.NoError(t, err)
80+
81+
jsonStr := string(jsonBytes)
82+
t.Logf("Generated JSON: %s", jsonStr)
83+
84+
// Verify that 'on' appears as a key in the JSON, not as boolean conversion
85+
assert.Contains(t, jsonStr, `"on":`, "JSON should contain 'on' as a key in schedule section")
86+
87+
// Verify that boolean conversions ('true'/'false' as keys) don't appear from 'on' conversion
88+
// With the old sigs.k8s.io/yaml library, 'on:' would become '"true":' in JSON
89+
assert.NotContains(t, jsonStr, `"true":{"any"`, "JSON should not contain 'true' as a key from 'on' conversion")
90+
assert.NotContains(t, jsonStr, `"false":{"any"`, "JSON should not contain 'false' as a key from 'on' conversion")
91+
}
92+
93+
func TestReadData_JSONInput(t *testing.T) {
94+
jsonInput := `{
95+
"version": "1.0",
96+
"workflows": {
97+
"test": {
98+
"name": "Test",
99+
"description": "Test workflow",
100+
"enabled": true
101+
}
102+
}
103+
}`
104+
105+
var definition WorkflowDefinitions
106+
result, err := readData([]byte(jsonInput), definition)
107+
require.NoError(t, err)
108+
require.NotNil(t, result)
109+
110+
assert.Equal(t, "1.0", result.Version)
111+
assert.NotNil(t, result.Workflows)
112+
113+
workflow, exists := result.Workflows["test"]
114+
assert.True(t, exists)
115+
assert.Equal(t, "Test", workflow.Name)
116+
}
117+
118+
func TestReadData_InvalidYAML(t *testing.T) {
119+
invalidYAML := `on:
120+
test: true
121+
invalid: indentation`
122+
123+
var definition WorkflowDefinitions
124+
result, err := readData([]byte(invalidYAML), definition)
125+
assert.Error(t, err, "should return error for invalid YAML")
126+
assert.Nil(t, result, "result should be nil for invalid YAML")
127+
}
128+
129+
func TestReadData_EmptyInput(t *testing.T) {
130+
var definition WorkflowDefinitions
131+
result, err := readData([]byte(""), definition)
132+
assert.Error(t, err, "should return error for empty input")
133+
assert.Nil(t, result, "result should be nil for empty input")
134+
}
135+
136+
// Benchmark to ensure the YAML parsing performance is reasonable
137+
func BenchmarkReadData_YAML(b *testing.B) {
138+
yamlInput := `version: "1.0"
139+
workflows:
140+
test:
141+
name: "Test"
142+
description: "Benchmark test"
143+
enabled: true`
144+
145+
var definition WorkflowDefinitions
146+
147+
b.ResetTimer()
148+
for i := 0; i < b.N; i++ {
149+
_, err := readData([]byte(yamlInput), definition)
150+
if err != nil {
151+
b.Fatal(err)
152+
}
153+
}
154+
}

0 commit comments

Comments
 (0)