Skip to content

Commit 523068d

Browse files
committed
Create approve_circleci_job.py
1 parent da51ee6 commit 523068d

File tree

1 file changed

+160
-0
lines changed

1 file changed

+160
-0
lines changed

approve_circleci_job.py

+160
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
#!/usr/bin/env python3
2+
import json
3+
import os
4+
import urllib.request
5+
import time
6+
import argparse
7+
8+
9+
def request_url(url, token, method="GET"):
10+
request = urllib.request.Request(url, method=method)
11+
request.add_header("Circle-Token", token)
12+
request.add_header("Content-Type", "application/json")
13+
try:
14+
with urllib.request.urlopen(request) as response:
15+
return json.loads(response.read())
16+
except urllib.error.HTTPError as e:
17+
body = e.read().decode()
18+
print(e, body)
19+
raise e
20+
21+
22+
def fetch_all_items(fetch_func, token, *args):
23+
items = []
24+
next_page_token = ""
25+
26+
while True:
27+
results = fetch_func(token, *args, next_page_token)
28+
items.extend(results["items"])
29+
30+
if not results.get("next_page_token"):
31+
break
32+
next_page_token = results["next_page_token"]
33+
34+
return items
35+
36+
37+
def get_jobs(token, workflow_id, next_page_token=""):
38+
url = f"https://circleci.com/api/v2/workflow/{workflow_id}/job?page_token={next_page_token}"
39+
return request_url(url, token)
40+
41+
42+
def get_workflow(token, workflow_id, next_page_token=""):
43+
url = f"https://circleci.com/api/v2/workflow/{workflow_id}/?page_token={next_page_token}"
44+
return request_url(url, token)
45+
46+
47+
def get_pipeline_workflows(token, pipeline_id, next_page_token=""):
48+
url = f"https://circleci.com/api/v2/pipeline/{pipeline_id}/workflow?page_token={next_page_token}"
49+
return request_url(url, token)
50+
51+
52+
def approve_job(token, workflow_id, approval_request_id):
53+
url = f"https://circleci.com/api/v2/workflow/{workflow_id}/approve/{approval_request_id}"
54+
return request_url(url, token, "POST")
55+
56+
57+
def main(token, repo_owner, repo_name, workflow_id, workflow_name, job):
58+
print("Fetching workflow info...")
59+
current_workflow = get_workflow(token, workflow_id)
60+
print(
61+
f"Current workflow name {current_workflow['name']}, workflow_id: {workflow_id}. Requested workflow name: {workflow_name}"
62+
)
63+
print(
64+
f"Running in pipeline_id: {current_workflow['pipeline_id']}, pipeline number: {current_workflow['pipeline_number']}"
65+
)
66+
print("Fetching workflows...")
67+
latest_job = None
68+
latest_workflow = None
69+
for workflow in fetch_all_items(
70+
get_pipeline_workflows, token, current_workflow["pipeline_id"]
71+
):
72+
target_workflow_name = (
73+
workflow_name if workflow_name else current_workflow["name"]
74+
)
75+
76+
if workflow["name"] != target_workflow_name:
77+
continue
78+
for job_item in fetch_all_items(get_jobs, token, workflow["id"]):
79+
if job_item["name"] != job:
80+
continue
81+
print(
82+
f"Found job in workflow {workflow['id']}, started at {job_item['started_at']}"
83+
)
84+
if latest_job is None or job_item["started_at"] > latest_job["started_at"]:
85+
latest_job = job_item
86+
latest_workflow = workflow
87+
88+
if latest_job:
89+
print(f"Latest job: {latest_job}")
90+
approval_request_id = latest_job.get("approval_request_id")
91+
if not approval_request_id:
92+
print("Job is not waiting for an approval")
93+
exit(1)
94+
else:
95+
max_retries = 12
96+
delay = 5
97+
print(
98+
f"Approving job {latest_job['name']} in a workflow: {latest_workflow['name']} ({latest_workflow['id']})"
99+
)
100+
for attempt in range(max_retries):
101+
try:
102+
print(
103+
f"Attempt {attempt + 1} of {max_retries} to approve job in workflow: {latest_workflow['id']}"
104+
)
105+
approve_result = approve_job(
106+
token, latest_workflow["id"], approval_request_id
107+
)
108+
print("Approval successful:", approve_result)
109+
break
110+
except Exception as e:
111+
print(f"Attempt {attempt + 1} failed with error: {e}")
112+
if attempt + 1 == max_retries:
113+
print("All retry attempts failed. Exiting.")
114+
raise
115+
print(f"Retrying in {delay} seconds...")
116+
time.sleep(delay)
117+
return
118+
print(f"No jobs found with name {job}")
119+
exit(1)
120+
121+
122+
if __name__ == "__main__":
123+
parser = argparse.ArgumentParser(description="CircleCI Job Approver")
124+
parser.add_argument(
125+
"--token",
126+
help="CircleCI API token",
127+
required=False,
128+
default=os.environ["CIRCLECI_API_TOKEN"],
129+
)
130+
parser.add_argument(
131+
"--repo_owner",
132+
required=False,
133+
help="Repository owner",
134+
default=os.getenv("CIRCLE_PROJECT_USERNAME"),
135+
)
136+
parser.add_argument(
137+
"--repo_name",
138+
required=False,
139+
help="Repository name",
140+
default=os.getenv("CIRCLE_PROJECT_REPONAME"),
141+
)
142+
parser.add_argument(
143+
"--workflow_id",
144+
required=False,
145+
help="Workflow ID",
146+
default=os.getenv("CIRCLE_WORKFLOW_ID"),
147+
)
148+
parser.add_argument("--workflow_name", required=False, help="Workflow name")
149+
parser.add_argument("--job", required=True, help="Job name")
150+
151+
args = parser.parse_args()
152+
153+
main(
154+
args.token,
155+
args.repo_owner,
156+
args.repo_name,
157+
args.workflow_id,
158+
args.workflow_name,
159+
args.job,
160+
)

0 commit comments

Comments
 (0)