Skip to main content
rootly-catalog-sync is a standalone CLI tool that reconciles external sources of truth into Rootly’s Catalog. It pulls data from your existing systems — GitHub repos, Backstage, internal APIs, CSV files, or any command — and syncs it one-way into Rootly, keeping services, teams, and metadata up to date automatically.
Rootly Catalog

Why use Catalog Sync?

  • Single source of truth — your service catalog lives in GitHub, Backstage, or a database. Rootly mirrors it automatically.
  • No manual data entry — add a service to your repo, it appears in Rootly on the next sync.
  • Safe by default — deletes are opt-in, empty sources abort, prune ratio thresholds prevent mass deletion.
  • Terraform-style workflowplan to preview, apply to execute, status to check drift.

Install

# Homebrew
brew install rootlyhq/tap/rootly-catalog-sync

# Go
go install github.com/rootlyhq/rootly-catalog-sync/cmd/rootly-catalog-sync@latest

# Docker (mount your config + catalog data)
docker run --rm -e ROOTLY_API_KEY \
  -v $PWD/rootly-catalog-sync.yaml:/config.yaml:ro \
  -v $PWD/catalog:/catalog:ro \
  rootlyhq/rootly-catalog-sync sync --config=/config.yaml

Quick start

# Set your API key
export ROOTLY_API_KEY=rootly_...

# Create a config
rootly-catalog-sync init

# Check your setup
rootly-catalog-sync doctor

# Preview changes
rootly-catalog-sync plan

# Apply
rootly-catalog-sync sync

Authentication

Two methods are supported, in priority order:

API key (CI / non-interactive)

export ROOTLY_API_KEY=rootly_...
Create an API key at Settings → API Keys in your Rootly dashboard.

OAuth 2.0 (interactive)

# Login via browser (Authorization Code + PKCE)
rootly-catalog-sync login

# Tokens saved to ~/.rootly-catalog-sync/config.yaml
# Auto-refreshed transparently on expiry

# Clear stored tokens
rootly-catalog-sync logout
If ROOTLY_API_KEY is set, it always takes precedence over OAuth tokens.

Configuration

The sync tool uses a declarative config file that defines pipelines — each pipeline connects sources to catalog outputs.
version: 1
sync_id: services
pipelines:
  - sources:
      - local:
          files: ["catalog/*.yaml"]
    outputs:
      - catalog: "Services"
        external_id: "{{ .id }}"
        name: "{{ .name }}"
        fields:
          owner: "{{ .owner }}"
          tier: "{{ .tier }}"
Config files are detected by extension: .yaml (default), .jsonnet, or .hcl. Credentials use $(ENV_VAR) substitution.

Sources

SourceDescription
inlineEntries defined directly in config
localYAML/JSON files from disk (glob patterns)
githubFiles from GitHub repositories (supports ** patterns)
execRun a command, parse stdout as JSON/YAML
backstageBackstage catalog API with pagination
graphqlArbitrary GraphQL endpoint with cursor/offset pagination
csvCSV files with header row
urlFetch YAML/JSON from remote URLs
httpGeneric REST API with JSONPath extraction

GitHub source example

sources:
  - github:
      token: "$(GITHUB_TOKEN)"
      owner: acme
      repos: ["payments", "auth", "gateway"]
      files: ["**/catalog.yaml"]
      ref: main

Exec source example

sources:
  - exec:
      command: bq
      args: ["query", "--format=json", "SELECT id, name, owner FROM dataset.services"]

URL source example

sources:
  - url:
      urls:
        - https://internal.company.com/catalog/services.yaml
        - https://internal.company.com/catalog/teams.json
      headers:
        Authorization: "Bearer $(API_TOKEN)"

HTTP source example

sources:
  - http:
      url: https://api.internal.com/v1/services
      method: GET
      headers:
        Authorization: "Bearer $(API_TOKEN)"
      result: data.services

Commands

CommandDescription
planPreview changes (creates a saved plan file)
apply <plan>Apply a saved plan (validates freshness first)
syncPlan + apply in one step
statusRead-only drift check (--fail-on-drift for CI gates)
initCreate a config file (add --interactive for guided wizard)
validateCheck config syntax
doctorVerify API key, connectivity, and permissions
sources inspectDump raw source entries before mapping
explain <id>Trace one entry through source → mapping → diff
adoptClaim existing UI entries under sync management
importOne-shot seed (no prune, no lock)
watchContinuous sync loop (--interval=5m)
tuiInteractive terminal UI for selective apply
loginAuthenticate via browser OAuth 2.0 (PKCE)
logoutClear stored OAuth tokens

Safety guarantees

  • Deletes are opt-in--allow-prune required, off by default
  • Empty source aborts — never wipes a catalog on a source failure
  • Prune ratio threshold — aborts if deletes exceed 20% of live entities (configurable via --prune-threshold)
  • Manual entries are safe — only entries with external_id (created by sync) are prunable
  • Order: create/update first, delete last — no window where entries are missing
  • Plan freshnessapply validates that live state hasn’t changed since the plan was created

CI/CD integration

GitHub Actions

name: Catalog Sync
on:
  push:
    branches: [main]
    paths:
      - "catalog/**"
      - "rootly-catalog-sync.yaml"

jobs:
  sync:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: stable
      - run: |
          go install github.com/rootlyhq/rootly-catalog-sync/cmd/rootly-catalog-sync@latest
          $(go env GOPATH)/bin/rootly-catalog-sync sync
        env:
          ROOTLY_API_KEY: ${{ secrets.ROOTLY_API_KEY }}

Dry-run on PRs

- run: rootly-catalog-sync plan --dry-run --output=json
  env:
    ROOTLY_API_KEY: ${{ secrets.ROOTLY_API_KEY }}

Nightly drift detection

- run: rootly-catalog-sync status --fail-on-drift
  env:
    ROOTLY_API_KEY: ${{ secrets.ROOTLY_API_KEY }}

Environment variables

VariableDescriptionDefault
ROOTLY_API_KEYAPI key (or use login for OAuth)
ROOTLY_API_URLOverride base URLhttps://api.rootly.com
ROOTLY_API_PATHOverride API path prefix/v1

Interactive TUI

The tui command launches a full-screen terminal UI for reviewing and selectively applying changes:
  • Browse changes with colored badges (CREATE/UPDATE/DELETE/NOOP)
  • Toggle individual changes with space, expand field diffs with enter
  • Filter by operation type (c/u/d) or search (/)
  • Detail pane shows full entity fields on wide terminals
  • Apply only selected changes with A

Resources