“Did you remember to export the updated Login journey before leaving on Friday?”

This Slack message used to haunt our team. Someone would make changes in dev, forget to export, and by Monday we’d be scratching our heads about what changed. Sound familiar?

The fix: wire up Frodo CLI with GitHub Actions and never worry about manual exports again. Here’s exactly how we set it up.

Why Bother with CI/CD for ForgeRock?

Manual Process CI/CD with Frodo
Export from admin console git push triggers export
Copy JSON files manually Automated version control
Import one-by-one Batch import with validation
No audit trail Full Git history
Human errors Consistent, repeatable

CI/CD Pipeline Flow:

graph LR
    subgraph "Development"
        DEV[Dev Tenant]
        CODE[Git Push]
    end

    subgraph "GitHub Actions"
        EXP[Export Job]
        TEST[Validation]
        IMP[Import Job]
    end

    subgraph "Production"
        PROD[Prod Tenant]
    end

    DEV -->|Trigger| CODE
    CODE --> EXP
    EXP --> TEST
    TEST -->|Approved| IMP
    IMP --> PROD

    style EXP fill:#667eea,color:#fff
    style IMP fill:#28a745,color:#fff

Prerequisites

Before setting up the pipeline:

  1. Frodo CLI installed locally for testing
  2. GitHub repository for your ForgeRock configurations
  3. Service account credentials for each tenant
  4. GitHub Secrets configured for credentials

Setting Up GitHub Secrets

Store your ForgeRock credentials securely in GitHub:

  1. Go to your repository → SettingsSecrets and variablesActions
  2. Add the following secrets:
Secret Name Value
FRODO_DEV_HOST https://openam-dev.forgeblocks.com/am
FRODO_DEV_USER [email protected]
FRODO_DEV_PASSWORD Your service account password
FRODO_PROD_HOST https://openam-prod.forgeblocks.com/am
FRODO_PROD_USER [email protected]
FRODO_PROD_PASSWORD Your service account password

Don’t use your personal admin account. Create a dedicated service account with only the permissions it needs. When (not if) credentials leak, you want to limit the blast radius.


Pipeline 1: Export on Schedule

Automatically export configurations daily and commit to Git:

# .github/workflows/export-config.yml
name: Export ForgeRock Configuration

on:
  schedule:
    # Run daily at 2 AM UTC
    - cron: '0 2 * * *'
  workflow_dispatch:  # Allow manual trigger

jobs:
  export:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '18'

      - name: Install Frodo CLI
        run: npm install -g @rockcarver/frodo-cli

      - name: Export Journeys
        env:
          FRODO_HOST: ${{ secrets.FRODO_DEV_HOST }}
          FRODO_USER: ${{ secrets.FRODO_DEV_USER }}
          FRODO_PASSWORD: ${{ secrets.FRODO_DEV_PASSWORD }}
        run: |
          mkdir -p exports/journeys
          frodo journey export --all --directory exports/journeys

      - name: Export Scripts
        env:
          FRODO_HOST: ${{ secrets.FRODO_DEV_HOST }}
          FRODO_USER: ${{ secrets.FRODO_DEV_USER }}
          FRODO_PASSWORD: ${{ secrets.FRODO_DEV_PASSWORD }}
        run: |
          mkdir -p exports/scripts
          frodo script export --all --directory exports/scripts

      - name: Export OAuth Clients
        env:
          FRODO_HOST: ${{ secrets.FRODO_DEV_HOST }}
          FRODO_USER: ${{ secrets.FRODO_DEV_USER }}
          FRODO_PASSWORD: ${{ secrets.FRODO_DEV_PASSWORD }}
        run: |
          mkdir -p exports/oauth
          frodo oauth client export --all --directory exports/oauth

      - name: Commit and Push
        run: |
          git config --local user.email "github-actions[bot]@users.noreply.github.com"
          git config --local user.name "github-actions[bot]"
          git add exports/
          git diff --staged --quiet || git commit -m "chore: daily config export $(date +%Y-%m-%d)"
          git push

Pipeline 2: Deploy to Production on Release

Deploy configurations when a new release is created:

# .github/workflows/deploy-prod.yml
name: Deploy to Production

