Build sideload IPA #318
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Build a sideload-oriented Telegram IPA on GitHub Actions. | |
| # | |
| # Research (upstream #1120/#1136 + ad/Telegram-iOS fake-codesigning docs): | |
| # • Build release_arm64 (not ci_arm64 dbg) with bundled fake-codesigning. | |
| # • Keep bundle_id/team_id matching fake profiles (ph.teleg.Telegrapf / C67CF9S4VU). | |
| # • Disable extensions + Siri/iCloud for personal-Apple-ID re-sign. | |
| # • Publish the Make.py IPA (embedded.mobileprovision + _CodeSignature intact). | |
| # • Final device install still requires Sideloadly/AltStore re-sign — see docs/SIDELOAD.md. | |
| # | |
| # Triggers (on-demand only — not every master push): | |
| # • Actions → Build sideload IPA → Run workflow | |
| # • or push a version tag: git tag vX.Y.Z && git push --tags | |
| # | |
| # Optional secrets: TELEGRAM_API_ID / TELEGRAM_API_HASH | |
| name: Build sideload IPA | |
| on: | |
| push: | |
| tags: ['v*'] | |
| workflow_dispatch: | |
| inputs: | |
| create_release: | |
| description: Publish / update a GitHub Release with the IPA | |
| type: boolean | |
| default: true | |
| generate_dsym: | |
| description: Also produce debug symbols (slower; needed to name a crash) | |
| type: boolean | |
| default: false | |
| concurrency: | |
| group: telegram-ios-ipa-${{ github.ref }} | |
| cancel-in-progress: false | |
| permissions: | |
| contents: write | |
| jobs: | |
| regression: | |
| name: Regression tests | |
| runs-on: macos-26 | |
| timeout-minutes: 20 | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: '22' | |
| - name: Accessibility contracts | |
| run: python3 -m unittest discover -s Tests/VoiceOverContracts | |
| - name: Reader links and signing key cleanup | |
| run: | | |
| node --test Tests/Readability/links.test.cjs | |
| python3 -m unittest discover -s Tests/BuildTools | |
| - name: MTProto malformed-input regression tests | |
| run: | | |
| clang -fobjc-arc -framework Foundation -I submodules/MtProtoKit/PublicHeaders \ | |
| submodules/MtProtoKit/Sources/MTInputStream.m Tests/MTInputStream/main.m -o /tmp/mtinput-tests | |
| /tmp/mtinput-tests | |
| - name: VLESS parser, configuration and lifecycle tests | |
| run: swift test --package-path submodules/TelegramVLESS | |
| - name: Archive session and account isolation tests | |
| run: python3 Tests/ArchiveLock/run.py | |
| - name: Player regression tests and production build | |
| working-directory: submodules/TelegramUniversalVideoContent/PlayerSource | |
| run: | | |
| npm ci --ignore-scripts | |
| npm test | |
| npm run build-release | |
| npm audit --audit-level=high | |
| build: | |
| needs: regression | |
| # Xcode 27 preview IPA crashed on device — stay on macos-26 / Xcode 26.2. | |
| name: Build IPA (macos-26) | |
| runs-on: macos-26 | |
| timeout-minutes: 240 | |
| env: | |
| BUILD_CONFIGURATION: release_arm64 | |
| BAZEL_USER_ROOT: /private/var/tmp/_bazel_telegram_ios | |
| BAZEL_CACHE_DIR: ${{ github.workspace }}/.telegram-bazel-cache | |
| BAZEL_REPO_CACHE: ${{ github.workspace }}/.telegram-bazel-repo-cache | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| fetch-depth: 0 | |
| submodules: false | |
| - name: Fix submodule URLs for forks | |
| run: | | |
| git config --file=.gitmodules submodule.submodules/rlottie/rlottie.url \ | |
| https://github.com/TelegramMessenger/rlottie.git | |
| git config --file=.gitmodules submodule.submodules/TgVoipWebrtc/tgcalls.url \ | |
| https://github.com/TelegramMessenger/tgcalls.git | |
| git submodule sync --recursive | |
| - name: Checkout submodules | |
| run: git submodule update --init --recursive --depth 1 --jobs 8 | |
| - name: Fetch libxray framework for VLESS support | |
| run: | | |
| mkdir -p third-party/libxray | |
| if [ ! -d third-party/libxray/LibXray.xcframework ]; then | |
| curl -sL -o /tmp/libxray-apple-cgo.zip "https://github.com/XTLS/libxray/releases/download/v26.9.9/libxray-apple-cgo.zip" | |
| echo "84a11f0923efb65be7631b3c4cac75bd4351fc13b9cc2394def6af5c1696e9fa /tmp/libxray-apple-cgo.zip" | shasum -a 256 -c - | |
| unzip -q -o /tmp/libxray-apple-cgo.zip -d /tmp/libxray-extract | |
| XCFW=$(find /tmp/libxray-extract -name "LibXray.xcframework" -type d | head -1) | |
| if [ -z "$XCFW" ]; then | |
| echo "LibXray.xcframework not found in the archive"; exit 1 | |
| fi | |
| # Keep only the slices the iOS app build needs (device + simulator). | |
| # gomobile names the simulator slice ios-arm64_x86_64-simulator (fat); | |
| # accept both known spellings. | |
| mkdir -p third-party/libxray/LibXray.xcframework | |
| cp "$XCFW/Info.plist" third-party/libxray/LibXray.xcframework/ | |
| for slice in ios-arm64 ios-arm64-simulator ios-arm64_x86_64-simulator; do | |
| if [ -d "$XCFW/$slice" ]; then | |
| cp -R "$XCFW/$slice" third-party/libxray/LibXray.xcframework/ | |
| fi | |
| done | |
| ls third-party/libxray/LibXray.xcframework | |
| echo "::warning::LibXray layout: $(find third-party/libxray -maxdepth 3 | tr '\n' '|' | head -c 900)" | |
| echo "::warning::LibXray binaries: $(find third-party/libxray -name '*.a' -o -name 'LibXray' -type f | head -5 | while read f; do echo -n "$f=$(file -b "$f" | head -c 60) "; done)" | |
| fi | |
| - name: Apply vendored tgcalls patch (managed SOCKS5 media routing) | |
| run: | | |
| cd submodules/TgVoipWebrtc/tgcalls | |
| git apply --check ../patches/vless-media-socks.patch | |
| git apply ../patches/vless-media-socks.patch | |
| - name: Select Xcode | |
| run: | | |
| set -euo pipefail | |
| XCODE_VERSION="$(python3 -c 'import json; print(json.load(open("versions.json"))["xcode"])')" | |
| DEVELOPER_DIR="/Applications/Xcode_${XCODE_VERSION}.app/Contents/Developer" | |
| if [ ! -d "$DEVELOPER_DIR" ]; then | |
| CANDIDATE="$(ls -d /Applications/Xcode_26*.app 2>/dev/null | sort -V | tail -1 || true)" | |
| [ -n "$CANDIDATE" ] || { echo "::error::No Xcode 26.x found"; exit 1; } | |
| DEVELOPER_DIR="${CANDIDATE}/Contents/Developer" | |
| echo "::warning::using $(basename "$CANDIDATE") instead of ${XCODE_VERSION}" | |
| fi | |
| sudo xcode-select -s "$DEVELOPER_DIR" | |
| xcodebuild -version | |
| echo "CPUS=$(sysctl -n hw.ncpu)" | |
| - name: Prepare Metal toolchain | |
| run: | | |
| set -euo pipefail | |
| if xcrun -f metal >/dev/null 2>&1; then | |
| echo "metal already available: $(xcrun -f metal)" | |
| else | |
| sudo xcodebuild -downloadComponent MetalToolchain | |
| rm -f "$(getconf DARWIN_USER_TEMP_DIR)/xcrun_db" || true | |
| fi | |
| xcrun -f metal | |
| xcrun metal -v | |
| - name: Free disk space | |
| run: | | |
| set -euo pipefail | |
| df -h / | |
| sudo rm -rf \ | |
| /Users/runner/Library/Developer/CoreSimulator/Caches \ | |
| /Library/Developer/CoreSimulator/Profiles/Runtimes/* \ | |
| /Users/runner/.cargo \ | |
| /Users/runner/.rustup \ | |
| /usr/local/lib/android \ | |
| /Users/runner/Library/Android || true | |
| df -h / | |
| - name: Cache Bazel (warm incremental compiles) | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ${{ env.BAZEL_CACHE_DIR }} | |
| ${{ env.BAZEL_REPO_CACHE }} | |
| # release_arm64 cache; versions.json bump isolates from Xcode 27 caches. | |
| key: bazel-warm-release_arm64-${{ hashFiles('versions.json', 'MODULE.bazel', 'MODULE.bazel.lock') }} | |
| restore-keys: | | |
| bazel-warm-release_arm64- | |
| bazel-warm-ci_arm64- | |
| bazel-pm-macOS-ci_arm64- | |
| bazel-turbo-macOS-ci_arm64- | |
| bazel-pm- | |
| bazel-turbo- | |
| bazel- | |
| - name: Write sideload configuration | |
| env: | |
| TELEGRAM_API_ID: ${{ secrets.TELEGRAM_API_ID }} | |
| TELEGRAM_API_HASH: ${{ secrets.TELEGRAM_API_HASH }} | |
| run: | | |
| set -euo pipefail | |
| python3 <<'PY' | |
| import json, os | |
| path = "build-system/ci-configuration.json" | |
| with open("build-system/sideload-configuration.json") as f: | |
| cfg = json.load(f) | |
| # Bundle/team MUST match build-system/fake-codesigning profiles. | |
| # Do not override from secrets unless real Apple profiles are also provided. | |
| cfg["bundle_id"] = "ph.teleg.Telegrapf" | |
| cfg["team_id"] = "C67CF9S4VU" | |
| cfg["enable_siri"] = False | |
| cfg["enable_icloud"] = False | |
| api_id = os.environ.get("TELEGRAM_API_ID") or "" | |
| api_hash = os.environ.get("TELEGRAM_API_HASH") or "" | |
| if api_id and api_hash: | |
| cfg["api_id"] = api_id | |
| cfg["api_hash"] = api_hash | |
| with open(path, "w") as f: | |
| json.dump(cfg, f, indent="\t") | |
| f.write("\n") | |
| print( | |
| f"api_id={cfg['api_id']} bundle_id={cfg['bundle_id']} " | |
| f"team_id={cfg['team_id']} siri={cfg['enable_siri']} icloud={cfg['enable_icloud']}" | |
| ) | |
| PY | |
| - name: Compute build number | |
| run: | | |
| set -euo pipefail | |
| BUILD_NUMBER_OFFSET="$(cat build_number_offset)" | |
| APP_VERSION="$(python3 -c 'import json; print(json.load(open("versions.json"))["app"])')" | |
| BUILD_NUMBER="$((BUILD_NUMBER_OFFSET + GITHUB_RUN_NUMBER))" | |
| { | |
| echo "APP_VERSION=$APP_VERSION" | |
| echo "BUILD_NUMBER=$BUILD_NUMBER" | |
| echo "IPA_NAME=Telegram-${APP_VERSION}-${BUILD_NUMBER}-sideload.ipa" | |
| } >> "$GITHUB_ENV" | |
| echo "Telegram ${APP_VERSION} (${BUILD_NUMBER})" | |
| - name: Archive sideload iPhone application | |
| env: | |
| # `release_arm64` is an optimised, stripped build, so a crash address in it resolves to | |
| # nothing at all — not from the binary, not from a dSYM that was never produced. Symbols | |
| # are off by default because `dsymutil` is slow and most builds never need them; ask for | |
| # them on the run where a crash has to be named. | |
| DSYM_FLAG: ${{ inputs.generate_dsym && ' ' || '--skipDsym' }} | |
| run: | | |
| set -euo pipefail | |
| mkdir -p "$BAZEL_CACHE_DIR" "$BAZEL_REPO_CACHE" artifacts build/artifacts | |
| python3 build-system/Make/ImportCertificates.py \ | |
| --path build-system/fake-codesigning/certs | |
| set -o pipefail | |
| python3 -u build-system/Make/Make.py \ | |
| --overrideXcodeVersion \ | |
| --bazelUserRoot="$BAZEL_USER_ROOT" \ | |
| --cacheDir="$BAZEL_CACHE_DIR" \ | |
| build \ | |
| --configurationPath=build-system/ci-configuration.json \ | |
| --codesigningInformationPath=build-system/fake-codesigning \ | |
| --configuration="$BUILD_CONFIGURATION" \ | |
| --buildNumber="$BUILD_NUMBER" \ | |
| --disableExtensions \ | |
| $DSYM_FLAG \ | |
| --ciFast \ | |
| --outputBuildArtifactsPath=build/artifacts \ | |
| 2>&1 | tee release-build.log | |
| - name: Report compiler errors | |
| if: failure() | |
| run: | | |
| { | |
| grep -E '(^| )error:' release-build.log | tail -20 | |
| grep -B2 -A10 'Undefined symbols for' release-build.log | tail -50 | |
| grep -E 'ld: |duplicate symbol|file was built for' release-build.log | tail -20 | |
| } \ | |
| | while IFS= read -r line; do | |
| safe="${line//'%'/'%25'}" | |
| safe="${safe//$'\r'/'%0D'}" | |
| safe="${safe//$'\n'/'%0A'}" | |
| echo "::error title=Telegram compiler::$safe" | |
| done | |
| - name: Collect Make.py IPA (keep fake signature) | |
| run: | | |
| set -euo pipefail | |
| # Prefer the Bazel/Make.py IPA — keeps embedded.mobileprovision + _CodeSignature. | |
| # Do NOT re-package in a way that drops signing metadata. | |
| SRC="$(find build/artifacts bazel-bin/Telegram bazel-out \ | |
| -name 'Telegram.ipa' -type f 2>/dev/null | head -1 || true)" | |
| [ -n "$SRC" ] || { echo "::error::no Telegram.ipa from Make.py"; exit 1; } | |
| cp -f "$SRC" "artifacts/${IPA_NAME}" | |
| ls -lh "artifacts/${IPA_NAME}" | |
| # Sanity: fake-signed IPA should contain provisioning + code signature. | |
| python3 - <<'PY' | |
| import os, sys, zipfile | |
| path = os.path.join("artifacts", os.environ["IPA_NAME"]) | |
| with zipfile.ZipFile(path) as z: | |
| names = z.namelist() | |
| has_prov = any(n.endswith("embedded.mobileprovision") for n in names) | |
| has_sig = any("/_CodeSignature/" in n for n in names) | |
| print(f"entries={len(names)} embedded.mobileprovision={has_prov} _CodeSignature={has_sig}") | |
| if not has_prov or not has_sig: | |
| print("::warning::IPA missing expected fake-signing artifacts; Sideloadly may still re-sign") | |
| PY | |
| - name: Publish GitHub release | |
| if: startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch' && inputs.create_release == true) | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| set -euo pipefail | |
| IPA="$PWD/artifacts/${IPA_NAME}" | |
| test -f "$IPA" | |
| if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then | |
| TAG="${GITHUB_REF_NAME}" | |
| else | |
| TAG="v${APP_VERSION}-${BUILD_NUMBER}" | |
| fi | |
| PRE_BANNER="" | |
| TITLE="Telegram ${APP_VERSION} (${BUILD_NUMBER}) sideload" | |
| # Do not expand an empty array under `set -u` — bash treats | |
| # "${RELEASE_FLAGS[@]}" as unbound when the array has no elements | |
| # (breaks non-*-pre / Latest releases). | |
| RELEASE_EXTRA_ARGS=() | |
| if [[ "$TAG" == *-pre ]]; then | |
| PRE_BANNER="⚠️ **Pre-release** for device testing. Not marked Latest." | |
| TITLE="Telegram ${APP_VERSION} (${BUILD_NUMBER}) sideload pre-release" | |
| RELEASE_EXTRA_ARGS=(--prerelease --latest=false) | |
| fi | |
| NOTES="$(mktemp)" | |
| # Prefer the matching ## [TAG] section from CHANGELOG.md; else git log since previous tag. | |
| CHANGELOG_BODY="$(mktemp)" | |
| if [[ -f CHANGELOG.md ]]; then | |
| python3 - "$TAG" CHANGELOG.md "$CHANGELOG_BODY" <<'PY' | |
| import re, sys | |
| tag, path, out = sys.argv[1], sys.argv[2], sys.argv[3] | |
| text = open(path, encoding="utf-8").read() | |
| # ## [v12.9.2-3845] or ## v12.9.2-3845 … | |
| pat = re.compile( | |
| r"^##\s*\[?" + re.escape(tag) + r"\]?[^\n]*\n(.*?)(?=^##\s|\Z)", | |
| re.M | re.S, | |
| ) | |
| m = pat.search(text) | |
| body = (m.group(1).strip() if m else "") | |
| open(out, "w", encoding="utf-8").write(body) | |
| PY | |
| fi | |
| if [[ ! -s "$CHANGELOG_BODY" ]]; then | |
| PREV="$(git describe --tags --abbrev=0 "${GITHUB_SHA}^" 2>/dev/null || true)" | |
| if [[ -n "$PREV" ]]; then | |
| { | |
| echo "### Commits since ${PREV}" | |
| echo | |
| git log --pretty=format:'- %s (%h)' "${PREV}..${GITHUB_SHA}" | |
| echo | |
| } > "$CHANGELOG_BODY" | |
| else | |
| echo "_No CHANGELOG.md section for ${TAG} and no previous tag._" > "$CHANGELOG_BODY" | |
| fi | |
| fi | |
| cat > "$NOTES" <<EOF | |
| Telegram iOS ${APP_VERSION} (${BUILD_NUMBER}) — sideload IPA | |
| ${PRE_BANNER} | |
| Fake-signed \`release_arm64\` build for re-signing with Sideloadly / AltStore. | |
| This is **not** App Store–installable as-is. | |
| ### Changelog | |
| $(cat "$CHANGELOG_BODY") | |
| ### Install (E-Sign / Sideloadly) | |
| 1. Change the Bundle ID when signing (avoids clashing with App Store Telegram). | |
| 2. Sign + install, then trust the certificate on the device. | |
| 3. This build falls back to Documents if App Groups are missing (fixes classic E-Sign black screen / Error 2). | |
| See \`docs/SIDELOAD.md\` in the repo for details. | |
| ### Build | |
| - Configuration: \`release_arm64\` + \`build-system/fake-codesigning\` | |
| - Toolchain: Xcode 26.2 (\`runs-on: macos-26\`) — Xcode 27 preview parked (device crash) | |
| - Bundle / team (build-time): \`ph.teleg.Telegrapf\` / \`C67CF9S4VU\` | |
| - Extensions / Siri / iCloud: disabled | |
| - Commit: \`${GITHUB_SHA}\` | |
| EOF | |
| if gh release view "$TAG" >/dev/null 2>&1; then | |
| gh release upload "$TAG" "$IPA" --clobber | |
| if ((${#RELEASE_EXTRA_ARGS[@]})); then | |
| gh release edit "$TAG" --title "$TITLE" --notes-file "$NOTES" "${RELEASE_EXTRA_ARGS[@]}" | |
| else | |
| gh release edit "$TAG" --title "$TITLE" --notes-file "$NOTES" --latest | |
| fi | |
| else | |
| if ((${#RELEASE_EXTRA_ARGS[@]})); then | |
| gh release create "$TAG" "$IPA" \ | |
| --target "$GITHUB_SHA" \ | |
| --title "$TITLE" \ | |
| --notes-file "$NOTES" \ | |
| "${RELEASE_EXTRA_ARGS[@]}" | |
| else | |
| gh release create "$TAG" "$IPA" \ | |
| --target "$GITHUB_SHA" \ | |
| --title "$TITLE" \ | |
| --notes-file "$NOTES" \ | |
| --latest | |
| fi | |
| fi | |
| - name: Upload debug symbols | |
| if: ${{ inputs.generate_dsym && (success() || failure()) }} | |
| uses: actions/upload-artifact@v5 | |
| with: | |
| name: Telegram-${{ env.APP_VERSION }}-${{ env.BUILD_NUMBER }}-dSYMs | |
| path: build/artifacts/Telegram.DSYMs.zip | |
| if-no-files-found: warn | |
| retention-days: 14 | |
| - name: Upload sideload IPA artifact | |
| if: success() || failure() | |
| uses: actions/upload-artifact@v5 | |
| with: | |
| name: Telegram-${{ env.APP_VERSION }}-${{ env.BUILD_NUMBER }}-sideload | |
| path: artifacts/${{ env.IPA_NAME }} | |
| if-no-files-found: ignore | |
| retention-days: 14 |