Sync .env files to GitLab CI/CD variables — bulk import, export, and manage environment variables via API
Fast, concurrent CLI tool for GitLab secrets management and dotenv synchronization
Quick Start • Features • Installation • Usage • Configuration • FAQ
glenv is a command-line tool that synchronizes .env files with GitLab CI/CD variables. It solves the problem of managing GitLab environment variables at scale — bulk import, export, diff, and sync hundreds of variables in seconds using the GitLab API.
Instead of clicking through the GitLab web UI or writing fragile bash scripts, glenv provides:
- Bulk operations — import/export entire
.envfiles with one command - Smart classification — auto-detects masked, protected, and file-type variables
- Safe workflow — preview changes with diff before applying
- Concurrent sync — parallel API calls with built-in rate limiting
- Multi-environment — manage production, staging, and custom environments from config
| Problem | glenv Solution |
|---|---|
| GitLab UI is slow for many variables | Bulk sync hundreds of variables in seconds |
| Bash scripts are fragile and sequential | Concurrent workers with retry and rate limiting |
| No preview before changes | Diff command shows create/update/delete before sync |
| Manual variable classification | Auto-detects masked/protected/file from key patterns |
| Different configs per environment | Single YAML config for all environments |
glenv is written in Go — single static binary, no runtime dependencies, works on Linux, macOS, and Windows.
- Concurrent sync — configurable worker pool with token bucket rate limiter
- Smart classification — auto-detects masked, protected, and file-type variables from key patterns
- Rate limit safe — respects GitLab API limits, handles 429 with Retry-After, exponential backoff
- Diff before sync — preview changes before applying (create/update/delete)
- Dry-run mode — see what would happen without making any API calls
- Multi-environment — sync production, staging, or any custom environment from config
- Export — download current GitLab variables to
.envfile format - .env parser — supports multiline values, quoted strings, comments, placeholder detection
- Zero config — works with just a token and project ID, config file is optional
- Self-hosted support — works with any GitLab instance, configurable rate limits
# Install
go install github.com/ohmylock/glenv/cmd/glenv@latest
# Set credentials (or use config file)
export GITLAB_TOKEN="glpat-xxxxxxxxxxxx"
export GITLAB_PROJECT_ID="12345678"
# Sync .env file to GitLab
glenv sync -f .env.production -e production
# Preview changes first (recommended)
glenv diff -f .env.production -e productionbrew install ohmylock/tools/glenvDownload the appropriate binary for your platform from releases:
| Platform | Architecture | File |
|---|---|---|
| macOS | Apple Silicon | glenv_*_darwin_arm64.tar.gz |
| macOS | Intel | glenv_*_darwin_amd64.tar.gz |
| Linux | x86_64 | glenv_*_linux_amd64.tar.gz |
| Linux | ARM64 | glenv_*_linux_arm64.tar.gz |
| Windows | x86_64 | glenv_*_windows_amd64.zip |
go install github.com/ohmylock/glenv/cmd/glenv@latestgit clone https://github.com/ohmylock/glenv.git
cd glenv
make build
# binary: bin/glenvThe primary command. Reads a .env file and syncs variables to GitLab:
# Sync single file to specific environment
glenv sync -f .env.production -e production
# Sync with dry-run (preview only)
glenv sync -f .env.staging -e staging --dry-run
# Sync all environments defined in config
glenv sync --all
# Override rate limits for self-hosted GitLab
glenv sync -f .env -e production --workers 10 --rate-limit 50Compare local .env file with current GitLab variables:
glenv diff -f .env.production -e productionOutput:
+ DB_HOST=postgres.internal
~ API_KEY: *** → ***
- OLD_VAR
= LOG_LEVEL
# List all variables
glenv list
# Filter by environment
glenv list -e productionDownload GitLab variables to a local .env file:
glenv export -e production -o .env.production.backupNote: File-type variables (certificates, PEM keys) are excluded from the output and replaced with a comment
# KEY (file type, skipped). Useglenv listto see their presence.
# Delete specific variable
glenv delete -e production OLD_SECRET
# Delete multiple variables
glenv delete -e staging KEY1 KEY2 KEY3 --forceglenv works with zero config (just env vars), but a config file unlocks multi-environment workflows.
Config file locations (checked in order):
--configflag path.glenv.ymlin current directory~/.glenv.yml
# GitLab connection
gitlab:
url: https://gitlab.com # self-hosted: https://gitlab.company.com
token: ${GITLAB_TOKEN} # env var expansion supported
project_id: "12345678"
# Rate limiting (safe defaults for gitlab.com)
rate_limit:
requests_per_second: 10 # max API requests/sec (gitlab.com allows ~33)
max_concurrent: 5 # parallel workers
retry_max: 3 # retries on failure
retry_initial_backoff: 1s # backoff before first retry
# Environments
environments:
production:
file: deploy/gitlab-envs/.env.production
staging:
file: deploy/gitlab-envs/.env.staging
# Custom classification rules (extend built-in defaults)
classify:
masked_patterns: # keys containing these → masked
- "_TOKEN"
- "SECRET"
- "PASSWORD"
- "API_KEY"
- "DSN"
masked_exclude: # exceptions (NOT masked)
- "MAX_TOKENS"
- "TIMEOUT"
- "PORT"
file_patterns: # keys containing these → file type
- "PRIVATE_KEY"
- "_CERT"
- "_PEM"
file_exclude: # exceptions (NOT file type)
- "_PATH"
- "_DIR"
- "_URL"| Variable | Description |
|---|---|
GITLAB_TOKEN |
GitLab Personal Access Token (scope: api) |
GITLAB_PROJECT_ID |
Project ID or URL-encoded path |
GITLAB_URL |
GitLab instance URL (default: https://gitlab.com) |
NO_COLOR |
Disable colored output when set to any non-empty value (standard convention) |
Environment variables take precedence over config file values. CLI flags take precedence over everything.
- Parse — reads
.envfile, handles multiline values, skips placeholders - Classify — auto-detects masked/protected/file properties from key patterns and values
- Fetch — gets current variables from GitLab (paginated, concurrent-safe)
- Diff — calculates creates, updates, deletes, and unchanged
- Apply — distributes changes across worker pool with rate limiting
- Report — color-coded summary with timing and API call stats
glenv auto-detects variable properties:
| Property | Condition |
|---|---|
| masked | Key matches secret pattern (_TOKEN, SECRET, PASSWORD, etc.) AND value is >= 8 characters AND value is single-line AND value contains only [a-zA-Z0-9_:@-.+~=/] characters |
| protected | Environment is production AND key matches secret pattern |
| file type | Key matches file pattern (PRIVATE_KEY, _CERT, _PEM) OR value contains PEM headers (-----BEGIN) |
Variables with placeholder values (your_, CHANGE_ME, REPLACE_WITH_) are skipped.
Variables with interpolation (${VAR}) are skipped.
glenv uses a token bucket rate limiter to stay within GitLab API limits:
| GitLab Instance | Limit | Default Config |
|---|---|---|
| gitlab.com | 2,000 req/min (~33/sec) | 10 req/sec, 5 workers |
| Self-hosted | Configurable | Adjust via config |
On 429 responses:
- Parse
Retry-Afterheader - Wait the specified duration
- Retry with exponential backoff + jitter
- Max 3 retries per operation
Supported syntax:
# Comments are skipped
KEY=value
QUOTED="value with spaces"
SINGLE_QUOTED='value'
# Multiline values
PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA...
-----END RSA PRIVATE KEY-----"
# These are skipped automatically:
PLACEHOLDER=your_api_key_here # placeholder detected
INTERPOLATED=${OTHER_VAR}/path # interpolation detected| Flag | Short | Env Var | Description | Default |
|---|---|---|---|---|
--config |
-c |
Config file path | .glenv.yml |
|
--token |
GITLAB_TOKEN |
GitLab access token | ||
--project |
GITLAB_PROJECT_ID |
Project ID | ||
--url |
GITLAB_URL |
GitLab URL | https://gitlab.com |
|
--dry-run |
-n |
Preview mode | false |
|
--no-color |
NO_COLOR |
Disable colors | false |
|
--workers |
-w |
Concurrent workers | 5 |
|
--rate-limit |
Max requests/sec | 10 |
| Flag | Short | Description |
|---|---|---|
--file |
-f |
Path to .env file |
--environment |
-e |
GitLab environment scope |
--all |
-a |
Sync all environments from config |
--delete-missing |
Delete variables not in .env file | |
--no-auto-classify |
Disable smart classification | |
--force |
Skip confirmation prompts |
| Flag | Short | Description |
|---|---|---|
--environment |
-e |
Environment to export |
--output |
-o |
Output file path (default: stdout) |
export GITLAB_TOKEN="glpat-xxxxxxxxxxxx"
export GITLAB_PROJECT_ID="12345678"
# Preview what will change
glenv diff -f .env.production -e production
# Apply changes
glenv sync -f .env.production -e production.glenv.yml:
gitlab:
token: ${GITLAB_TOKEN}
project_id: "12345678"
environments:
staging:
file: deploy/.env.staging
production:
file: deploy/.env.production
protected: true# Sync all environments
glenv sync --all
# Sync specific environment
glenv sync -e productiongitlab:
url: https://gitlab.company.com
token: ${GITLAB_TOKEN}
project_id: "42"
rate_limit:
requests_per_second: 50
max_concurrent: 10# .gitlab-ci.yml
sync-variables:
image: golang:1.23-alpine
script:
- go install github.com/ohmylock/glenv/cmd/glenv@latest
- glenv sync -f deploy/.env.${CI_ENVIRONMENT_NAME} -e ${CI_ENVIRONMENT_NAME}
variables:
GITLAB_TOKEN: ${DEPLOY_TOKEN}
GITLAB_PROJECT_ID: ${CI_PROJECT_ID}glenv uses goreleaser for cross-platform releases.
# Tag and release
git tag v1.0.0
git push origin v1.0.0
# GitHub Actions handles the rest
# Or release locally
make releasemake build # compile binary to bin/glenv
make test # run tests with race detector and coverage
make lint # run golangci-lint (fallback: go vet)
make install # install to /usr/local/bin
make release # goreleaser release
make release-check # goreleaser dry-run (no publish)
make clean # remove build artifacts- Go 1.25+
- golangci-lint (for
make lint) - goreleaser (for
make release)
make test # unit tests with coverage
make lint # static analysis
go test -race ./... # race detectorSet GITLAB_TOKEN and GITLAB_TEST_PROJECT_ID to run tests against a real GitLab instance:
GITLAB_TOKEN=glpat-xxx GITLAB_TEST_PROJECT_ID=123 go test -tags=integration ./...Is it safe to run with multiple workers?
Yes. glenv uses a token bucket rate limiter that controls the overall API request rate regardless of worker count. Workers share the rate limiter, so increasing workers doesn't increase the API request rate — it just distributes the work more efficiently. The default of 10 req/sec is well under gitlab.com's 2,000 req/min limit.
What happens if GitLab rate-limits me?
glenv handles 429 responses automatically. It reads the Retry-After header, waits the specified duration, then retries with exponential backoff. After 3 failed retries (configurable), the operation is marked as failed and reported in the summary.
Does it work with self-hosted GitLab?
Yes. Set gitlab.url in your config file or use --url flag. Self-hosted instances may have different (or no) rate limits — adjust rate_limit.requests_per_second accordingly.
How does it handle masked variables?
GitLab requires masked variables to be at least 8 characters and single-line. glenv auto-detects which variables should be masked based on key name patterns (TOKEN, SECRET, PASSWORD, etc.) and validates the value meets GitLab's requirements. You can customize patterns via config.
Can I sync multiple projects?
Currently glenv syncs one project per invocation. For multiple projects, use separate config files:
glenv sync --config project-a.yml --all
glenv sync --config project-b.yml --allWhat .env formats are supported?
Standard .env format: KEY=VALUE, with support for single/double quotes, multiline quoted values, and comments. Placeholders (your_, CHANGE_ME) and variable interpolation (${VAR}) are detected and skipped. See .env File Format for details.
| Tool | Language | Features |
|---|---|---|
| glenv | Go | Concurrent sync, auto-classification, rate limiting, diff preview |
| GlabEnv | Node.js | Basic sync/export |
| gitlab-dotenv | Python | Variable management |
| glab variable | Go | Official CLI (single variable ops) |
Contributions are welcome! Please read the Contributing Guide before submitting a PR.
MIT License — see LICENSE file.
Made with ❤️ for the DevOps community