Skip to content

Commit 26a3c19

Browse files
Make Awaiting Workflow Run A User Responsibility
Moves the responsibility of re-requesting a workflow run from the API to the user. This makes the requests much shorter-lived, and allows the client to control how they want to handle sleeping and retrying. This also has the benefit of removing the only real piece of async code, so now the view is completely sync once again. Signed-off-by: Hassan Abouelela <[email protected]>
1 parent 0404e00 commit 26a3c19

File tree

3 files changed

+131
-131
lines changed

3 files changed

+131
-131
lines changed

pydis_site/apps/api/github_utils.py

Lines changed: 40 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
"""Utilities for working with the GitHub API."""
22

3-
import asyncio
43
import datetime
54
import math
65

76
import httpx
87
import jwt
9-
from asgiref.sync import async_to_sync
108

119
from pydis_site import settings
1210

13-
MAX_POLLS = 20
14-
"""The maximum number of attempts at fetching a workflow run."""
11+
MAX_RUN_TIME = datetime.timedelta(minutes=3)
12+
"""The maximum time allowed before an action is declared timed out."""
13+
ISO_FORMAT_STRING = "%Y-%m-%dT%H:%M:%SZ"
14+
"""The datetime string format GitHub uses."""
1515

1616

1717
class ArtifactProcessingError(Exception):
@@ -44,6 +44,12 @@ class RunTimeoutError(ArtifactProcessingError):
4444
status = 408
4545

4646

47+
class RunPendingError(ArtifactProcessingError):
48+
"""The requested workflow run is still pending, try again later."""
49+
50+
status = 202
51+
52+
4753
def generate_token() -> str:
4854
"""
4955
Generate a JWT token to access the GitHub API.
@@ -66,7 +72,7 @@ def generate_token() -> str:
6672
)
6773

6874

69-
async def authorize(owner: str, repo: str) -> httpx.AsyncClient:
75+
def authorize(owner: str, repo: str) -> httpx.Client:
7076
"""
7177
Get an access token for the requested repository.
7278
@@ -75,15 +81,15 @@ async def authorize(owner: str, repo: str) -> httpx.AsyncClient:
7581
- POST <app_access_token> to get a token to access the given app
7682
- GET installation/repositories and check if the requested one is part of those
7783
"""
78-
client = httpx.AsyncClient(
84+
client = httpx.Client(
7985
base_url=settings.GITHUB_API,
8086
headers={"Authorization": f"bearer {generate_token()}"},
8187
timeout=settings.TIMEOUT_PERIOD,
8288
)
8389

8490
try:
8591
# Get a list of app installations we have access to
86-
apps = await client.get("app/installations")
92+
apps = client.get("app/installations")
8793
apps.raise_for_status()
8894

8995
for app in apps.json():
@@ -92,11 +98,11 @@ async def authorize(owner: str, repo: str) -> httpx.AsyncClient:
9298
continue
9399

94100
# Get the repositories of the specified owner
95-
app_token = await client.post(app["access_tokens_url"])
101+
app_token = client.post(app["access_tokens_url"])
96102
app_token.raise_for_status()
97103
client.headers["Authorization"] = f"bearer {app_token.json()['token']}"
98104

99-
repos = await client.get("installation/repositories")
105+
repos = client.get("installation/repositories")
100106
repos.raise_for_status()
101107

102108
# Search for the request repository
@@ -111,44 +117,39 @@ async def authorize(owner: str, repo: str) -> httpx.AsyncClient:
111117

112118
except BaseException as e:
113119
# Close the client if we encountered an unexpected exception
114-
await client.aclose()
120+
client.close()
115121
raise e
116122

117123

118-
async def wait_for_run(client: httpx.AsyncClient, run: dict) -> str:
119-
"""Wait for the provided `run` to finish, and return the URL to its artifacts."""
120-
polls = 0
121-
while polls <= MAX_POLLS:
122-
if run["status"] != "completed":
123-
# The action is still processing, wait a bit longer
124-
polls += 1
125-
await asyncio.sleep(10)
126-
127-
elif run["conclusion"] != "success":
128-
# The action failed, or did not run
129-
raise ActionFailedError(f"The requested workflow ended with: {run['conclusion']}")
124+
def check_run_status(run: dict) -> str:
125+
"""Check if the provided run has been completed, otherwise raise an exception."""
126+
created_at = datetime.datetime.strptime(run["created_at"], ISO_FORMAT_STRING)
127+
run_time = datetime.datetime.now() - created_at
130128

129+
if run["status"] != "completed":
130+
if run_time <= MAX_RUN_TIME:
131+
raise RunPendingError(
132+
f"The requested run is still pending. It was created "
133+
f"{run_time.seconds // 60}:{run_time.seconds % 60 :>02} minutes ago."
134+
)
131135
else:
132-
# The desired action was found, and it ended successfully
133-
return run["artifacts_url"]
136+
raise RunTimeoutError("The requested workflow was not ready in time.")
134137

135-
run = await client.get(run["url"])
136-
run.raise_for_status()
137-
run = run.json()
138+
if run["conclusion"] != "success":
139+
# The action failed, or did not run
140+
raise ActionFailedError(f"The requested workflow ended with: {run['conclusion']}")
138141

139-
raise RunTimeoutError("The requested workflow was not ready in time.")
142+
# The requested action is ready
143+
return run["artifacts_url"]
140144

141145

142-
@async_to_sync
143-
async def get_artifact(
144-
owner: str, repo: str, sha: str, action_name: str, artifact_name: str
145-
) -> str:
146+
def get_artifact(owner: str, repo: str, sha: str, action_name: str, artifact_name: str) -> str:
146147
"""Get a download URL for a build artifact."""
147-
client = await authorize(owner, repo)
148+
client = authorize(owner, repo)
148149

149150
try:
150151
# Get the workflow runs for this repository
151-
runs = await client.get(f"/repos/{owner}/{repo}/actions/runs", params={"per_page": 100})
152+
runs = client.get(f"/repos/{owner}/{repo}/actions/runs", params={"per_page": 100})
152153
runs.raise_for_status()
153154
runs = runs.json()
154155

@@ -161,16 +162,16 @@ async def get_artifact(
161162
"Could not find a run matching the provided settings in the previous hundred runs."
162163
)
163164

164-
# Wait for the workflow to finish
165-
url = await wait_for_run(client, run)
165+
# Check the workflow status
166+
url = check_run_status(run)
166167

167168
# Filter the artifacts, and return the download URL
168-
artifacts = await client.get(url)
169+
artifacts = client.get(url)
169170
artifacts.raise_for_status()
170171

171172
for artifact in artifacts.json()["artifacts"]:
172173
if artifact["name"] == artifact_name:
173-
data = await client.get(artifact["archive_download_url"])
174+
data = client.get(artifact["archive_download_url"])
174175
if data.status_code == 302:
175176
return str(data.next_request.url)
176177

@@ -180,4 +181,4 @@ async def get_artifact(
180181
raise NotFoundError("Could not find an artifact matching the provided name.")
181182

182183
finally:
183-
await client.aclose()
184+
client.close()

0 commit comments

Comments
 (0)