Generate AltStore
apps.json source files from GitHub Releases IPA assets.
Static app metadata (name, bundle identifier, icon and screenshot URLs, descriptions…) comes from a TOML config; everything dynamic (version, build version, release date, download URL, file size, release notes) is read live from the GitHub Releases API.
GitHub Releases API ──┐
├──► altgen ──► apps.json
app config TOML ──────┘
pip install altgen
altgen --versionRequires Python ≥ 3.11.
One TOML config = one app = one apps.json:
altgen -c piliplus.toml # writes apps.json next to the config
altgen -c piliplus.toml -o out/piliplus.json # override output pathOr skip the config file entirely for a quick single-app source (any CLI flag overrides its TOML counterpart):
altgen --repo owner/App --app-name App --bundle-id com.owner.app -o apps.jsonHosting many sources is just many configs — loop over them or use a CI
matrix, one altgen -c <config> per app.
Combine several apps.json files into one source (e.g. host many apps under a single source while keeping one config per app):
altgen merge a.json b.json -o merged.json
altgen merge -c merge.toml a.json b.json # root values + output from TOML
altgen merge --name MySource --tint-color "#00AEEF" a.json b.jsonappsare taken from the inputs and sorted by name (case-insensitive);newsentries from all inputs (rootnewsplus each app'snews) are combined and sorted newest-first.- Root values (
name,subtitle,description,icon_url,website,tint_color) come from the CLI flags or the config's[source]table — same rules as build mode, and--name(or[source] name) is required. - Merge configs only support
[source]and[output]tables; build-mode tables ([github],[app], …) are rejected. - Inputs are merged verbatim: duplicate
bundleIdentifiervalues and duplicate newsidentifiervalues across inputs are allowed, and both entries are kept.
See examples/merge.toml for a full merge config.
Unauthenticated requests are limited to 60/hour; a token raises that to
5,000/hour. Precedence: --token > GITHUB_TOKEN env var > [github] token.
Only [github] repo, [app] name, and [app] bundle_identifier are
required. Keys are snake_case in TOML and become the AltStore camelCase
JSON keys (bundle_identifier → bundleIdentifier, icon_url →
iconURL, min_os_version → minOSVersion, …). Unknown keys are
rejected with an error.
See examples/piliplus.toml for a full example.
[github]
repo = "owner/App" # REQUIRED: GitHub repo with releases
token = "" # optional (see above)
[source] # the source this apps.json describes
name = "App" # defaults to the repo name
subtitle = ""
description = ""
icon_url = "" # omitted from JSON when unset
website = ""
tint_color = "#00AEEF" # must be #RRGGBB
[app] # the app inside the source
name = "App" # REQUIRED
bundle_identifier = "com.x.y" # REQUIRED
developer_name = "" # defaults to the repo owner (CLI mode)
subtitle = ""
description = "" # fallback when a release body is empty
icon_url = "" # falls back to [source].icon_url
screenshots = ["https://…"]
tint_color = "" # falls back to [source].tint_color
min_os_version = "14.0" # omitted from versions when unset
[versions]
strip_v_prefix = true # tag "v1.2.3" → version "1.2.3"
include_prereleases = false # drafts are always skipped
asset_pattern = "\\.ipa$" # regex, case-insensitive search on asset name
build_version_pattern = "\\+(\\d+)\\.ipa$" # group 1 = buildVersion; no match → key omitted
# version_pattern = "…" # optional: extract version with this regex instead of the tag
# (for projects whose tags lack a version); no match → tag-derived version
# version_source = "release" # "release" (default, version_pattern on the release name),
# "filename" (version_pattern on the IPA asset name), or
# "ipa" (read the version from the IPA's Info.plist;
# version_pattern is ignored)
max_versions = 1 # default: newest version only; 0 = all versions
[news]
enabled = true
title_template = "{name} {version} - {date}" # default; placeholders: {name} {version} {tag} {date} (e.g. 07 Aug 2026)
caption_template = "" # optional; default: "{name} {version} is available."
image_url = "" # optional; omitted from JSON when unset
# news appID = [app] bundle_identifier
max_entries = 0 # 0 = unlimited; caps after sorting; news already limited to releases kept by max_versions
[output]
path = "apps.json" # resolved against THIS file's directory- Versions are sorted newest-first by
(date, version); one version entry per matching release asset. A release with several IPAs produces several entries — sharing one version by default, or one version each whenversion_source = "filename". - The version is the release tag by default (
strip_v_prefixstrips a leadingv). For projects whose tags carry no version (e.g.youproextra-ipa2), setversion_patternto extract it from the release name or IPA filename instead; on no match the version falls back to the tag-derived value. - For rolling tags such as
nightly, setversion_source = "ipa": altgen downloads each candidate IPA and readsCFBundleShortVersionString(falling back toCFBundleVersion) from the main app'sInfo.plist, so the emitted version matches the build inside the IPA. The tag is still used for news identifiers. Downloads are limited to releases that can survive themax_versionscap; on a download or parse failure altgen warns and falls back to the tag-derived version.version_patternis ignored in this mode. - By default only the newest version is emitted (
max_versions = 1); setmax_versions = 0(or--max-versions 0) to include all versions. - News follows the same convention: one news entry per release, kept only
when that release still has a version entry after the
max_versionscap. Matching is by release, not by version string, so several releases that resolve to the same version (e.g. a date-bearing tag likerelease-v1.2.3-2026-08-21vs...-2026-08-14) each get their own news entry only while their version entry survives the cap.[news] max_entriescan further cap the list. - News entries follow the AltStore spec:
appIDfirst (the app'sbundle_identifier), a full ISOdatetimestamp,identifierderived from the app name and release tag (<app-name>-release-<tag>), and an optionalimageURL;title_template/caption_templatesupport{name},{version},{tag},{date}placeholders. - A release with no matching asset contributes nothing — not even a news entry. One news entry is emitted per release that has assets.
- Empty output (no releases, only drafts, …) is a valid source: altgen warns on stderr and exits 0.
altgen [-c PATH] [--repo OWNER/REPO] [--token TOKEN]
[--name] [--subtitle] [--description] [--icon-url] [--website] [--tint-color]
[--app-name] [--bundle-id] [--developer-name] [--app-subtitle]
[--app-description] [--app-icon-url] [--app-tint-color] [--min-os-version]
[--screenshots URL …] [--include-prereleases] [--max-versions N]
[-o PATH] [-q] [-v] [--version]
altgen merge APPS_JSON… [-c PATH] [--name] [--subtitle] [--description]
[--icon-url] [--website] [--tint-color] [-o PATH] [-q]
- Without
-c,--repo,--app-name, and--bundle-idare required (build mode). --max-versions Ncaps the output after sorting (newest first); it defaults to1(latest version only) and0means all versions.- CLI flags override TOML values;
-oresolves against the current directory while[output] pathresolves against the config file's directory (so a config next to its sources works from any CWD). altgen mergecombines apps.json files; see Merge sources.-vlogs skipped releases (draft / prerelease / no matching assets) to stderr;-qsilences the success message.- Exit codes:
0success,1GitHub, IO, or write error,2usage or configuration error.
pip install -e ".[dev]"
pytest # fully offline — fixtures captured from the GitHub API