Skip to content

Commit c2feab3

Browse files
authored
Merge pull request #154 from buildkite-plugins/SUP-5965-env-config-map-format
Add map format support to env
2 parents d550200 + fa33e01 commit c2feab3

4 files changed

Lines changed: 545 additions & 26 deletions

File tree

README.md

Lines changed: 83 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -332,10 +332,89 @@ steps:
332332
build:
333333
message: "Deploying foo service"
334334
env:
335-
- HELLO=123
336-
- AWS_REGION
335+
HELLO: 123
336+
AWS_REGION: ~ # Null literal reads from $AWS_REGION
337337
```
338338

339+
### Environment Variables
340+
341+
Environment variables can be specified in two formats. Both formats are fully supported.
342+
343+
#### Map Format (Recommended)
344+
345+
The map format provides clean, readable syntax:
346+
347+
```yaml
348+
steps:
349+
- label: "Triggering pipelines"
350+
plugins:
351+
- monorepo-diff#v1.8.0:
352+
env:
353+
NODE_ENV: production
354+
API_URL: https://api.example.com
355+
PORT: 8080
356+
DEBUG: false
357+
AWS_REGION: ~ # Null literal reads from $AWS_REGION
358+
EMPTY_STRING: "" # Empty string sets to literal ""
359+
watch:
360+
- path: "services/"
361+
config:
362+
command: "npm test"
363+
env:
364+
TEST_ENV: integration
365+
MAX_WORKERS: 4
366+
```
367+
368+
Map format features:
369+
- Clean YAML syntax using key-value pairs
370+
- Supports non-string values (numbers, booleans) which are converted to strings automatically
371+
- Null values read from OS environment: use `KEY: ~` (recommended YAML null literal)
372+
- The explicit `~` ensures nothing is accidentally added during pipeline processing
373+
- Note: Unlike array format, you cannot use just `KEY` alone - you must use a null value
374+
- Empty string (`""`) is treated as a literal empty string value
375+
- Whitespace in values is preserved
376+
- Recommended for new configurations
377+
378+
#### Array Format (Fully Supported)
379+
380+
The array format uses key=value syntax:
381+
382+
```yaml
383+
steps:
384+
- label: "Triggering pipelines"
385+
plugins:
386+
- monorepo-diff#v1.8.0:
387+
env:
388+
- NODE_ENV=production
389+
- API_URL=https://api.example.com
390+
- AWS_REGION # Key-only reads from $AWS_REGION
391+
watch:
392+
- path: "services/"
393+
config:
394+
command: "npm test"
395+
env:
396+
- TEST_ENV=integration
397+
```
398+
399+
Array format features:
400+
- Key-only entries (e.g., `AWS_REGION`) read from OS environment variables
401+
- Supports values with equals signs: `BUILD_ARGS=--arg1=val1`
402+
- Whitespace trimmed from keys and values automatically
403+
- Fully supported alongside map format
404+
405+
#### Format Comparison
406+
407+
| Feature | Map Format | Array Format |
408+
|---------|-----------|--------------|
409+
| Syntax | `KEY: value` | `KEY=value` |
410+
| OS env reading | Null literal (`KEY: ~`) | Key-only entries (`KEY`) |
411+
| Empty string | `KEY: ""` sets to `""` | `KEY=` sets to `""` |
412+
| Type support | Numbers, booleans | Strings only |
413+
| Whitespace | Preserved in values | Trimmed |
414+
| Readability | High | Medium |
415+
416+
**Note:** The format is determined by YAML structure - you cannot mix array and map syntax at the same level. However, you can use different formats at different levels (e.g., map format at plugin level, array format at step level).
417+
339418
### `log_level` (optional)
340419

341420
Add `log_level` property to set the log level. Supported log levels are `debug` and `info`. Defaults to `info`.
@@ -513,7 +592,7 @@ steps:
513592
diff: "git diff --name-only $(head -n 1 last_successful_build)"
514593
interpolation: false
515594
env:
516-
- env1=env-1 # this will be appended to all env configuration
595+
env1: env-1 # this will be appended to all env configuration
517596
hooks:
518597
- command: "echo $(git rev-parse HEAD) > last_successful_build"
519598
watch:
@@ -544,7 +623,7 @@ steps:
544623
artifacts:
545624
- "logs/*"
546625
env:
547-
- FOO=bar
626+
FOO: bar
548627
549628
wait: true
550629
```

plugin.go

Lines changed: 63 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -504,32 +504,79 @@ func appendMetadata(watch *WatchConfig, metadata map[string]string) {
504504
}
505505
}
506506

