Skip to content

Commit d3aa81f

Browse files
CI: faster unsigned IPA + tag release (PrivateMusic-style)
Use the xcode-27 runner, skip app extensions and dSYMs, enable parallel swiftmodule generation, cache Bazel outputs, and build in-place (no /Users/Shared copy). Tag pushes (v*) and manual workflow_dispatch with create_release publish an unsigned IPA GitHub Release the same way PrivateMusic2 does. Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: D3C0Y <decoder-dev@users.noreply.github.com>
1 parent 798447b commit d3aa81f

2 files changed

Lines changed: 118 additions & 99 deletions

File tree

‎.github/workflows/build.yml‎

Lines changed: 81 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
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

107
name: Build iOS
118

129
on:
1310
push:
1411
branches: [master]
12+
tags: ['v*']
1513
pull_request:
1614
workflow_dispatch:
1715
inputs:
@@ -36,25 +34,22 @@ permissions:
3634

3735
jobs:
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

‎build-system/Make/Make.py‎

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ def __init__(self, bazel, override_bazel_version, override_xcode_version, bazel_
4646
self.show_actions = False
4747
self.enable_sandbox = False
4848
self.disable_provisioning_profiles = False
49+
self.disable_extensions = False
50+
self.skip_dsym = False
4951
self.profile_swift = False
5052
self.embed_watch_app = False
5153
self.watch_api_id = None
@@ -138,6 +140,12 @@ def set_split_swiftmodules(self, value):
138140
def set_disable_provisioning_profiles(self):
139141
self.disable_provisioning_profiles = True
140142

143+
def set_disable_extensions(self, value=True):
144+
self.disable_extensions = value
145+
146+
def set_skip_dsym(self, value=True):
147+
self.skip_dsym = value
148+
141149
def set_profile_swift(self, value):
142150
self.profile_swift = value
143151

@@ -185,13 +193,16 @@ def set_configuration(self, configuration):
185193

186194
# Always build universal Watch binaries.
187195
'--watchos_cpus=arm64_32',
188-
189-
# Generate DSYM files when building.
190-
'--apple_generate_dsym',
191-
192-
# Require DSYM files as build output.
193-
'--output_groups=+dsyms',
194-
] + self.common_release_args
196+
]
197+
if not self.skip_dsym:
198+
self.configuration_args += [
199+
# Generate DSYM files when building.
200+
'--apple_generate_dsym',
201+
202+
# Require DSYM files as build output.
203+
'--output_groups=+dsyms',
204+
]
205+
self.configuration_args += self.common_release_args
195206
else:
196207
raise Exception('Unknown configuration {}'.format(configuration))
197208

@@ -299,6 +310,9 @@ def invoke_build(self):
299310
if self.disable_provisioning_profiles:
300311
combined_arguments += ['--//Telegram:disableProvisioningProfiles']
301312

313+
if self.disable_extensions:
314+
combined_arguments += ['--//Telegram:disableExtensions']
315+
302316
combined_arguments += self.common_args
303317
combined_arguments += self.common_build_args
304318
combined_arguments += self.get_define_arguments()
@@ -678,6 +692,10 @@ def build(bazel, arguments):
678692
additional_codesigning_output_path=None
679693
)
680694

695+
if getattr(arguments, 'disableExtensions', False):
696+
bazel_command_line.set_disable_extensions(True)
697+
if getattr(arguments, 'skipDsym', False):
698+
bazel_command_line.set_skip_dsym(True)
681699
bazel_command_line.set_configuration(arguments.configuration)
682700
if arguments.embedWatchApp:
683701
if arguments.configuration in ('debug_arm64', 'release_arm64'):
@@ -1056,6 +1074,18 @@ def add_project_and_build_common_arguments(current_parser: argparse.ArgumentPars
10561074
help='Generate .swiftmodule files in parallel to building modules, can speed up compilation on multi-core '
10571075
'systems. '
10581076
)
1077+
buildParser.add_argument(
1078+
'--disableExtensions',
1079+
action='store_true',
1080+
default=False,
1081+
help='Skip app extensions (Share/Widget/NSE/…) for a faster CI IPA.'
1082+
)
1083+
buildParser.add_argument(
1084+
'--skipDsym',
1085+
action='store_true',
1086+
default=False,
1087+
help='Skip dSYM generation on release_arm64 builds (much faster CI artifacts).'
1088+
)
10591089
buildParser.add_argument(
10601090
'--profileSwift',
10611091
action='store_true',

0 commit comments

Comments
 (0)