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
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:
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:
DB_HOST='localhost'
DB_PASS='devpassword'
DB_URL='postgresql://app:devpassword@localhost/mydb'
DB_USER='app'
Source them into your shell:
Or write them to a file:
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:
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:
envs:
prod:
sh: vault kv get -field=secret secret/app
local:
literal: dev-secret
OAUTH_CLIENT_ID:
tags:
envs:
prod:
sh: vault kv get -field=client_id secret/oauth
local:
literal: local-client-id
# Without --tag, only untagged variables are included:
DB_HOST='localhost'
# Include vault-tagged variables (and all untagged ones):
DB_HOST='localhost'
VAULT_SECRET='dev-secret'
# Include everything:
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:
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
- Parse the YAML config file.
- Filter out variables excluded by
--tagflags (if any). - For each remaining variable, select the source matching the target environment (or the default).
- Extract template dependencies and topologically sort all variables using Kahn's algorithm.
- Resolve values in dependency order -- literals are used as-is, commands and shell scripts are executed, templates are rendered with already-resolved values.
- 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:
Run a single test:
License
MIT OR Apache-2.0