Skip to content

Instantly share code, notes, and snippets.

@mloureiro
Last active January 27, 2026 15:34
Show Gist options
  • Select an option

  • Save mloureiro/24c980e1da8abbc639462e4dce7c593f to your computer and use it in GitHub Desktop.

Select an option

Save mloureiro/24c980e1da8abbc639462e4dce7c593f to your computer and use it in GitHub Desktop.
cci - CircleCI CLI wrapper for pipelines, jobs, steps, logs, and auto-approvals

cci - CircleCI CLI Wrapper

A convenient command-line wrapper for common CircleCI API operations. Simplifies pipeline management, job inspection, and approval workflows.

Features

  • Pipeline Management: List pipelines, workflows, and jobs
  • Job Inspection: View job details, steps, and logs
  • Quick Approvals: Auto-approve pending jobs with a single command
  • Summary View: Get a quick overview of pipeline status with job counts

Installation

  1. Install dependencies:

  2. Set up CircleCI authentication:

    circleci setup

    This creates ~/.circleci/cli.yml with your API token.

  3. Download and install the script:

    curl -o ~/.local/bin/cci https://gist.githubusercontent.com/mloureiro/24c980e1da8abbc639462e4dce7c593f/raw/cci
    chmod +x ~/.local/bin/cci
  4. Configure your project (add to ~/.bashrc or ~/.zshrc):

    export CIRCLECI_PROJECT_SLUG="gh/your-org/your-repo"

Environment Variables

Variable Description Default
CIRCLECI_PROJECT_SLUG Project slug (e.g., gh/myorg/myrepo) gh/owner/repo
CIRCLECI_TOKEN_FILE Path to CircleCI config with token ~/.circleci/cli.yml

Usage

Pipeline Operations

# List recent pipelines for a branch
cci pipelines my-feature-branch

# List workflows for a pipeline
cci workflows <pipeline-id>

# List all jobs across all workflows in a pipeline
cci pipeline-jobs <pipeline-id>

# Filter jobs by status
cci pipeline-jobs <pipeline-id> --status failed

# Get a summary of pipeline status
cci pipeline-jobs <pipeline-id> --summary

Job Operations

# List jobs for a workflow
cci jobs <workflow-id>

# Get job details (status, duration, URL)
cci job <job-number>

# Get failed tests for a job
cci tests <job-number>

# List all steps for a job
cci steps <job-number>

# Get logs for a specific step
cci logs <job-number> <step-index>

Approval Workflow

# Auto-approve pending job for current git branch
cci auto-approve

# Auto-approve for a specific branch
cci auto-approve my-feature-branch

# Manual approval (if you need more control)
cci approve <workflow-id> <approval-request-id>

Examples

Typical CI Workflow

# 1. Push your changes, then approve the pipeline
git push && cci auto-approve

# 2. Check pipeline status
cci pipelines my-branch
cci pipeline-jobs <pipeline-id> --summary

# 3. If something failed, investigate
cci pipeline-jobs <pipeline-id> --status failed
cci steps <job-number>
cci logs <job-number> 5

Quick Status Check

# Get the latest pipeline and show summary
pipeline_id=$(cci pipelines my-branch | jq -r 'select(.number) | .id' | head -1)
cci pipeline-jobs $pipeline_id --summary

Valid Job Statuses

For --status filtering: success, failed, running, blocked, canceled, on_hold, not_run, infrastructure_fail, timedout, queued

License

MIT

