Skip to content

Commit a32c7e0

Browse files
authored
Merge pull request #31 from GeorgeK1ng/auto-update
Add workflow to generate JSON files for automated VCMI update
2 parents 2d01602 + 9fb9176 commit a32c7e0

File tree

5 files changed

+293
-0
lines changed

5 files changed

+293
-0
lines changed

.github/generate_update.py

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
import urllib.request
2+
import re
3+
from datetime import datetime, timezone
4+
from dateutil import parser
5+
import json
6+
import tempfile
7+
import pefile
8+
from collections import OrderedDict
9+
10+
# Folder name → (system, variant)
11+
platform_dirs = {
12+
"windows-x64": ("windows", "x64"),
13+
"windows-x86": ("windows", "x86"),
14+
"windows-arm64": ("windows", "arm64"),
15+
"macos-intel": ("macos", "intel"),
16+
"macos-arm": ("macos", "arm"),
17+
"android-armeabi-v7a": ("android", "armeabi-v7a"),
18+
"android-arm64-v8a": ("android", "arm64-v8a"),
19+
"ios": ("ios", "ios")
20+
}
21+
22+
# File extensions
23+
extensions = {
24+
"windows": ".exe",
25+
"macos": ".dmg",
26+
"android": ".apk",
27+
"ios": ".ipa"
28+
}
29+
30+
def fetch_html(url):
31+
"""Download and return HTML content from directory listing."""
32+
with urllib.request.urlopen(url) as response:
33+
return response.read().decode("utf-8")
34+
35+
def extract_file_and_date(html, ext, system="", variant="", url=""):
36+
"""Extract the most recent file based on the date column."""
37+
rows = re.findall(
38+
r'<tr><td><a href="([^"]+%s)".*?</a></td><td[^>]*>\s*\d+\s*</td><td[^>]*>([^<]+)</td>' % re.escape(ext),
39+
html,
40+
flags=re.IGNORECASE
41+
)
42+
if not rows:
43+
print(f"❌ No match for {system}/{variant} at {url}")
44+
return None, None
45+
46+
def parse_date(date_str):
47+
try:
48+
return datetime.strptime(date_str, "%Y-%b-%d %H:%M")
49+
except ValueError:
50+
return datetime.min
51+
52+
rows.sort(key=lambda x: parse_date(x[1]), reverse=True)
53+
filename, date_str = rows[0]
54+
print(f"✅ Found newest file for {system}/{variant}{filename}")
55+
return filename, date_str
56+
57+
def get_file_version_from_exe_url(url):
58+
"""Extract FileVersion from PE header in EXE."""
59+
try:
60+
with urllib.request.urlopen(url) as response:
61+
with tempfile.NamedTemporaryFile(delete=False) as tmp_file:
62+
tmp_file.write(response.read())
63+
tmp_path = tmp_file.name
64+
65+
pe = pefile.PE(tmp_path)
66+
for fileinfo in pe.FileInfo:
67+
for entry in fileinfo:
68+
if hasattr(entry, 'StringTable'):
69+
for st in entry.StringTable:
70+
version = st.entries.get(b"FileVersion") or st.entries.get("FileVersion")
71+
if version:
72+
version = version.decode("utf-8") if isinstance(version, bytes) else version
73+
return version.replace(" ", "").strip()
74+
except Exception as e:
75+
print(f"⚠️ Could not extract version from EXE: {e}")
76+
return "1.6.8"
77+
78+
# Create default download key map
79+
empty_download_map = OrderedDict(
80+
(f"{system}-{variant}", "") for _, (system, variant) in platform_dirs.items()
81+
)
82+
83+
def make_empty_channel():
84+
# Use the global empty_download_map prepared above
85+
ch = OrderedDict()
86+
ch["version"] = ""
87+
ch["commit"] = ""
88+
ch["buildDate"] = ""
89+
ch["changeLog"] = ""
90+
ch["download"] = OrderedDict(empty_download_map)
91+
return ch
92+
93+
# Process nightly branches
94+
channels = ["develop", "beta"]
95+
channel_results = {}
96+
97+
for channel in channels:
98+
base_url = f"https://builds.vcmi.download/branch/{channel}"
99+
channel_obj = make_empty_channel()
100+
found_any = False # track if we found at least one artifact
101+
102+
# Try to set metadata from Windows x64 (anchor build)
103+
win_url = f"{base_url}/windows-x64/"
104+
try:
105+
html = fetch_html(win_url)
106+
except Exception as e:
107+
html = ""
108+
filename, date_str = extract_file_and_date(html, ".exe", "windows", "x64", win_url)
109+
110+
if filename and date_str:
111+
# we have our anchor → set metadata
112+
build_hash_match = re.search(r'VCMI-branch-[\w\-]+-([a-fA-F0-9]+)\.exe', filename)
113+
if build_hash_match:
114+
build_hash = build_hash_match.group(1)
115+
else:
116+
build_hash = ""
117+
118+
build_date = ""
119+
try:
120+
build_date = datetime.strptime(date_str, "%Y-%b-%d %H:%M").strftime("%Y-%m-%d %H:%M:%S")
121+
except Exception:
122+
pass
123+
124+
exe_url = f"{win_url}{filename}"
125+
version_string = get_file_version_from_exe_url(exe_url) or ""
126+
127+
channel_obj["version"] = version_string
128+
channel_obj["commit"] = build_hash
129+
channel_obj["buildDate"] = build_date
130+
channel_obj["changeLog"] = f"Latest nightly build from {channel} branch."
131+
132+
# Set the anchor platform download here to avoid re-fetching and duplicate logs
133+
channel_obj["download"]["windows-x64"] = exe_url
134+
135+
# Record that we found at least something
136+
found_any = True
137+
# also record the windows-x64 download below in the general loop
138+
139+
# Try to find files for all platforms (fills downloads, independent of metadata)
140+
for folder_name, (system, variant) in platform_dirs.items():
141+
# We already processed windows-x64 as the anchor; skip to avoid duplicate log lines
142+
if folder_name == "windows-x64":
143+
continue
144+
145+
url = f"{base_url}/{folder_name}/"
146+
try:
147+
html = fetch_html(url)
148+
except Exception:
149+
continue
150+
151+
# Use a different name to avoid shadowing the outer 'filename'
152+
fname, _ = extract_file_and_date(html, extensions[system], system, variant, url)
153+
if not fname:
154+
continue
155+
156+
download_url = url + fname
157+
key = f"{system}-{variant}"
158+
channel_obj["download"][key] = download_url
159+
160+
# If nothing at all was found for this channel, keep everything empty
161+
# (channel_obj already initialized as empty)
162+
channel_results[channel] = channel_obj
163+
164+
# Write develop and beta JSON
165+
for channel, data in channel_results.items():
166+
filename = f"vcmi-{channel}.json"
167+
with open(filename, "w", encoding="utf-8") as f:
168+
json.dump(data, f, indent=2, ensure_ascii=False)
169+
print(f"📄 Written {filename}")
170+
171+
# Stable channel from GitHub releases
172+
print("\n🔍 Fetching stable release from GitHub...")
173+
try:
174+
with urllib.request.urlopen("https://api.github.com/repos/vcmi/vcmi/releases/latest") as response:
175+
release = json.load(response)
176+
177+
stable_obj = OrderedDict()
178+
stable_obj["version"] = release["tag_name"]
179+
stable_obj["buildDate"] = parser.isoparse(release["published_at"]).strftime("%Y-%m-%d %H:%M:%S")
180+
stable_obj["changeLog"] = release.get("body", "Latest stable release.")
181+
stable_obj["download"] = OrderedDict(empty_download_map)
182+
183+
stable_mapping = {
184+
"windows": {
185+
"x64": "VCMI-Windows.exe",
186+
"x86": "VCMI-Windows32bit.exe"
187+
},
188+
"macos": {
189+
"arm": "VCMI-macOS-arm.dmg",
190+
"intel": "VCMI-macOS-intel.dmg"
191+
},
192+
"android": {
193+
"armeabi-v7a": "VCMI-Android-armeabi-v7a.apk",
194+
"arm64-v8a": "VCMI-Android-arm64-v8a.apk"
195+
},
196+
"ios": {
197+
"ios": "VCMI-iOS.ipa"
198+
}
199+
}
200+
201+
for system, variants in stable_mapping.items():
202+
for variant, filename in variants.items():
203+
key = f"{system}-{variant}"
204+
asset = next((a for a in release.get("assets", []) if a["name"] == filename), None)
205+
if asset:
206+
print(f"✅ Found stable {key}: {filename}")
207+
stable_obj["download"][key] = asset["browser_download_url"]
208+
else:
209+
print(f"❌ Missing stable {key}: {filename}")
210+
211+
with open("vcmi-stable.json", "w", encoding="utf-8") as f:
212+
json.dump(stable_obj, f, indent=2, ensure_ascii=False)
213+
print("📄 Written vcmi-stable.json")
214+
215+
except Exception as e:
216+
print(f"⚠️ Failed to fetch stable release: {e}")

