add ipa signing; integrate repo; add entitlements
36
.github/workflows/release.yml
vendored
|
|
@ -9,6 +9,9 @@ concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref_name }}
|
group: ${{ github.workflow }}-${{ github.ref_name }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
ZSIGN_VERSION: '0.7'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-release-android-apks:
|
build-and-release-android-apks:
|
||||||
permissions:
|
permissions:
|
||||||
|
|
@ -77,8 +80,12 @@ jobs:
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
steps:
|
steps:
|
||||||
# Checkout branch
|
# Checkout branch
|
||||||
- name: checkout branch
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
ref: ${{ github.ref }}
|
||||||
|
path: .
|
||||||
|
|
||||||
- name: setup flutter
|
- name: setup flutter
|
||||||
uses: subosito/flutter-action@v2
|
uses: subosito/flutter-action@v2
|
||||||
|
|
@ -94,16 +101,27 @@ jobs:
|
||||||
- name: flutter pub get
|
- name: flutter pub get
|
||||||
run: flutter pub get
|
run: flutter pub get
|
||||||
|
|
||||||
|
- name: setup signing environment
|
||||||
|
env:
|
||||||
|
P12_BASE64: ${{ secrets.P12_CERTIFICATE }}
|
||||||
|
PROVISIONING_PROFILE_BASE64: ${{ secrets.PROVISIONING_PROFILE }}
|
||||||
|
run: |
|
||||||
|
echo "$P12_BASE64" | base64 -d > certificate.p12
|
||||||
|
echo "$PROVISIONING_PROFILE_BASE64" | base64 -d > profile.mobileprovision
|
||||||
|
curl -L -o zsign.zip "https://github.com/zhlynn/zsign/releases/download/v${{ env.ZSIGN_VERSION }}/zsign-v${{ env.ZSIGN_VERSION }}-macos-x64.zip"
|
||||||
|
unzip zsign.zip
|
||||||
|
chmod +x zsign || chmod +x zsign-*
|
||||||
|
|
||||||
- name: build ios
|
- name: build ios
|
||||||
|
env:
|
||||||
|
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
|
||||||
run: |
|
run: |
|
||||||
flutter build ios --release --no-codesign
|
flutter build ios --release --no-codesign
|
||||||
|
mkdir -p build/ios/iphoneos/Payload
|
||||||
|
ln -s ../Runner.app build/ios/iphoneos/Payload/Runner.app
|
||||||
|
./zsign -f -k ./certificate.p12 -p "$P12_PASSWORD" -m ./profile.mobileprovision ./build/ios/iphoneos/Payload/Runner.app
|
||||||
cd build/ios/iphoneos
|
cd build/ios/iphoneos
|
||||||
mkdir Payload
|
zip -r ./Mangayomi-${{ github.ref_name }}-ios.ipa Payload
|
||||||
cd Payload
|
|
||||||
ln -s ../Runner.app
|
|
||||||
cd ..
|
|
||||||
zip -r app-release.ipa Payload
|
|
||||||
mv app-release.ipa Mangayomi-${{ github.ref_name }}-ios.ipa
|
|
||||||
|
|
||||||
- name: upload artifact ios ipa
|
- name: upload artifact ios ipa
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
|
|
@ -402,3 +420,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
artifacts: dist/Mangayomi-*.deb
|
artifacts: dist/Mangayomi-*.deb
|
||||||
allowUpdates: true
|
allowUpdates: true
|
||||||
|
|
||||||
|
ios-source-build:
|
||||||
|
needs: build-and-release-ios-ipa
|
||||||
|
uses: ./.github/workflows/update_sideloading_source.yml
|
||||||
44
.github/workflows/update_sideloading_source.yml
vendored
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
name: Update Sideloading Source
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
workflow_dispatch: # Allow manual triggering of this workflow
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update-source:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
ref: main
|
||||||
|
|
||||||
|
- name: Get Latest Release Tag
|
||||||
|
run: echo "RELEASE_TAG=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Setup Python
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: '3.12'
|
||||||
|
|
||||||
|
- name: Install Python Dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install requests
|
||||||
|
|
||||||
|
- name: Run source updater
|
||||||
|
run: python repo/update_source.py
|
||||||
|
|
||||||
|
- name: Commit and Push changes
|
||||||
|
run: |
|
||||||
|
git config --global user.name 'GitHub Action'
|
||||||
|
git config --global user.email 'action@github.com'
|
||||||
|
git add repo/source.json
|
||||||
|
if git diff --cached --quiet; then
|
||||||
|
echo "No changes detected, skipping commit."
|
||||||
|
else
|
||||||
|
git commit -m "source update: $RELEASE_TAG"
|
||||||
|
git push
|
||||||
|
fi
|
||||||
11
README.md
|
|
@ -32,7 +32,16 @@ Features include:
|
||||||
## Download
|
## Download
|
||||||
Get the app from our [releases page](https://github.com/kodjodevf/mangayomi/releases).
|
Get the app from our [releases page](https://github.com/kodjodevf/mangayomi/releases).
|
||||||
|
|
||||||
iOS / ipadOS users can also get the latest app from AltStore / Feather / SideStore via [Dominics GitHub repository](https://github.com/tanakrit-d/mangayomi-source).
|
## iOS Sideloading Sources
|
||||||
|
<a href="https://intradeus.github.io/http-protocol-redirector?r=altstore://source?url=https://raw.githubusercontent.com/kodjodevf/mangayomi/refs/heads/main/repo/source.json"><img alt="AltStore Source" src="repo/images/buttons/altstore_button.png" width="150"></a>
|
||||||
|
|
||||||
|
<a href="https://intradeus.github.io/http-protocol-redirector?r=feather://source/https://raw.githubusercontent.com/kodjodevf/mangayomi/refs/heads/main/repo/source.json"><img alt="Feather Source" src="repo/images/buttons/feather_button.png" width="150"></a>
|
||||||
|
|
||||||
|
<a href="https://intradeus.github.io/http-protocol-redirector?r=sidestore://source?url=https://raw.githubusercontent.com/kodjodevf/mangayomi/refs/heads/main/repo/source.json"><img alt="Sidestore Source" src="repo/images/buttons/sidestore_button.png" width="150"></a>
|
||||||
|
|
||||||
|
<a href="https://raw.githubusercontent.com/kodjodevf/mangayomi/refs/heads/main/repo/source.json"><img alt="Direct URL Source" src="repo/images/buttons/url_button.png" width="150"></a>
|
||||||
|
|
||||||
|
Note: Only future releases (> 0.5.2) will be signed (and therefore have AltStore/SideStore compatibility).
|
||||||
|
|
||||||
# Contributing
|
# Contributing
|
||||||
|
|
||||||
|
|
|
||||||
20
repo/colours.json
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"Themes": {
|
||||||
|
"Default": {
|
||||||
|
"Tint": "#EF4444",
|
||||||
|
"Primary": "#EF4444",
|
||||||
|
"Gradient": {
|
||||||
|
"Primary": "#EF4444",
|
||||||
|
"Secondary": "#991B1B"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Black": {
|
||||||
|
"Tint": "#71717A",
|
||||||
|
"Primary": "#18181B",
|
||||||
|
"Gradient": {
|
||||||
|
"Primary": "#3F3F46",
|
||||||
|
"Secondary": "#18181B"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
repo/config.json
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"repo_url": "kodjodevf/mangayomi",
|
||||||
|
"json_file": "repo/source.json",
|
||||||
|
"app_id": "com.kodjodevf.mangayomi",
|
||||||
|
"app_name": "Mangayomi",
|
||||||
|
"caption": "Update for Mangayomi now available!",
|
||||||
|
"tint_colour": "EF4444",
|
||||||
|
"image_url": "https://raw.githubusercontent.com/kodjodevf/mangayomi/refs/heads/main/repo/images/news/update.webp"
|
||||||
|
}
|
||||||
BIN
repo/images/buttons/altstore_button.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
repo/images/buttons/altstore_button_resized.png
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
BIN
repo/images/buttons/feather_button.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
repo/images/buttons/feather_button_resized.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
repo/images/buttons/sidestore_button.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
repo/images/buttons/sidestore_button_resized.png
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
BIN
repo/images/buttons/url_button.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
repo/images/buttons/url_button_resized.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
repo/images/headers/readme_header_black.webp
Normal file
|
After Width: | Height: | Size: 181 KiB |
BIN
repo/images/headers/readme_header_default.webp
Normal file
|
After Width: | Height: | Size: 204 KiB |
BIN
repo/images/headers/source_header_black.webp
Normal file
|
After Width: | Height: | Size: 129 KiB |
BIN
repo/images/headers/source_header_default.webp
Normal file
|
After Width: | Height: | Size: 142 KiB |
BIN
repo/images/icons/icon_black.webp
Normal file
|
After Width: | Height: | Size: 528 KiB |
BIN
repo/images/icons/icon_default.webp
Normal file
|
After Width: | Height: | Size: 854 KiB |
BIN
repo/images/news/available_black.webp
Normal file
|
After Width: | Height: | Size: 228 KiB |
BIN
repo/images/news/available_default.webp
Normal file
|
After Width: | Height: | Size: 251 KiB |
BIN
repo/images/news/update_black.webp
Normal file
|
After Width: | Height: | Size: 348 KiB |
BIN
repo/images/news/update_default.webp
Normal file
|
After Width: | Height: | Size: 369 KiB |
BIN
repo/images/screenshots/image_0_black.webp
Normal file
|
After Width: | Height: | Size: 306 KiB |
BIN
repo/images/screenshots/image_0_default.webp
Normal file
|
After Width: | Height: | Size: 314 KiB |
BIN
repo/images/screenshots/image_1_black.webp
Normal file
|
After Width: | Height: | Size: 250 KiB |
BIN
repo/images/screenshots/image_1_default.webp
Normal file
|
After Width: | Height: | Size: 259 KiB |
BIN
repo/images/screenshots/image_2_black.webp
Normal file
|
After Width: | Height: | Size: 219 KiB |
BIN
repo/images/screenshots/image_2_default.webp
Normal file
|
After Width: | Height: | Size: 227 KiB |
BIN
repo/images/templates/Archive.7z
Normal file
86
repo/source.json
Executable file
|
|
@ -0,0 +1,86 @@
|
||||||
|
{
|
||||||
|
"name": "Mangayomi",
|
||||||
|
"identifier": "com.kodjodevf.mangayomi",
|
||||||
|
"headerURL": "https://raw.githubusercontent.com/kodjodevf/mangayomi/refs/heads/main/repo/images/headers/source_header_default.webp",
|
||||||
|
"website": "https://github.com/kodjodevf/mangayomi",
|
||||||
|
"iconURL": "https://raw.githubusercontent.com/kodjodevf/mangayomi/refs/heads/main/repo/images/icons/icon_default.webp",
|
||||||
|
"subtitle": "An experience like no other.",
|
||||||
|
"description": "This is the official source for Mangayomi.\n\nFor full details, check the GitHub repository:\nhttps://github.com/kodjodevf/mangayomi",
|
||||||
|
"tintColor": "EF4444",
|
||||||
|
"apps": [
|
||||||
|
{
|
||||||
|
"beta": false,
|
||||||
|
"name": "Mangayomi",
|
||||||
|
"bundleIdentifier": "com.kodjodevf.mangayomi",
|
||||||
|
"developerName": "Moustapha Kodjo Amadou",
|
||||||
|
"subtitle": "Read manga, novels, and watch anime",
|
||||||
|
"version": "0.5.2",
|
||||||
|
"versionDate": "2025-02-23T16:08:37Z",
|
||||||
|
"versionDescription": "What's Changed\r\n* Update quarkuc extractor by @yxxyun in https://github.com/kodjodevf/mangayomi/pull/393\r\n* added long press for 2x playback speed by @Schnitzel5 in https://github.com/kodjodevf/mangayomi/pull/396\r\n* fix broken category \r\n* add confirmation dialog for adding repositories\r\n\r\n\r\n**Full Changelog**: https://github.com/kodjodevf/mangayomi/compare/v0.5.1...v0.5.2",
|
||||||
|
"downloadURL": "https://github.com/kodjodevf/mangayomi/releases/download/v0.5.2/Mangayomi-v0.5.2-ios.ipa",
|
||||||
|
"localizedDescription": "Mangayomi is an open-source Flutter app for reading manga, novels, and watching anime across multiple platforms.",
|
||||||
|
"iconURL": "https://raw.githubusercontent.com/kodjodevf/mangayomi/refs/heads/main/repo/images/icons/icon_default.webp",
|
||||||
|
"tintColor": "EF4444",
|
||||||
|
"category": "entertainment",
|
||||||
|
"size": 56680831,
|
||||||
|
"screenshotURLs": [
|
||||||
|
"https://raw.githubusercontent.com/kodjodevf/mangayomi/refs/heads/main/repo/images/screenshots/image_0_default.webp",
|
||||||
|
"https://raw.githubusercontent.com/kodjodevf/mangayomi/refs/heads/main/repo/images/screenshots/image_1_default.webp",
|
||||||
|
"https://raw.githubusercontent.com/kodjodevf/mangayomi/refs/heads/main/repo/images/screenshots/image_2_default.webp"
|
||||||
|
],
|
||||||
|
"versions": [
|
||||||
|
{
|
||||||
|
"version": "0.5.2",
|
||||||
|
"date": "2025-02-23T16:08:37Z",
|
||||||
|
"localizedDescription": "What's Changed\r\n* Update quarkuc extractor by @yxxyun in https://github.com/kodjodevf/mangayomi/pull/393\r\n* added long press for 2x playback speed by @Schnitzel5 in https://github.com/kodjodevf/mangayomi/pull/396\r\n* fix broken category \r\n* add confirmation dialog for adding repositories\r\n\r\n\r\n**Full Changelog**: https://github.com/kodjodevf/mangayomi/compare/v0.5.1...v0.5.2",
|
||||||
|
"downloadURL": "https://github.com/kodjodevf/mangayomi/releases/download/v0.5.2/Mangayomi-v0.5.2-ios.ipa",
|
||||||
|
"size": 56680831
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.5.1",
|
||||||
|
"date": "2025-02-20T14:29:54Z",
|
||||||
|
"localizedDescription": "What's Changed\r\n* added deep links for repo urls by @Schnitzel5 in https://github.com/kodjodevf/mangayomi/pull/379\r\n* enhanced sync feature by @Schnitzel5 in https://github.com/kodjodevf/mangayomi/pull/380\r\n* optimized sync and bug fixes by @Schnitzel5 in https://github.com/kodjodevf/mangayomi/pull/381\r\n\r\n\r\n**Full Changelog**: https://github.com/kodjodevf/mangayomi/compare/v0.5.0...v0.5.1",
|
||||||
|
"downloadURL": "https://github.com/kodjodevf/mangayomi/releases/download/v0.5.1/Mangayomi-v0.5.1-ios.ipa",
|
||||||
|
"size": 56754892
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.5.0",
|
||||||
|
"date": "2025-02-12T11:16:16Z",
|
||||||
|
"localizedDescription": "\u2022 improve chapter download\r\n\u2022 improve custom navigation settings\r\n\u2022 remove embedded URLs from extensions\r\n\u2022 co\u2022written with @Schnitzel5 : update source fetching methods to utilize the given repositories",
|
||||||
|
"downloadURL": "https://github.com/kodjodevf/mangayomi/releases/download/v0.5.0/Mangayomi-v0.5.0-ios.ipa",
|
||||||
|
"size": 56707308
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"appPermissions": {
|
||||||
|
"entitlements": [
|
||||||
|
{
|
||||||
|
"name": "com.apple.security.application-groups"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "com.apple.developer.associated-domains"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "keychain-access-groups"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "aps-environment"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"privacy": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"news": [
|
||||||
|
{
|
||||||
|
"appID": "com.kodjodevf.mangayomi",
|
||||||
|
"title": "0.5.2 - 23 Feb",
|
||||||
|
"identifier": "release-0.5.2",
|
||||||
|
"caption": "Update for Mangayomi now available!",
|
||||||
|
"date": "2025-02-23T16:08:37Z",
|
||||||
|
"tintColor": "EF4444",
|
||||||
|
"imageURL": "https://raw.githubusercontent.com/kodjodevf/mangayomi/refs/heads/main/repo/images/news/update_default.webp",
|
||||||
|
"notify": true,
|
||||||
|
"url": "https://github.com/kodjodevf/mangayomi/releases/tag/v0.5.2"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
385
repo/update_source.py
Executable file
|
|
@ -0,0 +1,385 @@
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import requests
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Dict, List, Optional, Any, TypedDict
|
||||||
|
|
||||||
|
|
||||||
|
class ReleaseAsset(TypedDict):
|
||||||
|
browser_download_url: str
|
||||||
|
name: str
|
||||||
|
size: int
|
||||||
|
|
||||||
|
|
||||||
|
class GitHubRelease(TypedDict):
|
||||||
|
tag_name: str
|
||||||
|
published_at: str
|
||||||
|
body: str
|
||||||
|
assets: List[ReleaseAsset]
|
||||||
|
|
||||||
|
|
||||||
|
class VersionEntry(TypedDict):
|
||||||
|
version: str
|
||||||
|
date: str
|
||||||
|
localizedDescription: str
|
||||||
|
downloadURL: Optional[str]
|
||||||
|
size: Optional[int]
|
||||||
|
|
||||||
|
|
||||||
|
class AppInfo(TypedDict):
|
||||||
|
versions: List[VersionEntry]
|
||||||
|
version: str
|
||||||
|
versionDate: str
|
||||||
|
versionDescription: str
|
||||||
|
downloadURL: Optional[str]
|
||||||
|
size: Optional[int]
|
||||||
|
|
||||||
|
|
||||||
|
class NewsEntry(TypedDict):
|
||||||
|
appID: str
|
||||||
|
title: str
|
||||||
|
identifier: str
|
||||||
|
caption: str
|
||||||
|
date: str
|
||||||
|
tintColor: str
|
||||||
|
imageURL: str
|
||||||
|
notify: bool
|
||||||
|
url: str
|
||||||
|
|
||||||
|
|
||||||
|
class AppData(TypedDict):
|
||||||
|
apps: List[Dict[str, Any]]
|
||||||
|
news: List[NewsEntry]
|
||||||
|
|
||||||
|
|
||||||
|
class AppConfig(TypedDict):
|
||||||
|
repo_url: str
|
||||||
|
json_file: str
|
||||||
|
app_id: str
|
||||||
|
app_name: str
|
||||||
|
caption: str
|
||||||
|
tint_colour: str
|
||||||
|
image_url: str
|
||||||
|
|
||||||
|
|
||||||
|
def load_config(config_path: str) -> AppConfig:
|
||||||
|
"""
|
||||||
|
Load repo configuration values.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with open(config_path, 'r') as config_file:
|
||||||
|
config_data = json.load(config_file)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"repo_url": config_data["repo_url"],
|
||||||
|
"json_file": config_data["json_file"],
|
||||||
|
"app_id": config_data["app_id"],
|
||||||
|
"app_name": config_data["app_name"],
|
||||||
|
"caption": config_data["caption"],
|
||||||
|
"tint_colour": config_data["tint_colour"],
|
||||||
|
"image_url": config_data["image_url"],
|
||||||
|
}
|
||||||
|
|
||||||
|
except FileNotFoundError:
|
||||||
|
print(f"Configuration file not found at {config_path}")
|
||||||
|
raise
|
||||||
|
except (json.JSONDecodeError, KeyError) as e:
|
||||||
|
print(f"Error parsing configuration: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_all_releases(repo_url: str) -> List[GitHubRelease]:
|
||||||
|
"""
|
||||||
|
Fetch all GitHub releases for the repository, sorted by published date (oldest first).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[GitHubRelease]: List of all releases sorted by publication date
|
||||||
|
"""
|
||||||
|
api_url: str = f"https://api.github.com/repos/{repo_url}/releases"
|
||||||
|
headers: Dict[str, str] = {"Accept": "application/vnd.github+json"}
|
||||||
|
|
||||||
|
response = requests.get(api_url, headers=headers)
|
||||||
|
response.raise_for_status() # Raise exception for HTTP errors
|
||||||
|
|
||||||
|
releases: List[GitHubRelease] = response.json()
|
||||||
|
sorted_releases = sorted(releases, key=lambda x: x["published_at"], reverse=False)
|
||||||
|
|
||||||
|
return sorted_releases
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_latest_release(repo_url: str) -> GitHubRelease:
|
||||||
|
"""
|
||||||
|
Fetch the latest GitHub release for the repository.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
GitHubRelease: The latest release
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If no releases are found
|
||||||
|
"""
|
||||||
|
api_url: str = f"https://api.github.com/repos/{repo_url}/releases"
|
||||||
|
headers: Dict[str, str] = {"Accept": "application/vnd.github+json"}
|
||||||
|
|
||||||
|
response = requests.get(api_url, headers=headers)
|
||||||
|
response.raise_for_status() # Raise exception for HTTP errors
|
||||||
|
|
||||||
|
releases: List[GitHubRelease] = response.json()
|
||||||
|
sorted_releases = sorted(releases, key=lambda x: x["published_at"], reverse=True)
|
||||||
|
|
||||||
|
if sorted_releases:
|
||||||
|
return sorted_releases[0]
|
||||||
|
|
||||||
|
raise ValueError("No release found.")
|
||||||
|
|
||||||
|
|
||||||
|
# 2025-03-25: Reimplement this at a later date (@tanakrit-d)
|
||||||
|
# def purge_old_news(data: AppData, fetched_versions: List[str]) -> None:
|
||||||
|
# """
|
||||||
|
# Remove news entries for versions that no longer exist.
|
||||||
|
|
||||||
|
# Args:
|
||||||
|
# data: The app data dictionary
|
||||||
|
# fetched_versions: List of valid version strings
|
||||||
|
# """
|
||||||
|
# if "news" not in data:
|
||||||
|
# return
|
||||||
|
|
||||||
|
# valid_identifiers: Set[str] = {f"release-{version}" for version in fetched_versions}
|
||||||
|
# data["news"] = [
|
||||||
|
# entry for entry in data["news"] if entry["identifier"] in valid_identifiers
|
||||||
|
# ]
|
||||||
|
|
||||||
|
|
||||||
|
def format_description(description: str) -> str:
|
||||||
|
"""
|
||||||
|
Format release description by removing HTML tags and replacing certain characters.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
description: The raw description text
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Cleaned description text
|
||||||
|
"""
|
||||||
|
formatted = re.sub(r"<[^<]+?>", "", description) # HTML tags
|
||||||
|
formatted = re.sub(r"#{1,6}\s?", "", formatted) # Markdown header tags
|
||||||
|
formatted = formatted.replace(r"\*{2}", "").replace("-", "•").replace("`", '"')
|
||||||
|
|
||||||
|
return formatted
|
||||||
|
|
||||||
|
|
||||||
|
def find_download_url_and_size(
|
||||||
|
release: GitHubRelease,
|
||||||
|
) -> tuple[Optional[str], Optional[int]]:
|
||||||
|
"""
|
||||||
|
Find the download URL and size for a release's IPA file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
release: The GitHub release
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: (download_url, size) or (None, None) if not found
|
||||||
|
"""
|
||||||
|
for asset in release["assets"]:
|
||||||
|
if asset["name"].endswith(".ipa"):
|
||||||
|
return asset["browser_download_url"], asset["size"]
|
||||||
|
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_version(version: str) -> str:
|
||||||
|
"""
|
||||||
|
Strip the version tag (e.g., -hotfix) from a version string.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
version: Version string (e.g., v0.5.2-hotfix, 0.5.2-beta)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Normalized version string without the tag (e.g., 0.5.2)
|
||||||
|
"""
|
||||||
|
version = version.lstrip("v")
|
||||||
|
|
||||||
|
match = re.search(r"(\d+\.\d+\.\d+)", version)
|
||||||
|
if match:
|
||||||
|
return match.group(1)
|
||||||
|
return version
|
||||||
|
|
||||||
|
|
||||||
|
def process_versions(versions_data: List[VersionEntry]) -> List[VersionEntry]:
|
||||||
|
"""
|
||||||
|
Process the versions list to remove duplicate versions, keeping the newest version.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
versions_data (List[VersionEntry]): List of version dictionaries containing:
|
||||||
|
version: str
|
||||||
|
date: str
|
||||||
|
localizedDescription: str
|
||||||
|
downloadURL: Optional[str]
|
||||||
|
size: Optional[int]
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[VersionEntry]: Processed list with only the newest versions.
|
||||||
|
"""
|
||||||
|
# Create a list to store unique versions with their details
|
||||||
|
version_entries: List[VersionEntry] = []
|
||||||
|
|
||||||
|
# Iterate through the versions in the order they appear
|
||||||
|
for version in versions_data:
|
||||||
|
# Parse the date for comparison
|
||||||
|
current_date = datetime.fromisoformat(version["date"].replace("Z", "+00:00"))
|
||||||
|
|
||||||
|
# Check if this version already exists in unique_versions
|
||||||
|
existing_version_index = next(
|
||||||
|
(
|
||||||
|
index
|
||||||
|
for index, v in enumerate(version_entries)
|
||||||
|
if v["version"] == version["version"]
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
if existing_version_index is not None:
|
||||||
|
# Compare dates and keep the newer version
|
||||||
|
existing_date = datetime.fromisoformat(
|
||||||
|
version_entries[existing_version_index]["date"].replace("Z", "+00:00")
|
||||||
|
)
|
||||||
|
|
||||||
|
if current_date > existing_date:
|
||||||
|
version_entries[existing_version_index] = version
|
||||||
|
else:
|
||||||
|
# If no duplicate found, add to unique versions
|
||||||
|
version_entries.append(version)
|
||||||
|
|
||||||
|
return version_entries
|
||||||
|
|
||||||
|
|
||||||
|
def update_json_file(
|
||||||
|
config: AppConfig,
|
||||||
|
json_file: str,
|
||||||
|
fetched_data_all: List[GitHubRelease],
|
||||||
|
fetched_data_latest: GitHubRelease,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Update the apps.json file with the fetched GitHub releases.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
json_file: Path to the JSON file
|
||||||
|
fetched_data_all: List of all GitHub releases
|
||||||
|
fetched_data_latest: The latest GitHub release
|
||||||
|
"""
|
||||||
|
with open(json_file, "r") as file:
|
||||||
|
data: AppData = json.load(file)
|
||||||
|
|
||||||
|
app = data["apps"][0]
|
||||||
|
|
||||||
|
releases = []
|
||||||
|
|
||||||
|
# Process all releases
|
||||||
|
for release in fetched_data_all:
|
||||||
|
full_version = release["tag_name"].lstrip("v")
|
||||||
|
version_match = re.search(r"(\d+\.\d+\.\d+)(?:-([a-zA-Z0-9]+))?", full_version)
|
||||||
|
|
||||||
|
if not version_match:
|
||||||
|
continue
|
||||||
|
|
||||||
|
version_date = release["published_at"]
|
||||||
|
|
||||||
|
# Get base version without tags
|
||||||
|
base_version = normalize_version(full_version)
|
||||||
|
|
||||||
|
# Clean up description
|
||||||
|
description = release["body"]
|
||||||
|
keyword = "{APP_NAME} Release Information"
|
||||||
|
if keyword in description:
|
||||||
|
description = description.split(keyword, 1)[1].strip()
|
||||||
|
description = format_description(description)
|
||||||
|
|
||||||
|
# Find download URL and size
|
||||||
|
download_url, size = find_download_url_and_size(release)
|
||||||
|
|
||||||
|
# Skip release entries without a download URL
|
||||||
|
if not download_url:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Create version entry
|
||||||
|
version_entry: VersionEntry = {
|
||||||
|
"version": base_version,
|
||||||
|
"date": version_date,
|
||||||
|
"localizedDescription": description,
|
||||||
|
"downloadURL": download_url,
|
||||||
|
"size": size,
|
||||||
|
}
|
||||||
|
|
||||||
|
releases.append(version_entry)
|
||||||
|
|
||||||
|
deduplicated_versions = process_versions(releases)
|
||||||
|
app["versions"] = []
|
||||||
|
for i in deduplicated_versions:
|
||||||
|
app["versions"].insert(0, i)
|
||||||
|
app["versions"] = sorted(
|
||||||
|
app["versions"], key=lambda x: x.get("date", ""), reverse=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update app info with latest release
|
||||||
|
latest_version = fetched_data_latest["tag_name"].lstrip("v")
|
||||||
|
tag = fetched_data_latest["tag_name"]
|
||||||
|
version_match = re.search(r"(\d+\.\d+\.\d+)(?:-([a-zA-Z0-9]+))?", latest_version)
|
||||||
|
|
||||||
|
if not version_match:
|
||||||
|
raise ValueError("Invalid version format")
|
||||||
|
|
||||||
|
app["version"] = normalize_version(full_version)
|
||||||
|
app["versionDate"] = fetched_data_latest["published_at"]
|
||||||
|
app["versionDescription"] = format_description(fetched_data_latest["body"])
|
||||||
|
|
||||||
|
# Find latest download URL and size
|
||||||
|
download_url, size = find_download_url_and_size(fetched_data_latest)
|
||||||
|
app["downloadURL"] = download_url
|
||||||
|
app["size"] = size
|
||||||
|
|
||||||
|
# Update news entries
|
||||||
|
# 2025-03-25: Reimplement this at a later date (@tanakrit-d)
|
||||||
|
# purge_old_news(data, fetched_versions)
|
||||||
|
|
||||||
|
if "news" not in data:
|
||||||
|
data["news"] = []
|
||||||
|
|
||||||
|
# Add news entry for the latest version if it doesn't exist
|
||||||
|
news_identifier = f"release-{latest_version}"
|
||||||
|
if not any(item["identifier"] == news_identifier for item in data["news"]):
|
||||||
|
formatted_date = datetime.strptime(
|
||||||
|
fetched_data_latest["published_at"], "%Y-%m-%dT%H:%M:%SZ"
|
||||||
|
).strftime("%d %b")
|
||||||
|
|
||||||
|
news_entry: NewsEntry = {
|
||||||
|
"appID": config["app_id"],
|
||||||
|
"title": f"{latest_version} - {formatted_date}",
|
||||||
|
"identifier": news_identifier,
|
||||||
|
"caption": config["caption"],
|
||||||
|
"date": fetched_data_latest["published_at"],
|
||||||
|
"tintColor": config["tint_colour"],
|
||||||
|
"imageURL": config["image_url"],
|
||||||
|
"notify": True,
|
||||||
|
"url": f"https://github.com/{config["repo_url"]}/releases/tag/{tag}",
|
||||||
|
}
|
||||||
|
data["news"].append(news_entry)
|
||||||
|
|
||||||
|
with open(json_file, "w") as file:
|
||||||
|
json.dump(data, file, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""
|
||||||
|
Entrypoint for GitHub workflow action.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
config = load_config("repo/config.json")
|
||||||
|
fetched_data_all = fetch_all_releases(config["repo_url"])
|
||||||
|
fetched_data_latest = fetch_latest_release(config["repo_url"])
|
||||||
|
update_json_file(config, "repo/source.json", fetched_data_all, fetched_data_latest)
|
||||||
|
print("Successfully updated repo/source.json with latest releases.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error updating releases: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||