Skip to content

Commit

Permalink
Implement helper for processing specific failed CI jobs
Browse files Browse the repository at this point in the history
Signed-off-by: Chris Evich <[email protected]>
  • Loading branch information
cevich committed Oct 29, 2020
1 parent c557224 commit 5d87290
Show file tree
Hide file tree
Showing 4 changed files with 174 additions and 0 deletions.
25 changes: 25 additions & 0 deletions cirrus-ci_asr/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Description

This is a quickly hacked-together script which examines a completed Cirrus-CI
build for specific tasks. It will print out two lines of information to stdout:

1. "FAILED" followed by a space separated list of task ID's which failed for any reason
2. "CIASR" followed by a space separated list of task ID's which failed specifically
due to an "CI agent stopped responding!" condition.

# Installation

As a user, install the python3 module requirements using pip3:
(Note: These typically go into `$HOME/.local/lib/python<version>`)

```
$ pip3 install --user --requirement ./requirements.txt
```

# Usage

Simply execute the script, providing as arguments:

1. The *user* component of a github repository
2. The *name* component of a github repository
3. The *commit SHA* for the target Cirrus-CI build
125 changes: 125 additions & 0 deletions cirrus-ci_asr/cirrus-ci_asr.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
#!/usr/bin/env python3

"""Print list of failed and agent-stopped-responding task IDs"""

import sys
from collections import namedtuple
from pprint import pprint

# Ref: https://gql.readthedocs.io/en/latest/index.html
# pip3 install --user --requirement ./requirements.txt
# (and/or in a python virtual environment)
from gql import gql, Client
from gql.transport.requests import RequestsHTTPTransport


def get_raw_builds(client, owner, repo, sha):
"""Retrieve list of builds for the specified owner/repo @ commit SHA"""
# Generated using https://cirrus-ci.com/explorer
query = gql('''
query buildBySha($owner: String!, $repo: String!, $sha: String!) {
searchBuilds(repositoryOwner: $owner, repositoryName: $repo, SHA: $sha) {
id
buildCreatedTimestamp
}
}
''')
query_vars = dict(owner=owner, repo=repo, sha=sha)
result = client.execute(query, variable_values=query_vars)
if "searchBuilds" in result and len(result["searchBuilds"]):
return result["searchBuilds"]
else:
raise RuntimeError(f"No Cirrus-CI builds found for {owner}/{repo} commit {sha}")


def latest_build_id(raw_builds):
"""Return the build_id of the most recent build among raw_builds"""
latest_ts = 0
latest_bid = 0
for build in raw_builds:
bts = build["buildCreatedTimestamp"]
if bts > latest_ts:
latest_ts = bts
latest_bid = build["id"]
if latest_bid:
return latest_bid
raise RuntimeError(f"Empty raw_builds list")


def get_raw_tasks(client, build_id):
"""Retrieve raw GraphQL task list from a build"""
query = gql('''
query tasksByBuildID($build_id: ID!) {
build(id: $build_id) {
tasks {
name
id
status
notifications {
level
message
}
automaticReRun
previousRuns {
id
}
}
}
}
''')
query_vars = dict(build_id=build_id)
result = client.execute(query, variable_values=query_vars)
if "build" in result and result["build"]:
result = result["build"]
if "tasks" in result and len(result["tasks"]):
return result["tasks"]
else:
raise RuntimeError(f"No tasks found for build with id {build_id}")
else:
raise RuntimeError(f"No Cirrus-CI build found with id {build_id}")


def failed_tids(raw_tasks):
"""Return set of unsuccessful task IDs"""
return set([task["id"]
for task in raw_tasks
if task["status"] != "COMPLETED"])


def error_tids(raw_tasks, reason):
"""Return set of tasks which errored with specified reason"""
result=[]
for task in raw_tasks:
if task["status"] != "FAILED":
continue
for notif in task["notifications"]:
if notif["level"] == "ERROR" and reason in notif["message"]:
result.append(task["id"])
return set(result)


def output_tids(keyword, tids):
"""Write line of space separated task IDs prefixed by a keyword"""
sys.stdout.write(keyword)
for tid in tids:
sys.stdout.write(f" {tid}")
sys.stdout.write("\n")


if __name__ == "__main__":
# Ref: https://cirrus-ci.org/api/
cirrus_graphql_url = "https://api.cirrus-ci.com/graphql"
cirrus_graphql_xport = RequestsHTTPTransport(
url=cirrus_graphql_url,
verify=True,
retries=3)
client = Client(transport=cirrus_graphql_xport, fetch_schema_from_transport=True)

try:
raw_builds = get_raw_builds(client, sys.argv[1], sys.argv[2], sys.argv[3])
except IndexError as xcpt:
print(f"Error: argument {xcpt}\n\nUsage: {sys.argv[0]} <user> <repo> <sha>")
sys.exit(1)
raw_tasks = get_raw_tasks(client, latest_build_id(raw_builds))
output_tids("FAILED", failed_tids(raw_tasks))
output_tids("CIASR", error_tids(raw_tasks, "CI agent stopped responding!"))
9 changes: 9 additions & 0 deletions cirrus-ci_asr/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
aiohttp==3.6.2
async-timeout==3.0.1
attrs==20.2.0
certifi==2020.6.20
gql==3.0.0a3
multidict==4.7.6
requests==2.24.0
websockets==8.1
yarl==1.5.1
15 changes: 15 additions & 0 deletions cirrus-ci_asr/test/run_all_tests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/bin/bash

# Stupid-simple, very basic "can it run" test

set -e

if [[ -n "$CIRRUS_CI" ]] && [[ "$CIRRUS_CI" == "true" ]]; then
echo -e "\nSkipping: Test must be executed under Cirrus-CI\n"
exit 0
fi

cd "$(dirname ${BASH_SOURCE[0]})/../"
pip3 install --user --requirement ./requirements.txt
echo "Testing cirrus-ci_asr.py $CIRRUS_REPO_OWNER $CIRRUS_REPO_NAME $CIRRUS_CHANGE_IN_REPO"
./cirrus-ci_asr.py $CIRRUS_REPO_OWNER $CIRRUS_REPO_NAME $CIRRUS_CHANGE_IN_REPO

0 comments on commit 5d87290

Please sign in to comment.