Improvements to Version Update Bot (#113)
Currently, the mod index will prioritise using the name of the tag from
the latest release version, regardless of the download source provided
by the mod. This is problematic for mods using a link to HEAD
(`archive/refs/heads/<main>.zip`) as their `downloadURL` but that also
have previously created a Release. This PR instead checks if the mod
links to HEAD, and uses the corresponding version.
Also included are improvements to error logging and wait time checks
when the GitHub REST API rate limits are hit, which appears to be
happening with inconsistent frequency. Hopefully these improvements will
help to narrow down the cause of the rate limit problems.
- Check if download url links to latest head, and if so, use version of
latest commit hash instead of release version
- Merge `get_latest_release` and `get_latest_commit` into
`get_version_string` for less duplicate code
- Check and print GitHub API rate limit details given by REST response
including api resource, remaining calls, and reset time
- On exceeding rate limit or 403 error, check if primary or secondary
rate limit has been reached
- On primary rate limit breach, wait until hourly rate reset time
- On secondary rate limit breach, wait for `retry-after` response time
or an exponential time, starting at 60 seconds and doubling for each
attempt (following requirements from GitHub API docs)
- Prevent program waiting for more than 30 minutes for API rate reset
time
This commit is contained in:
123
.github/scripts/update_mod_versions.py
vendored
123
.github/scripts/update_mod_versions.py
vendored
@@ -24,53 +24,74 @@ def extract_repo_info(repo_url):
|
||||
return owner, repo
|
||||
return None, None
|
||||
|
||||
def get_latest_release(owner, repo):
|
||||
"""Get the latest release version from GitHub."""
|
||||
url = f'https://api.github.com/repos/{owner}/{repo}/releases/latest'
|
||||
def get_version_string(source, owner, repo, start_timestamp, n = 1):
|
||||
"""Get the version string from a given GitHub repo."""
|
||||
|
||||
if source == 'release':
|
||||
url = f'https://api.github.com/repos/{owner}/{repo}/releases/latest'
|
||||
else:
|
||||
url = f'https://api.github.com/repos/{owner}/{repo}/commits'
|
||||
|
||||
try:
|
||||
response = requests.get(url, headers=HEADERS)
|
||||
|
||||
api_rate_limit = int(response.headers.get('x-ratelimit-limit'))
|
||||
api_rate_usage = int(response.headers.get('x-ratelimit-used'))
|
||||
api_rate_remaining = int(response.headers.get('x-ratelimit-remaining'))
|
||||
api_reset_timestamp = int(response.headers.get('x-ratelimit-reset'))
|
||||
api_resource = response.headers.get('x-ratelimit-resource')
|
||||
print(f"GitHub API ({api_resource}) calls: {api_rate_usage}/{api_rate_limit}")
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
return data.get('tag_name')
|
||||
if source == 'release':
|
||||
data = response.json()
|
||||
# Return name of latest tag
|
||||
return data.get('tag_name')
|
||||
else:
|
||||
commits = response.json()
|
||||
if commits and len(commits) > 0:
|
||||
# Return shortened commit hash (first 7 characters)
|
||||
return commits[0]['sha'][:7]
|
||||
elif response.status_code == 404:
|
||||
# No releases found
|
||||
# Not found
|
||||
return None
|
||||
elif response.status_code == 403 and 'rate limit exceeded' in response.text.lower():
|
||||
print("GitHub API rate limit exceeded. Waiting for 5 minutes...")
|
||||
time.sleep(300) # Wait for 5 minutes
|
||||
return get_latest_release(owner, repo) # Retry
|
||||
elif api_rate_remaining == 0 or (response.status_code == 403 and 'rate limit exceeded' in response.text.lower()):
|
||||
print(f"GitHub API access is being rate limited!")
|
||||
current_timestamp = int(time.time())
|
||||
|
||||
# Check if primary rate limit is okay
|
||||
if api_rate_remaining > 0:
|
||||
# Secondary rate limit hit, follow GitHub instructions
|
||||
if 'retry-after' in response.headers:
|
||||
wait_time = int(response.headers.get('retry-after')) + 5
|
||||
print(f"Response retry-after {wait_time}s")
|
||||
else:
|
||||
# Start at 60 seconds and double wait time with each new attempt
|
||||
print(f"Attempt {n}")
|
||||
wait_time = 60 * pow(2, n - 1)
|
||||
else:
|
||||
api_reset_time = datetime.fromtimestamp(api_reset_timestamp).strftime('%H:%M:%S')
|
||||
print(f"GitHub API rate limit resets at {api_reset_time}")
|
||||
wait_time = api_reset_timestamp - current_timestamp + 5
|
||||
|
||||
# Wait only if the wait time would finish less than 1800 seconds (30 min) after program start time
|
||||
if current_timestamp + wait_time - start_timestamp < 1800:
|
||||
print(f"Waiting {wait_time} seconds until next attempt...")
|
||||
time.sleep(wait_time)
|
||||
n += 1
|
||||
return get_version_string(source, owner, repo, start_timestamp, n) # Retry
|
||||
else:
|
||||
print(f"Next attempt in {wait_time} seconds, but Action run time would exceed 1800 seconds - Stopping...")
|
||||
sys.exit(1)
|
||||
|
||||
else:
|
||||
print(f"Error fetching releases: HTTP {response.status_code} - {response.text}")
|
||||
print(f"Error fetching {source}s: HTTP {response.status_code} - {response.text}")
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"Exception while fetching releases: {str(e)}")
|
||||
print(f"Exception while fetching {source}s: {str(e)}")
|
||||
return None
|
||||
|
||||
def get_latest_commit(owner, repo):
|
||||
"""Get the latest commit hash from GitHub."""
|
||||
url = f'https://api.github.com/repos/{owner}/{repo}/commits'
|
||||
try:
|
||||
response = requests.get(url, headers=HEADERS)
|
||||
|
||||
if response.status_code == 200:
|
||||
commits = response.json()
|
||||
if commits and len(commits) > 0:
|
||||
# Return shortened commit hash (first 7 characters)
|
||||
return commits[0]['sha'][:7]
|
||||
elif response.status_code == 403 and 'rate limit exceeded' in response.text.lower():
|
||||
print("GitHub API rate limit exceeded. Waiting for 5 minutes...")
|
||||
time.sleep(300) # Wait for 5 minutes
|
||||
return get_latest_commit(owner, repo) # Retry
|
||||
else:
|
||||
print(f"Error fetching commits: HTTP {response.status_code} - {response.text}")
|
||||
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"Exception while fetching commits: {str(e)}")
|
||||
return None
|
||||
|
||||
def process_mods():
|
||||
def process_mods(start_timestamp):
|
||||
"""Process all mods and update versions where needed."""
|
||||
mods_dir = Path('mods')
|
||||
updated_mods = []
|
||||
@@ -106,16 +127,24 @@ def process_mods():
|
||||
|
||||
print(f"Checking GitHub repo: {owner}/{repo}")
|
||||
|
||||
# Try to get latest release version first
|
||||
new_version = get_latest_release(owner, repo)
|
||||
version_source = "release"
|
||||
|
||||
# If no releases, fall back to latest commit
|
||||
if not new_version:
|
||||
print("No releases found, checking latest commit...")
|
||||
new_version = get_latest_commit(owner, repo)
|
||||
# If download url links to latest head, use version of latest commit hash
|
||||
download_url = meta.get('downloadURL')
|
||||
if "/archive/refs/heads/" in download_url:
|
||||
print("Download URL links to HEAD, checking latest commit...")
|
||||
version_source = "commit"
|
||||
new_version = get_version_string(version_source, owner, repo, start_timestamp)
|
||||
else:
|
||||
# Try to get latest release version
|
||||
print("Checking releases for latest version tag...")
|
||||
version_source = "release"
|
||||
new_version = get_version_string(version_source, owner, repo, start_timestamp)
|
||||
|
||||
# If no releases, fall back to latest commit
|
||||
if not new_version:
|
||||
print("No releases found, checking latest commit...")
|
||||
version_source = "commit"
|
||||
new_version = get_version_string(version_source, owner, repo, start_timestamp)
|
||||
|
||||
if not new_version:
|
||||
print(f"⚠️ Warning: Could not determine version for {mod_dir.name}")
|
||||
continue
|
||||
@@ -163,8 +192,10 @@ def generate_commit_message(updated_mods):
|
||||
return message
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(f"🔄 Starting automatic mod version update at {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}...")
|
||||
updated_mods = process_mods()
|
||||
start_timestamp = int(time.time())
|
||||
start_datetime = datetime.fromtimestamp(start_timestamp).strftime('%H:%M:%S')
|
||||
print(f"🔄 Starting automatic mod version update at {start_datetime}...")
|
||||
updated_mods = process_mods(start_timestamp)
|
||||
|
||||
if updated_mods:
|
||||
# Write commit message to a file that the GitHub Action can use
|
||||
|
||||
Reference in New Issue
Block a user