#!/bin/bash
#
# cci - CircleCI CLI wrapper for common API operations
#
# Environment variables:
# CIRCLECI_PROJECT_SLUG Project slug (e.g., gh/myorg/myrepo)
# CIRCLECI_TOKEN_FILE Path to CircleCI config file (default: ~/.circleci/cli.yml)
#
# Setup: Install CircleCI CLI and run 'circleci setup' to create the token file.
# See: https://circleci.com/docs/local-cli/
#
# Usage:
# cci pipelines <branch> List recent pipelines for a branch
# cci workflows <pipeline-id> List workflows for a pipeline
# cci jobs <workflow-id> List jobs for a workflow
# cci job <job-number> Get job details (status, parallel runs, web URL)
# cci tests <job-number> Get failed tests for a job
# cci steps <job-number> List all steps for a job with status and duration
# cci logs <job-number> <step-index> Get logs for a specific step
# cci pipeline-jobs <pipeline-id> [--status <status>] [--summary]
# cci approve <workflow-id> <job-id> Approve a pending approval job
# cci auto-approve [branch] Find and approve pending job for branch (default: current git branch)
# List all jobs across all workflows in a pipeline
#
set -e
# Configuration - can be overridden via environment variables
PROJECT_SLUG="${CIRCLECI_PROJECT_SLUG:-gh/owner/repo}"
TOKEN_FILE="${CIRCLECI_TOKEN_FILE:-$HOME/.circleci/cli.yml}"
# Convert gh/owner/repo to github/owner/repo for v1 API
PROJECT_PATH_V1="${PROJECT_SLUG/gh\//github/}"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
get_token() {
if [[ ! -f "$TOKEN_FILE" ]]; then
echo -e "${RED}Error: CircleCI config not found at $TOKEN_FILE${NC}" >&2
exit 1
fi
grep token "$TOKEN_FILE" | awk '{print $2}'
}
api_call() {
local method="${1:-GET}"
local endpoint="$2"
local token
token=$(get_token)
if [[ "$method" == "POST" ]]; then
curl -s -X POST -H "Circle-Token: $token" "https://circleci.com/api/v2$endpoint"
else
curl -s -H "Circle-Token: $token" "https://circleci.com/api/v2$endpoint"
fi
}
api_call_v1() {
local method="${1:-GET}"
local endpoint="$2"
local token
token=$(get_token)
curl -s -H "Circle-Token: $token" "https://circleci.com/api/v1.1$endpoint"
}
cmd_pipelines() {
local branch="$1"
if [[ -z "$branch" ]]; then
echo -e "${RED}Usage: cci pipelines <branch>${NC}" >&2
exit 1
fi
echo -e "${BLUE}Fetching pipelines for branch: $branch${NC}" >&2
api_call GET "/project/$PROJECT_SLUG/pipeline?branch=$branch" | jq '.items[:5] | .[] | {id, number, state, created_at: .created_at}'
}
cmd_workflows() {
local pipeline_id="$1"
if [[ -z "$pipeline_id" ]]; then
echo -e "${RED}Usage: cci workflows <pipeline-id>${NC}" >&2
exit 1
fi
echo -e "${BLUE}Fetching workflows for pipeline: $pipeline_id${NC}" >&2
api_call GET "/pipeline/$pipeline_id/workflow" | jq '.items[] | {id, name, status}'
}
cmd_jobs() {
local workflow_id="$1"
if [[ -z "$workflow_id" ]]; then
echo -e "${RED}Usage: cci jobs <workflow-id>${NC}" >&2
exit 1
fi
echo -e "${BLUE}Fetching jobs for workflow: $workflow_id${NC}" >&2
api_call GET "/workflow/$workflow_id/job" | jq '.items[] | {job_number, name, status, type, approval_request_id}'
}
cmd_job() {
local job_number="$1"
if [[ -z "$job_number" ]]; then
echo -e "${RED}Usage: cci job <job-number>${NC}" >&2
exit 1
fi
echo -e "${BLUE}Fetching job details: $job_number${NC}" >&2
local job_data
job_data=$(api_call GET "/project/$PROJECT_SLUG/job/$job_number")
# Show basic info
echo "$job_data" | jq '{name, status, duration: (.duration / 1000 | floor | "\(. / 60 | floor)m \(. % 60)s"), web_url}'
# Show failed parallel runs if any
local failed_runs
failed_runs=$(echo "$job_data" | jq '[.parallel_runs[] | select(.status == "failed") | .index]')
if [[ "$failed_runs" != "[]" ]]; then
echo -e "${RED}Failed parallel runs: $failed_runs${NC}" >&2
fi
}
cmd_tests() {
local job_number="$1"
if [[ -z "$job_number" ]]; then
echo -e "${RED}Usage: cci tests <job-number>${NC}" >&2
exit 1
fi
echo -e "${BLUE}Fetching failed tests for job: $job_number${NC}" >&2
api_call GET "/project/$PROJECT_SLUG/$job_number/tests" | jq '[.items[] | select(.result == "failure")] | if length == 0 then "No failed tests" else .[] | {file, name, message: (.message | split("\n")[0])} end'
}
cmd_steps() {
local job_number="$1"
if [[ -z "$job_number" ]]; then
echo -e "${RED}Usage: cci steps <job-number>${NC}" >&2
exit 1
fi
echo -e "${BLUE}Fetching steps for job: $job_number${NC}" >&2
api_call_v1 GET "/project/$PROJECT_PATH_V1/$job_number" | jq '.steps | to_entries | .[] | {index: .key, name: .value.name, status: .value.actions[0].status, duration: ((.value.actions[0].run_time_millis // 0) / 1000 | floor | "\(. / 60 | floor)m \(. % 60)s")}'
}
cmd_logs() {
local job_number="$1"
local step_index="$2"
if [[ -z "$job_number" || -z "$step_index" ]]; then
echo -e "${RED}Usage: cci logs <job-number> <step-index>${NC}" >&2
exit 1
fi
echo -e "${BLUE}Fetching logs for job $job_number, step $step_index${NC}" >&2
local output_url
output_url=$(api_call_v1 GET "/project/$PROJECT_PATH_V1/$job_number" | jq -r ".steps[$step_index].actions[0].output_url")
if [[ -z "$output_url" || "$output_url" == "null" ]]; then
echo -e "${RED}No output URL found for step $step_index${NC}" >&2
exit 1
fi
curl -s "$output_url" | jq -r '.[].message'
}
cmd_pipeline_jobs() {
local pipeline_id=""
local filter_status=""
local show_summary=false
local valid_statuses="success|failed|running|blocked|canceled|on_hold|not_run|infrastructure_fail|timedout|queued"
# Parse arguments
while [[ $# -gt 0 ]]; do
case "$1" in
--status)
if [[ -z "$2" ]]; then
echo -e "${RED}Error: --status requires a value${NC}" >&2
exit 1
fi
filter_status="$2"
shift 2
;;
--summary)
show_summary=true
shift
;;
*)
if [[ -z "$pipeline_id" ]]; then
pipeline_id="$1"
fi
shift
;;
esac
done
if [[ -z "$pipeline_id" ]]; then
echo -e "${RED}Usage: cci pipeline-jobs <pipeline-id> [--status <status>] [--summary]${NC}" >&2
echo -e "${YELLOW}Valid statuses: success, failed, running, blocked, canceled, on_hold, not_run, infrastructure_fail, timedout, queued${NC}" >&2
exit 1
fi
# Validate status if provided
if [[ -n "$filter_status" && ! "$filter_status" =~ ^($valid_statuses)$ ]]; then
echo -e "${RED}Error: Invalid status '$filter_status'${NC}" >&2
echo -e "${YELLOW}Valid statuses: success, failed, running, blocked, canceled, on_hold, not_run, infrastructure_fail, timedout, queued${NC}" >&2
exit 1
fi
# Summary mode
if [[ "$show_summary" == true ]]; then
echo -e "${BLUE}Fetching summary for pipeline: $pipeline_id${NC}" >&2
# Get pipeline info for timing
local pipeline_info
pipeline_info=$(api_call GET "/pipeline/$pipeline_id")
local pipeline_number created_at
pipeline_number=$(echo "$pipeline_info" | jq -r '.number')
created_at=$(echo "$pipeline_info" | jq -r '.created_at')
# Calculate duration
local created_ts now_ts duration_secs
created_ts=$(date -j -f "%Y-%m-%dT%H:%M:%S" "${created_at%%.*}" "+%s" 2>/dev/null || date -d "${created_at}" "+%s" 2>/dev/null)
now_ts=$(date "+%s")
duration_secs=$((now_ts - created_ts))
local duration_min=$((duration_secs / 60))
local duration_sec=$((duration_secs % 60))
# Collect all jobs
local all_jobs=""
local workflows
workflows=$(api_call GET "/pipeline/$pipeline_id/workflow" | jq -c '.items[] | {id, name, status}')
while read -r workflow; do
[[ -z "$workflow" ]] && continue
local workflow_id
workflow_id=$(echo "$workflow" | jq -r '.id')
local jobs
jobs=$(api_call GET "/workflow/$workflow_id/job" | jq -c '.items[]')
all_jobs+="$jobs"$'\n'
done <<< "$workflows"
# Count by status using jq for reliability
local total success failed running blocked canceled on_hold other
total=$(echo "$all_jobs" | jq -s 'length')
success=$(echo "$all_jobs" | jq -s '[.[] | select(.status == "success")] | length')
failed=$(echo "$all_jobs" | jq -s '[.[] | select(.status == "failed")] | length')
running=$(echo "$all_jobs" | jq -s '[.[] | select(.status == "running")] | length')
blocked=$(echo "$all_jobs" | jq -s '[.[] | select(.status == "blocked")] | length')
canceled=$(echo "$all_jobs" | jq -s '[.[] | select(.status == "canceled")] | length')
on_hold=$(echo "$all_jobs" | jq -s '[.[] | select(.status == "on_hold")] | length')
other=$((total - success - failed - running - blocked - canceled - on_hold))
# Display summary
echo ""
echo -e "Pipeline #${pipeline_number}"
echo -e "Started: ${created_at}"
echo -e "Duration: ${duration_min}m ${duration_sec}s"
echo ""
echo -e "Jobs: ${total} total"
[[ $success -gt 0 ]] && echo -e " ${GREEN}✓ success: ${success}${NC}"
[[ $failed -gt 0 ]] && echo -e " ${RED}✗ failed: ${failed}${NC}"
[[ $running -gt 0 ]] && echo -e " ${BLUE}● running: ${running}${NC}"
[[ $blocked -gt 0 ]] && echo -e " ${YELLOW}◌ blocked: ${blocked}${NC}"
[[ $canceled -gt 0 ]] && echo -e " ○ canceled: ${canceled}"
[[ $on_hold -gt 0 ]] && echo -e " ${YELLOW}⏸ on_hold: ${on_hold}${NC}"
[[ $other -gt 0 ]] && echo -e " ? other: ${other}"
echo ""
return
fi
echo -e "${BLUE}Fetching all jobs for pipeline: $pipeline_id${NC}" >&2
if [[ -n "$filter_status" ]]; then
echo -e "${BLUE}Filtering by status: $filter_status${NC}" >&2
fi
# Get all workflows for the pipeline
local workflows
workflows=$(api_call GET "/pipeline/$pipeline_id/workflow" | jq -c '.items[] | {id, name, status}')
# For each workflow, get jobs and add workflow info
echo "$workflows" | while read -r workflow; do
local workflow_id workflow_name workflow_status
workflow_id=$(echo "$workflow" | jq -r '.id')
workflow_name=$(echo "$workflow" | jq -r '.name')
workflow_status=$(echo "$workflow" | jq -r '.status')
# Get jobs for this workflow and add workflow info, optionally filter by status
if [[ -n "$filter_status" ]]; then
api_call GET "/workflow/$workflow_id/job" | jq --arg wid "$workflow_id" --arg wname "$workflow_name" --arg wstatus "$workflow_status" --arg fstatus "$filter_status" \
'.items[] | select(.status == $fstatus) | {workflow_id: $wid, workflow_name: $wname, workflow_status: $wstatus, job_number, name, status, type}'
else
api_call GET "/workflow/$workflow_id/job" | jq --arg wid "$workflow_id" --arg wname "$workflow_name" --arg wstatus "$workflow_status" \
'.items[] | {workflow_id: $wid, workflow_name: $wname, workflow_status: $wstatus, job_number, name, status, type}'
fi
done
}
cmd_approve() {
local workflow_id="$1"
local approval_request_id="$2"
if [[ -z "$workflow_id" || -z "$approval_request_id" ]]; then
echo -e "${RED}Usage: cci approve <workflow-id> <approval-request-id>${NC}" >&2
exit 1
fi
echo -e "${YELLOW}Approving job...${NC}" >&2
local result
result=$(api_call POST "/workflow/$workflow_id/approve/$approval_request_id")
if echo "$result" | jq -e '.message' > /dev/null 2>&1; then
echo -e "${GREEN}Approved successfully${NC}" >&2
echo "$result" | jq '.'
else
echo -e "${RED}Failed to approve${NC}" >&2
echo "$result" | jq '.'
exit 1
fi
}
cmd_auto_approve() {
local branch="$1"
# Default to current git branch if not provided
if [[ -z "$branch" ]]; then
branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
if [[ -z "$branch" ]]; then
echo -e "${RED}Error: No branch provided and not in a git repository${NC}" >&2
exit 1
fi
echo -e "${BLUE}Using current branch: $branch${NC}" >&2
fi
# Get latest pipeline
echo -e "${BLUE}Finding latest pipeline for: $branch${NC}" >&2
local pipeline_id
pipeline_id=$(api_call GET "/project/$PROJECT_SLUG/pipeline?branch=$branch" | jq -r '.items[0].id // empty')
if [[ -z "$pipeline_id" ]]; then
echo -e "${RED}No pipeline found for branch: $branch${NC}" >&2
exit 1
fi
echo -e "${GREEN}Found pipeline: $pipeline_id${NC}" >&2
# Get workflows
echo -e "${BLUE}Finding workflows...${NC}" >&2
local workflows
workflows=$(api_call GET "/pipeline/$pipeline_id/workflow")
# Find workflow with on_hold status (has pending approval)
local workflow_id
workflow_id=$(echo "$workflows" | jq -r '.items[] | select(.status == "on_hold") | .id' | head -1)
if [[ -z "$workflow_id" ]]; then
# Check if there's a running workflow
local running
running=$(echo "$workflows" | jq -r '.items[] | select(.status == "running") | .id' | head -1)
if [[ -n "$running" ]]; then
echo -e "${YELLOW}Workflow is already running (no approval needed)${NC}" >&2
echo "$workflows" | jq '.items[] | {id, name, status}'
exit 0
fi
echo -e "${YELLOW}No workflow with pending approval found${NC}" >&2
echo "$workflows" | jq '.items[] | {id, name, status}'
exit 0
fi
echo -e "${GREEN}Found workflow with pending approval: $workflow_id${NC}" >&2
# Get jobs and find approval job
echo -e "${BLUE}Finding approval job...${NC}" >&2
local jobs
jobs=$(api_call GET "/workflow/$workflow_id/job")
local approval_info
approval_info=$(echo "$jobs" | jq -r '.items[] | select(.type == "approval" and .status == "on_hold") | "\(.name)|\(.approval_request_id)"' | head -1)
if [[ -z "$approval_info" ]]; then
echo -e "${YELLOW}No pending approval job found${NC}" >&2
echo "$jobs" | jq '.items[] | {name, status, type}'
exit 0
fi
local job_name approval_request_id
job_name=$(echo "$approval_info" | cut -d'|' -f1)
approval_request_id=$(echo "$approval_info" | cut -d'|' -f2)
echo -e "${GREEN}Found approval job: $job_name ($approval_request_id)${NC}" >&2
# Approve
cmd_approve "$workflow_id" "$approval_request_id"
echo -e "${GREEN}Done! CI pipeline should now be running.${NC}" >&2
}
cmd_help() {
cat << 'EOF'
cci - CircleCI CLI wrapper for common API operations
Environment variables:
CIRCLECI_PROJECT_SLUG Project slug (default: gh/owner/repo)
Example: gh/myorg/myrepo
CIRCLECI_TOKEN_FILE Path to CircleCI config with token
(default: ~/.circleci/cli.yml)
Setup:
Install CircleCI CLI and run 'circleci setup' to create the token file.
See: https://circleci.com/docs/local-cli/
Usage:
cci pipelines <branch> List recent pipelines for a branch
cci workflows <pipeline-id> List workflows for a pipeline
cci jobs <workflow-id> List jobs for a workflow
cci job <job-number> Get job details (status, parallel runs, web URL)
cci tests <job-number> Get failed tests for a job
cci steps <job-number> List all steps for a job with status and duration
cci logs <job-number> <step-index> Get logs for a specific step
cci pipeline-jobs <pipeline-id> [--status <status>] [--summary]
List all jobs across all workflows in a pipeline
--status: filter by status (success, failed, running,
blocked, canceled, on_hold, not_run, infrastructure_fail,
timedout, queued)
--summary: show counts by status and timing info
cci approve <workflow-id> <job-id> Approve a pending approval job
cci auto-approve [branch] Find and approve pending job for branch
(defaults to current git branch)
Examples:
cci pipelines sc-12345/my-feature
cci workflows 3186a68f-7417-4dd2-81d6-95fec5ce2ba7
cci jobs cd3ca155-27e0-4ab9-af62-6c407bcfbe60
cci job 1110581
cci tests 1110581
cci steps 1110581 # List all steps for job
cci logs 1110581 5 # Get logs for step index 5
cci pipeline-jobs 3186a68f-... # List all jobs in pipeline
cci pipeline-jobs 3186a68f-... --status failed # Only failed jobs
cci pipeline-jobs 3186a68f-... --summary # Show summary with counts
cci approve cd3ca155-27e0-4ab9-af62-6c407bcfbe60 687c2d58-eaf9-480c-b812-c8b09c57c40b
cci auto-approve # Uses current git branch
cci auto-approve sc-12345/feature # Specify branch explicitly
EOF
}
# Main command dispatcher
case "${1:-}" in
pipelines)
cmd_pipelines "$2"
;;
workflows)
cmd_workflows "$2"
;;
jobs)
cmd_jobs "$2"
;;
job)
cmd_job "$2"
;;
tests)
cmd_tests "$2"
;;
steps)
cmd_steps "$2"
;;
logs)
cmd_logs "$2" "$3"
;;
pipeline-jobs)
shift
cmd_pipeline_jobs "$@"
;;
approve)
cmd_approve "$2" "$3"
;;
auto-approve)
cmd_auto_approve "$2"
;;
help|--help|-h)
cmd_help
;;
*)
cmd_help
exit 1
;;
esac
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment