Skip to content

Instantly share code, notes, and snippets.

@mloureiro
Last active March 18, 2026 13:01
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, logs, and artifacts
  • Compact Output: --compact flag for colored one-line-per-item output (avoids piping to grep/python)
  • Filtering: Filter jobs by name pattern, steps by status, tests grouped by file
  • Parallel Run Support: Fetch logs for specific parallel run indices
  • 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 (JSON or compact)
cci workflows <pipeline-id>
cci workflows <pipeline-id> --compact

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

Job Operations

# List jobs (JSON, compact, or filtered)
cci jobs <workflow-id>
cci jobs <workflow-id> --compact
cci jobs <workflow-id> --filter playwright --compact

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

# Get failed tests
cci tests <job-number>                 # All failures as JSON
cci tests <job-number> --count         # Just the failure count
cci tests <job-number> --group-by file # Group by file with counts

# List steps (with optional status filter)
cci steps <job-number>
cci steps <job-number> --compact
cci steps <job-number> --status failed

# Get logs (with parallel run support)
cci logs <job-number> <step-index>
cci logs <job-number> <step-index> --parallel 3

# List artifacts
cci artifacts <job-number>

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. Quick status check
cci pipelines my-branch
cci workflows <pipeline-id> --compact

# 3. If something failed, investigate
cci jobs <workflow-id> --filter e2e --compact
cci tests <job-number> --group-by file
cci logs <job-number> 25 --parallel 3

Flags Reference

