← Back

CircleCI API Integration: Automated CI/CD Management

·code-quality

CircleCI API Integration: Automated CI/CD Management

Checking CI/CD pipeline status required navigating through CircleCI's web UI—logging in, finding the project, locating the branch, clicking through build details. For each pull request review, this added 2-3 minutes of context switching. Developers opened 15-20 PRs per day across the team, resulting in 30-60 minutes daily spent clicking through web interfaces.

The information we needed was simple: "Did the build pass?" The process to get that information was complex: open browser, navigate to CircleCI, authenticate, search for pipeline, click through to build details, scan for green checkmarks. We automated CI status checks using CircleCI's API, reducing 3-minute workflows to 5-second commands.

The Problem: Manual CI Status Checks

Pull request reviews required verifying that CI passed before merging. The workflow looked like:

Before: Manual CI Status Check

CI Status Check
┌──────────────────────────────────────┐
│ 1. Open browser                      │
│ 2. Navigate to circleci.com          │
│ 3. Log in (if session expired)       │
│ 4. Find alphazed-alqosh project      │
│ 5. Locate feature branch             │
│ 6. Click into latest pipeline        │
│ 7. Scan build steps for failures     │
│ 8. Mentally track which steps passed │
│ 9. Determine overall status          │
└──────────────────────────────────────┘
    Time: 2-3 minutes per check
    Context switches: 3-4 (terminal → browser → CircleCI → PR)

This workflow interrupted focus. A developer reviewing code on GitHub had to switch to a browser tab, navigate through CircleCI, remember which branch they were checking, and return to GitHub to continue the review.

Multiplied across the team:

Developers: 5
PR reviews per developer per day: 3-4
CI checks per review: 2 (initial + after updates)
Time per check: 2.5 minutes

Total daily time:
  5 developers × 3.5 PRs × 2 checks × 2.5 min = 87.5 min/day

Per week: 437.5 minutes = 7.3 hours
Per month: 1,750 minutes = 29 hours

Nearly 30 hours per month spent clicking through web UIs to answer simple yes/no questions.

The Solution: CircleCI CLI + API Integration

We created command-line tools that interact with CircleCI's API to fetch pipeline status, trigger builds, and retrieve test results—all from the terminal without leaving the development environment.

After: Automated CI Status Check

CI Status Check (Automated)
┌──────────────────────────────────────┐
 $ circleci-status feature/my-branch  
                                      
  Pipeline passed                    
  All tests passed (324/324)         
  Build successful                   
  Lint checks passed                 
                                      
 Duration: 2m 34s                     
 URL: https://circleci.com/gh/...     
└──────────────────────────────────────┘
    Time: 5 seconds
    Context switches: 0 (stay in terminal)

The same information, obtained 30× faster with zero context switching.

Implementation Details

We built a set of CLI utilities on top of CircleCI's API v2:

1. Authentication Setup

CircleCI API requires personal access tokens for authentication:

Generate token:

  1. Go to CircleCI → User Settings → Personal API Tokens
  2. Create new token with name "CLI Access"
  3. Store token in environment variable

~/.bashrc or ~/.zshrc:

export CIRCLECI_TOKEN="your_token_here"

For security, we also support token storage in .env files that git ignores:

.env:

CIRCLECI_TOKEN=your_token_here

2. Core API Wrapper

We created a Python wrapper around CircleCI's API:

scripts/circleci_client.py:

import os
import requests
from typing import Dict, List, Optional

