envoke-cli 0.1.3

Resolve environment variables from a declarative YAML config file
envoke-cli-0.1.3 is not a library.

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 templates that reference other variables.

Installation

From source

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

From GitHub releases

Pre-built binaries are available on the releases page for:

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

Quick start

Create an envoke.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:

$ envoke local
DB_HOST='localhost'
DB_PASS='devpassword'
DB_URL='postgresql://app:devpassword@localhost/mydb'
DB_USER='app'

Source them into your shell:

eval "$(envoke local)"

Or write them to a file:

envoke local --output .env --prepend-export

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.

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.

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.

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

sh

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

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

template

A minijinja template string, compatible with Jinja2. Reference other variables with {{ VAR_NAME }}. Dependencies are automatically detected and resolved first via topological sorting.

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

The urlencode filter is available for escaping special characters:

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.

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:

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.

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

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. Adds an @generated header with timestamp.
-t, --tag <TAG> Only include tagged variables with a matching tag. Repeatable. Untagged variables are always included.
--prepend-export Prefix each line with export .
--schema Print the JSON Schema for envoke.yaml and exit.

JSON Schema

Generate a JSON Schema for editor autocompletion and validation:

envoke --schema > envoke-schema.json

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

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

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

# 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).
  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. Output 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.

Development

This project uses mise as a task runner. After installing mise:

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:

cargo nextest run -E 'test(test_name)'

License

MIT OR Apache-2.0