.github/workflows/github.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: Generate VCMI Update JSON
2+
3+
on:
4+
schedule:
5+
- cron: '0 0 * * *' # every day at 00:00 UTC
6+
workflow_dispatch:
7+
8+
jobs:
9+
generate-json:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- name: Checkout develop branch
13+
uses: actions/checkout@v4
14+
15+
- name: Install dependencies
16+
run: pip3 install pefile
17+
18+
- name: Generate JSON files
19+
run: python3 .github/generate_update.py
20+
21+
- name: Commit and push
22+
uses: EndBug/add-and-commit@v9
23+
with:
24+
default_author: github_actions
25+
message: "Auto-update VCMI update JSON files"
26+
add: |
27+
vcmi-develop.json
28+
vcmi-beta.json
29+
vcmi-stable.json
30+
push: true

vcmi-beta.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"version": "",
3+
"commit": "",
4+
"buildDate": "",
5+
"changeLog": "",
6+
"download": {
7+
"windows-x64": "",
8+
"windows-x86": "",
9+
"windows-arm64": "",
10+
"macos-intel": "",
11+
"macos-arm": "",
12+
"android-armeabi-v7a": "",
13+
"android-arm64-v8a": "",
14+
"ios-ios": ""
15+
}
16+
}

vcmi-develop.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"version": "1.7.0",
3+
"commit": "72bc79a",
4+
"buildDate": "2025-08-23 20:20:00",
5+
"changeLog": "Latest nightly build from develop branch.",
6+
"download": {
7+
"windows-x64": "https://builds.vcmi.download/branch/develop/windows-x64/VCMI-branch-develop-72bc79a.exe",
8+
"windows-x86": "https://builds.vcmi.download/branch/develop/windows-x86/VCMI-branch-develop-72bc79a.exe",
9+
"windows-arm64": "https://builds.vcmi.download/branch/develop/windows-arm64/VCMI-branch-develop-72bc79a.exe",
10+
"macos-intel": "https://builds.vcmi.download/branch/develop/macos-intel/VCMI-branch-develop-72bc79a.dmg",
11+
"macos-arm": "https://builds.vcmi.download/branch/develop/macos-arm/VCMI-branch-develop-72bc79a.dmg",
12+
"android-armeabi-v7a": "https://builds.vcmi.download/branch/develop/android-armeabi-v7a/VCMI-branch-develop-72bc79a.apk",
13+
"android-arm64-v8a": "https://builds.vcmi.download/branch/develop/android-arm64-v8a/VCMI-branch-develop-72bc79a.apk",
14+
"ios-ios": "https://builds.vcmi.download/branch/develop/ios/VCMI-branch-develop-72bc79a.ipa"
15+
}
16+
}