class CircleCIClient:
    """Client for CircleCI API v2."""

    BASE_URL = "https://circleci.com/api/v2"

    def __init__(self, token: Optional[str] = None):
        """Initialize client with API token."""
        self.token = token or os.getenv("CIRCLECI_TOKEN")
        if not self.token:
            raise ValueError("CircleCI token not found. Set CIRCLECI_TOKEN env var.")

        self.headers = {
            "Circle-Token": self.token,
            "Accept": "application/json"
        }

    def get_pipeline_status(self, project_slug: str, branch: str) -> Dict:
        """
        Get latest pipeline status for a branch.

        Args:
            project_slug: Format: "gh/org/repo" (e.g., "gh/alphazed/alqosh")
            branch: Branch name

        Returns:
            Dict with pipeline status, including:
            - status: "success", "failed", "running", etc.
            - created_at: ISO timestamp
            - duration: Seconds
            - workflow_id: Workflow ID for fetching details
        """
        url = f"{self.BASE_URL}/project/{project_slug}/pipeline"
        params = {"branch": branch}

        response = requests.get(url, headers=self.headers, params=params)
        response.raise_for_status()

        data = response.json()
        pipelines = data.get("items", [])

        if not pipelines:
            return {"status": "no_pipeline", "branch": branch}

        # Get most recent pipeline
        latest = pipelines[0]
        pipeline_id = latest["id"]

        # Get workflow details
        workflow = self.get_workflow_status(pipeline_id)

        return {
            "status": workflow["status"],
            "created_at": latest["created_at"],
            "duration": workflow.get("duration"),
            "pipeline_id": pipeline_id,
            "workflow_id": workflow["id"],
            "url": f"https://app.circleci.com/pipelines/{project_slug}/{pipeline_id}"
        }

    def get_workflow_status(self, pipeline_id: str) -> Dict:
        """Get workflow status for a pipeline."""
        url = f"{self.BASE_URL}/pipeline/{pipeline_id}/workflow"

        response = requests.get(url, headers=self.headers)
        response.raise_for_status()

        data = response.json()
        workflows = data.get("items", [])

        if not workflows:
            return {"status": "no_workflow"}

        return workflows[0]  # Latest workflow

    def get_job_details(self, workflow_id: str) -> List[Dict]:
        """Get details of all jobs in a workflow."""
        url = f"{self.BASE_URL}/workflow/{workflow_id}/job"

        response = requests.get(url, headers=self.headers)
        response.raise_for_status()

        data = response.json()
        return data.get("items", [])

    def trigger_pipeline(self, project_slug: str, branch: str, params: Optional[Dict] = None) -> Dict:
        """Trigger a new pipeline for a branch."""
        url = f"{self.BASE_URL}/project/{project_slug}/pipeline"

        payload = {
            "branch": branch,
            "parameters": params or {}
        }

        response = requests.post(url, headers=self.headers, json=payload)
        response.raise_for_status()

        return response.json()

3. CLI Commands

We wrapped the API client in user-friendly CLI commands:

scripts/circleci-status:

#!/usr/bin/env python3
"""
Check CircleCI pipeline status for a branch.

Usage:
    circleci-status <branch>
    circleci-status --current  # Use current git branch
"""

import sys
import subprocess
from circleci_client import CircleCIClient

def get_current_branch():
    """Get current git branch name."""
    result = subprocess.run(
        ["git", "rev-parse", "--abbrev-ref", "HEAD"],
        capture_output=True,
        text=True
    )
    return result.stdout.strip()

def format_duration(seconds):
    """Format duration in seconds to human-readable string."""
    if seconds is None:
        return "N/A"

    minutes = seconds // 60
    secs = seconds % 60
    return f"{minutes}m {secs}s"

def main():
    # Parse arguments
    if len(sys.argv) < 2 or sys.argv[1] == "--current":
        branch = get_current_branch()
    else:
        branch = sys.argv[1]

    # Initialize client
    client = CircleCIClient()
    project_slug = "gh/alphazed/alphazed-alqosh"

    # Fetch pipeline status
    print(f"Checking pipeline for branch: {branch}")
    print()

    status_info = client.get_pipeline_status(project_slug, branch)

    if status_info["status"] == "no_pipeline":
        print(f"❌ No pipeline found for branch: {branch}")
        sys.exit(1)

    status = status_info["status"]
    duration = format_duration(status_info.get("duration"))
    url = status_info["url"]

    # Get job details
    workflow_id = status_info.get("workflow_id")
    jobs = client.get_job_details(workflow_id) if workflow_id else []

    # Display results
    if status == "success":
        print("✓ Pipeline passed")
        print(f"✓ Duration: {duration}")

        # Show job summary
        print(f"✓ Jobs: {len(jobs)} passed")
        for job in jobs:
            job_name = job["name"]
            job_status = job["status"]
            emoji = "✓" if job_status == "success" else "✗"
            print(f"  {emoji} {job_name}")

    elif status == "failed":
        print("✗ Pipeline failed")
        print(f"✗ Duration: {duration}")

        # Show failed jobs
        failed_jobs = [j for j in jobs if j["status"] != "success"]
        print(f"✗ Failed jobs: {len(failed_jobs)}")
        for job in failed_jobs:
            print(f"  ✗ {job['name']}: {job['status']}")

    elif status == "running":
        print("⏳ Pipeline running...")
        completed = sum(1 for j in jobs if j["status"] in ["success", "failed"])
        print(f"⏳ Progress: {completed}/{len(jobs)} jobs completed")

    else:
        print(f"? Unknown status: {status}")

    print()
    print(f"URL: {url}")

    # Exit with appropriate code
    sys.exit(0 if status == "success" else 1)

