1- # Build Telegram-iOS (Bazel) on GitHub-hosted macOS runners.
1+ # Build unsigned Telegram-iOS IPA (Bazel), styled after decoder-dev/PrivateMusic2 CI:
2+ # fast runner, unsigned artifact, tag → GitHub Release.
23#
3- # Optional repository secrets (override the committed appstore-configuration.json):
4- # TELEGRAM_API_ID / TELEGRAM_API_HASH – from https://my.telegram.org
5- # TELEGRAM_BUNDLE_ID / TELEGRAM_TEAM_ID
6- #
7- # If secrets are unset, CI uses build-system/appstore-configuration.json (same as
8- # the previous open-source workflow). Artifacts: Telegram.ipa + Telegram.DSYMs.zip.
4+ # Optional secrets TELEGRAM_API_ID / TELEGRAM_API_HASH / TELEGRAM_BUNDLE_ID / TELEGRAM_TEAM_ID
5+ # override build-system/appstore-configuration.json when set.
96
107name : Build iOS
118
129on :
1310 push :
1411 branches : [master]
12+ tags : ['v*']
1513 pull_request :
1614 workflow_dispatch :
1715 inputs :
@@ -36,25 +34,22 @@ permissions:
3634
3735jobs :
3836 build :
39- name : Build (${{ github.event.inputs.configuration || 'release_arm64' }} )
40- runs-on : macos-26
41- timeout-minutes : 480
37+ name : Build IPA (xcode-27 )
38+ runs-on : xcode-27
39+ timeout-minutes : 360
4240
4341 env :
4442 BUILD_CONFIGURATION : ${{ github.event.inputs.configuration || 'release_arm64' }}
45- SOURCE_DIR : /Users/Shared/telegram-ios
4643 BAZEL_USER_ROOT : /private/var/tmp/_bazel_telegram_ios
47- BAZEL_CACHE_DIR : /Users/Shared/ telegram-bazel-cache
44+ BAZEL_CACHE_DIR : ${{ github.workspace }}/. telegram-bazel-cache
4845
4946 steps :
5047 - name : Checkout
51- uses : actions/checkout@v4
48+ uses : actions/checkout@v5
5249 with :
5350 fetch-depth : 0
5451 submodules : false
5552
56- # Relative submodule URLs (../rlottie.git, ../tgcalls.git) resolve under the
57- # fork owner and 404. Point them at the upstream TelegramMessenger repos.
5853 - name : Fix submodule URLs for forks
5954 run : |
6055 git config --file=.gitmodules submodule.submodules/rlottie/rlottie.url \
@@ -70,29 +65,26 @@ jobs:
7065 - name : Select Xcode
7166 run : |
7267 set -euo pipefail
73- XCODE_VERSION="$(python3 -c 'import json; print(json.load(open("versions.json"))["xcode"])')"
74- DEVELOPER_DIR="/Applications/Xcode_${XCODE_VERSION}.app/Contents/Developer"
75- if [ ! -d "$DEVELOPER_DIR" ]; then
76- echo "Xcode ${XCODE_VERSION} not found. Installed:"
77- ls /Applications | grep -i Xcode || true
78- # Prefer an exact match; otherwise use the newest installed 26.x.
79- CANDIDATE="$(ls -d /Applications/Xcode_26*.app 2>/dev/null | sort -V | tail -1 || true)"
80- if [ -z "$CANDIDATE" ]; then
81- echo "::error::No Xcode 26.x installation found on the runner."
82- exit 1
68+ if [ -d /Applications/Xcode_27.0.app ]; then
69+ sudo xcode-select -s /Applications/Xcode_27.0.app
70+ elif [ -d /Applications/Xcode.app ]; then
71+ sudo xcode-select -s /Applications/Xcode.app
72+ else
73+ XCODE_VERSION="$(python3 -c 'import json; print(json.load(open("versions.json"))["xcode"])')"
74+ DEVELOPER_DIR="/Applications/Xcode_${XCODE_VERSION}.app/Contents/Developer"
75+ if [ ! -d "$DEVELOPER_DIR" ]; then
76+ CANDIDATE="$(ls -d /Applications/Xcode_26*.app 2>/dev/null | sort -V | tail -1 || true)"
77+ [ -n "$CANDIDATE" ] || { echo "::error::No Xcode found"; exit 1; }
78+ DEVELOPER_DIR="${CANDIDATE}/Contents/Developer"
8379 fi
84- DEVELOPER_DIR="${CANDIDATE}/Contents/Developer"
85- echo "::warning::versions.json requests Xcode ${XCODE_VERSION}; using $(basename "$CANDIDATE") instead."
80+ sudo xcode-select -s "$DEVELOPER_DIR"
8681 fi
87- sudo xcode-select -s "$DEVELOPER_DIR"
8882 xcodebuild -version
89- echo "DEVELOPER_DIR=$DEVELOPER_DIR" >> "$GITHUB_ENV"
9083
9184 - name : Free disk space
9285 run : |
9386 set -euo pipefail
9487 df -h /
95- # Drop unused simulator runtimes / large caches that Bazel does not need.
9688 sudo rm -rf \
9789 /Users/runner/Library/Developer/CoreSimulator/Caches \
9890 /Library/Developer/CoreSimulator/Profiles/Runtimes/* \
@@ -102,17 +94,16 @@ jobs:
10294 /Users/runner/Library/Android || true
10395 df -h /
10496
105- - name : Create canonical source directory
106- run : |
107- set -euo pipefail
108- # Source paths are embedded in the binary; keep them stable across CI runs.
109- sudo mkdir -p /Users/Shared
110- sudo rm -rf "$SOURCE_DIR"
111- sudo cp -R "$GITHUB_WORKSPACE" "$SOURCE_DIR"
112- sudo chown -R "$(whoami)" "$SOURCE_DIR"
97+ - name : Cache Bazel
98+ uses : actions/cache@v4
99+ with :
100+ path : ${{ env.BAZEL_CACHE_DIR }}
101+ key : bazel-${{ runner.os }}-${{ env.BUILD_CONFIGURATION }}-${{ hashFiles('versions.json', 'MODULE.bazel', 'MODULE.bazel.lock') }}
102+ restore-keys : |
103+ bazel-${{ runner.os }}-${{ env.BUILD_CONFIGURATION }}-
104+ bazel-${{ runner.os }}-
113105
114106 - name : Write build configuration
115- working-directory : ${{ env.SOURCE_DIR }}
116107 env :
117108 TELEGRAM_API_ID : ${{ secrets.TELEGRAM_API_ID }}
118109 TELEGRAM_API_HASH : ${{ secrets.TELEGRAM_API_HASH }}
@@ -122,34 +113,28 @@ jobs:
122113 set -euo pipefail
123114 python3 <<'PY'
124115 import json, os
125-
126116 path = "build-system/ci-configuration.json"
127- base_path = "build-system/appstore-configuration.json"
128- with open(base_path) as f:
117+ with open("build-system/appstore-configuration.json") as f:
129118 cfg = json.load(f)
130-
131119 api_id = os.environ.get("TELEGRAM_API_ID") or ""
132120 api_hash = os.environ.get("TELEGRAM_API_HASH") or ""
133121 if api_id and api_hash:
134122 cfg["api_id"] = api_id
135123 cfg["api_hash"] = api_hash
136124 print("Using TELEGRAM_API_* repository secrets")
137125 else:
138- print(f"Secrets unset — using {base_path} (api_id={cfg.get('api_id')})")
139-
126+ print(f"Secrets unset — using appstore-configuration.json (api_id={cfg.get('api_id')})")
140127 if os.environ.get("TELEGRAM_BUNDLE_ID"):
141128 cfg["bundle_id"] = os.environ["TELEGRAM_BUNDLE_ID"]
142- if "TELEGRAM_TEAM_ID" in os.environ and os.environ[ "TELEGRAM_TEAM_ID"] != "" :
129+ if os.environ.get( "TELEGRAM_TEAM_ID") :
143130 cfg["team_id"] = os.environ["TELEGRAM_TEAM_ID"]
144-
145131 with open(path, "w") as f:
146132 json.dump(cfg, f, indent="\t")
147133 f.write("\n")
148134 print(f"Wrote {path} (api_id={cfg['api_id']}, bundle_id={cfg['bundle_id']})")
149135 PY
150136
151137 - name : Compute build number
152- working-directory : ${{ env.SOURCE_DIR }}
153138 run : |
154139 set -euo pipefail
155140 BUILD_NUMBER_OFFSET="$(cat build_number_offset)"
@@ -163,14 +148,14 @@ jobs:
163148 echo "Telegram ${APP_VERSION} (${BUILD_NUMBER})"
164149
165150 - name : Build IPA
166- working-directory : ${{ env.SOURCE_DIR }}
167151 run : |
168152 set -euo pipefail
169153 mkdir -p "$BAZEL_CACHE_DIR"
170154
171155 python3 build-system/Make/ImportCertificates.py \
172156 --path build-system/fake-codesigning/certs
173157
158+ # PrivateMusic-style fast path: unsigned/fake-signed IPA, no extensions, no dSYM.
174159 python3 -u build-system/Make/Make.py \
175160 --overrideXcodeVersion \
176161 --bazelUserRoot="$BAZEL_USER_ROOT" \
@@ -179,59 +164,63 @@ jobs:
179164 --configurationPath=build-system/ci-configuration.json \
180165 --codesigningInformationPath=build-system/fake-codesigning \
181166 --configuration="$BUILD_CONFIGURATION" \
182- --buildNumber="$BUILD_NUMBER"
167+ --buildNumber="$BUILD_NUMBER" \
168+ --disableExtensions \
169+ --skipDsym \
170+ --enableParallelSwiftmoduleGeneration \
171+ --outputBuildArtifactsPath=build/artifacts
183172
184173 - name : Collect artifacts
185- working-directory : ${{ env.SOURCE_DIR }}
186174 run : |
187175 set -euo pipefail
188176 OUTPUT_PATH="build/artifacts"
189- rm -rf "$OUTPUT_PATH"
190177 mkdir -p "$OUTPUT_PATH"
191-
192- IPA="$(find -L bazel-out -path '*/Telegram/Telegram.ipa' -type f 2>/dev/null | head -1 || true)"
193- if [ -z "$IPA" ]; then
194- echo "::error::Telegram.ipa not found under bazel-out"
195- find -L bazel-out -name '*.ipa' 2>/dev/null | head -50 || true
196- exit 1
178+ if [ ! -f "$OUTPUT_PATH/Telegram.ipa" ]; then
179+ IPA="$(find -L bazel-out -path '*/Telegram/Telegram.ipa' -type f 2>/dev/null | head -1 || true)"
180+ [ -n "$IPA" ] || { echo "::error::Telegram.ipa not found"; exit 1; }
181+ cp "$IPA" "$OUTPUT_PATH/Telegram.ipa"
197182 fi
198- cp "$IPA" "$OUTPUT_PATH/Telegram.ipa"
199- echo "IPA=$IPA"
200-
201- mkdir -p build/DSYMs
202- while IFS= read -r dsym; do
203- cp -R "$dsym" build/DSYMs/
204- done < <(find -L bazel-out -path '*/Telegram/*.dSYM' -type d 2>/dev/null || true)
205- if [ -n "$(ls -A build/DSYMs 2>/dev/null || true)" ]; then
206- zip -r "$OUTPUT_PATH/Telegram.DSYMs.zip" build/DSYMs >/dev/null
207- else
208- echo "::warning::No dSYM bundles found"
209- fi
210-
183+ mv "$OUTPUT_PATH/Telegram.ipa" \
184+ "$OUTPUT_PATH/Telegram-${APP_VERSION}-${BUILD_NUMBER}-unsigned.ipa"
211185 ls -lh "$OUTPUT_PATH"
212186
213- - name : Upload build artifacts
214- uses : actions/upload-artifact@v4
187+ - name : Upload unsigned IPA
188+ if : success() || failure()
189+ uses : actions/upload-artifact@v5
215190 with :
216- name : Telegram-iOS-${{ env.APP_VERSION }}-${{ env.BUILD_NUMBER }}
217- path : |
218- ${{ env.SOURCE_DIR }}/build/artifacts/Telegram.ipa
219- ${{ env.SOURCE_DIR }}/build/artifacts/Telegram.DSYMs.zip
191+ name : Telegram-iOS-${{ env.APP_VERSION }}-${{ env.BUILD_NUMBER }}-unsigned
192+ path : build/artifacts/*.ipa
220193 if-no-files-found : error
221194 retention-days : 14
222195
223- - name : Create GitHub Release
224- if : github.event_name == 'workflow_dispatch' && inputs.create_release == true
225- uses : softprops/action-gh-release@v2
226- with :
227- tag_name : build-${{ env.BUILD_NUMBER }}
228- name : Telegram ${{ env.APP_VERSION }} (${{ env.BUILD_NUMBER }})
229- body : |
230- Unsigned (fake-codesigned) Telegram iOS build **${{ env.APP_VERSION }}** (${{ env.BUILD_NUMBER }}).
231- Configuration: `${{ env.BUILD_CONFIGURATION }}`.
232- Commit: `${{ github.sha }}`.
233- files : |
234- ${{ env.SOURCE_DIR }}/build/artifacts/Telegram.ipa
235- ${{ env.SOURCE_DIR }}/build/artifacts/Telegram.DSYMs.zip
236- fail_on_unmatched_files : false
237- generate_release_notes : true
196+ - name : Publish GitHub Release
197+ if : startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch' && inputs.create_release == true)
198+ env :
199+ GH_TOKEN : ${{ github.token }}
200+ run : |
201+ set -euo pipefail
202+ IPA="build/artifacts/Telegram-${APP_VERSION}-${BUILD_NUMBER}-unsigned.ipa"
203+ test -f "$IPA"
204+ if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then
205+ TAG="${GITHUB_REF_NAME}"
206+ else
207+ TAG="v${APP_VERSION}-${BUILD_NUMBER}"
208+ fi
209+ NOTES="$(mktemp)"
210+ cat > "$NOTES" <<EOF
211+ Telegram iOS ${APP_VERSION} (${BUILD_NUMBER})
212+
213+ Unsigned (fake-codesigned) IPA — extensions and dSYMs skipped for CI speed.
214+ Sign with your own certificate before installing on a device.
215+
216+ Configuration: \`${BUILD_CONFIGURATION}\`
217+ Commit: \`${GITHUB_SHA}\`
218+ EOF
219+ if gh release view "$TAG" >/dev/null 2>&1; then
220+ gh release upload "$TAG" "$IPA" --clobber
221+ gh release edit "$TAG" --title "Telegram ${APP_VERSION} (${BUILD_NUMBER})" --notes-file "$NOTES"
222+ else
223+ gh release create "$TAG" "$IPA" \
224+ --title "Telegram ${APP_VERSION} (${BUILD_NUMBER})" \
225+ --notes-file "$NOTES"
226+ fi
0 commit comments