# 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
```

## Shell integration with mise

If your project uses [mise](https://mise.jdx.dev/) to manage tools, the
[envoke-env](https://github.com/glennib/envoke-env) plugin activates envoke
on shell entry — no manual `eval` or sourcing needed.

```toml
# mise.toml
[tools]
"github:glennib/envoke" = "1.9.0"

[plugins]
envoke = "https://github.com/glennib/envoke-env#v1.0.0"

[env]
_.envoke = { fallback_environment = "local", tools = true }
```

Put the target environment name in `.envoke-env` (gitignored):

```
staging
```

When mise activates, the plugin runs `envoke staging` against your
`envoke.yaml` and injects the resolved variables. Switch environments with
`echo prod > .envoke-env`; the plugin watches the file and re-evaluates
on the next activation. Tags and overrides can be added on subsequent lines
(`tag:vault`, `override:read-replica`).

See the [envoke-env README](https://github.com/glennib/envoke-env) for
caching, the fallback environment, and other configuration options.

## 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 }}"
```

A `meta` object is available in variable templates with the following fields:

| Field | Description |
|-------|-------------|
| `meta.environment` | The target environment name passed to envoke. |

```yaml
API_URL:
  default:
    template: "https://{{ meta.environment }}.example.com/api"
```

```sh
$ envoke staging
API_URL='https://staging.example.com/api'
```

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]
```

| Option | Description |
|--------|-------------|
| `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. |
| `--completions <SHELL>` | Generate shell completions for the given shell (`bash`, `zsh`, `fish`, `elvish`, `powershell`) 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:

| 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

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
{
{% for name, var in variables | items %}  "{{ name }}": "{{ var.value }}"{% if not loop.last %},{% endif %}
{% endfor %}}
```

```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 }}
{% for name, var in variables | items -%}
{{ name }}={{ v[name] }}
{% 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:
  name: {{ meta.environment | lower }}-env
  labels:
    app: myapp
    environment: {{ meta.environment | lower }}
    generated-by: envoke
data:
{% for name, var in variables | items %}  {{ name }}: "{{ var.value }}"
{% endfor %}
```

This example uses `lower` and `items` filters to generate a Kubernetes-compatible
manifest directly from your envoke config.

### Shell completions

Generate completions for your shell:

```sh
# Bash
envoke --completions bash > ~/.local/share/bash-completion/completions/envoke

# Zsh
envoke --completions zsh > ~/.zfunc/_envoke

# Fish
envoke --completions fish > ~/.config/fish/completions/envoke.fish
```

## 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
