Debian-style repository for iOS package managers (Cydia, Zebra, Sileo). Index files and .deb payloads are published together to the dist branch and served through github.com/<owner>/<repo>/raw/; GitHub Pages carries the landing page and depictions.
Built as a single Go binary (repotool) with no external tool dependencies.
pool/<suite>/<component>/: source.debfiles per suite..gitkeepplaceholders keep empty directories tracked in git; the build mirrors only validated.debfiles into the published output, so placeholders never leak.repo.toml: repository configuration (TOML)internal/page/templates/index.html.tmpl: landing page template, embedded intorepotoolat build time. Override with--template <path>.resources/CydiaIcon.png: source icon file (Made by Evehly)resources/source-moved/: stub.debadvertised by the superseded Pages suite index (see "Two publish targets")distbranch: the published tree, one directory per suite, rebuilt as a single orphan commit on every deploy. Never edit it by hand.
Notes:
repo.nameandrepo.urlare required inrepo.toml.repo.urlmust usehttps://.metadata.componentis a single string ("main" by default).- Published suite roots use
./source style (deb <source-url> ./), where the source URL is the suite's directory on thedistbranch. - Set
SOURCE_DATE_EPOCHfor reproducible builds (Unix timestamp). The build workflow derives this from the latest commit timestamp automatically. A non-empty but unparseable value is rejected. - Index files are hashed with MD5, SHA1, SHA256, and SHA512 for compatibility with older clients.
- Go 1.26 or newer. The
go 1.26.0directive ingo.modtriggers automatic toolchain download whenGOTOOLCHAIN=auto(the default since Go 1.21).
- Add packages to
pool/<suite>/<component>/, or use org import. - Build:
go build -o repotool ./cmd/repotool && ./repotool build - GitHub Actions force-pushes the built tree to the
distbranch, then deploys_site/to GitHub Pages.
Main workflow: .github/workflows/build-and-deploy.yml
- Set
repo.urlinrepo.tomlto the final Pages URL. - Add one
.debtopool/stable/main/. - Push to
main. - In repository settings, enable Pages with source set to GitHub Actions.
Filename: in a Packages stanza is resolved by APT and Cydia through plain
concatenation onto the source base URI, so the index and the payloads it names
must share an origin and path prefix. The pool is far larger than the 1 GB
GitHub Pages cap, so both live on a git branch served through
github.com/<owner>/<repo>/raw/<branch>/.
That prefix and not raw.githubusercontent.com directly, because Cydia adds
sources with dist ./ and therefore requests every file as <base>/./<file>.
Its APT fork follows only 301 and 302 redirects. raw.githubusercontent.com
answers a /./ path with a 307, which Cydia reports as a failure, including
for Release and Packages. github.com/.../raw/ answers with a 302 to the
normalized raw URL, which Cydia follows.
GitHub Releases was the earlier answer and does not work here: github.com
answers an asset download with a 302 to a signed release-assets URL, and
Cydia re-encodes the % escapes in that URL when it follows the redirect,
which invalidates the signature. The blob store then rejects the request with
403 Server failed to authenticate the request. A cache hit at the CDN edge
masks this, so the failure looks intermittent.
repotool build writes two trees:
--dist-output(default_dist/) holds the published tree: per suite, the real index plus every.debit lists. CI force-pushes this to the branch named byhosting.branchas a single orphan commit. This is what package managers are pointed at.--output(default_site/) holds the Pages bundle: landing page, depictions, and a stand-in index per suite. Pages cannot redirect, so rather than leave anyone who subscribed to the old suite URL with downloads that 404, that index advertises a single stub package naming the source URL to add instead.
The push is cheap despite the pool size: the .deb blobs are already in the
repository under pool/, so git sends only the index files.
Expected files after build:
.repotool-output— marker file written at the root of each output tree.repotool buildrefuses to wipe an existing output directory unless this marker is present, so an accidental--output ~/important-stuffis rejected. It is excluded from thedistpush.- In
_dist/<suite>/:Packages(+.gz,.xz,.bz2)Release,Release.gpg,InRelease(signed variants only when a key is supplied)CydiaIcon.png- every
.debthe suite's index lists, flat in the suite directory
- In
_site/:CydiaIcon.png(root)index.html(root landing page)- Per suite: the stub index (
Packages,Release, signed variants),CydiaIcon.png,index.html, and the stub.debitself
repo-public.key(_site/root, only if arepo-public.keyfile exists at the repo root; the landing page links to it only when this file is present)depictions/(when at least one suite has entries):style.css— shared stylesheet for HTML depictions<DebBasename>/depiction.html— Cydia HTML depiction<DebBasename>/sileo.json— Sileo native depiction JSON
Source lines:
- Stable:
deb https://github.com/PlayDay-iOS/repo/raw/dist/stable/ ./ - Beta:
deb https://github.com/PlayDay-iOS/repo/raw/dist/beta/ ./
The first entry of metadata.suites is the primary suite and carries the full
catalogue. Every other suite is an overlay holding only the packages exclusive
to it, so it is added alongside the primary source rather than instead of it.
A relative Filename: cannot reach across suite directories, so each suite
carries a copy of every .deb its own index lists. That is why overlays do not
mirror the primary suite: a mirrored package would be published twice.
Nothing needs pruning: the dist branch is rebuilt from scratch on every
deploy, so a package dropped from pool/ disappears with the next push.
repotool build auto-generates Cydia HTML and Sileo native JSON depictions for every .deb in the pool, keyed by the .deb filename (without the .deb extension). Each entry's Packages stanza gets Depiction: and SileoDepiction: URLs injected that point at the generated files.
- Content is derived from the
.debcontrol file only — no per-package source files. - A compatibility banner ("iOS X.Y – Z.W") is rendered when the control sets
Depends: firmware (>= X), firmware (<< Y)clauses, or when a free-textX-Supported-iOS:field is present. - If the control already had a
Depiction:value, it is preserved underHomepage:whenHomepage:was empty.
repotool build [--output _site] [--dist-output _dist] [--config repo.toml] [--template <path>] [--depiction-template <path>] [--depiction-style <path>]
repotool import [--config repo.toml] [--allowlist org-import-allowlist.txt] [--suite <name>] [--include-prereleases] [--timeout 30m]
repotool render [--output _site] [--config repo.toml] [--template <path>]
repotool --versionFlag defaults:
--outputdefaults to<cwd>/_site--dist-outputdefaults to<cwd>/_dist--configdefaults to<cwd>/repo.toml--templateis empty by default —repotoolrenders the embedded template. Pass a file path to override.--depiction-templateis empty by default —repotooluses the embedded depiction HTML template. Pass a file path to override.--depiction-styleis empty by default —repotooluses the embedded depiction stylesheet. Pass a file path to override.--allowlistdefaults to<cwd>/org-import-allowlist.txt
The --suite flag on import defaults to the first entry of metadata.suites in repo.toml, or the TARGET_SUITE env var when set.
| Variable | Purpose |
|---|---|
SOURCE_DATE_EPOCH |
Pins Date: and landing-page timestamp for reproducible builds. |
GPG_PRIVATE_KEY |
Armored signing key. Empty = signing skipped (no error). |
GPG_PASSPHRASE |
Passphrase for GPG_PRIVATE_KEY when required. |
GH_TOKEN / GITHUB_TOKEN |
GitHub API token for import; GH_TOKEN takes precedence. |
GITHUB_API_BASE |
Alternate GitHub API endpoint (e.g. GitHub Enterprise). |
ORG_NAME |
Overrides github.org_name from repo.toml. |
TARGET_SUITE |
Default target suite for import when --suite is not passed. |
INCLUDE_PRERELEASES |
true/false; default for --include-prereleases when flag not passed. |
IMPORT_TIMEOUT |
Go duration (e.g. 30m, 2h); default for --timeout on import. |
[repo]
name = "PlayDay iOS Repo" # required
url = "https://example.com/repo/" # required, https:// only
[metadata]
origin = "PlayDay-iOS" # optional, written into Release
label = "PlayDay-iOS" # optional, written into Release
description = "..." # optional, written into Release
suites = ["stable", "beta"] # default: ["stable"]
component = "main" # default: "main"; single string
architectures = ["iphoneos-arm64", "all"] # example override; default is ["iphoneos-arm", "iphoneos-arm64", "all"]
[github]
org_name = "PlayDay-iOS" # required only for `import`
[hosting]
# owner defaults to github.org_name; repo defaults to cwd basename
# The branch is also the APT source base: index and payloads share it.
branch = "dist" # branch the built tree is force-pushed to; default: "dist"repotool reads signing data from runtime env vars (GPG_PRIVATE_KEY, GPG_PASSPHRASE). When no key is provided, signing is silently skipped: only the plain Release is written, and no Release.gpg / InRelease is produced.
To export the public key for client trust setup:
gpg --armor --export <key-id> > repo-public.keyThe build copies repo-public.key (if present at the repo root) into the output directory. The landing page links to it only when the file is present, so unsigned repos do not show a dead link.
In GitHub Actions, set these repository secrets (workflow maps them to the runtime env vars above):
APT_GPG_PRIVATE_KEYAPT_GPG_PASSPHRASE(if key is protected)
To load a key from a file rather than env var, expand it inline in the shell that invokes repotool:
GPG_PRIVATE_KEY="$(cat key.asc)" ./repotool buildFiles used by import:
- Allowlist:
org-import-allowlist.txt - Import workflow:
.github/workflows/import-org-packages.yml
Required configuration:
github.org_nameinrepo.toml(orORG_NAMEenv var).- A GitHub token via
GH_TOKEN(recommended) orGITHUB_TOKEN. The import command errors out if either is missing because unauthenticated access is rate-limited to 60 requests per hour.
How it works:
- Add allowed repository names to
org-import-allowlist.txt. - Run the import workflow manually or wait for the schedule.
- Set
target_suiteto the desired suite name when running manually (defaults tostable). - Imported packages are validated and placed into
pool/<target_suite>/<component>/. - The import workflow commits new packages and triggers the build/deploy workflow, which publishes updated metadata.
Validation checks:
- Required control fields:
Package,Version,Architecture,Maintainer,Description - Allowed architectures from
repo.toml - Duplicate canonical names with different content are rejected