Integration / DevOps
A practical guide to exporting Celigo integrations using the Celigo CLI — unpacking exports into a versioned Git repository, redacting secrets automatically, and pushing a clean backup to GitHub on demand or on a schedule.
Use at your own risk: The scripts and workflow described in this guide are provided for informational purposes only. Teknuro is not responsible for any unintended consequences, data loss, or issues that arise from executing these scripts in your environment. Always review what each script does, test in a non-production environment first, and make sure you have a clear understanding of the impact before running anything.
Managing integrations across multiple Celigo environments — or across client accounts — eventually raises a familiar question: when did this flow last change, and what was different before? The Celigo platform keeps its own history, but having clean, versioned exports in Git gives you diffs, audit trails, and a recovery baseline that lives outside the platform.
This guide builds a shell-based workflow using the Celigo CLI. These scripts export the integrations that are downloadable in the Celigo CLI for each configured profile, unpack them into a consistent folder structure, redact likely secrets, and commit only what has changed to a private GitHub repository.
Framing this correctly: This is a backup and versioning workflow, not a deployment pipeline. Celigo’s own guidance treats Git as a companion tool for backup, version history, and collaboration — the platform remains the source of truth for building and testing changes. If you need full environment promotion, pair this approach with Celigo ILM or a GitHub Actions pipeline using the REST API.
What this setup does
The two scripts cover the full export-to-commit cycle:
- Uses the Celigo CLI with local profiles so API tokens are kept out of Git
- Exports the integrations that are downloadable in the Celigo CLI for each configured profile and unpacks each one into a consistent
account/environment/integration-namefolder - Redacts common secret-like fields from exported JSON, YAML, and other files before anything is staged
- Commits only what has changed and pushes to your private GitHub repository
Before you start
You’ll need the following on the machine running the scripts:
- Node.js and npm
- Git
jq— for JSON processingunzip- Python 3 — for the redaction step
- A private GitHub repository
- A Celigo API token for each environment you want to back up
On macOS, install the shell dependencies with:
brew install jq unzip 1. Install the Celigo CLI
You can download the Celigo npm package from here:
https://www.npmjs.com/package/@celigo/celigo-cli
If global npm installs fail with a permissions error once you have the package, use a Node version manager such as nvm rather than reaching for sudo.
2. Create Celigo profiles
Create one profile per Celigo environment. The scripts expect profiles named in account-environment format — this is how the folder structure is derived automatically.
celigo profile add client-prod --api-token YOUR_PROD_TOKEN --api-base-url https://api.eu.integrator.io
celigo profile add client-sandbox --api-token YOUR_SANDBOX_TOKEN --api-base-url https://api.eu.integrator.io Then verify your profiles are registered:
celigo profile list The examples above use the EU API endpoint. For North America accounts, use the corresponding North America base URL instead.
3. Set up the repository
Clone your private GitHub repository and create a .gitignore to keep temporary export files out of version control:
git clone https://github.com/YOUR_ORG/YOUR_REPO.git
cd YOUR_REPO # .gitignore
tmp_celigo_backup/
*.zip
.DS_Store
*.log 4. Add the export script
Save the following as export_all_integrations.sh in your repository root. This script handles all the heavy lifting: fetching profiles, downloading integrations as ZIPs, unpacking them, and running the redaction pass.
#!/usr/bin/env bash
set -Eeuo pipefail
BASE_DIR="${BASE_DIR:-./integrations}"
TMP_DIR="${TMP_DIR:-./tmp_celigo_backup}"
PROFILE_FILTER="${PROFILE_FILTER:-}"
REDACT_EXPORTS="${REDACT_EXPORTS:-true}"
require_cmd() {
command -v "$1" >/dev/null 2>&1 || {
echo "Missing required command: $1" >&2
exit 1
}
}
require_cmd celigo
require_cmd jq
require_cmd unzip
require_cmd awk
require_cmd sed
require_cmd find
require_cmd python3
mkdir -p "$BASE_DIR" "$TMP_DIR"
cleanup() {
rm -rf "$TMP_DIR"
}
trap cleanup EXIT
sanitize_name() {
printf '%s' "$1" \
| tr '[:upper:]' '[:lower:]' \
| sed 's/[[:space:]]\+/_/g' \
| sed 's|[/:]|-|g' \
| tr -cd '[:alnum:]_.-'
}
matches_filter() {
local profile="$1"
if [ -z "$PROFILE_FILTER" ]; then
return 0
fi
case "$profile" in
*"$PROFILE_FILTER"*) return 0 ;;
*) return 1 ;;
esac
}
redact_file() {
local file="$1"
python3 - "$file" <<'PY'
import json
import pathlib
import re
import sys
path = pathlib.Path(sys.argv[1])
text = path.read_text(encoding="utf-8", errors="ignore")
SENSITIVE_KEYS = {
"apiToken", "api_token", "token", "accessToken", "access_token",
"refreshToken", "refresh_token", "clientSecret", "client_secret",
"password", "pass", "secret", "consumerSecret", "consumer_secret",
"privateKey", "private_key", "authorization", "authHeader",
"signedURL", "signedUrl", "awsAccessKeyId", "accessKeyId",
"accessKey", "secretAccessKey", "bearerToken", "jwt", "licenseKey"
}
KEY_PATTERN = re.compile(
r'(?i)("?(?:api[_-]?token|token|access[_-]?token|refresh[_-]?token|client[_-]?secret|password|pass|secret|consumer[_-]?secret|private[_-]?key|authorization|auth[_-]?header|signedurl|awsaccesskeyid|access[_-]?key(?:id)?|secretaccesskey|bearer[_-]?token|jwt|license[_-]?key)"?\s*[:=]\s*)(".*?"|\'.*?\'|[^\s,}\]]+)'
)
def redact(obj):
if isinstance(obj, dict):
out = {}
for k, v in obj.items():
if k in SENSITIVE_KEYS or k.lower() in {s.lower() for s in SENSITIVE_KEYS}:
out[k] = "__REDACTED__"
else:
out[k] = redact(v)
return out
if isinstance(obj, list):
return [redact(v) for v in obj]
return obj
updated = text
try:
parsed = json.loads(text)
redacted = redact(parsed)
updated = json.dumps(redacted, indent=2, ensure_ascii=False) + "\n"
except Exception:
updated = KEY_PATTERN.sub(r'\1"__REDACTED__"', text)
if updated != text:
path.write_text(updated, encoding="utf-8")
PY
}
redact_directory() {
local dir="$1"
find "$dir" -type f \(
-name '*.json' -o -name '*.js' -o -name '*.ts' -o -name '*.yaml' -o -name '*.yml' \
-o -name '*.txt' -o -name '*.env' -o -name '*.properties' -o -name '*.xml'
\) -print0 | while IFS= read -r -d '' file; do
redact_file "$file"
done
find "$dir" -type f \(
-name '.env' -o -name '.npmrc' -o -name '*.pem' -o -name '*.key' -o -name '*.p12' -o -name '*.crt'
\) -delete
}
echo "Fetching Celigo profiles..."
PROFILE_LINES="$(celigo profile list || true)"
if [ -z "$PROFILE_LINES" ]; then
echo "No Celigo profiles found."
exit 1
fi
PROFILE_NAMES=()
while IFS= read -r line; do
PROFILE_NAME="$(printf '%s' "$line" | awk 'NF {print $1}')"
case "$PROFILE_NAME" in
""|NAME|Name|PROFILE|Profile) continue ;;
esac
if [ -n "$PROFILE_NAME" ] && matches_filter "$PROFILE_NAME"; then
PROFILE_NAMES+=("$PROFILE_NAME")
fi
done <<< "$PROFILE_LINES"
if [ ${#PROFILE_NAMES[@]} -eq 0 ]; then
echo "No matching Celigo profiles found."
exit 1
fi
echo "Profiles selected:"
for PROFILE_NAME in "${PROFILE_NAMES[@]}"; do
echo " - $PROFILE_NAME"
done
for PROFILE_NAME in "${PROFILE_NAMES[@]}"; do
if [[ "$PROFILE_NAME" != *-* ]]; then
echo "Skipping '$PROFILE_NAME' — profile name must follow account-environment format."
continue
fi
ACCOUNT_NAME="${PROFILE_NAME%%-*}"
ENV_NAME="${PROFILE_NAME#*-}"
SAFE_ACCOUNT="$(sanitize_name "$ACCOUNT_NAME")"
SAFE_ENV="$(sanitize_name "$ENV_NAME")"
ACCOUNT_DIR="$BASE_DIR/$SAFE_ACCOUNT"
ENV_DIR="$ACCOUNT_DIR/$SAFE_ENV"
TMP_JSON="$TMP_DIR/${SAFE_ACCOUNT}_${SAFE_ENV}_integrations.json"
mkdir -p "$ENV_DIR"
echo "Fetching integrations for $PROFILE_NAME..."
celigo --profile "$PROFILE_NAME" integrations list --format json > "$TMP_JSON"
if [ ! -s "$TMP_JSON" ]; then
echo "No output returned for $PROFILE_NAME"
rm -f "$TMP_JSON"
continue
fi
COUNT="$(jq 'length' "$TMP_JSON")"
echo "Found $COUNT integrations in $PROFILE_NAME"
if [ "$COUNT" -eq 0 ]; then
rm -f "$TMP_JSON"
continue
fi
while IFS= read -r row; do
ID="$(printf '%s' "$row" | jq -r '._id')"
NAME="$(printf '%s' "$row" | jq -r '.name // .title // ._id')"
SAFE_NAME="$(sanitize_name "$NAME")"
TARGET_DIR="$ENV_DIR/$SAFE_NAME"
ZIP_PATH="$TMP_DIR/${SAFE_ACCOUNT}_${SAFE_ENV}_${SAFE_NAME}.zip"
echo " Downloading: $NAME"
celigo --profile "$PROFILE_NAME" integrations download "$ID" -o "$ZIP_PATH"
rm -rf "$TARGET_DIR"
mkdir -p "$TARGET_DIR"
unzip -oq "$ZIP_PATH" -d "$TARGET_DIR"
rm -f "$ZIP_PATH"
if [ "$REDACT_EXPORTS" = "true" ]; then
redact_directory "$TARGET_DIR"
fi
done < <(jq -c '.[]' "$TMP_JSON")
rm -f "$TMP_JSON"
done
echo ""
echo "Export complete. Output: $BASE_DIR" Make it executable:
chmod +x export_all_integrations.sh 5. Add the Git push script
Save this as backup_to_git.sh in the same folder. It calls the export script, checks for changes, and pushes a timestamped commit:
#!/usr/bin/env bash
set -Eeuo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
EXPORT_SCRIPT="$ROOT_DIR/export_all_integrations.sh"
if [ ! -f "$EXPORT_SCRIPT" ]; then
echo "export_all_integrations.sh not found alongside this script." >&2
exit 1
fi
if ! git -C "$ROOT_DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
echo "Not a Git repository: $ROOT_DIR" >&2
exit 1
fi
echo "Running Celigo export from: $ROOT_DIR"
"$EXPORT_SCRIPT"
cd "$ROOT_DIR"
git add integrations/ .gitignore export_all_integrations.sh backup_to_git.sh 2>/dev/null || true
COMMIT_MSG="Backup Celigo integrations $(date +%Y-%m-%d_%H-%M)"
if git diff --cached --quiet; then
echo "No changes since last backup."
exit 0
fi
git commit -m "$COMMIT_MSG"
CURRENT_BRANCH="$(git branch --show-current)"
if [ -z "$CURRENT_BRANCH" ]; then
CURRENT_BRANCH="main"
fi
echo "Pushing to GitHub (branch: $CURRENT_BRANCH)..."
git push origin "$CURRENT_BRANCH"
echo "Backup complete." Make it executable:
chmod +x backup_to_git.sh 6. Run the backup
./backup_to_git.sh The first run will commit all integrations. Subsequent runs commit only what has changed — so your Git log gives you a clear diff of exactly what was modified between backups.
To filter to a specific account without editing the script, set the PROFILE_FILTER variable at runtime:
PROFILE_FILTER=client ./backup_to_git.sh Resulting folder structure
After a successful run, your repository will look like this:
integrations/
client/
prod/
order-to-cash/
export.json
...
inventory-sync/
...
sandbox/
order-to-cash/
...
export_all_integrations.sh
backup_to_git.sh
.gitignore Each integration gets its own folder named from the integration’s name field, sanitised to lowercase with underscores. Adding a new profile automatically creates the right account and environment folders on the next run.
Security considerations
The redaction step covers the most common cases — API tokens, client secrets, passwords, private keys, bearer tokens, signed URLs — in both JSON-parseable files and plain-text fallback via regex. A few additional habits worth keeping in place:
- Keep the backup repository private. Even with redaction, exported integration structure and flow logic can reveal business logic and system architecture.
- Store API tokens only in local CLI profiles, never in environment variables that might be logged or in scripts that get committed.
- Review a diff before making any repository public. The redaction logic is broad but not exhaustive — custom field names for secrets won’t be caught automatically.
- Treat certificate and key files as out of scope. The script deletes
.pem,.key,.p12, and.crtfiles from the export directory — verify these are being cleaned up if they appear in your exports.
Limitations to be aware of
This workflow is well-suited for backup, change review, and version history. It’s not a substitute for a full deployment pipeline. A few specifics worth keeping in mind:
- This workflow is intended for integrations that your Celigo account and CLI can download as ZIP files. Integration apps are handled differently and may require different tooling or cloning workflows.
- After importing into another environment, review connections, credentials, and environment-specific settings carefully rather than assuming they transfer cleanly. Celigo’s documentation is not fully consistent on this point, so a manual review is the safer assumption.
- This workflow does not replace Celigo ILM for structured environment promotion. If you’re moving integrations from sandbox to production in a controlled, repeatable way, ILM is the right tool.
Running on Windows
These are Bash scripts — they use features like here-docs, process substitution, and chmod +x that don’t work natively in CMD or PowerShell. On Windows, run them through one of these two setups:
- Git Bash — included with Git for Windows, provides Bash emulation that supports the full script.
- WSL (Windows Subsystem for Linux) — runs a full GNU/Linux environment directly on Windows. Install Node, jq, and the other dependencies inside the WSL distribution.
If you want scheduled runs on Windows, use Task Scheduler to trigger the script inside Git Bash or WSL, rather than cron.
Perspective from the field
The most common reason teams reach for this kind of setup is an incident: a flow changed, something broke, and the question is what was different. Having versioned exports in Git means that question has an answer — and the answer doesn’t require digging through platform audit logs or asking someone what they remember changing last week.
Running this on a cron schedule (daily or after significant change windows) gives you a lightweight but effective change ledger. It’s not a replacement for the Celigo platform’s own history, but it’s a format you can search, diff, and hand to a developer without giving them platform access.
If you’d like help setting up a more complete Celigo versioning or deployment pipeline — including ILM, GitHub Actions, or multi-environment management — get in touch. As a certified Celigo partner, Teknuro can help you design and build it.