From 7cd9e68bd0a5dc48c331fd72f820eacb7598fe1c Mon Sep 17 00:00:00 2001 From: Fritz Larco Date: Mon, 2 Mar 2026 13:20:24 -0300 Subject: [PATCH 1/2] fix: add condition to break on HTML content in response headers --- api/specs/github.yaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/api/specs/github.yaml b/api/specs/github.yaml index 866006cb6..17886c0d3 100644 --- a/api/specs/github.yaml +++ b/api/specs/github.yaml @@ -45,9 +45,13 @@ defaults: backoff_base: 2 - action: break - condition: response.status == 422 + condition: response.status == 422 message: "stopping iteration due to code 422 (Unprocessable Content)" + - action: break + condition: contains(response.headers.content_type, "text/html") + message: "returned HTML content" + - action: "stop" condition: response.status == 403 && request.attempts > 5 From ceb32a4b201ad7bdad3e6f8dbb09efda0f203223 Mon Sep 17 00:00:00 2001 From: Fritz Larco Date: Mon, 2 Mar 2026 13:55:03 -0300 Subject: [PATCH 2/2] feat: implement ParseDotEnv function to parse .env file content and add unit tests --- core/env/envfile.go | 55 ++++++++++++---- core/env/envfile_test.go | 138 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 181 insertions(+), 12 deletions(-) create mode 100644 core/env/envfile_test.go diff --git a/core/env/envfile.go b/core/env/envfile.go index 52c558011..380792dcc 100644 --- a/core/env/envfile.go +++ b/core/env/envfile.go @@ -142,8 +142,24 @@ func LoadDotEnvSling() map[string]string { return dotEnvMap.Items() // file doesn't exist or can't be read } - for _, line := range strings.Split(string(bytes), "\n") { - line = strings.TrimSpace(line) + for key, val := range ParseDotEnv(string(bytes)) { + // don't overwrite existing env vars + if _, exists := os.LookupEnv(key); !exists { + dotEnvMap.Set(key, val) + os.Setenv(key, val) + } + } + return dotEnvMap.Items() +} + +// ParseDotEnv parses a .env file content into key-value pairs. +// It supports single-line and multi-line values enclosed in matching quotes (' or "). +func ParseDotEnv(content string) map[string]string { + result := map[string]string{} + lines := strings.Split(content, "\n") + + for i := 0; i < len(lines); i++ { + line := strings.TrimSpace(lines[i]) if line == "" || strings.HasPrefix(line, "#") { continue } @@ -156,21 +172,36 @@ func LoadDotEnvSling() map[string]string { key = strings.TrimSpace(key) val = strings.TrimSpace(val) - // remove surrounding quotes - if len(val) >= 2 { - if (val[0] == '"' && val[len(val)-1] == '"') || - (val[0] == '\'' && val[len(val)-1] == '\'') { + // check for quoted multi-line values + if len(val) >= 1 && (val[0] == '\'' || val[0] == '"') { + quote := val[0] + + // check if closing quote is on the same line + if len(val) >= 2 && val[len(val)-1] == quote { + // single-line quoted value val = val[1 : len(val)-1] + } else { + // multi-line: accumulate lines until we find the closing quote + var buf strings.Builder + buf.WriteString(val[1:]) // content after opening quote + for i++; i < len(lines); i++ { + raw := lines[i] + trimmed := strings.TrimRight(raw, " \t") + if len(trimmed) > 0 && trimmed[len(trimmed)-1] == quote { + buf.WriteByte('\n') + buf.WriteString(trimmed[:len(trimmed)-1]) + break + } + buf.WriteByte('\n') + buf.WriteString(raw) + } + val = buf.String() } } - // don't overwrite existing env vars - if _, exists := os.LookupEnv(key); !exists { - dotEnvMap.Set(key, val) - os.Setenv(key, val) - } + result[key] = val } - return dotEnvMap.Items() + return result } func UnsetEnvKeys(keys []string) { diff --git a/core/env/envfile_test.go b/core/env/envfile_test.go new file mode 100644 index 000000000..33d413a4d --- /dev/null +++ b/core/env/envfile_test.go @@ -0,0 +1,138 @@ +package env + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseDotEnv(t *testing.T) { + tests := []struct { + name string + content string + expected map[string]string + }{ + { + name: "simple key=value", + content: "FOO=bar", + expected: map[string]string{ + "FOO": "bar", + }, + }, + { + name: "single-line single-quoted JSON", + content: `KEY='{"a": "b"}'`, + expected: map[string]string{ + "KEY": `{"a": "b"}`, + }, + }, + { + name: "single-line double-quoted JSON", + content: `KEY="{\"a\": \"b\"}"`, + expected: map[string]string{ + "KEY": `{\"a\": \"b\"}`, + }, + }, + { + name: "multi-line single-quoted JSON", + content: `KEY='{ + "a": "b" +}'`, + expected: map[string]string{ + "KEY": "{\n \"a\": \"b\"\n}", + }, + }, + { + name: "multi-line double-quoted value", + content: "KEY=\"hello\nworld\"", + expected: map[string]string{ + "KEY": "hello\nworld", + }, + }, + { + name: "multi-line with multiple keys", + content: `BEFORE=hello +JSON_VAL='{ + "key": "value", + "num": 42 +}' +AFTER=world`, + expected: map[string]string{ + "BEFORE": "hello", + "JSON_VAL": "{\n \"key\": \"value\",\n \"num\": 42\n}", + "AFTER": "world", + }, + }, + { + name: "comments and blank lines are skipped", + content: `# this is a comment +FOO=bar + +# another comment +BAZ=qux`, + expected: map[string]string{ + "FOO": "bar", + "BAZ": "qux", + }, + }, + { + name: "value with equals sign", + content: `CONN=postgres://user:pass@host/db?sslmode=require`, + expected: map[string]string{ + "CONN": "postgres://user:pass@host/db?sslmode=require", + }, + }, + { + name: "multi-line with nested braces", + content: `CONFIG='{ + "database": { + "host": "localhost", + "port": 5432 + } +}'`, + expected: map[string]string{ + "CONFIG": "{\n \"database\": {\n \"host\": \"localhost\",\n \"port\": 5432\n }\n}", + }, + }, + { + name: "unquoted value", + content: `KEY=some value here`, + expected: map[string]string{ + "KEY": "some value here", + }, + }, + { + name: "empty value", + content: `KEY=`, + expected: map[string]string{ + "KEY": "", + }, + }, + { + name: "double-quoted value with single quotes inside", + content: `KEY="{'a': 'b'}"`, + expected: map[string]string{ + "KEY": "{'a': 'b'}", + }, + }, + { + name: "multi-line double-quoted with single quotes inside", + content: "KEY=\"{\n 'a': 'b'\n}\"", + expected: map[string]string{ + "KEY": "{\n 'a': 'b'\n}", + }, + }, + { + name: "line without equals is skipped", + content: "NOPE", + expected: map[string]string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ParseDotEnv(tt.content) + assert.Equal(t, tt.expected, result) + }) + } +}