graft is a domain-specific YAML merging tool, designed to make it easy to manage and merge multi-part YAML configurations.
brew install graft# Download the latest release
curl -L https://github.com/wayneeseguin/graft/releases/latest/download/graft-$(uname -s | tr '[:upper:]' '[:lower:]')-amd64 -o graft
# Make it executable
chmod +x graft
# Move to PATH
sudo mv graft /usr/local/bin/go get github.com/wayneeseguin/graft/cmd/graftgraft --versionCreate two YAML files:
base.yml:
name: my-app
version: 1.0.0
server:
port: 8080
host: localhostproduction.yml:
server:
port: 443
host: app.example.com
ssl: trueMerge them:
graft merge base.yml production.ymlOutput:
name: my-app
version: 1.0.0
server:
port: 443
host: app.example.com
ssl: trueFiles are merged left-to-right. Later files override values from earlier files:
graft merge first.yml second.yml third.ymlfirst.ymlis the basesecond.ymloverrides/adds to firstthird.ymloverrides/adds to the result
graft's power comes from its operators - special expressions that perform operations during merging.
Reference other parts of the document:
# config.yml
domain: example.com
api:
url: (( concat "https://api." domain ))
web:
url: (( concat "https://www." domain ))Result:
domain: example.com
api:
url: https://api.example.com
web:
url: https://www.example.comBuild strings from parts:
# Example
environment: production
region: us-east-1
resource_name: (( concat "myapp-" environment "-" region ))Result:
resource_name: myapp-production-us-east-1Use the ternary operator for conditionals:
environment: production
debug_mode: (( environment == "production" ? false : true ))
replicas: (( environment == "production" ? 3 : 1 ))Arrays of maps with name fields merge by name:
# base.yml
servers:
- name: web
port: 8080
- name: db
port: 5432
# override.yml
servers:
- name: web
port: 80
ssl: trueResult:
servers:
- name: web
port: 80
ssl: true
- name: db
port: 5432Control array merging explicitly:
# Append to array
items:
- (( append ))
- new-item
# Replace array
items:
- (( replace ))
- only-item
# Prepend to array
items:
- (( prepend ))
- first-item# Direct usage
database_url: (( grab $DATABASE_URL ))
# With defaults
port: (( grab $PORT || 8080 ))
# In paths (environment variable expansion)
config: (( grab settings.$ENVIRONMENT.database ))app:
name: (( grab $APP_NAME || "my-app" ))
port: (( grab $PORT || 8080 ))
debug: (( grab $DEBUG || false ))
database:
host: (( grab $DB_HOST || "localhost" ))
port: (( grab $DB_PORT || 5432 ))Structure your configurations in layers:
config/
base.yml # Shared configuration
development.yml # Development overrides
staging.yml # Staging overrides
production.yml # Production overridesUsage:
# Development
graft merge config/base.yml config/development.yml
# Production
graft merge config/base.yml config/production.ymlRemove temporary scaffolding:
# base.yml
meta:
environment: production
region: us-east-1
app:
name: myapp
full_name: (( concat meta.environment "-" app.name "-" meta.region ))graft merge base.yml --prune metaRequire values to be provided:
# base.yml
database:
host: (( param "Please provide database.host" ))
port: 5432
username: (( param "Please provide database.username" ))
password: (( param "Please provide database.password" ))Process files with multiple documents:
---
name: doc1
---
name: doc2graft merge --multi-doc multi.ymlExtract specific parts:
graft merge large-manifest.yml --cherry-pick jobs.webPull secrets from HashiCorp Vault:
database:
password: (( vault "secret/db:password" ))
api:
key: (( vault "secret/api:key" ))manifests/
base/
app.yml
database.yml
networking.yml
environments/
dev.yml
staging.yml
prod.yml
# Good
database_connection_timeout: 30
api_rate_limit: 1000
# Less clear
db_timeout: 30
limit: 1000# Calculate replica count based on environment
# Production: 3, Others: 1
replicas: (( environment == "production" ? 3 : 1 ))required:
api_key: (( param "API_KEY is required" ))
database_url: (( param "DATABASE_URL is required" ))# Don't do this
password: supersecret
# Do this
password: (( vault "secret/app:password" ))
# Or
password: (( grab $PASSWORD ))See what graft is doing:
graft merge --debug base.yml override.ymlMissing references:
# Error: $.does.not.exist could not be found
value: (( grab does.not.exist ))
# Fix: Provide a default
value: (( grab does.not.exist || "default" ))Circular references:
# Error: Circular reference detected
a: (( grab b ))
b: (( grab a ))Type mismatches:
# Error: Cannot concatenate non-string values
result: (( concat "prefix" 123 )) # 123 is not a string
# Fix: Convert to string first
result: (( concat "prefix" (stringify 123) ))- Explore Operators Reference for all available operators
- See Examples for real-world usage
- Read about Advanced Concepts
- Check out Integration Guides
- Run
graft -hfor command help - Check the FAQ
- Visit the GitHub repository
- Read the Troubleshooting Guide