Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion api/specs/github.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
55 changes: 43 additions & 12 deletions core/env/envfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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) {
Expand Down
138 changes: 138 additions & 0 deletions core/env/envfile_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
}