Back to all blogs

Automate Celigo Integration Backups to GitHub with a Shell Script 

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.

Blog_field_Auteur Nuri Ensing
Category Celigo
Blog_field_Datum April 15, 2026

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-name folder
  • 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 processing
  • unzip
  • 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:

Bash
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.

Bash
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:

Bash
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:

Bash
git clone https://github.com/YOUR_ORG/YOUR_REPO.git cd YOUR_REPO
Bash
# .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.

Bash
#!/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) + " " 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:

Bash
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:

Bash
#!/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:

Bash
chmod +x backup_to_git.sh

6. Run the backup

Bash
./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:

Bash
PROFILE_FILTER=client ./backup_to_git.sh

Resulting folder structure

After a successful run, your repository will look like this:

Text
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 .crt files 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.