vcmi-stable.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"version": "1.6.8",
3+
"buildDate": "2025-04-25 16:12:54",
4+
"changeLog": "Note: saved games from 1.5 release can be loaded in 1.6\r\n\r\n### Changelog\r\n- Player Changelog: [1.6.7 -> 1.6.8](https://github.com/vcmi/vcmi/blob/master/ChangeLog.md#167---168)\r\n- Full Changelog: https://github.com/vcmi/vcmi/compare/1.6.7..1.6.8\r\n\r\n### Additional builds\r\n<!-- - Android release will be available on Google Play shortly -->\r\n<!-- - Linux release will be available on Flathub shortly -->\r\n<!-- - Ubuntu release will be available on VCMI PPA shortly -->\r\n<!-- - Android release is available on [Google Play](https://play.google.com/store/apps/details?id=is.xyz.vcmi) -->\r\n<!-- - Linux release is available on [Flathub](https://flathub.org/apps/eu.vcmi.VCMI) -->\r\n<!-- - Ubuntu release is available on [VCMI PPA](https://launchpad.net/~vcmi/+archive/ubuntu/ppa) -->\r\n\r\n- Android release is available on [Google Play](https://play.google.com/store/apps/details?id=is.xyz.vcmi)\r\n- Linux release is available on [Flathub](https://flathub.org/apps/eu.vcmi.VCMI)\r\n- Ubuntu release is available on [VCMI PPA](https://launchpad.net/~vcmi/+archive/ubuntu/ppa)\r\n- macOS release can be installed via Homebrew: `brew install --cask --no-quarantine vcmi/vcmi/vcmi`",
5+
"download": {
6+
"windows-x64": "https://github.com/vcmi/vcmi/releases/download/1.6.8/VCMI-Windows.exe",
7+
"windows-x86": "https://github.com/vcmi/vcmi/releases/download/1.6.8/VCMI-Windows32bit.exe",
8+
"windows-arm64": "",
9+
"macos-intel": "https://github.com/vcmi/vcmi/releases/download/1.6.8/VCMI-macOS-intel.dmg",
10+
"macos-arm": "https://github.com/vcmi/vcmi/releases/download/1.6.8/VCMI-macOS-arm.dmg",
11+
"android-armeabi-v7a": "https://github.com/vcmi/vcmi/releases/download/1.6.8/VCMI-Android-armeabi-v7a.apk",
12+
"android-arm64-v8a": "https://github.com/vcmi/vcmi/releases/download/1.6.8/VCMI-Android-arm64-v8a.apk",
13+
"ios-ios": "https://github.com/vcmi/vcmi/releases/download/1.6.8/VCMI-iOS.ipa"
14+
}
15+
}

0 commit comments

Comments
 (0)