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:
- Go to CircleCI → User Settings → Personal API Tokens
- Create new token with name "CLI Access"
- 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 ciand 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
-
Generate CircleCI personal access token:
- Go to CircleCI → User Settings → Personal API Tokens
- Create token
-
Add token to environment:
echo 'export CIRCLECI_TOKEN="your_token"' >> ~/.bashrc source ~/.bashrc -
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.