on:
  release:
    types: [published]
  workflow_dispatch:
    inputs:
      confirm:
        description: 'Type "deploy" to confirm production deployment'
        required: true

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - name: Validate deployment confirmation
        if: github.event_name == 'workflow_dispatch'
        run: |
          if [ "${{ github.event.inputs.confirm }}" != "deploy" ]; then
            echo "Deployment not confirmed. Exiting."
            exit 1
          fi

  deploy:
    needs: validate
    runs-on: ubuntu-latest
    environment: production  # Requires approval in GitHub

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '18'

      - name: Install Frodo CLI
        run: npm install -g @rockcarver/frodo-cli

      - name: Import Scripts First
        env:
          FRODO_HOST: ${{ secrets.FRODO_PROD_HOST }}
          FRODO_USER: ${{ secrets.FRODO_PROD_USER }}
          FRODO_PASSWORD: ${{ secrets.FRODO_PROD_PASSWORD }}
        run: |
          echo "Importing scripts..."
          frodo script import --all --directory exports/scripts

      - name: Import Journeys
        env:
          FRODO_HOST: ${{ secrets.FRODO_PROD_HOST }}
          FRODO_USER: ${{ secrets.FRODO_PROD_USER }}
          FRODO_PASSWORD: ${{ secrets.FRODO_PROD_PASSWORD }}
        run: |
          echo "Importing journeys..."
          frodo journey import --all --directory exports/journeys

      - name: Import OAuth Clients
        env:
          FRODO_HOST: ${{ secrets.FRODO_PROD_HOST }}
          FRODO_USER: ${{ secrets.FRODO_PROD_USER }}
          FRODO_PASSWORD: ${{ secrets.FRODO_PROD_PASSWORD }}
        run: |
          echo "Importing OAuth clients..."
          frodo oauth client import --all --directory exports/oauth

      - name: Deployment Summary
        run: |
          echo "## Deployment Complete! :rocket:" >> $GITHUB_STEP_SUMMARY
          echo "- Scripts: $(ls exports/scripts/*.json 2>/dev/null | wc -l) files" >> $GITHUB_STEP_SUMMARY
          echo "- Journeys: $(ls exports/journeys/*.json 2>/dev/null | wc -l) files" >> $GITHUB_STEP_SUMMARY
          echo "- OAuth: $(ls exports/oauth/*.json 2>/dev/null | wc -l) files" >> $GITHUB_STEP_SUMMARY

Pipeline 3: PR-Based Promotion

Deploy to staging when a PR is merged, with manual approval for production:

# .github/workflows/promote-config.yml
name: Promote Configuration

on:
  push:
    branches:
      - main
    paths:
      - 'exports/**'
  pull_request:
    branches:
      - main
    paths:
      - 'exports/**'

jobs:
  # Validate changes in PR
  validate:
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Validate JSON files
        run: |
          echo "Validating JSON syntax..."
          find exports -name "*.json" -exec python3 -m json.tool {} \; > /dev/null
          echo "All JSON files are valid!"

      - name: List changes
        run: |
          echo "## Changed Files" >> $GITHUB_STEP_SUMMARY
          git diff --name-only origin/main...HEAD -- exports/ >> $GITHUB_STEP_SUMMARY

  # Deploy to staging on merge
  deploy-staging:
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: staging

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '18'

      - name: Install Frodo CLI
        run: npm install -g @rockcarver/frodo-cli

      - name: Deploy to Staging
        env:
          FRODO_HOST: ${{ secrets.FRODO_STAGING_HOST }}
          FRODO_USER: ${{ secrets.FRODO_STAGING_USER }}
          FRODO_PASSWORD: ${{ secrets.FRODO_STAGING_PASSWORD }}
        run: |
          frodo script import --all --directory exports/scripts
          frodo journey import --all --directory exports/journeys
          frodo oauth client import --all --directory exports/oauth

      - name: Create production deployment issue
        uses: actions/github-script@v7
        with:
          script: |
            github.rest.issues.create({
              owner: context.repo.owner,
              repo: context.repo.repo,
              title: `Production deployment ready: ${context.sha.substring(0, 7)}`,
              body: `Staging deployment successful. Review and approve production deployment.\n\nCommit: ${context.sha}\nWorkflow: ${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`,
              labels: ['deployment', 'production']
            })

Pipeline 4: Multi-Environment Matrix

Deploy to multiple tenants in parallel:

# .github/workflows/multi-env-deploy.yml
name: Multi-Environment Deploy

on:
  workflow_dispatch:
    inputs:
      environments:
        description: 'Environments to deploy (comma-separated)'
        required: true
        default: 'dev,staging'
        type: string