Flag Available On Description
--compact workflows, jobs, steps One colored line per item instead of JSON
--filter <pattern> jobs Filter by name (case-insensitive regex)
--status <status> steps, pipeline-jobs Filter by status
--count tests Show only the failure count
--group-by file tests Group failures by file with counts
--parallel <index> logs Fetch logs for a specific parallel run
--summary pipeline-jobs Show job counts by status and timing

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: Run 'cci help' for full usage and examples.
#
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}'
}
color_for_status() {
case "$1" in
success) echo "$GREEN" ;;
failed) echo "$RED" ;;
on_hold) echo "$YELLOW" ;;
running) echo "$BLUE" ;;
*) echo "$NC" ;;
esac
}
cmd_workflows() {
local pipeline_id=""
local compact=false
# Parse arguments
while [[ $# -gt 0 ]]; do
case "$1" in
--compact) compact=true; shift ;;
*) if [[ -z "$pipeline_id" ]]; then pipeline_id="$1"; fi; shift ;;
esac
done
if [[ -z "$pipeline_id" ]]; then
echo -e "${RED}Usage: cci workflows <pipeline-id> [--compact]${NC}" >&2
exit 1
fi
echo -e "${BLUE}Fetching workflows for pipeline: $pipeline_id${NC}" >&2
if [[ "$compact" == true ]]; then
api_call GET "/pipeline/$pipeline_id/workflow" | jq -r '.items[] | "\(.name)\t\(.status)"' | while IFS=$'\t' read -r name status; do
local color
color=$(color_for_status "$status")
printf "%-40s %b%s%b\n" "$name" "$color" "$status" "$NC"
done
else
api_call GET "/pipeline/$pipeline_id/workflow" | jq '.items[] | {id, name, status}'
fi
}
cmd_jobs() {
local workflow_id=""
local compact=false
local filter=""
# Parse arguments
while [[ $# -gt 0 ]]; do
case "$1" in
--compact) compact=true; shift ;;
--filter) filter="$2"; shift 2 ;;
*) if [[ -z "$workflow_id" ]]; then workflow_id="$1"; fi; shift ;;
esac
done
if [[ -z "$workflow_id" ]]; then
echo -e "${RED}Usage: cci jobs <workflow-id> [--compact] [--filter <pattern>]${NC}" >&2
exit 1
fi
echo -e "${BLUE}Fetching jobs for workflow: $workflow_id${NC}" >&2
local jq_filter='.items[]'
if [[ -n "$filter" ]]; then
jq_filter=".items[] | select(.name | test(\"$filter\"; \"i\"))"
echo -e "${BLUE}Filtering by: $filter${NC}" >&2
fi
if [[ "$compact" == true ]]; then
api_call GET "/workflow/$workflow_id/job" | jq -r "$jq_filter | \"\(.job_number // \"-\")\t\(.name)\t\(.status)\"" | while IFS=$'\t' read -r job_number name status; do
local color
color=$(color_for_status "$status")
printf "#%-10s %-40s %b%s%b\n" "$job_number" "$name" "$color" "$status" "$NC"
done
else
api_call GET "/workflow/$workflow_id/job" | jq "$jq_filter | {job_number, name, status, type, approval_request_id}"
fi
}
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=""
local group_by=""
local count_only=false
# Parse arguments
while [[ $# -gt 0 ]]; do
case "$1" in
--group-by) group_by="$2"; shift 2 ;;
--count) count_only=true; shift ;;
*) if [[ -z "$job_number" ]]; then job_number="$1"; fi; shift ;;
esac
done
if [[ -z "$job_number" ]]; then
echo -e "${RED}Usage: cci tests <job-number> [--count] [--group-by file]${NC}" >&2
exit 1
fi
echo -e "${BLUE}Fetching failed tests for job: $job_number${NC}" >&2
local result
result=$(api_call GET "/project/$PROJECT_SLUG/$job_number/tests")
if [[ "$count_only" == true ]]; then
echo "$result" | jq '[.items[] | select(.result == "failure")] | length'
return
fi
if [[ "$group_by" == "file" ]]; then
echo "$result" | jq -r '[.items[] | select(.result == "failure")] | group_by(.classname) | sort_by(-length) | .[] | "\(.| length)\t\(.[0].classname // .[0].file // "unknown")\t\([.[] | .name] | join(", "))"' | while IFS=$'\t' read -r count file tests; do
printf "%b%-4s%b %s\n" "$RED" "$count" "$NC" "$file"
printf " %s\n" "$tests"
done
return
fi
echo "$result" | 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=""
local compact=false
local filter_status=""
# Parse arguments
while [[ $# -gt 0 ]]; do
case "$1" in
--compact) compact=true; shift ;;
--status) filter_status="$2"; shift 2 ;;
*) if [[ -z "$job_number" ]]; then job_number="$1"; fi; shift ;;
esac
done
if [[ -z "$job_number" ]]; then
echo -e "${RED}Usage: cci steps <job-number> [--compact] [--status <status>]${NC}" >&2
exit 1
fi
echo -e "${BLUE}Fetching steps for job: $job_number${NC}" >&2
local status_filter=""
if [[ -n "$filter_status" ]]; then
status_filter="| select(.value.actions[0].status == \"$filter_status\")"
echo -e "${BLUE}Filtering by status: $filter_status${NC}" >&2
fi
if [[ "$compact" == true ]]; then
api_call_v1 GET "/project/$PROJECT_PATH_V1/$job_number" | jq -r ".steps | to_entries | .[] $status_filter | \"\(.key)\t\(.value.name)\t\(.value.actions[0].status)\t\((.value.actions[0].run_time_millis // 0) / 1000 | floor | \"\(. / 60 | floor)m \(. % 60)s\")\"" | while IFS=$'\t' read -r index name status duration; do
local color
color=$(color_for_status "$status")
printf "[%-3s] %-50s %b%-16s%b %s\n" "$index" "$name" "$color" "$status" "$NC" "$duration"
done
else
api_call_v1 GET "/project/$PROJECT_PATH_V1/$job_number" | jq ".steps | to_entries | .[] $status_filter | {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\")}"
fi
}
cmd_logs() {
local job_number=""
local step_index=""
local parallel_index=""
# Parse arguments
while [[ $# -gt 0 ]]; do
case "$1" in
--parallel) parallel_index="$2"; shift 2 ;;
*)
if [[ -z "$job_number" ]]; then
job_number="$1"
elif [[ -z "$step_index" ]]; then
step_index="$1"
fi
shift
;;
esac
done
if [[ -z "$job_number" || -z "$step_index" ]]; then
echo -e "${RED}Usage: cci logs <job-number> <step-index> [--parallel <index>]${NC}" >&2
exit 1
fi
echo -e "${BLUE}Fetching logs for job $job_number, step $step_index${NC}" >&2
local job_data
job_data=$(api_call_v1 GET "/project/$PROJECT_PATH_V1/$job_number")
if [[ -n "$parallel_index" ]]; then
# For parallel runs, fetch the specific action's output URL
local action_count
action_count=$(echo "$job_data" | jq ".steps[$step_index].actions | length")
if [[ "$parallel_index" -ge "$action_count" ]]; then
echo -e "${RED}Parallel index $parallel_index out of range (0-$((action_count - 1)))${NC}" >&2
exit 1
fi
local output_url
output_url=$(echo "$job_data" | jq -r ".steps[$step_index].actions[$parallel_index].output_url")
if [[ -z "$output_url" || "$output_url" == "null" ]]; then
echo -e "${RED}No output URL found for step $step_index, parallel $parallel_index${NC}" >&2
exit 1
fi
echo -e "${BLUE}Fetching parallel run $parallel_index${NC}" >&2
curl -s "$output_url" | jq -r '.[].message'
else
local output_url
output_url=$(echo "$job_data" | 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'
fi
}
cmd_artifacts() {
local job_number="$1"
if [[ -z "$job_number" ]]; then
echo -e "${RED}Usage: cci artifacts <job-number>${NC}" >&2
exit 1
fi
echo -e "${BLUE}Fetching artifacts for job: $job_number${NC}" >&2
api_call GET "/project/$PROJECT_SLUG/$job_number/artifacts" | jq '.items[] | {path, url, node_index}'
}
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> [--compact]
List workflows for a pipeline
cci jobs <workflow-id> [--compact] [--filter <pattern>]
List jobs for a workflow
--filter: filter jobs by name (case-insensitive regex)
cci job <job-number> Get job details (status, parallel runs, web URL)
cci tests <job-number> [--count] [--group-by file]
Get failed tests for a job
--count: show only the number of failures
--group-by file: group failures by file with counts
cci steps <job-number> [--compact] [--status <status>]
List all steps for a job with status and duration
--status: filter by step status (success, failed, canceled)
cci logs <job-number> <step-index> [--parallel <index>]
Get logs for a specific step
--parallel: fetch logs for a specific parallel run index
cci artifacts <job-number> List artifacts for a job
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)
Note: --compact outputs one colored line per item instead of JSON.
Examples:
cci pipelines sc-12345/my-feature
cci workflows 3186a68f-... --compact # Compact colored output
cci jobs cd3ca155-... --compact # Compact colored output
cci jobs cd3ca155-... --filter playwright # Only Playwright jobs
cci jobs cd3ca155-... --filter e2e --compact # Combine filter + compact
cci tests 1110581 # Show all failed tests
cci tests 1110581 --count # Just the failure count
cci tests 1110581 --group-by file # Group failures by file
cci steps 1110581 --compact # Compact step listing
cci steps 1110581 --status failed # Only failed steps
cci logs 1110581 5 # Logs for step index 5
cci logs 1110581 25 --parallel 3 # Logs for parallel run 3
cci artifacts 1110581 # List job artifacts
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 auto-approve # Uses current git branch
EOF
}
# Main command dispatcher
case "${1:-}" in
pipelines)
cmd_pipelines "$2"
;;
workflows)
shift; cmd_workflows "$@"
;;
jobs)
shift; cmd_jobs "$@"
;;
job)
cmd_job "$2"
;;
tests)
shift; cmd_tests "$@"
;;
steps)
shift; cmd_steps "$@"
;;
logs)
shift; cmd_logs "$@"
;;
artifacts)
cmd_artifacts "$2"
;;
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