envoke-cli 0.1.5

Resolve environment variables from a declarative YAML config file
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
# envoke

Resolve environment variables from a declarative YAML config file.

envoke reads an `envoke.yaml` file, resolves variables in dependency order, and
outputs shell-safe `VAR='value'` lines. Variables can be literal strings, command
output, shell scripts, or [minijinja](https://github.com/mitsuhiko/minijinja)
templates that reference other variables.

## Installation

### From source

```sh
cargo install --git https://github.com/glennib/envoke envoke-cli
```

### From GitHub releases

Pre-built binaries are available on the
[releases page](https://github.com/glennib/envoke/releases) for:

- Linux (x86_64, aarch64)
- macOS (x86_64, Apple Silicon)
- Windows (x86_64)

## Quick start

Create an `envoke.yaml`:

```yaml
variables:
  DB_HOST:
    default:
      literal: localhost
    envs:
      prod:
        literal: db.example.com

  DB_USER:
    default:
      literal: app

  DB_PASS:
    tags: [secrets]
    envs:
      local:
        literal: devpassword
      prod:
        sh: vault kv get -field=password secret/db

  DB_URL:
    default:
      template: "postgresql://{{ DB_USER }}:{{ DB_PASS | urlencode }}@{{ DB_HOST }}/mydb"
```

Generate variables for an environment:

```sh
$ envoke local
# @generated by `envoke local` at 2025-06-15T10:30:00+02:00
# Do not edit manually. Modify envoke.yaml instead.

DB_HOST='localhost'
DB_PASS='devpassword'
DB_URL='postgresql://app:devpassword@localhost/mydb'
DB_USER='app'
```

> **Note:** All output includes an `@generated` header with the invocation command
> and timestamp. Examples below omit this header for brevity.

Source them into your shell:

```sh
eval "$(envoke local --prepend-export)"
```

Or write them to a file:

```sh
envoke local --output .env
```

## Configuration

The config file (default: `envoke.yaml`) has a single top-level key `variables`
that maps variable names to their definitions.

### Variable definition

Each variable can have:

| Field | Description |
|-------|-------------|
| `description` | Optional. Rendered as a `# comment` above the variable in output. |
| `tags` | Optional. List of tags for conditional inclusion. Variable is only included when at least one of its tags is passed via `--tag`. Untagged variables are always included. |
| `default` | Optional. Fallback source used when the target environment has no entry in `envs`. |
| `envs` | Map of environment names to sources. |
| `overrides` | Optional. Map of override names to alternative source definitions (each with its own `default`/`envs`). Activated via `--override`. |

A variable must have either an `envs` entry matching the target environment or a
`default`. If neither exists, resolution fails with an error.

### Source types

Each source specifies exactly one of the following fields:

#### `literal`

A fixed string value.

```yaml
DB_HOST:
  default:
    literal: localhost
```

#### `cmd`

Run a command and capture its stdout (trimmed). The value is a list where the
first element is the executable and the rest are arguments.

```yaml
GIT_SHA:
  default:
    cmd: [git, rev-parse, --short, HEAD]
```

#### `sh`

Run a shell script via `sh -c` and capture its stdout (trimmed).

```yaml
TIMESTAMP:
  default:
    sh: date -u +%Y-%m-%dT%H:%M:%SZ
```

#### `template`

A [minijinja](https://github.com/mitsuhiko/minijinja) template string, compatible
with [Jinja2](https://jinja.palletsprojects.com/). Reference other variables
with `{{ VAR_NAME }}`. Dependencies are automatically detected and resolved first
via topological sorting.

```yaml
DB_URL:
  default:
    template: "postgresql://{{ DB_USER }}:{{ DB_PASS }}@{{ DB_HOST }}/{{ DB_NAME }}"
```

The `urlencode` filter is available for escaping special characters:

```yaml
CONN_STRING:
  default:
    template: "postgresql://{{ USER | urlencode }}:{{ PASS | urlencode }}@localhost/db"
```

#### `skip`

Omit this variable from the output. Useful for conditionally excluding a
variable in certain environments while including it in others.

```yaml
DEBUG_TOKEN:
  default:
    skip: true
  envs:
    local:
      literal: debug-token-value
```

### Environments and defaults

envoke selects the source for each variable by checking the `envs` map for the
target environment. If no match is found, it falls back to `default`. This lets
you define shared defaults and override them per environment:

```yaml
LOG_LEVEL:
  default:
    literal: info
  envs:
    local:
      literal: debug
    prod:
      literal: warn
```

### Tags

Variables can be tagged for conditional inclusion. Tagged variables are only
included when at least one of their tags is passed via `--tag`. Untagged
variables are always included. This is useful for gating expensive-to-resolve
variables (e.g. vault lookups) or optional components behind explicit opt-in.

```yaml
variables:
  DB_HOST:
    default:
      literal: localhost

  VAULT_SECRET:
    tags: [vault]
    envs:
      prod:
        sh: vault kv get -field=secret secret/app
      local:
        literal: dev-secret

  OAUTH_CLIENT_ID:
    tags: [oauth]
    envs:
      prod:
        sh: vault kv get -field=client_id secret/oauth
      local:
        literal: local-client-id
```

```sh
# Without --tag, only untagged variables are included:
$ envoke local
DB_HOST='localhost'

# Include vault-tagged variables (and all untagged ones):
$ envoke local --tag vault
DB_HOST='localhost'
VAULT_SECRET='dev-secret'

# Include everything:
$ envoke local --tag vault --tag oauth
DB_HOST='localhost'
OAUTH_CLIENT_ID='local-client-id'
VAULT_SECRET='dev-secret'
```

Variables without tags are always included regardless of which `--tag` flags are
passed. Tagged variables require explicit opt-in.

### Overrides

Overrides add a third dimension for varying values alongside environments and
tags. A variable can declare named overrides, each with its own `default`/`envs`
sources. Activate them with `--override`:

```yaml
variables:
  DATABASE_HOST:
    default:
      literal: localhost
    envs:
      prod:
        literal: 172.10.0.1
    overrides:
      read-replica:
        default:
          literal: localhost-ro
        envs:
          prod:
            literal: 172.10.0.2

  CACHE_STRATEGY:
    envs:
      prod:
        literal: lru
    overrides:
      aggressive-cache:
        envs:
          prod:
            literal: lfu-with-prefetch

  DATABASE_PORT:
    default:
      literal: "5432"
    # No overrides -- unaffected by --override flag
```

```sh
# Base values:
$ envoke prod
CACHE_STRATEGY='lru'
DATABASE_HOST='172.10.0.1'
DATABASE_PORT='5432'

# Activate an override:
$ envoke prod --override read-replica
CACHE_STRATEGY='lru'
DATABASE_HOST='172.10.0.2'
DATABASE_PORT='5432'

# Multiple overrides on disjoint variables:
$ envoke prod --override read-replica --override aggressive-cache
CACHE_STRATEGY='lfu-with-prefetch'
DATABASE_HOST='172.10.0.2'
DATABASE_PORT='5432'
```

When an override is active for a variable, the source is selected using a
4-level fallback chain:

1. Override `envs[environment]`
2. Override `default`
3. Base `envs[environment]`
4. Base `default`

Variables without a matching override definition are unaffected and use the
normal base fallback. If multiple active overrides are defined on the same
variable, envoke reports an error. Unknown override names (not defined on any
variable) produce a warning on stderr.

## CLI usage

```
envoke [OPTIONS] [ENVIRONMENT]
```

| Option | Description |
|--------|-------------|
| `ENVIRONMENT` | Target environment name (e.g. `local`, `prod`). Required unless `--schema` is used. |
| `-c, --config <PATH>` | Path to config file. Default: `envoke.yaml`. |
| `-o, --output <PATH>` | Write output to a file instead of stdout. |
| `-t, --tag <TAG>` | Only include tagged variables with a matching tag. Repeatable. Untagged variables are always included. |
| `-O, --override <NAME>` | Activate a named override for source selection. Repeatable. Per variable, at most one active override may be defined. |
| `--prepend-export` | **Deprecated.** Switches to a built-in template that prefixes each variable with `export `. Prefer `--template` with a custom template instead. |
| `--template <PATH>` | Use a custom output template file instead of the built-in format. |
| `--schema` | Print the JSON Schema for `envoke.yaml` and exit. |

### JSON Schema

Generate a JSON Schema for editor autocompletion and validation:

```sh
envoke --schema > envoke-schema.json
```

Use it in your `envoke.yaml` with a schema comment for editors that support it:

```yaml
# yaml-language-server: $schema=envoke-schema.json
variables:
  # ...
```

Alternatively, point directly at the hosted schema without writing a local file:

```yaml
# yaml-language-server: $schema=https://raw.githubusercontent.com/glennib/envoke/refs/heads/main/envoke.schema.json
variables:
  # ...
```

## How it works

1. Parse the YAML config file.
2. Filter out variables excluded by `--tag` flags (if any).
3. For each remaining variable, select the source matching the target environment
   (or the default), applying the override fallback chain if `--override` flags
   are active.
4. Extract template dependencies and topologically sort all variables using
   Kahn's algorithm.
5. Resolve values in dependency order -- literals are used as-is, commands and
   shell scripts are executed, templates are rendered with already-resolved
   values.
6. Render output using a built-in or custom Jinja2 template (see
   [Custom templates]#custom-templates). The default template produces an
   `@generated` header followed by sorted `VAR='value'` lines with shell-safe
   escaping.

Circular dependencies and references to undefined variables are detected before
any resolution begins and reported as errors.

## Custom templates

By default, envoke outputs shell `VAR='value'` lines with an `@generated`
header. You can supply your own [minijinja](https://github.com/mitsuhiko/minijinja)
(Jinja2-compatible) template via `--template`:

```sh
envoke local --template my-template.j2
```

### Template context

The template receives the following variables:

| Name | Type | Description |
|------|------|-------------|
| `variables` | map of name -> `{value, description}` | Rich access: `{{ variables.DB_URL.value }}`. Iteration: `{% for name, var in variables \| items %}`. Sorted alphabetically. |
| `v` | map of name -> value string | Flat shorthand: `{{ v.DATABASE_URL }}`. |
| `meta.timestamp` | string | RFC 3339 timestamp of invocation. |
| `meta.invocation` | string | Full CLI invocation as a single string. |
| `meta.invocation_args` | list of strings | CLI args as individual elements. |
| `meta.environment` | string | Target environment name. |
| `meta.config_file` | string | Path to the config file used. |

### Filters

- `shell_escape` -- escapes single quotes for shell safety (`'` -> `'\''`).

### Example: JSON output

```jinja2
{
{% for name, var in variables | items %}  "{{ name }}": "{{ var.value }}"{% if not loop.last %},{% endif %}
{% endfor %}}
```

```sh
envoke local --template json.j2
```

### Example: Docker .env format

```jinja2
# Generated for {{ meta.environment }}
{% for name, var in variables | items -%}
{{ name }}={{ v[name] }}
{% endfor -%}
```

## Development

This project uses [mise](https://mise.jdx.dev/) as a task runner. After
installing mise:

```sh
mise install       # Install tool dependencies
mise run build     # Build release binary
mise run test      # Run tests (via cargo-nextest)
mise run clippy    # Run lints
mise run fmt       # Format code
mise run ci        # Run all checks (fmt, clippy, test, build)
```

Run a single test:

```sh
cargo nextest run -E 'test(test_name)'
```

## License

MIT OR Apache-2.0