# envoke
Resolve variable values from a declarative YAML configuration.
envoke reads an `envoke.yaml` file, resolves variables from multiple sources
(literals, commands, shell scripts, and [minijinja](https://github.com/mitsuhiko/minijinja)
templates) in dependency order, and renders the results as shell-safe
`VAR='value'` lines — ready to source as environment variables, write to `.env`
files, or feed into custom output templates.
## Installation
### With [mise](https://mise.jdx.dev/) (recommended)
```sh
mise use -g github:glennib/envoke
```
### From crates.io
```sh
cargo install envoke-cli
```
### With cargo-binstall
```sh
cargo binstall envoke-cli
```
### With mise (from crates.io)
```sh
mise use -g cargo:envoke-cli
```
### 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:
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:** Output is sorted alphabetically by variable name. 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:
| `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 }}"
```
All [minijinja built-in filters](https://docs.rs/minijinja/latest/minijinja/filters)
are available (`upper`, `lower`, `replace`, `trim`, `default`, `join`, etc.), plus
the following additional filters:
- `urlencode` -- percent-encodes special characters for use in URLs.
- `shell_escape` -- escapes single quotes for shell safety (`'` -> `'\''`).
```yaml
CONN_STRING:
default:
template: "postgresql://{{ USER | urlencode }}:{{ PASS | urlencode }}@localhost/db"
APP_NAME_LOWER:
default:
template: "{{ APP_NAME | lower }}"
```
#### `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]
```
| `ENVIRONMENT` | Target environment name (e.g. `local`, `prod`). Required unless `--schema` or `--list-*` flags are used. |
| `-c, --config <PATH>` | Path to config file. Default: `envoke.yaml`. |
| `-o, --output <PATH>` | Write output to a file instead of stdout. Also works with `--schema`. |
| `-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` | Prefix each line with `export`. Ignored when `--template` is used. |
| `--template <PATH>` | Use a custom output template file instead of the built-in format. See [Custom templates](#custom-templates). |
| `--schema` | Print the JSON Schema for `envoke.yaml` and exit. |
| `--list-environments` | List all environment names found in the config and exit. |
| `--list-overrides` | List all override names found in the config and exit. |
| `--list-tags` | List all tag names found in the config and exit. |
| `-q, --quiet` | Suppress informational messages on stderr. |
### 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:
| `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
All [minijinja built-in filters](https://docs.rs/minijinja/latest/minijinja/filters)
are available (`upper`, `lower`, `replace`, `trim`, `default`, `join`, `length`,
`first`, `last`, `sort`, `unique`, `tojson`, etc.), plus these additional filters:
- `shell_escape` -- escapes single quotes for shell safety (`'` -> `'\''`).
- `urlencode` -- percent-encodes special characters.
All filters are available in both variable templates (the `template` source type)
and custom output templates.
### Example: JSON output
```jinja2
{
```
```sh
envoke local --template json.j2
```
> **Note:** This simplified example does not escape JSON special characters
> (`"`, `\`, newlines) in values. For production use, consider a template that
> handles escaping.
### Example: Docker .env format
```jinja2
# Generated for {{ meta.environment }}
{% endfor -%}
```
> **Note:** This simplified example does not quote or escape values. Values
> containing `=`, `#`, or whitespace may not parse correctly in all `.env`
> implementations.
### Example: Kubernetes ConfigMap
```jinja2
apiVersion: v1
kind: ConfigMap
metadata:
app: myapp
environment: {{ meta.environment | lower }}
generated-by: envoke
data:
```
This example uses `lower` and `items` filters to generate a Kubernetes-compatible
manifest directly from your envoke config.
## 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)'
```
### Debugging
envoke uses [tracing](https://docs.rs/tracing) for diagnostic output. Set the
`RUST_LOG` environment variable to see debug messages on stderr:
```sh
RUST_LOG=debug envoke local
```
This is useful for troubleshooting tag filtering, override fallback chains, and
source resolution order.
## License
MIT OR Apache-2.0