Skip to content

Commit 9094d32

Browse files
committed
feat: add a CircleCI logs script
1 parent 46d70fd commit 9094d32

4 files changed

Lines changed: 204 additions & 0 deletions

File tree

scripts/.local/bin/circle

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
#!/usr/bin/env bash
2+
3+
if [[ -f .env ]]; then
4+
# Mark variables which are modified or created for export
5+
set -a
6+
source .env
7+
set +a
8+
fi
9+
10+
if ! git is-repo; then
11+
echo "You must run this in a git repo" >&2
12+
exit 1
13+
fi
14+
15+
if [[ -z "$CIRCLECI_PROJECT_SLUG" ]]; then
16+
echo "CIRCLECI_PROJECT_SLUG not set" >&2
17+
exit 1
18+
fi
19+
20+
export circle_token=$(keyring get circleci Circle-Token)
21+
if [[ -z "$circle_token" ]]; then
22+
echo "Circle-Token not found in keyring" >&2
23+
echo "Store it by running: 'keyring set circleci Circle-Token'" >&2
24+
exit 1
25+
fi
26+
27+
function main() {
28+
if [[ "$1" == "logs" ]]; then
29+
job_name="$2"
30+
step_name="$3"
31+
fi
32+
33+
revision=$(git rev-parse --verify HEAD)
34+
35+
pipeline=$( \
36+
curl --fail --silent --show-error --location \
37+
--header "Circle-Token: ${circle_token}" \
38+
--url "https://circleci.com/api/v2/project/${CIRCLECI_PROJECT_SLUG}/pipeline?branch=$(git branch --show-current)" \
39+
| jq --arg revision "$revision" '.items | map(select(.vcs.revision == $revision)) | .[0]' \
40+
)
41+
42+
pipeline_id=$(echo "$pipeline" | jq -r '.id')
43+
pipeline_number=$(echo "$pipeline" | jq -r '.number')
44+
45+
workflow_name="${CIRCLECI_WORKFLOW_NAME:-main}"
46+
workflow=$( \
47+
curl --fail --silent --show-error --location \
48+
--header "Circle-Token: ${circle_token}" \
49+
--url "https://circleci.com/api/v2/pipeline/${pipeline_id}/workflow" \
50+
| jq --arg workflow_name "$workflow_name" '.items | map(select(.name == $workflow_name)) | .[0]' \
51+
)
52+
53+
if [[ "$workflow" == "null" ]]; then
54+
echo "Workflow '${workflow_name}' not found for this pipeline" >&2
55+
exit 1
56+
fi
57+
58+
workflow_id=$(echo "$workflow" | jq -r '.id')
59+
circleci_ui="https://app.circleci.com/pipelines/${CIRCLECI_PROJECT_SLUG}/${pipeline_number}/workflows/${workflow_id}"
60+
61+
selected_job=$(get_a_job "$workflow_id" "$job_name")
62+
63+
if [[ -z "$selected_job" ]]; then
64+
echo "$circleci_ui"
65+
exit 0
66+
fi
67+
68+
job_number=$(echo "$selected_job" | cut -d' ' -f1)
69+
job_name=$(echo "$selected_job" | cut -d' ' -f2-)
70+
circleci_ui="${circleci_ui}/jobs/${job_number}"
71+
steps_file=$(mktemp)
72+
73+
step_name=$(get_step "$job_number" "$steps_file" "$step_name")
74+
75+
if [[ -z "$step_name" ]]; then
76+
echo "$circleci_ui"
77+
exit 0
78+
fi
79+
80+
if [[ "$1" == "logs" ]]; then
81+
circle-log-fetch.sh "$steps_file" "$step_name"
82+
else
83+
echo "You can go straight to these logs with:"
84+
echo "circle logs '$job_name' '$step_name'"
85+
circle-log-fetch.sh "$steps_file" "$step_name" | less -R
86+
fi
87+
}
88+
89+
function get_a_job() {
90+
workflow_id="$1"
91+
job_name="$2"
92+
93+
jobs=$( \
94+
curl --fail --silent --show-error --location \
95+
--header "Circle-Token: ${circle_token}" \
96+
--url "https://circleci.com/api/v2/workflow/${workflow_id}/job" \
97+
| jq '.items' \
98+
)
99+
100+
if [[ -z "$job_name" ]]; then
101+
job_name=$( \
102+
echo "$jobs" \
103+
| jq -r '.[].name' \
104+
| gum choose --header "What job do you want to see?" \
105+
)
106+
fi
107+
108+
if [[ -n "$job_name" ]]; then
109+
job_number=$( \
110+
echo "$jobs" \
111+
| jq -r --arg job_name "$job_name" 'map(select(.name == $job_name)) | .[0].job_number' \
112+
)
113+
echo "$job_number $job_name"
114+
fi
115+
}
116+
117+
function get_step() {
118+
job_number="$1"
119+
steps_file="$2"
120+
step_name="$3"
121+
122+
curl --fail --silent --show-error --location \
123+
--header "Circle-Token: ${circle_token}" \
124+
--url "https://circleci.com/api/v1.1/project/${CIRCLECI_PROJECT_SLUG}/${job_number}" \
125+
| jq '.steps' \
126+
> $steps_file
127+
128+
if [[ -z "$step_name" ]]; then
129+
cat $steps_file \
130+
| jq -r '.[].name' \
131+
| fzf --ansi \
132+
--delimiter='\x01' \
133+
--preview="circle-log-preview.sh $steps_file {}"
134+
else
135+
echo "$step_name"
136+
fi
137+
}
138+
139+
main "$@"
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#!/usr/bin/env bash
2+
3+
function main() {
4+
steps_file="$1"
5+
step_name="$2"
6+
7+
cache=""
8+
if command -v bkt &> /dev/null; then
9+
cache="bkt --ttl=30m --stale=1m --"
10+
fi
11+
12+
cat $steps_file \
13+
| filter-json name "$step_name" \
14+
| jq -r '.[].actions[].output_url' \
15+
| while read -r output_url; do
16+
$cache bash -c "$GET_CIRCLECI_LOGS; get_circleci_logs '$output_url'"
17+
done
18+
}
19+
20+
function get_circleci_logs() {
21+
output_url="$1"
22+
23+
curl --fail --silent --show-error --location \
24+
--header "Circle-Token: ${circle_token}" \
25+
--url "$output_url" \
26+
| jq -r ".[].message"
27+
}
28+
# export -f get_circleci_logs
29+
export GET_CIRCLECI_LOGS="$(declare -f get_circleci_logs)"
30+
31+
main "$@"
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#!/usr/bin/env bash
2+
3+
steps_file="$1"
4+
step_name="$2"
5+
6+
circle-log-fetch.sh "$steps_file" "$step_name"
7+
8+
echo "\n--------------------------------\n"
9+
10+
cat "$steps_file" \
11+
| filter-json name "$step_name" \
12+
| jq -r '.[0].actions[0].bash_command' \
13+
| bat --color=always --language=sh # could be better

scripts/.local/bin/filter-json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#!/usr/bin/env -S uv --quiet run --script
2+
# /// script
3+
# requires-python = ">=3.12"
4+
# ///
5+
6+
import json
7+
import sys
8+
9+
if len(sys.argv) < 3:
10+
print("Usage: filter-json <filter_key> <filter_text>", file=sys.stderr)
11+
sys.exit(2)
12+
13+
filter_key = sys.argv[1]
14+
filter_text = sys.argv[2]
15+
16+
data = json.load(sys.stdin)
17+
if not isinstance(data, list):
18+
raise RuntimeError('JSON must be a list')
19+
20+
filtered = [item for item in data if filter_text == str(item.get(filter_key))]
21+
json.dump(filtered, sys.stdout)

0 commit comments

Comments
 (0)