507-
// parse env in format from env=env-value to map[env] = env-value
507+
// parseEnv converts env configuration from various formats to map[string]string.
508+
// Supports two formats:
509+
// - Array format: ["KEY=value", "KEY2"] - existing format, KEY2 reads from OS env
510+
// - Map format: {"KEY": "value", "KEY2": nil} - new format, only nil reads from OS env
508511
func parseEnv(raw interface{}) (map[string]string, error) {
509512
if raw == nil {
510513
return nil, nil
511514
}
512515

513-
if _, ok := raw.([]interface{}); !ok {
514-
return nil, errors.New("failed to parse plugin configuration")
515-
}
516-
517-
result := make(map[string]string)
518-
for _, v := range raw.([]interface{}) {
519-
split := strings.SplitN(v.(string), "=", 2)
520-
key := strings.TrimSpace(split[0])
516+
switch v := raw.(type) {
517+
case map[string]string:
518+
// Direct string map - all values are literal (including empty strings)
519+
result := make(map[string]string, len(v))
520+
for k, val := range v {
521+
key := strings.TrimSpace(k)
522+
if key == "" {
523+
continue
524+
}
525+
// Preserve all values including empty strings
526+
result[key] = val
527+
}
528+
return result, nil
521529

522-
// only key exists. set value from env
523-
if len(key) > 0 && len(split) == 1 {
524-
result[key] = env(key, "")
530+
case map[string]interface{}:
531+
// Generic map - only nil reads from OS environment
532+
result := make(map[string]string, len(v))
533+
for k, val := range v {
534+
key := strings.TrimSpace(k)
535+
if key == "" {
536+
continue
537+
}
538+
// Only null values read from OS environment
539+
if val == nil {
540+
result[key] = env(key, "")
541+
} else {
542+
// Convert to string, preserving empty strings
543+
result[key] = fmt.Sprintf("%v", val)
544+
}
525545
}
546+
return result, nil
547+
548+
case []interface{}:
549+
// Array format - preserve existing behavior exactly
550+
result := make(map[string]string)
551+
for _, item := range v {
552+
str, ok := item.(string)
553+
if !ok {
554+
continue
555+
}
556+
557+
split := strings.SplitN(str, "=", 2)
558+
key := strings.TrimSpace(split[0])
559+
560+
if key == "" {
561+
continue
562+
}
526563

527-
if len(split) == 2 {
528-
result[key] = strings.TrimSpace(split[1])
564+
// Only key exists - read from OS environment
565+
if len(split) == 1 {
566+
result[key] = env(key, "")
567+
continue
568+
}
569+
570+
// Key=value - trim both (backwards compatibility)
571+
if len(split) == 2 {
572+
result[key] = strings.TrimSpace(split[1])
573+
}
529574
}
530-
}
575+
return result, nil
531576

532-
return result, nil
577+
default:
578+
return nil, errors.New("env configuration must be an array of strings (e.g., ['KEY=value']) or a map (e.g., {KEY: 'value'})")
579+
}
533580
}
534581

535582
// parse metadata in format from key:value to map[key] = value

plugin.yml

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ configuration:
1616
interpolation:
1717
type: boolean
1818
env:
19-
type: array
19+
type: [array, object]
20+
description: >
21+
Environment variables. Array: ["KEY=value", "KEY"] or Map: {KEY: "value", KEY2: ~}
22+
Array format: KEY-only reads from OS. Map format: use "KEY: ~" (null literal) to read from OS, "KEY: ''" for empty string.
2023
binary_folder:
2124
type: string
2225
notify:
@@ -86,7 +89,10 @@ configuration:
8689
branch:
8790
type: string
8891
env:
89-
type: array
92+
type: [array, object]
93+
description: >
94+
Environment variables. Array: ["KEY=value", "KEY"] or Map: {KEY: "value", KEY2: ~}
95+
Array: KEY-only reads from OS. Map: use "KEY: ~" (null literal) to read from OS, "KEY: ''" for empty string.
9096
agents:
9197
type: object
9298
properties:
@@ -101,7 +107,10 @@ configuration:
101107
type: array
102108
description: Artifact paths to upload (preferred field name)
103109
env:
104-
type: array
110+
type: [array, object]
111+
description: >
112+
Environment variables. Array: ["KEY=value", "KEY"] or Map: {KEY: "value", KEY2: ~}
113+
Array: KEY-only reads from OS. Map: use "KEY: ~" (null literal) to read from OS, "KEY: ''" for empty string.
105114
wait:
106115
type: boolean
107116
hooks:

0 commit comments

Comments
 (0)