if __name__ == "__main__":
    main()

Make executable:

chmod +x scripts/circleci-status
# Add to PATH or create alias
alias ci-status="python scripts/circleci-status"

Usage:

# Check specific branch
ci-status feature/my-branch

# Check current branch
ci-status --current

Example output:

Checking pipeline for branch: feature/adaptive-learning

 Pipeline passed
 Duration: 2m 34s
 Jobs: 5 passed
   checkout
   install-dependencies
   lint
   test
   build

URL: https://app.circleci.com/pipelines/gh/alphazed/alphazed-alqosh/1234

4. Trigger Pipeline Command

scripts/circleci-trigger:

#!/usr/bin/env python3
"""
Trigger a CircleCI pipeline for a branch.

Usage:
    circleci-trigger <branch>
    circleci-trigger --current
"""

import sys
from circleci_client import CircleCIClient, get_current_branch

def main():
    if len(sys.argv) < 2 or sys.argv[1] == "--current":
        branch = get_current_branch()
    else:
        branch = sys.argv[1]

    client = CircleCIClient()
    project_slug = "gh/alphazed/alphazed-alqosh"

    print(f"Triggering pipeline for branch: {branch}")

    result = client.trigger_pipeline(project_slug, branch)

    pipeline_id = result["id"]
    pipeline_number = result["number"]

    print(f"✓ Pipeline triggered")
    print(f"  ID: {pipeline_id}")
    print(f"  Number: {pipeline_number}")
    print(f"  URL: https://app.circleci.com/pipelines/{project_slug}/{pipeline_number}")

if __name__ == "__main__":
    main()

5. Integration with Git Workflow

We added git aliases for common workflows:

~/.gitconfig:

[alias]
    # Check CI status for current branch
    ci = !python /path/to/scripts/circleci-status --current

    # Trigger CI for current branch
    ci-trigger = !python /path/to/scripts/circleci-trigger --current

    # Check and wait for CI to complete
    ci-wait = !python /path/to/scripts/circleci-wait --current

Usage:

# Push code and check CI status
git push
git ci

# Trigger CI re-run
git ci-trigger

# Wait for CI to complete (polls every 30s)
git ci-wait

6. CI Wait Script (Polling)

scripts/circleci-wait:

#!/usr/bin/env python3
"""
Wait for CircleCI pipeline to complete.

Polls every 30 seconds until pipeline finishes.
"""

import sys
import time
from circleci_client import CircleCIClient, get_current_branch

def main():
    branch = sys.argv[1] if len(sys.argv) > 1 else get_current_branch()

    client = CircleCIClient()
    project_slug = "gh/alphazed/alphazed-alqosh"

    print(f"Waiting for pipeline on branch: {branch}")
    print("Polling every 30 seconds... (Ctrl+C to cancel)")
    print()

    while True:
        status_info = client.get_pipeline_status(project_slug, branch)
        status = status_info["status"]

        if status == "success":
            print("✓ Pipeline passed!")
            sys.exit(0)
        elif status == "failed":
            print("✗ Pipeline failed")
            sys.exit(1)
        elif status == "running":
            workflow_id = status_info.get("workflow_id")
            jobs = client.get_job_details(workflow_id)
            completed = sum(1 for j in jobs if j["status"] in ["success", "failed"])
            print(f"⏳ Running... {completed}/{len(jobs)} jobs completed", end="\r")
        else:
            print(f"? Status: {status}")

        time.sleep(30)

if __name__ == "__main__":
    main()

Real-World Impact

Before Implementation:

  • Manual CI checks via web UI
  • 2-3 minutes per check
  • 3-4 context switches per check
  • 30 hours/month across team

After Implementation:

  • Automated CLI checks
  • 5 seconds per check
  • 0 context switches
  • <1 hour/month across team

Time Saved:

Per check: 2.5 min → 5 sec (96% reduction)
Per developer per day: 17.5 min → 1 min (94% reduction)
Per team per month: 30 hours → 1 hour (97% reduction)

Developer Feedback:

"I used to dread checking CI status because it meant leaving my flow. Now I just type git ci and instantly know if my build passed."

"Being able to trigger reruns from the terminal is a game-changer. No more hunting through the UI to find the 'Rerun' button."

"The wait command is perfect for those times when I want to merge as soon as CI passes. I start the wait, grab coffee, and come back to a merge-ready PR."

Advanced Use Cases