jobs:
  deploy:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        environment: ${{ fromJson(format('["{0}"]', join(fromJson(format('["{0}"]', inputs.environments)), '","'))) }}
      fail-fast: false

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '18'

      - name: Install Frodo CLI
        run: npm install -g @rockcarver/frodo-cli

      - name: Deploy to ${{ matrix.environment }}
        env:
          FRODO_HOST: ${{ secrets[format('FRODO_{0}_HOST', matrix.environment)] }}
          FRODO_USER: ${{ secrets[format('FRODO_{0}_USER', matrix.environment)] }}
          FRODO_PASSWORD: ${{ secrets[format('FRODO_{0}_PASSWORD', matrix.environment)] }}
        run: |
          echo "Deploying to ${{ matrix.environment }}..."
          frodo journey import --all --directory exports/journeys

Best Practices

1. Use GitHub Environments

Configure environments with protection rules:

# In your workflow
jobs:
  deploy:
    environment: production  # Requires approval

In GitHub Settings → Environments → production:

  • Add required reviewers
  • Set deployment branch rules
  • Add environment secrets

2. Import Order Matters

Always import dependencies first:

# Correct order
- name: Import in correct order
  run: |
    # 1. Scripts (dependencies for journeys)
    frodo script import --all --directory exports/scripts

    # 2. Email templates (used by journeys)
    frodo email template import --all --directory exports/email

    # 3. Journeys (depend on scripts and templates)
    frodo journey import --all --directory exports/journeys

    # 4. OAuth clients (may reference journeys)
    frodo oauth client import --all --directory exports/oauth

3. Handle Secrets Properly

Never export secrets to Git:

- name: Export ESV Variables Only
  run: |
    # Export variables (non-sensitive)
    frodo esv variable export --all --directory exports/esv

    # DON'T export secrets to Git!
    # frodo esv secret export --all  # NEVER DO THIS

4. Add Rollback Capability

- name: Backup before deploy
  run: |
    mkdir -p backup
    frodo journey export --all --directory backup/journeys

- name: Deploy
  run: |
    frodo journey import --all --directory exports/journeys

- name: Rollback on failure
  if: failure()
  run: |
    echo "Deployment failed, rolling back..."
    frodo journey import --all --directory backup/journeys

5. Use Caching for Speed

- name: Cache npm dependencies
  uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}

- name: Install Frodo CLI
  run: npm install -g @rockcarver/frodo-cli

Troubleshooting

Authentication Failures

- name: Test connection
  env:
    FRODO_HOST: ${{ secrets.FRODO_DEV_HOST }}
    FRODO_USER: ${{ secrets.FRODO_DEV_USER }}
    FRODO_PASSWORD: ${{ secrets.FRODO_DEV_PASSWORD }}
  run: |
    frodo journey list || {
      echo "Connection failed!"
      echo "Check your secrets are correctly configured"
      exit 1
    }

Rate Limiting

- name: Import with delay
  run: |
    for file in exports/journeys/*.json; do
      echo "Importing $file..."
      frodo journey import --file "$file"
      sleep 2  # Add delay between imports
    done

Debugging

- name: Debug mode
  env:
    DEBUG: frodo:*  # Enable Frodo debug logging
  run: |
    frodo journey list --verbose

Complete Repository Structure

forgerock-config/
├── .github/
│   └── workflows/
│       ├── export-config.yml
│       ├── deploy-staging.yml
│       └── deploy-prod.yml
├── exports/
│   ├── journeys/
│   │   ├── Login.journey.json
│   │   └── Registration.journey.json
│   ├── scripts/
│   │   └── CustomValidation.script.json
│   ├── oauth/
│   │   └── my-spa-client.oauth2.json
│   └── esv/
│       └── variables.json
├── README.md
└── .gitignore

.gitignore:

# Never commit secrets
exports/esv/secrets*.json
*.secret.json

# Local Frodo cache
.frodo/
TokenCache.json

Frodo CLI Series

CI/CD Resources


What We Learned

After running these pipelines for several months, a few things became clear:

  1. Start with exports only - Get comfortable with automated backups before adding deployment automation. Trust builds gradually.

  2. The 2 AM cron job is a lifesaver - When someone inevitably makes an undocumented change, you have yesterday’s export to compare against.

  3. Production approval gates are non-negotiable - One accidental push to prod taught us that lesson. Use GitHub Environments with required reviewers.

  4. Git history becomes your audit log - “When did we add that MFA step to the Login journey?” Just check the commit history.

The setup takes maybe an hour. The peace of mind? Priceless.