grep "^DB_HOST=" .env | cut -d= -f2
grep "^DB_HOST=" .env | cut -d= -f2
grep "^DB_HOST=" .env | cut -d= -f2
grep "^DB_HOST=" .env | cut -d= -f2 | tr -d '"'
grep "^DB_HOST=" .env | cut -d= -f2 | tr -d '"'
grep "^DB_HOST=" .env | cut -d= -f2 | tr -d '"'
go install github.com/andreimerlescu/goenv@latest
go install github.com/andreimerlescu/goenv@latest
go install github.com/andreimerlescu/goenv@latest
GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -o bin/goenv-linux-amd64 .
GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -o bin/goenv-linux-amd64 .
GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -o bin/goenv-linux-amd64 .
go get -u github.com/andreimerlescu/goenv/env
go get -u github.com/andreimerlescu/goenv/env
go get -u github.com/andreimerlescu/goenv/env
#!/bin/bash
set -euo pipefail SERVICE_DIR="/opt/services/payments"
ENV_FILE="${SERVICE_DIR}/service.env" # Create the file if it doesn't exist yet
goenv -file "${ENV_FILE}" -init -write # Populate with values derived at deploy time
goenv -file "${ENV_FILE}" -write -add -env SERVICE_NAME -value "payments"
goenv -file "${ENV_FILE}" -write -add -env SERVICE_PORT -value "8443"
goenv -file "${ENV_FILE}" -write -add -env DEPLOY_SHA -value "$(git rev-parse --short HEAD)"
goenv -file "${ENV_FILE}" -write -add -env DEPLOY_DATE -value "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
goenv -file "${ENV_FILE}" -write -add -env DATA_FOLDER -value "$(pwd)/data"
goenv -file "${ENV_FILE}" -write -add -env LOG_LEVEL -value "info" # Confirm everything landed
goenv -file "${ENV_FILE}" -print # Validate required keys exist before handing off to the service
goenv -file "${ENV_FILE}" -has -env SERVICE_NAME || { echo "SERVICE_NAME missing"; exit 1; }
goenv -file "${ENV_FILE}" -has -env DEPLOY_SHA || { echo "DEPLOY_SHA missing"; exit 1; } echo "Environment bootstrapped successfully."
#!/bin/bash
set -euo pipefail SERVICE_DIR="/opt/services/payments"
ENV_FILE="${SERVICE_DIR}/service.env" # Create the file if it doesn't exist yet
goenv -file "${ENV_FILE}" -init -write # Populate with values derived at deploy time
goenv -file "${ENV_FILE}" -write -add -env SERVICE_NAME -value "payments"
goenv -file "${ENV_FILE}" -write -add -env SERVICE_PORT -value "8443"
goenv -file "${ENV_FILE}" -write -add -env DEPLOY_SHA -value "$(git rev-parse --short HEAD)"
goenv -file "${ENV_FILE}" -write -add -env DEPLOY_DATE -value "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
goenv -file "${ENV_FILE}" -write -add -env DATA_FOLDER -value "$(pwd)/data"
goenv -file "${ENV_FILE}" -write -add -env LOG_LEVEL -value "info" # Confirm everything landed
goenv -file "${ENV_FILE}" -print # Validate required keys exist before handing off to the service
goenv -file "${ENV_FILE}" -has -env SERVICE_NAME || { echo "SERVICE_NAME missing"; exit 1; }
goenv -file "${ENV_FILE}" -has -env DEPLOY_SHA || { echo "DEPLOY_SHA missing"; exit 1; } echo "Environment bootstrapped successfully."
#!/bin/bash
set -euo pipefail SERVICE_DIR="/opt/services/payments"
ENV_FILE="${SERVICE_DIR}/service.env" # Create the file if it doesn't exist yet
goenv -file "${ENV_FILE}" -init -write # Populate with values derived at deploy time
goenv -file "${ENV_FILE}" -write -add -env SERVICE_NAME -value "payments"
goenv -file "${ENV_FILE}" -write -add -env SERVICE_PORT -value "8443"
goenv -file "${ENV_FILE}" -write -add -env DEPLOY_SHA -value "$(git rev-parse --short HEAD)"
goenv -file "${ENV_FILE}" -write -add -env DEPLOY_DATE -value "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
goenv -file "${ENV_FILE}" -write -add -env DATA_FOLDER -value "$(pwd)/data"
goenv -file "${ENV_FILE}" -write -add -env LOG_LEVEL -value "info" # Confirm everything landed
goenv -file "${ENV_FILE}" -print # Validate required keys exist before handing off to the service
goenv -file "${ENV_FILE}" -has -env SERVICE_NAME || { echo "SERVICE_NAME missing"; exit 1; }
goenv -file "${ENV_FILE}" -has -env DEPLOY_SHA || { echo "DEPLOY_SHA missing"; exit 1; } echo "Environment bootstrapped successfully."
#!/bin/bash
set -euo pipefail ENV_FILE=".env.staging" echo "==> Validating staging environment..." # Assert APP_ENV equals "staging"
if ! goenv -file "${ENV_FILE}" -is -env APP_ENV -value staging; then echo "ERROR: APP_ENV must be 'staging' in ${ENV_FILE}" exit 1
fi # Assert DEBUG is not enabled in staging
if goenv -file "${ENV_FILE}" -is -env DEBUG -value true; then echo "ERROR: DEBUG must not be 'true' in staging" exit 1
fi # Assert a required key exists
if ! goenv -file "${ENV_FILE}" -has -env DATABASE_URL; then echo "ERROR: DATABASE_URL is required" exit 1
fi # Assert a key that should have been removed is actually gone
if ! goenv -file "${ENV_FILE}" -not -has -env LEGACY_API_KEY; then echo "ERROR: LEGACY_API_KEY should have been removed from ${ENV_FILE}" exit 1
fi # Assert DB_PORT is not pointing at a dev-only value
if goenv -file "${ENV_FILE}" -is -env DB_PORT -value 5433; then echo "ERROR: DB_PORT 5433 is the dev port — use 5432 in staging" exit 1
fi echo "==> Validation passed."
#!/bin/bash
set -euo pipefail ENV_FILE=".env.staging" echo "==> Validating staging environment..." # Assert APP_ENV equals "staging"
if ! goenv -file "${ENV_FILE}" -is -env APP_ENV -value staging; then echo "ERROR: APP_ENV must be 'staging' in ${ENV_FILE}" exit 1
fi # Assert DEBUG is not enabled in staging
if goenv -file "${ENV_FILE}" -is -env DEBUG -value true; then echo "ERROR: DEBUG must not be 'true' in staging" exit 1
fi # Assert a required key exists
if ! goenv -file "${ENV_FILE}" -has -env DATABASE_URL; then echo "ERROR: DATABASE_URL is required" exit 1
fi # Assert a key that should have been removed is actually gone
if ! goenv -file "${ENV_FILE}" -not -has -env LEGACY_API_KEY; then echo "ERROR: LEGACY_API_KEY should have been removed from ${ENV_FILE}" exit 1
fi # Assert DB_PORT is not pointing at a dev-only value
if goenv -file "${ENV_FILE}" -is -env DB_PORT -value 5433; then echo "ERROR: DB_PORT 5433 is the dev port — use 5432 in staging" exit 1
fi echo "==> Validation passed."
#!/bin/bash
set -euo pipefail ENV_FILE=".env.staging" echo "==> Validating staging environment..." # Assert APP_ENV equals "staging"
if ! goenv -file "${ENV_FILE}" -is -env APP_ENV -value staging; then echo "ERROR: APP_ENV must be 'staging' in ${ENV_FILE}" exit 1
fi # Assert DEBUG is not enabled in staging
if goenv -file "${ENV_FILE}" -is -env DEBUG -value true; then echo "ERROR: DEBUG must not be 'true' in staging" exit 1
fi # Assert a required key exists
if ! goenv -file "${ENV_FILE}" -has -env DATABASE_URL; then echo "ERROR: DATABASE_URL is required" exit 1
fi # Assert a key that should have been removed is actually gone
if ! goenv -file "${ENV_FILE}" -not -has -env LEGACY_API_KEY; then echo "ERROR: LEGACY_API_KEY should have been removed from ${ENV_FILE}" exit 1
fi # Assert DB_PORT is not pointing at a dev-only value
if goenv -file "${ENV_FILE}" -is -env DB_PORT -value 5433; then echo "ERROR: DB_PORT 5433 is the dev port — use 5432 in staging" exit 1
fi echo "==> Validation passed."
#!/bin/bash
set -euo pipefail ENV_FILE="infra/deploy.env" echo "==> Exporting ${ENV_FILE} to all formats..." # Generate all formats in one command
goenv -file "${ENV_FILE}" -mkall -write # Resulting files:
# infra/deploy.env.json ← Terraform input
# infra/deploy.env.yaml ← Helm values
# infra/deploy.env.ini ← Ansible inventory vars
# infra/deploy.env.toml ← any TOML-native tooling
# infra/deploy.env.xml ← any XML-native tooling ls -lh infra/deploy.env* # Verify the JSON is valid before passing it to Terraform
cat infra/deploy.env.json | python3 -m json.tool > /dev/null \ && echo "JSON valid" \ || { echo "JSON invalid"; exit 1; } echo "==> Export complete."
#!/bin/bash
set -euo pipefail ENV_FILE="infra/deploy.env" echo "==> Exporting ${ENV_FILE} to all formats..." # Generate all formats in one command
goenv -file "${ENV_FILE}" -mkall -write # Resulting files:
# infra/deploy.env.json ← Terraform input
# infra/deploy.env.yaml ← Helm values
# infra/deploy.env.ini ← Ansible inventory vars
# infra/deploy.env.toml ← any TOML-native tooling
# infra/deploy.env.xml ← any XML-native tooling ls -lh infra/deploy.env* # Verify the JSON is valid before passing it to Terraform
cat infra/deploy.env.json | python3 -m json.tool > /dev/null \ && echo "JSON valid" \ || { echo "JSON invalid"; exit 1; } echo "==> Export complete."
#!/bin/bash
set -euo pipefail ENV_FILE="infra/deploy.env" echo "==> Exporting ${ENV_FILE} to all formats..." # Generate all formats in one command
goenv -file "${ENV_FILE}" -mkall -write # Resulting files:
# infra/deploy.env.json ← Terraform input
# infra/deploy.env.yaml ← Helm values
# infra/deploy.env.ini ← Ansible inventory vars
# infra/deploy.env.toml ← any TOML-native tooling
# infra/deploy.env.xml ← any XML-native tooling ls -lh infra/deploy.env* # Verify the JSON is valid before passing it to Terraform
cat infra/deploy.env.json | python3 -m json.tool > /dev/null \ && echo "JSON valid" \ || { echo "JSON invalid"; exit 1; } echo "==> Export complete."
# Jenkins pipeline step: export for Ansible only
goenv -file deploy.env -ini -write # GitLab CI step: export for Helm only
goenv -file deploy.env -yaml -write
# Jenkins pipeline step: export for Ansible only
goenv -file deploy.env -ini -write # GitLab CI step: export for Helm only
goenv -file deploy.env -yaml -write
# Jenkins pipeline step: export for Ansible only
goenv -file deploy.env -ini -write # GitLab CI step: export for Helm only
goenv -file deploy.env -yaml -write
#!/bin/bash
# cleanup.sh — run as a post-pipeline hook ENV_FILE="deploy.env" echo "==> Cleaning derived format files..."
goenv -file "${ENV_FILE}" -cleanall -write # Only deploy.env remains; .json .yaml .toml .ini .xml are removed
ls -la *.env* echo "==> Clean complete."
#!/bin/bash
# cleanup.sh — run as a post-pipeline hook ENV_FILE="deploy.env" echo "==> Cleaning derived format files..."
goenv -file "${ENV_FILE}" -cleanall -write # Only deploy.env remains; .json .yaml .toml .ini .xml are removed
ls -la *.env* echo "==> Clean complete."
#!/bin/bash
# cleanup.sh — run as a post-pipeline hook ENV_FILE="deploy.env" echo "==> Cleaning derived format files..."
goenv -file "${ENV_FILE}" -cleanall -write # Only deploy.env remains; .json .yaml .toml .ini .xml are removed
ls -la *.env* echo "==> Clean complete."
export AM_GO_ENV_NEVER_DELETE=true
goenv -file deploy.env -cleanall -write
# → no files deleted; flag is silently respected
export AM_GO_ENV_NEVER_DELETE=true
goenv -file deploy.env -cleanall -write
# → no files deleted; flag is silently respected
export AM_GO_ENV_NEVER_DELETE=true
goenv -file deploy.env -cleanall -write
# → no files deleted; flag is silently respected
# This exits with an error — no -prod flag
goenv -file .env.production -write -add -env SECRET_KEY -value "new-secret" # This works — -prod flag grants access
goenv -prod -file .env.production -write -add -env SECRET_KEY -value "new-secret"
# This exits with an error — no -prod flag
goenv -file .env.production -write -add -env SECRET_KEY -value "new-secret" # This works — -prod flag grants access
goenv -prod -file .env.production -write -add -env SECRET_KEY -value "new-secret"
# This exits with an error — no -prod flag
goenv -file .env.production -write -add -env SECRET_KEY -value "new-secret" # This works — -prod flag grants access
goenv -prod -file .env.production -write -add -env SECRET_KEY -value "new-secret"
export GOENV_NEVER_WRITE_PRODUCTION=true # Even with -prod, writes are blocked
goenv -prod -file .env.production -write -add -env SECRET_KEY -value "x"
# → exits with error
export GOENV_NEVER_WRITE_PRODUCTION=true # Even with -prod, writes are blocked
goenv -prod -file .env.production -write -add -env SECRET_KEY -value "x"
# → exits with error
export GOENV_NEVER_WRITE_PRODUCTION=true # Even with -prod, writes are blocked
goenv -prod -file .env.production -write -add -env SECRET_KEY -value "x"
# → exits with error
#!/bin/bash
set -euo pipefail ENV_FILE="n8n.env"
DOMAIN="gh.dev"
SUBDOMAIN="n8n" goenv -init -write -file "${ENV_FILE}" goenv -write -file "${ENV_FILE}" -add -env DATA_FOLDER -value "$(pwd)"
goenv -write -file "${ENV_FILE}" -add -env DOMAIN -value "${DOMAIN}"
goenv -write -file "${ENV_FILE}" -add -env SUBDOMAIN -value "${SUBDOMAIN}"
goenv -write -file "${ENV_FILE}" -add -env SSL_EMAIL -value "webmaster@${SUBDOMAIN}.${DOMAIN}" # Inspect before deploying
goenv -file "${ENV_FILE}" -print # Validate the SSL email was formed correctly
goenv -file "${ENV_FILE}" -is -env SSL_EMAIL -value "[email protected]" \ && echo "SSL_EMAIL correct" \ || { echo "SSL_EMAIL malformed"; exit 1; } # Check GENERIC_TIMEZONE — expected to be absent until set
if goenv -file "${ENV_FILE}" -not -has -env GENERIC_TIMEZONE; then echo "WARNING: GENERIC_TIMEZONE not set — n8n will default to UTC"
fi # Export for review as TOML and XML
goenv -file "${ENV_FILE}" -toml
goenv -file "${ENV_FILE}" -xml
#!/bin/bash
set -euo pipefail ENV_FILE="n8n.env"
DOMAIN="gh.dev"
SUBDOMAIN="n8n" goenv -init -write -file "${ENV_FILE}" goenv -write -file "${ENV_FILE}" -add -env DATA_FOLDER -value "$(pwd)"
goenv -write -file "${ENV_FILE}" -add -env DOMAIN -value "${DOMAIN}"
goenv -write -file "${ENV_FILE}" -add -env SUBDOMAIN -value "${SUBDOMAIN}"
goenv -write -file "${ENV_FILE}" -add -env SSL_EMAIL -value "webmaster@${SUBDOMAIN}.${DOMAIN}" # Inspect before deploying
goenv -file "${ENV_FILE}" -print # Validate the SSL email was formed correctly
goenv -file "${ENV_FILE}" -is -env SSL_EMAIL -value "[email protected]" \ && echo "SSL_EMAIL correct" \ || { echo "SSL_EMAIL malformed"; exit 1; } # Check GENERIC_TIMEZONE — expected to be absent until set
if goenv -file "${ENV_FILE}" -not -has -env GENERIC_TIMEZONE; then echo "WARNING: GENERIC_TIMEZONE not set — n8n will default to UTC"
fi # Export for review as TOML and XML
goenv -file "${ENV_FILE}" -toml
goenv -file "${ENV_FILE}" -xml
#!/bin/bash
set -euo pipefail ENV_FILE="n8n.env"
DOMAIN="gh.dev"
SUBDOMAIN="n8n" goenv -init -write -file "${ENV_FILE}" goenv -write -file "${ENV_FILE}" -add -env DATA_FOLDER -value "$(pwd)"
goenv -write -file "${ENV_FILE}" -add -env DOMAIN -value "${DOMAIN}"
goenv -write -file "${ENV_FILE}" -add -env SUBDOMAIN -value "${SUBDOMAIN}"
goenv -write -file "${ENV_FILE}" -add -env SSL_EMAIL -value "webmaster@${SUBDOMAIN}.${DOMAIN}" # Inspect before deploying
goenv -file "${ENV_FILE}" -print # Validate the SSL email was formed correctly
goenv -file "${ENV_FILE}" -is -env SSL_EMAIL -value "[email protected]" \ && echo "SSL_EMAIL correct" \ || { echo "SSL_EMAIL malformed"; exit 1; } # Check GENERIC_TIMEZONE — expected to be absent until set
if goenv -file "${ENV_FILE}" -not -has -env GENERIC_TIMEZONE; then echo "WARNING: GENERIC_TIMEZONE not set — n8n will default to UTC"
fi # Export for review as TOML and XML
goenv -file "${ENV_FILE}" -toml
goenv -file "${ENV_FILE}" -xml
#!/bin/bash
set -euo pipefail SOURCE=".env.development"
TARGET=".env.staging" echo "==> Promoting ${SOURCE} to ${TARGET}..." # Read values from dev and write into staging
# (goenv -add will not overwrite if key already exists in staging)
for KEY in APP_NAME APP_VERSION DEPLOY_SHA LOG_LEVEL; do VALUE=$(goenv -file "${SOURCE}" -is -env "${KEY}" -value "" || true) goenv -file "${TARGET}" -write -add -env "${KEY}" -value "${VALUE}"
done # Scrub dev-only keys from the staging file
goenv -file "${TARGET}" -write -rm -env LOCAL_DB_PATH
goenv -file "${TARGET}" -write -rm -env DEV_MOCK_PAYMENTS
goenv -file "${TARGET}" -write -rm -env SKIP_AUTH # Confirm dev-only keys are absent
goenv -file "${TARGET}" -not -has -env LOCAL_DB_PATH || { echo "LOCAL_DB_PATH still present"; exit 1; }
goenv -file "${TARGET}" -not -has -env DEV_MOCK_PAYMENTS || { echo "DEV_MOCK_PAYMENTS still present"; exit 1; } # Assert staging-specific values are correct
goenv -file "${TARGET}" -is -env APP_ENV -value staging || { echo "APP_ENV must be staging"; exit 1; } echo "==> Promotion complete."
goenv -file "${TARGET}" -print
#!/bin/bash
set -euo pipefail SOURCE=".env.development"
TARGET=".env.staging" echo "==> Promoting ${SOURCE} to ${TARGET}..." # Read values from dev and write into staging
# (goenv -add will not overwrite if key already exists in staging)
for KEY in APP_NAME APP_VERSION DEPLOY_SHA LOG_LEVEL; do VALUE=$(goenv -file "${SOURCE}" -is -env "${KEY}" -value "" || true) goenv -file "${TARGET}" -write -add -env "${KEY}" -value "${VALUE}"
done # Scrub dev-only keys from the staging file
goenv -file "${TARGET}" -write -rm -env LOCAL_DB_PATH
goenv -file "${TARGET}" -write -rm -env DEV_MOCK_PAYMENTS
goenv -file "${TARGET}" -write -rm -env SKIP_AUTH # Confirm dev-only keys are absent
goenv -file "${TARGET}" -not -has -env LOCAL_DB_PATH || { echo "LOCAL_DB_PATH still present"; exit 1; }
goenv -file "${TARGET}" -not -has -env DEV_MOCK_PAYMENTS || { echo "DEV_MOCK_PAYMENTS still present"; exit 1; } # Assert staging-specific values are correct
goenv -file "${TARGET}" -is -env APP_ENV -value staging || { echo "APP_ENV must be staging"; exit 1; } echo "==> Promotion complete."
goenv -file "${TARGET}" -print
#!/bin/bash
set -euo pipefail SOURCE=".env.development"
TARGET=".env.staging" echo "==> Promoting ${SOURCE} to ${TARGET}..." # Read values from dev and write into staging
# (goenv -add will not overwrite if key already exists in staging)
for KEY in APP_NAME APP_VERSION DEPLOY_SHA LOG_LEVEL; do VALUE=$(goenv -file "${SOURCE}" -is -env "${KEY}" -value "" || true) goenv -file "${TARGET}" -write -add -env "${KEY}" -value "${VALUE}"
done # Scrub dev-only keys from the staging file
goenv -file "${TARGET}" -write -rm -env LOCAL_DB_PATH
goenv -file "${TARGET}" -write -rm -env DEV_MOCK_PAYMENTS
goenv -file "${TARGET}" -write -rm -env SKIP_AUTH # Confirm dev-only keys are absent
goenv -file "${TARGET}" -not -has -env LOCAL_DB_PATH || { echo "LOCAL_DB_PATH still present"; exit 1; }
goenv -file "${TARGET}" -not -has -env DEV_MOCK_PAYMENTS || { echo "DEV_MOCK_PAYMENTS still present"; exit 1; } # Assert staging-specific values are correct
goenv -file "${TARGET}" -is -env APP_ENV -value staging || { echo "APP_ENV must be staging"; exit 1; } echo "==> Promotion complete."
goenv -file "${TARGET}" -print
name: Deploy on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: go-version: '1.22' - name: Install goenv run: go install github.com/andreimerlescu/goenv@latest - name: Validate environment file run: | goenv -file .env.staging -has -env DATABASE_URL || exit 1 goenv -file .env.staging -has -env SERVICE_PORT || exit 1 goenv -file .env.staging -not -has -env DEBUG_KEY || exit 1 goenv -file .env.staging -is -env APP_ENV -value staging || exit 1 - name: Export to JSON for Terraform run: | goenv -file .env.staging -json -write cat .env.staging.json - name: Deploy run: ./scripts/deploy.sh - name: Clean up artifacts if: always() run: goenv -file .env.staging -cleanall -write
name: Deploy on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: go-version: '1.22' - name: Install goenv run: go install github.com/andreimerlescu/goenv@latest - name: Validate environment file run: | goenv -file .env.staging -has -env DATABASE_URL || exit 1 goenv -file .env.staging -has -env SERVICE_PORT || exit 1 goenv -file .env.staging -not -has -env DEBUG_KEY || exit 1 goenv -file .env.staging -is -env APP_ENV -value staging || exit 1 - name: Export to JSON for Terraform run: | goenv -file .env.staging -json -write cat .env.staging.json - name: Deploy run: ./scripts/deploy.sh - name: Clean up artifacts if: always() run: goenv -file .env.staging -cleanall -write
name: Deploy on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: go-version: '1.22' - name: Install goenv run: go install github.com/andreimerlescu/goenv@latest - name: Validate environment file run: | goenv -file .env.staging -has -env DATABASE_URL || exit 1 goenv -file .env.staging -has -env SERVICE_PORT || exit 1 goenv -file .env.staging -not -has -env DEBUG_KEY || exit 1 goenv -file .env.staging -is -env APP_ENV -value staging || exit 1 - name: Export to JSON for Terraform run: | goenv -file .env.staging -json -write cat .env.staging.json - name: Deploy run: ./scripts/deploy.sh - name: Clean up artifacts if: always() run: goenv -file .env.staging -cleanall -write
stages: - validate - export - deploy - cleanup validate_env: stage: validate image: golang:1.22 script: - go install github.com/andreimerlescu/goenv@latest - goenv -file .env.production -has -env DB_HOST || exit 1 - goenv -file .env.production -has -env REDIS_URL || exit 1 - goenv -file .env.production -not -has -env DEV_FLAG || exit 1 - goenv -file .env.production -is -env APP_ENV -value production || exit 1 export_formats: stage: export image: golang:1.22 script: - go install github.com/andreimerlescu/goenv@latest - goenv -file .env.production -mkall -write artifacts: paths: - .env.production.json - .env.production.yaml cleanup: stage: cleanup image: golang:1.22 when: always script: - go install github.com/andreimerlescu/goenv@latest - goenv -file .env.production -cleanall -write
stages: - validate - export - deploy - cleanup validate_env: stage: validate image: golang:1.22 script: - go install github.com/andreimerlescu/goenv@latest - goenv -file .env.production -has -env DB_HOST || exit 1 - goenv -file .env.production -has -env REDIS_URL || exit 1 - goenv -file .env.production -not -has -env DEV_FLAG || exit 1 - goenv -file .env.production -is -env APP_ENV -value production || exit 1 export_formats: stage: export image: golang:1.22 script: - go install github.com/andreimerlescu/goenv@latest - goenv -file .env.production -mkall -write artifacts: paths: - .env.production.json - .env.production.yaml cleanup: stage: cleanup image: golang:1.22 when: always script: - go install github.com/andreimerlescu/goenv@latest - goenv -file .env.production -cleanall -write
stages: - validate - export - deploy - cleanup validate_env: stage: validate image: golang:1.22 script: - go install github.com/andreimerlescu/goenv@latest - goenv -file .env.production -has -env DB_HOST || exit 1 - goenv -file .env.production -has -env REDIS_URL || exit 1 - goenv -file .env.production -not -has -env DEV_FLAG || exit 1 - goenv -file .env.production -is -env APP_ENV -value production || exit 1 export_formats: stage: export image: golang:1.22 script: - go install github.com/andreimerlescu/goenv@latest - goenv -file .env.production -mkall -write artifacts: paths: - .env.production.json - .env.production.yaml cleanup: stage: cleanup image: golang:1.22 when: always script: - go install github.com/andreimerlescu/goenv@latest - goenv -file .env.production -cleanall -write
package main import ( "fmt" "log" "net/http" "time" "github.com/andreimerlescu/goenv/env"
) type Config struct { Host string Port int Debug bool Timeout time.Duration MaxConns int AllowedOrigins []string DBCredentials map[string]string
} func LoadConfig() Config { // MustExist exits (or panics) if these are absent — // use in init() to catch misconfiguration at startup env.MustExist("DB_HOST") env.MustExist("DB_PASS") return Config{ Host: env.String("HOST", "0.0.0.0"), Port: env.Int("PORT", 8080), Debug: env.Bool("DEBUG", false), Timeout: env.Duration("REQUEST_TIMEOUT", 30*time.Second), MaxConns: env.Int("DB_MAX_CONNS", 10), // ALLOWED_ORIGINS="https://a.com,https://b.com" AllowedOrigins: env.List("ALLOWED_ORIGINS", env.ZeroList), // DB_CREDENTIALS="host=localhost,port=5432,name=mydb" DBCredentials: env.Map("DB_CREDENTIALS", env.ZeroMap), }
} func main() { cfg := LoadConfig() addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port) log.Printf("starting on %s (debug=%v timeout=%s)", addr, cfg.Debug, cfg.Timeout) http.ListenAndServe(addr, nil)
}
package main import ( "fmt" "log" "net/http" "time" "github.com/andreimerlescu/goenv/env"
) type Config struct { Host string Port int Debug bool Timeout time.Duration MaxConns int AllowedOrigins []string DBCredentials map[string]string
} func LoadConfig() Config { // MustExist exits (or panics) if these are absent — // use in init() to catch misconfiguration at startup env.MustExist("DB_HOST") env.MustExist("DB_PASS") return Config{ Host: env.String("HOST", "0.0.0.0"), Port: env.Int("PORT", 8080), Debug: env.Bool("DEBUG", false), Timeout: env.Duration("REQUEST_TIMEOUT", 30*time.Second), MaxConns: env.Int("DB_MAX_CONNS", 10), // ALLOWED_ORIGINS="https://a.com,https://b.com" AllowedOrigins: env.List("ALLOWED_ORIGINS", env.ZeroList), // DB_CREDENTIALS="host=localhost,port=5432,name=mydb" DBCredentials: env.Map("DB_CREDENTIALS", env.ZeroMap), }
} func main() { cfg := LoadConfig() addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port) log.Printf("starting on %s (debug=%v timeout=%s)", addr, cfg.Debug, cfg.Timeout) http.ListenAndServe(addr, nil)
}
package main import ( "fmt" "log" "net/http" "time" "github.com/andreimerlescu/goenv/env"
) type Config struct { Host string Port int Debug bool Timeout time.Duration MaxConns int AllowedOrigins []string DBCredentials map[string]string
} func LoadConfig() Config { // MustExist exits (or panics) if these are absent — // use in init() to catch misconfiguration at startup env.MustExist("DB_HOST") env.MustExist("DB_PASS") return Config{ Host: env.String("HOST", "0.0.0.0"), Port: env.Int("PORT", 8080), Debug: env.Bool("DEBUG", false), Timeout: env.Duration("REQUEST_TIMEOUT", 30*time.Second), MaxConns: env.Int("DB_MAX_CONNS", 10), // ALLOWED_ORIGINS="https://a.com,https://b.com" AllowedOrigins: env.List("ALLOWED_ORIGINS", env.ZeroList), // DB_CREDENTIALS="host=localhost,port=5432,name=mydb" DBCredentials: env.Map("DB_CREDENTIALS", env.ZeroMap), }
} func main() { cfg := LoadConfig() addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port) log.Printf("starting on %s (debug=%v timeout=%s)", addr, cfg.Debug, cfg.Timeout) http.ListenAndServe(addr, nil)
}
package main import ( "fmt" "os" "github.com/andreimerlescu/goenv/env"
) func validateEnvironment() { errors := []string{} // Port must be in user range if !env.IntInRange("PORT", 8080, 1024, 49151) { errors = append(errors, "PORT must be between 1024 and 49151") } // Max connections must not exceed a hard limit if !env.IntLessThan("DB_MAX_CONNS", 10, 101) { errors = append(errors, "DB_MAX_CONNS must be less than 101") } // Must have at least one allowed origin if !env.ListIsLength("ALLOWED_ORIGINS", env.ZeroList, 0) { if env.ListLength("ALLOWED_ORIGINS", env.ZeroList) == 0 { errors = append(errors, "ALLOWED_ORIGINS must contain at least one entry") } } // Feature map must contain required keys if !env.MapHasKeys("FEATURE_FLAGS", env.ZeroMap, "auth", "payments", "notifications") { errors = append(errors, "FEATURE_FLAGS must contain auth, payments, and notifications keys") } // DEBUG must be off in production if env.IsTrue("DEBUG") && env.String("APP_ENV", "") == "production" { errors = append(errors, "DEBUG must not be true in production") } // All gating flags must be false before going live if !env.AreFalse("MAINTENANCE_MODE", "READ_ONLY", "LOCKED") { errors = append(errors, "MAINTENANCE_MODE, READ_ONLY, and LOCKED must all be false") } if len(errors) > 0 { fmt.Fprintln(os.Stderr, "Environment validation failed:") for _, e := range errors { fmt.Fprintf(os.Stderr, " - %s\n", e) } os.Exit(1) }
} func main() { validateEnvironment() fmt.Println("Environment OK — starting service")
}
package main import ( "fmt" "os" "github.com/andreimerlescu/goenv/env"
) func validateEnvironment() { errors := []string{} // Port must be in user range if !env.IntInRange("PORT", 8080, 1024, 49151) { errors = append(errors, "PORT must be between 1024 and 49151") } // Max connections must not exceed a hard limit if !env.IntLessThan("DB_MAX_CONNS", 10, 101) { errors = append(errors, "DB_MAX_CONNS must be less than 101") } // Must have at least one allowed origin if !env.ListIsLength("ALLOWED_ORIGINS", env.ZeroList, 0) { if env.ListLength("ALLOWED_ORIGINS", env.ZeroList) == 0 { errors = append(errors, "ALLOWED_ORIGINS must contain at least one entry") } } // Feature map must contain required keys if !env.MapHasKeys("FEATURE_FLAGS", env.ZeroMap, "auth", "payments", "notifications") { errors = append(errors, "FEATURE_FLAGS must contain auth, payments, and notifications keys") } // DEBUG must be off in production if env.IsTrue("DEBUG") && env.String("APP_ENV", "") == "production" { errors = append(errors, "DEBUG must not be true in production") } // All gating flags must be false before going live if !env.AreFalse("MAINTENANCE_MODE", "READ_ONLY", "LOCKED") { errors = append(errors, "MAINTENANCE_MODE, READ_ONLY, and LOCKED must all be false") } if len(errors) > 0 { fmt.Fprintln(os.Stderr, "Environment validation failed:") for _, e := range errors { fmt.Fprintf(os.Stderr, " - %s\n", e) } os.Exit(1) }
} func main() { validateEnvironment() fmt.Println("Environment OK — starting service")
}
package main import ( "fmt" "os" "github.com/andreimerlescu/goenv/env"
) func validateEnvironment() { errors := []string{} // Port must be in user range if !env.IntInRange("PORT", 8080, 1024, 49151) { errors = append(errors, "PORT must be between 1024 and 49151") } // Max connections must not exceed a hard limit if !env.IntLessThan("DB_MAX_CONNS", 10, 101) { errors = append(errors, "DB_MAX_CONNS must be less than 101") } // Must have at least one allowed origin if !env.ListIsLength("ALLOWED_ORIGINS", env.ZeroList, 0) { if env.ListLength("ALLOWED_ORIGINS", env.ZeroList) == 0 { errors = append(errors, "ALLOWED_ORIGINS must contain at least one entry") } } // Feature map must contain required keys if !env.MapHasKeys("FEATURE_FLAGS", env.ZeroMap, "auth", "payments", "notifications") { errors = append(errors, "FEATURE_FLAGS must contain auth, payments, and notifications keys") } // DEBUG must be off in production if env.IsTrue("DEBUG") && env.String("APP_ENV", "") == "production" { errors = append(errors, "DEBUG must not be true in production") } // All gating flags must be false before going live if !env.AreFalse("MAINTENANCE_MODE", "READ_ONLY", "LOCKED") { errors = append(errors, "MAINTENANCE_MODE, READ_ONLY, and LOCKED must all be false") } if len(errors) > 0 { fmt.Fprintln(os.Stderr, "Environment validation failed:") for _, e := range errors { fmt.Fprintf(os.Stderr, " - %s\n", e) } os.Exit(1) }
} func main() { validateEnvironment() fmt.Println("Environment OK — starting service")
}
package main import ( "fmt" "github.com/andreimerlescu/goenv/env"
) func main() { // Your upstream system exports pipe-separated lists // SERVERS="web1|web2|web3" env.ListSeparator = "|" servers := env.List("SERVERS", env.ZeroList) for i, s := range servers { fmt.Printf("server[%d] = %s\n", i, s) } // Your config map uses pipe separation and tilde as key~value delimiter // ROUTES="home~/,admin~/admin,api~/v1" env.MapSeparator = "|" env.MapItemSeparator = "~" env.MapSplitN = 2 routes := env.Map("ROUTES", env.ZeroMap) for path, target := range routes { fmt.Printf("route %s -> %s\n", path, target) }
}
package main import ( "fmt" "github.com/andreimerlescu/goenv/env"
) func main() { // Your upstream system exports pipe-separated lists // SERVERS="web1|web2|web3" env.ListSeparator = "|" servers := env.List("SERVERS", env.ZeroList) for i, s := range servers { fmt.Printf("server[%d] = %s\n", i, s) } // Your config map uses pipe separation and tilde as key~value delimiter // ROUTES="home~/,admin~/admin,api~/v1" env.MapSeparator = "|" env.MapItemSeparator = "~" env.MapSplitN = 2 routes := env.Map("ROUTES", env.ZeroMap) for path, target := range routes { fmt.Printf("route %s -> %s\n", path, target) }
}
package main import ( "fmt" "github.com/andreimerlescu/goenv/env"
) func main() { // Your upstream system exports pipe-separated lists // SERVERS="web1|web2|web3" env.ListSeparator = "|" servers := env.List("SERVERS", env.ZeroList) for i, s := range servers { fmt.Printf("server[%d] = %s\n", i, s) } // Your config map uses pipe separation and tilde as key~value delimiter // ROUTES="home~/,admin~/admin,api~/v1" env.MapSeparator = "|" env.MapItemSeparator = "~" env.MapSplitN = 2 routes := env.Map("ROUTES", env.ZeroMap) for path, target := range routes { fmt.Printf("route %s -> %s\n", path, target) }
}
export AM_GO_ENV_LIST_SEPARATOR="|"
export AM_GO_ENV_MAP_SEPARATOR="|"
export AM_GO_ENV_MAP_ITEM_SEPARATOR="~"
export AM_GO_ENV_MAP_SPLIT_N=2
export AM_GO_ENV_LIST_SEPARATOR="|"
export AM_GO_ENV_MAP_SEPARATOR="|"
export AM_GO_ENV_MAP_ITEM_SEPARATOR="~"
export AM_GO_ENV_MAP_SPLIT_N=2
export AM_GO_ENV_LIST_SEPARATOR="|"
export AM_GO_ENV_MAP_SEPARATOR="|"
export AM_GO_ENV_MAP_ITEM_SEPARATOR="~"
export AM_GO_ENV_MAP_SPLIT_N=2
package main import ( "fmt" "path/filepath" "github.com/andreimerlescu/goenv/env"
) func main() { u := env.User() // Derive and set a path based on the current user's home directory cacheDir := filepath.Join(u.HomeDir, ".cache", "myapp") if !env.WasSet("CACHE_DIR", cacheDir) { panic("failed to configure CACHE_DIR") } // Set with error handling if err := env.Set("RUNTIME_USER", u.Username); err != nil { panic(err) } // Remove a value that should not be visible to subprocesses if !env.WasUnset("CI_JOB_TOKEN") { fmt.Println("warning: could not unset CI_JOB_TOKEN") } // Verify cleanup if env.Exists("CI_JOB_TOKEN") { panic("CI_JOB_TOKEN still visible — aborting") } fmt.Println("CACHE_DIR:", env.String("CACHE_DIR", "")) fmt.Println("RUNTIME_USER:", env.String("RUNTIME_USER", ""))
}
package main import ( "fmt" "path/filepath" "github.com/andreimerlescu/goenv/env"
) func main() { u := env.User() // Derive and set a path based on the current user's home directory cacheDir := filepath.Join(u.HomeDir, ".cache", "myapp") if !env.WasSet("CACHE_DIR", cacheDir) { panic("failed to configure CACHE_DIR") } // Set with error handling if err := env.Set("RUNTIME_USER", u.Username); err != nil { panic(err) } // Remove a value that should not be visible to subprocesses if !env.WasUnset("CI_JOB_TOKEN") { fmt.Println("warning: could not unset CI_JOB_TOKEN") } // Verify cleanup if env.Exists("CI_JOB_TOKEN") { panic("CI_JOB_TOKEN still visible — aborting") } fmt.Println("CACHE_DIR:", env.String("CACHE_DIR", "")) fmt.Println("RUNTIME_USER:", env.String("RUNTIME_USER", ""))
}
package main import ( "fmt" "path/filepath" "github.com/andreimerlescu/goenv/env"
) func main() { u := env.User() // Derive and set a path based on the current user's home directory cacheDir := filepath.Join(u.HomeDir, ".cache", "myapp") if !env.WasSet("CACHE_DIR", cacheDir) { panic("failed to configure CACHE_DIR") } // Set with error handling if err := env.Set("RUNTIME_USER", u.Username); err != nil { panic(err) } // Remove a value that should not be visible to subprocesses if !env.WasUnset("CI_JOB_TOKEN") { fmt.Println("warning: could not unset CI_JOB_TOKEN") } // Verify cleanup if env.Exists("CI_JOB_TOKEN") { panic("CI_JOB_TOKEN still visible — aborting") } fmt.Println("CACHE_DIR:", env.String("CACHE_DIR", "")) fmt.Println("RUNTIME_USER:", env.String("RUNTIME_USER", ""))
}
package main import ( "log" "github.com/andreimerlescu/goenv/env"
) func init() { // Disable automatic AM_GO_ENV_* discovery env.UseMagic = false // Configure explicitly for this service's requirements env.AllowPanic = false // never panic — return fallbacks instead env.PrintErrors = true // write parse errors to stderr env.UseLogger = true // use structured INFO/ERR logger env.EnableVerboseLogging = false // no debug noise in production env.PanicNoUser = false // return default user on error env.ListSeparator = "," env.MapSeparator = "," env.MapItemSeparator = "=" env.MapSplitN = 2 // Use base-10 int64 with 64-bit size (the defaults, made explicit) env.Int64Base = 10 env.Int64BitSize = 64 log.Println("env package configured")
} func main() { port := env.Int("PORT", 8080) log.Printf("port: %d", port)
}
package main import ( "log" "github.com/andreimerlescu/goenv/env"
) func init() { // Disable automatic AM_GO_ENV_* discovery env.UseMagic = false // Configure explicitly for this service's requirements env.AllowPanic = false // never panic — return fallbacks instead env.PrintErrors = true // write parse errors to stderr env.UseLogger = true // use structured INFO/ERR logger env.EnableVerboseLogging = false // no debug noise in production env.PanicNoUser = false // return default user on error env.ListSeparator = "," env.MapSeparator = "," env.MapItemSeparator = "=" env.MapSplitN = 2 // Use base-10 int64 with 64-bit size (the defaults, made explicit) env.Int64Base = 10 env.Int64BitSize = 64 log.Println("env package configured")
} func main() { port := env.Int("PORT", 8080) log.Printf("port: %d", port)
}
package main import ( "log" "github.com/andreimerlescu/goenv/env"
) func init() { // Disable automatic AM_GO_ENV_* discovery env.UseMagic = false // Configure explicitly for this service's requirements env.AllowPanic = false // never panic — return fallbacks instead env.PrintErrors = true // write parse errors to stderr env.UseLogger = true // use structured INFO/ERR logger env.EnableVerboseLogging = false // no debug noise in production env.PanicNoUser = false // return default user on error env.ListSeparator = "," env.MapSeparator = "," env.MapItemSeparator = "=" env.MapSplitN = 2 // Use base-10 int64 with 64-bit size (the defaults, made explicit) env.Int64Base = 10 env.Int64BitSize = 64 log.Println("env package configured")
} func main() { port := env.Int("PORT", 8080) log.Printf("port: %d", port)
}
export AM_GO_ENV_ALWAYS_ALLOW_PANIC=true
export AM_GO_ENV_ALWAYS_PRINT_ERRORS=true
export AM_GO_ENV_ALWAYS_USE_LOGGER=true
export AM_GO_ENV_ENABLE_VERBOSE_LOGGING=true go run main.go
export AM_GO_ENV_ALWAYS_ALLOW_PANIC=true
export AM_GO_ENV_ALWAYS_PRINT_ERRORS=true
export AM_GO_ENV_ALWAYS_USE_LOGGER=true
export AM_GO_ENV_ENABLE_VERBOSE_LOGGING=true go run main.go
export AM_GO_ENV_ALWAYS_ALLOW_PANIC=true
export AM_GO_ENV_ALWAYS_PRINT_ERRORS=true
export AM_GO_ENV_ALWAYS_USE_LOGGER=true
export AM_GO_ENV_ENABLE_VERBOSE_LOGGING=true go run main.go
env.String("KEY", "fallback")
env.Int("KEY", 0)
env.Int64("KEY", int64(0))
env.Float32("KEY", float32(0.0))
env.Float64("KEY", float64(0.0))
env.Bool("KEY", false)
env.Duration("KEY", 5*time.Second)
env.UnitDuration("KEY", 10, time.Second) // KEY=10 → 10s
env.List("KEY", env.ZeroList) // "a,b,c" → []string
env.Map("KEY", env.ZeroMap) // "k=v,k2=v2" → map[string]string
env.String("KEY", "fallback")
env.Int("KEY", 0)
env.Int64("KEY", int64(0))
env.Float32("KEY", float32(0.0))
env.Float64("KEY", float64(0.0))
env.Bool("KEY", false)
env.Duration("KEY", 5*time.Second)
env.UnitDuration("KEY", 10, time.Second) // KEY=10 → 10s
env.List("KEY", env.ZeroList) // "a,b,c" → []string
env.Map("KEY", env.ZeroMap) // "k=v,k2=v2" → map[string]string
env.String("KEY", "fallback")
env.Int("KEY", 0)
env.Int64("KEY", int64(0))
env.Float32("KEY", float32(0.0))
env.Float64("KEY", float64(0.0))
env.Bool("KEY", false)
env.Duration("KEY", 5*time.Second)
env.UnitDuration("KEY", 10, time.Second) // KEY=10 → 10s
env.List("KEY", env.ZeroList) // "a,b,c" → []string
env.Map("KEY", env.ZeroMap) // "k=v,k2=v2" → map[string]string
env.Exists("KEY") // bool
env.MustExist("KEY") // exits/panics if absent
env.IsTrue("KEY") // true if value is "true"/"1"/"t"
env.IsFalse("KEY") // true if value is "false"/"0"/"f" or unset
env.AreTrue("KEY1", "KEY2", "KEY3") // true if all are true
env.AreFalse("KEY1", "KEY2", "KEY3") // true if all are false/unset
env.Exists("KEY") // bool
env.MustExist("KEY") // exits/panics if absent
env.IsTrue("KEY") // true if value is "true"/"1"/"t"
env.IsFalse("KEY") // true if value is "false"/"0"/"f" or unset
env.AreTrue("KEY1", "KEY2", "KEY3") // true if all are true
env.AreFalse("KEY1", "KEY2", "KEY3") // true if all are false/unset
env.Exists("KEY") // bool
env.MustExist("KEY") // exits/panics if absent
env.IsTrue("KEY") // true if value is "true"/"1"/"t"
env.IsFalse("KEY") // true if value is "false"/"0"/"f" or unset
env.AreTrue("KEY1", "KEY2", "KEY3") // true if all are true
env.AreFalse("KEY1", "KEY2", "KEY3") // true if all are false/unset
env.Set("KEY", "value") // error
env.Unset("KEY") // error
env.WasSet("KEY", "value") // bool — sets and verifies
env.WasUnset("KEY") // bool — unsets and confirms
env.Set("KEY", "value") // error
env.Unset("KEY") // error
env.WasSet("KEY", "value") // bool — sets and verifies
env.WasUnset("KEY") // bool — unsets and confirms
env.Set("KEY", "value") // error
env.Unset("KEY") // error
env.WasSet("KEY", "value") // bool — sets and verifies
env.WasUnset("KEY") // bool — unsets and confirms
env.IntLessThan("KEY", fallback, max)
env.IntGreaterThan("KEY", fallback, min)
env.IntInRange("KEY", fallback, min, max)
env.Int64LessThan("KEY", fallback, max)
env.Int64GreaterThan("KEY", fallback, min)
env.Int64InRange("KEY", fallback, min, max)
env.IntLessThan("KEY", fallback, max)
env.IntGreaterThan("KEY", fallback, min)
env.IntInRange("KEY", fallback, min, max)
env.Int64LessThan("KEY", fallback, max)
env.Int64GreaterThan("KEY", fallback, min)
env.Int64InRange("KEY", fallback, min, max)
env.IntLessThan("KEY", fallback, max)
env.IntGreaterThan("KEY", fallback, min)
env.IntInRange("KEY", fallback, min, max)
env.Int64LessThan("KEY", fallback, max)
env.Int64GreaterThan("KEY", fallback, min)
env.Int64InRange("KEY", fallback, min, max)
env.ListLength("KEY", fallback)
env.ListIsLength("KEY", fallback, wantLength)
env.ListContains("KEY", fallback, "needle")
env.MapHasKey("KEY", fallback, "key")
env.MapHasKeys("KEY", fallback, "k1", "k2", "k3")
env.ListLength("KEY", fallback)
env.ListIsLength("KEY", fallback, wantLength)
env.ListContains("KEY", fallback, "needle")
env.MapHasKey("KEY", fallback, "key")
env.MapHasKeys("KEY", fallback, "k1", "k2", "k3")
env.ListLength("KEY", fallback)
env.ListIsLength("KEY", fallback, wantLength)
env.ListContains("KEY", fallback, "needle")
env.MapHasKey("KEY", fallback, "key")
env.MapHasKeys("KEY", fallback, "k1", "k2", "k3")
env.User() // *user.User — current OS user with safe fallback
env.User() // *user.User — current OS user with safe fallback
env.User() // *user.User — current OS user with safe fallback