Use Case 1: Pre-Merge Checks

We integrated CI status checks into our merge script:

scripts/merge-pr:

#!/bin/bash
# Safely merge PR after verifying CI

BRANCH=$1

# Check CI status
echo "Verifying CI status..."
python scripts/circleci-status "$BRANCH"

if [ $? -ne 0 ]; then
    echo "❌ CI not passing. Merge aborted."
    exit 1
fi

# Merge
echo "✓ CI passed. Merging..."
git checkout main
git merge "$BRANCH"
git push

# Delete branch
git push origin --delete "$BRANCH"
git branch -d "$BRANCH"

echo "✓ Merged and cleaned up $BRANCH"

Use Case 2: Slack Notifications

We added Slack notifications when CI finishes:

scripts/circleci-notify:

#!/usr/bin/env python3
"""
Wait for CI and post Slack notification when complete.
"""

import sys
import requests
from circleci_wait import wait_for_pipeline

def post_slack(message):
    """Post message to Slack."""
    webhook_url = os.getenv("SLACK_WEBHOOK_URL")
    requests.post(webhook_url, json={"text": message})

def main():
    branch = sys.argv[1]

    status = wait_for_pipeline(branch)

    if status == "success":
        post_slack(f"✓ CI passed for {branch}")
    else:
        post_slack(f"✗ CI failed for {branch}")

if __name__ == "__main__":
    main()

Use Case 3: Dashboard

We built a simple dashboard showing CI status for all active branches:

scripts/circleci-dashboard:

#!/usr/bin/env python3
"""
Display CI status for all active branches.
"""

import subprocess
from circleci_client import CircleCIClient

def get_active_branches():
    """Get list of active branches."""
    result = subprocess.run(
        ["git", "branch", "-r"],
        capture_output=True,
        text=True
    )

    branches = []
    for line in result.stdout.splitlines():
        branch = line.strip().replace("origin/", "")
        if branch not in ["HEAD", "main", "develop"]:
            branches.append(branch)

    return branches

def main():
    client = CircleCIClient()
    project_slug = "gh/alphazed/alphazed-alqosh"
    branches = get_active_branches()

    print("CI Status Dashboard")
    print("=" * 60)

    for branch in branches:
        status_info = client.get_pipeline_status(project_slug, branch)
        status = status_info.get("status", "unknown")

        emoji = {
            "success": "✓",
            "failed": "✗",
            "running": "⏳",
            "no_pipeline": "?"
        }.get(status, "?")

        print(f"{emoji} {branch:40s} {status}")

if __name__ == "__main__":
    main()

Example output:

CI Status Dashboard
============================================================
✓ feature/adaptive-learning                success
⏳ feature/tts-refactor                     running
✗ bugfix/auth-redirect                     failed
✓ feature/content-duo                      success
? feature/new-experiment                   no_pipeline

Documentation

We documented all CLI tools in CLAUDE.md:

CLAUDE.md:

## CircleCI CLI Tools

Check CI status without leaving the terminal.

### Commands

```bash
# Check CI status for current branch
git ci

# Check CI status for specific branch
circleci-status feature/my-branch

# Trigger CI pipeline
circleci-trigger feature/my-branch

# Wait for CI to complete
circleci-wait feature/my-branch

# View CI dashboard for all branches
circleci-dashboard

Setup

  1. Generate CircleCI personal access token:

    • Go to CircleCI → User Settings → Personal API Tokens
    • Create token
  2. Add token to environment:

    echo 'export CIRCLECI_TOKEN="your_token"' >> ~/.bashrc
    source ~/.bashrc
    
  3. Make scripts executable:

    chmod +x scripts/circleci-*
    

## Key Takeaways

1. **API access beats UI clicking** - Automate repetitive web workflows
2. **CLI tools reduce context switching** - Stay in the terminal, maintain flow
3. **Simple scripts have high leverage** - 200 lines of Python saved 30 hours/month
4. **Make tools easy to discover** - Git aliases and documentation drive adoption
5. **Start simple, extend as needed** - Basic status check → advanced dashboard

CircleCI API integration eliminated a category of developer friction. What was once a tedious, flow-breaking process (checking CI status) became instantaneous and frictionless. The investment—a few hours building API wrappers and CLI tools—paid for itself within the first week and continues delivering value on every PR review.

Developer tools don't have to be complex to be valuable. A simple CLI that answers "Did CI pass?" saves minutes per use and hours per week. Those hours compound into days per year—time that can be spent building features instead of clicking through web UIs.