add ipa signing; integrate repo; add entitlements

This commit is contained in:
Tanakrit-D 2025-03-16 16:21:22 +11:00
parent 13fddac7bf
commit 5728ea46db
32 changed files with 583 additions and 8 deletions

View file

@ -9,6 +9,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}
cancel-in-progress: true
env:
ZSIGN_VERSION: '0.7'
jobs:
build-and-release-android-apks:
permissions:
@ -77,8 +80,12 @@ jobs:
runs-on: macos-latest
steps:
# Checkout branch
- name: checkout branch
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.ref }}
path: .
- name: setup flutter
uses: subosito/flutter-action@v2
@ -94,16 +101,27 @@ jobs:
- name: 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
env:
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
run: |
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
mkdir Payload
cd Payload
ln -s ../Runner.app
cd ..
zip -r app-release.ipa Payload
mv app-release.ipa Mangayomi-${{ github.ref_name }}-ios.ipa
zip -r ./Mangayomi-${{ github.ref_name }}-ios.ipa Payload
- name: upload artifact ios ipa
uses: actions/upload-artifact@v4
@ -402,3 +420,7 @@ jobs:
with:
artifacts: dist/Mangayomi-*.deb
allowUpdates: true
ios-source-build:
needs: build-and-release-ios-ipa
uses: ./.github/workflows/update_sideloading_source.yml

View 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

View file

@ -32,7 +32,16 @@ Features include:
## Download
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>
&nbsp;
<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>
&nbsp;
<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>
&nbsp;
<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

20
repo/colours.json Normal file
View 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
View 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"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 528 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 854 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 369 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

86
repo/source.json Executable file
View 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
View 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()