@thinesjs/cmenv
v0.0.1
Published
Sync environment variables from codemagic.plains.yaml to Codemagic all through CLI.
Maintainers
Readme
cmenv-cli
CLI tool for syncing environment variables to Codemagic from a version-controlled YAML config.
Define your variable groups in codemagic.plains.yaml, keep secrets in a gitignored codemagic.secrets.yaml, and sync them to Codemagic with a single command.
Install
npm install -g @thinesjs/cmenvOr run directly:
npx @thinesjs/cmenv sync --dry-runQuick Start
1. Add to .gitignore
# cmenv
codemagic.config.json
codemagic.secrets.yaml
.codemagic/2. Create codemagic.plains.yaml
This is the source of truth for your Codemagic variable groups. Committed to git.
groups:
global-vars:
variables:
- name: NODEJS_VERSION
value: "22"
staging-vars:
secure: true
variables:
- name: BUILD_ENV
value: "staging"
- name: BUNDLE_ID
value: "com.example.app.staging"
secure: false
- name: GOOGLE_SERVICES_JSON
file: staging/google-services.json
- name: GOOGLE_SERVICES_PLIST
file: staging/GoogleService-Info.plist
- name: ENV_STAGING
file: staging/.env.staging
prod-vars:
secure: true
variables:
- name: BUILD_ENV
value: "production"
- name: BUNDLE_ID
value: "com.example.app"
secure: false
- name: PROD_BUILD_NUMBER
value: "1"
secure: false
- name: GOOGLE_SERVICES_JSON
file: production/google-services.json
- name: GOOGLE_SERVICES_PLIST
file: production/GoogleService-Info.plist
- name: ENV_PRODUCTION
file: production/.env.production
- name: FIREBASE_SERVICE_ACCOUNT
file_raw: credentials/firebase-service-account.json
ios-global-vars:
variables:
- name: XCODE_VERSION
value: "26.1"
- name: COCOAPODS_VERSION
value: "1.16.2"
- name: RUBY_VERSION
value: "3.4.1"
- name: XCODE_WORKSPACE
value: "MyApp.xcworkspace"
- name: XCODE_SCHEME
value: "MyApp"3. Create codemagic.config.json (gitignored)
{
"api_key": "your-codemagic-api-key",
"team_id": "your-team-id",
"app_id": "your-app-id"
}4. Set up .codemagic/ directory (gitignored)
Drop your secret files here. File paths in the YAML resolve relative to this directory.
.codemagic/
├── staging/
│ ├── google-services.json
│ ├── GoogleService-Info.plist
│ └── .env.staging
├── production/
│ ├── google-services.json
│ ├── GoogleService-Info.plist
│ └── .env.production
└── credentials/
└── firebase-service-account.json5. Sync
cmenv sync --dry-run # preview
cmenv sync --update # push to codemagic
cmenv list # verify remote state6. Compatible codemagic.yaml
Minimal Codemagic workflow config that works with the variable groups above:
definitions:
environments:
- &base_env
vars:
CM_CLONE_DEPTH: 3
- &android_base_env
node: $NODEJS_VERSION
android_signing:
- your-keystore-name
- &ios_base_env
node: $NODEJS_VERSION
xcode: $XCODE_VERSION
cocoapods: $COCOAPODS_VERSION
ruby: $RUBY_VERSION
- &ios_appstore_signing
distribution_type: app_store
bundle_identifier: $BUNDLE_ID
- &ios_adhoc_signing
distribution_type: ad_hoc
bundle_identifier: $BUNDLE_ID
env_groups:
- &staging_android_groups
- global-vars
- staging-vars
- &staging_ios_groups
- global-vars
- ios-global-vars
- staging-vars
- &prod_android_groups
- global-vars
- prod-vars
- &prod_ios_groups
- global-vars
- ios-global-vars
- prod-vars
steps:
- step: &android_files_config
name: Load Android files configuration
script: |
#!/usr/bin/env sh
set -e
echo $GOOGLE_SERVICES_JSON | base64 --decode > android/app/src/$BUILD_ENV/google-services.json
- step: &ios_files_config
name: Load iOS files configuration
script: |
#!/usr/bin/env sh
set -e
echo $GOOGLE_SERVICES_PLIST | base64 --decode > ios/GoogleService-Info.plist
- step: &set_android_sdk_location
name: Set Android SDK location
script: echo "sdk.dir=$ANDROID_SDK_ROOT" > "android/local.properties"
- step: &install_deps
name: Install dependencies
script: yarn install
- step: &install_cp_deps
name: Install CocoaPods dependencies
script: |
gem install cocoapods -v $COCOAPODS_VERSION
gem install xcpretty
cd ios && pod install
- step: &generate_env_staging
name: Export .env.staging
script: |
PACKAGE_VERSION=$(cat package.json | jq -r '.version')
UPDATED_BUILD_NUMBER=$((10000 + $PROJECT_BUILD_NUMBER))
echo $ENV_STAGING | base64 --decode > .env.staging
echo "APP_BUILD_NUMBER=$UPDATED_BUILD_NUMBER" >> .env.staging
echo "APP_VERSION=$PACKAGE_VERSION" >> .env.staging
- step: &generate_env_prod
name: Export .env.production
script: |
PACKAGE_VERSION=$(cat package.json | jq -r '.version')
echo $ENV_PRODUCTION | base64 --decode > .env.production
echo "APP_BUILD_NUMBER=$PROD_BUILD_NUMBER" >> .env.production
echo "APP_VERSION=$PACKAGE_VERSION" >> .env.production
signing_steps:
- step: &setup_adhoc_code_signing
name: Set up ad-hoc code signing
script: |
keychain initialize
app-store-connect fetch-signing-files "$BUNDLE_ID" --type IOS_APP_ADHOC --create
keychain add-certificates
xcode-project use-profiles --project ios/*.xcodeproj --archive-method=ad-hoc
- step: &setup_appstore_code_signing
name: Set up App Store code signing
script: |
keychain initialize
app-store-connect fetch-signing-files "$BUNDLE_ID" --type IOS_APP_STORE --create
keychain add-certificates
xcode-project use-profiles --project ios/*.xcodeproj
builds:
- step: &build_android_staging
name: Build Android staging
script: cd android && ./gradlew assembleStagingRelease
- step: &build_android_prod
name: Build Android production
script: cd android && ./gradlew bundleProductionRelease
- step: &build_ios
name: Build iOS IPA
script: |
xcode-project build-ipa \
--workspace "ios/$XCODE_WORKSPACE" \
--scheme "$XCODE_SCHEME"
workflows:
staging-android-release:
name: "[Android] Staging Release"
max_build_duration: 60
instance_type: linux_x2
environment:
<<: [*base_env, *android_base_env]
groups: *staging_android_groups
scripts:
- *set_android_sdk_location
- *android_files_config
- *generate_env_staging
- *install_deps
- *build_android_staging
staging-ios-release:
name: "[iOS] Staging Release"
max_build_duration: 60
instance_type: mac_mini_m2
environment:
<<: [*base_env, *ios_base_env]
groups: *staging_ios_groups
ios_signing:
<<: *ios_adhoc_signing
scripts:
- *ios_files_config
- *generate_env_staging
- *install_deps
- *install_cp_deps
- *setup_adhoc_code_signing
- *build_ios
production-android-release:
name: "[Android] Production Release"
max_build_duration: 60
instance_type: mac_mini_m2
environment:
<<: [*base_env, *android_base_env]
groups: *prod_android_groups
scripts:
- *set_android_sdk_location
- *android_files_config
- *generate_env_prod
- *install_deps
- *build_android_prod
publishing:
google_play:
credentials: $FIREBASE_SERVICE_ACCOUNT
track: internal
submit_as_draft: true
production-ios-release:
name: "[iOS] Production Release"
max_build_duration: 60
instance_type: mac_mini_m2
integrations:
app_store_connect: codemagic
environment:
<<: [*base_env, *ios_base_env]
groups: *prod_ios_groups
ios_signing:
<<: *ios_appstore_signing
scripts:
- *ios_files_config
- *generate_env_prod
- *install_deps
- *install_cp_deps
- *setup_appstore_code_signing
- *build_ios
publishing:
app_store_connect:
auth: integration
submit_to_testflight: trueFile Structure
| File | Committed | Purpose |
|------|-----------|---------|
| codemagic.plains.yaml | Yes | Variable groups and non-secret values |
| codemagic.secrets.yaml | No | Secret values, merged into plains at sync time |
| codemagic.config.json | No | Codemagic API credentials |
| .codemagic/ | No | Secret files referenced by file / file_raw |
Only codemagic.plains.yaml is required.
Variable Definitions
Variables support three value sources:
groups:
my-group:
secure: true # default secure flag for all variables in group
variables:
# inline value
- name: API_URL
value: "https://api.example.com"
# file reference — base64-encoded at sync time, secure by default
- name: GOOGLE_SERVICES_JSON
file: staging/google-services.json
# raw file reference — read as-is, secure by default
- name: SERVICE_ACCOUNT
file_raw: credentials/service-account.json
# per-variable secure override
- name: BUILD_NUMBER
value: "42"
secure: falseFile paths (file, file_raw) resolve relative to the .codemagic/ directory.
Secrets
codemagic.secrets.yaml uses the same format as the plains file. At sync time, it's deep-merged into the plains config:
- Variables are matched by name within each group
- Secrets file wins on conflict
- New variables are appended
- New groups are added
# codemagic.secrets.yaml
groups:
my-group:
variables:
- name: API_KEY
value: "sk-live-abc123"Commands
sync
cmenv sync # sync all groups (create only)
cmenv sync --update # create + update existing
cmenv sync --dry-run # preview without changes
cmenv sync --group <name> # sync specific group
cmenv sync --delete-extra # remove remote vars not in configlist
cmenv list # show remote groups and variablesclean
cmenv clean --group <name> --dry-run # preview deletions
cmenv clean --group <name> --yes # delete variables in group
cmenv clean --all --yes # delete all variables
cmenv clean --all --delete-groups --yes # delete groups tooCredentials
Resolved in this order (first wins):
- CLI flags:
--api-key,--team-id,--app-id codemagic.config.json(sibling of plains file)- Environment variables:
CODEMAGIC_API_KEY,CODEMAGIC_TEAM_ID,CODEMAGIC_APP_ID
--env-file <path> loads a dotenv file into the environment before credential resolution.
Config Discovery
cmenv finds codemagic.plains.yaml automatically:
--config <path>flag (explicit)- Current working directory
- Walk up parent directories
Once found, sibling files (codemagic.secrets.yaml, codemagic.config.json) and .codemagic/ are resolved relative to the same directory.
License
MIT
