Skip to content

Commit b39220f

Browse files
authored
ci: auto-update ReShade versions via GitHub Action (#34)
* feat: add GitHub Action to auto-update ReShade versions Adds a weekly scheduled workflow that scrapes reshade.me for the latest version, downloads the binaries, computes sha256 hashes, and opens a PR to update package.json, main.py, and reshade-install.sh. Keeps the 3 most recent versions and prunes older ones. * fix: resolve ruff lint violations (SIM117, RUF005) * fix: use set literal instead of set() constructor (C405)
1 parent caadd80 commit b39220f

2 files changed

Lines changed: 246 additions & 0 deletions

File tree

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Scrapes reshade.me for the latest ReShade version and updates package.json,
4+
main.py, and reshade-install.sh with new version entries.
5+
6+
Keeps up to MAX_VERSIONS versions (newest first), pruning the oldest.
7+
Exits 0 if changes were made, 1 if already up to date.
8+
"""
9+
10+
import hashlib
11+
import json
12+
import re
13+
import sys
14+
import tempfile
15+
import urllib.request
16+
from pathlib import Path
17+
18+
MAX_VERSIONS = 3
19+
RESHADE_URL = 'https://reshade.me'
20+
RESHADE_DOWNLOAD_PATTERN = 'https://reshade.me/downloads/ReShade_Setup_{version}.exe'
21+
RESHADE_ADDON_PATTERN = 'https://reshade.me/downloads/ReShade_Setup_{version}_Addon.exe'
22+
23+
REPO_ROOT = Path(__file__).resolve().parent.parent.parent
24+
PACKAGE_JSON = REPO_ROOT / 'package.json'
25+
MAIN_PY = REPO_ROOT / 'main.py'
26+
INSTALL_SH = REPO_ROOT / 'defaults' / 'assets' / 'reshade-install.sh'
27+
28+
29+
def fetch_latest_version() -> str:
30+
"""Scrape reshade.me homepage to find the latest version number."""
31+
req = urllib.request.Request(RESHADE_URL, headers={'User-Agent': 'Mozilla/5.0'})
32+
with urllib.request.urlopen(req, timeout=30) as resp:
33+
html = resp.read().decode('utf-8', errors='replace')
34+
35+
match = re.search(r'ReShade_Setup_(\d+\.\d+\.\d+)\.exe', html)
36+
if not match:
37+
print('ERROR: Could not find ReShade version on reshade.me')
38+
sys.exit(2)
39+
return match.group(1)
40+
41+
42+
def get_existing_reshade_versions(pkg: dict) -> list[str]:
43+
"""Extract version numbers from existing reshade remote_binary entries."""
44+
versions = set()
45+
for entry in pkg.get('remote_binary', []):
46+
name = entry.get('name', '')
47+
m = re.match(r'^reshade_(\d+\.\d+\.\d+)\.exe$', name, re.IGNORECASE)
48+
if m:
49+
versions.add(m.group(1))
50+
return sorted(versions, key=version_key, reverse=True)
51+
52+
53+
def version_key(v: str) -> tuple[int, ...]:
54+
"""Convert version string to tuple for sorting."""
55+
return tuple(int(x) for x in v.split('.'))
56+
57+
58+
def download_and_hash(url: str) -> str:
59+
"""Download a file and return its sha256 hex digest."""
60+
print(f' Downloading {url} ...')
61+
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
62+
sha = hashlib.sha256()
63+
with urllib.request.urlopen(req, timeout=120) as resp, tempfile.NamedTemporaryFile() as tmp:
64+
while True:
65+
chunk = resp.read(65536)
66+
if not chunk:
67+
break
68+
sha.update(chunk)
69+
tmp.write(chunk)
70+
digest = sha.hexdigest()
71+
print(f' SHA256: {digest}')
72+
return digest
73+
74+
75+
def build_reshade_entries(version: str) -> list[dict]:
76+
"""Create the two remote_binary entries for a given version."""
77+
std_url = RESHADE_DOWNLOAD_PATTERN.format(version=version)
78+
addon_url = RESHADE_ADDON_PATTERN.format(version=version)
79+
80+
std_hash = download_and_hash(std_url)
81+
addon_hash = download_and_hash(addon_url)
82+
83+
return [
84+
{
85+
'name': f'reshade_{version}.exe',
86+
'url': std_url,
87+
'sha256hash': std_hash,
88+
},
89+
{
90+
'name': f'reshade_{version}_addon.exe',
91+
'url': addon_url,
92+
'sha256hash': addon_hash,
93+
},
94+
]
95+
96+
97+
def update_package_json(pkg: dict, versions_to_keep: list[str]) -> dict:
98+
"""
99+
Update remote_binary in package.json:
100+
- Keep only ReShade entries for versions_to_keep
101+
- Keep all non-ReShade entries
102+
- Order: ReShade entries (newest first), then non-ReShade entries
103+
"""
104+
non_reshade = []
105+
reshade_by_version: dict[str, list[dict]] = {}
106+
107+
for entry in pkg.get('remote_binary', []):
108+
name = entry.get('name', '')
109+
m = re.match(r'^reshade_(\d+\.\d+\.\d+)(?:_addon)?\.exe$', name, re.IGNORECASE)
110+
if m:
111+
v = m.group(1)
112+
reshade_by_version.setdefault(v, []).append(entry)
113+
else:
114+
non_reshade.append(entry)
115+
116+
# Build new reshade entries list
117+
new_reshade = []
118+
for v in versions_to_keep:
119+
if v in reshade_by_version:
120+
# Keep existing entries (preserves hashes)
121+
new_reshade.extend(reshade_by_version[v])
122+
else:
123+
# New version, download and create entries
124+
new_reshade.extend(build_reshade_entries(v))
125+
126+
pkg['remote_binary'] = new_reshade + non_reshade
127+
return pkg
128+
129+
130+
def update_default_version_in_file(path: Path, patterns: list[tuple[str, str]], new_version: str):
131+
"""Replace version defaults in a file using regex patterns."""
132+
content = path.read_text()
133+
original = content
134+
for pattern, replacement in patterns:
135+
content = re.sub(pattern, replacement.format(version=new_version), content)
136+
if content != original:
137+
path.write_text(content)
138+
print(f' Updated {path.relative_to(REPO_ROOT)}')
139+
140+
141+
def main():
142+
print('Fetching latest ReShade version from reshade.me ...')
143+
latest = fetch_latest_version()
144+
print(f'Latest version: {latest}')
145+
146+
pkg = json.loads(PACKAGE_JSON.read_text())
147+
existing = get_existing_reshade_versions(pkg)
148+
print(f'Existing versions in package.json: {existing}')
149+
150+
if existing and existing[0] == latest:
151+
print('Already up to date. No changes needed.')
152+
sys.exit(1)
153+
154+
# Determine versions to keep: latest + existing, capped at MAX_VERSIONS
155+
all_versions = sorted({latest, *existing}, key=version_key, reverse=True)
156+
versions_to_keep = all_versions[:MAX_VERSIONS]
157+
print(f'Versions to keep: {versions_to_keep}')
158+
159+
pruned = set(existing) - set(versions_to_keep)
160+
if pruned:
161+
print(f'Pruning old versions: {sorted(pruned)}')
162+
163+
# Update package.json
164+
print('Updating package.json ...')
165+
pkg = update_package_json(pkg, versions_to_keep)
166+
PACKAGE_JSON.write_text(json.dumps(pkg, indent=2) + '\n')
167+
print(' Updated package.json')
168+
169+
# Update default version in main.py
170+
print('Updating default version references ...')
171+
update_default_version_in_file(
172+
MAIN_PY,
173+
[
174+
# 'RESHADE_VERSION': 'X.Y.Z' or 'RESHADE_VERSION': 'latest'
175+
(
176+
r"('RESHADE_VERSION':\s*')[^']+(')",
177+
"'RESHADE_VERSION': '{version}'",
178+
),
179+
# version: str = "X.Y.Z" or version: str = "latest"
180+
(
181+
r'(version:\s*str\s*=\s*")[^"]+(")',
182+
'version: str = "{version}"',
183+
),
184+
],
185+
latest,
186+
)
187+
188+
# Update default version in reshade-install.sh
189+
update_default_version_in_file(
190+
INSTALL_SH,
191+
[
192+
# RESHADE_VERSION=${RESHADE_VERSION:-"X.Y.Z"}
193+
(
194+
r'(RESHADE_VERSION=\$\{RESHADE_VERSION:-")[^"]+("\})',
195+
'RESHADE_VERSION=${{RESHADE_VERSION:-"{version}"}}',
196+
),
197+
],
198+
latest,
199+
)
200+
201+
print(f'\nDone. Updated to ReShade {latest}.')
202+
sys.exit(0)
203+
204+
205+
if __name__ == '__main__':
206+
main()
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: Update ReShade Versions
2+
3+
on:
4+
schedule:
5+
# Every Monday at 09:00 UTC
6+
- cron: "0 9 * * 1"
7+
workflow_dispatch:
8+
9+
permissions:
10+
contents: write
11+
pull-requests: write
12+
13+
jobs:
14+
check-and-update:
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: actions/checkout@v5
18+
19+
- name: Check for new ReShade version
20+
id: update
21+
run: |
22+
python3 .github/scripts/update-reshade-versions.py
23+
echo "updated=true" >> "$GITHUB_OUTPUT"
24+
continue-on-error: true
25+
26+
- name: Create Pull Request
27+
if: steps.update.outcome == 'success'
28+
uses: peter-evans/create-pull-request@v7
29+
with:
30+
token: ${{ secrets.GITHUB_TOKEN }}
31+
commit-message: "chore: update ReShade versions"
32+
title: "chore: update ReShade versions"
33+
body: |
34+
Automated update from [reshade.me](https://reshade.me).
35+
36+
This PR was created by the `update-reshade` workflow.
37+
Review the updated `package.json` entries and verify the sha256 hashes are correct.
38+
branch: chore/auto-reshade-update
39+
delete-branch: true
40+
labels: automated,dependencies

0 commit comments

Comments
 (0)