Skip to content

CI: macos-26 + verified Metal for Bazel IPA #15

CI: macos-26 + verified Metal for Bazel IPA

CI: macos-26 + verified Metal for Bazel IPA #15

Workflow file for this run

# Build unsigned Telegram-iOS IPA (Bazel), styled after decoder-dev/PrivateMusic2 CI:
# fast runner, unsigned artifact, tag → GitHub Release.
#
# Optional secrets TELEGRAM_API_ID / TELEGRAM_API_HASH / TELEGRAM_BUNDLE_ID / TELEGRAM_TEAM_ID
# override build-system/appstore-configuration.json when set.
name: Build iOS
on:
push:
branches: [master]
tags: ['v*']
pull_request:
workflow_dispatch:
inputs:
configuration:
description: Bazel build configuration
type: choice
options:
- release_arm64
- debug_arm64
default: release_arm64
create_release:
description: Publish a GitHub Release with the IPA
type: boolean
default: false
concurrency:
group: ios-build-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: write
jobs:
build:
name: Build IPA (macos-26)
runs-on: macos-26
timeout-minutes: 360
env:
BUILD_CONFIGURATION: ${{ github.event.inputs.configuration || 'release_arm64' }}
BAZEL_USER_ROOT: /private/var/tmp/_bazel_telegram_ios
BAZEL_CACHE_DIR: ${{ github.workspace }}/.telegram-bazel-cache
steps:
- name: Checkout
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: Select Xcode
run: |
set -euo pipefail
# Prefer versions.json (26.2). Xcode 27 preview runners often download Metal
# but xcrun still cannot find `metal` for Bazel MetalCompile.
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
# PrivateMusic-style Metal install + xcrun cache reset (Bazel calls `xcrun metal`).
- name: Prepare Metal toolchain
run: |
set -euo pipefail
sudo xcodebuild -downloadComponent MetalToolchain
rm -f "$(getconf DARWIN_USER_TEMP_DIR)/xcrun_db" || true
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
uses: actions/cache@v4
with:
path: ${{ env.BAZEL_CACHE_DIR }}
key: bazel-${{ runner.os }}-${{ env.BUILD_CONFIGURATION }}-${{ hashFiles('versions.json', 'MODULE.bazel', 'MODULE.bazel.lock') }}
restore-keys: |
bazel-${{ runner.os }}-${{ env.BUILD_CONFIGURATION }}-
bazel-${{ runner.os }}-
- name: Write build configuration
env:
TELEGRAM_API_ID: ${{ secrets.TELEGRAM_API_ID }}
TELEGRAM_API_HASH: ${{ secrets.TELEGRAM_API_HASH }}
TELEGRAM_BUNDLE_ID: ${{ secrets.TELEGRAM_BUNDLE_ID }}
TELEGRAM_TEAM_ID: ${{ secrets.TELEGRAM_TEAM_ID }}
run: |
set -euo pipefail
python3 <<'PY'
import json, os
path = "build-system/ci-configuration.json"
with open("build-system/appstore-configuration.json") as f:
cfg = json.load(f)
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
print("Using TELEGRAM_API_* repository secrets")
else:
print(f"Secrets unset — using appstore-configuration.json (api_id={cfg.get('api_id')})")
if os.environ.get("TELEGRAM_BUNDLE_ID"):
cfg["bundle_id"] = os.environ["TELEGRAM_BUNDLE_ID"]
if os.environ.get("TELEGRAM_TEAM_ID"):
cfg["team_id"] = os.environ["TELEGRAM_TEAM_ID"]
with open(path, "w") as f:
json.dump(cfg, f, indent="\t")
f.write("\n")
print(f"Wrote {path} (api_id={cfg['api_id']}, bundle_id={cfg['bundle_id']})")
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"])')"
COMMIT_COUNT="$(git rev-list --count HEAD)"
BUILD_NUMBER="$((COMMIT_COUNT + BUILD_NUMBER_OFFSET))"
{
echo "APP_VERSION=$APP_VERSION"
echo "BUILD_NUMBER=$BUILD_NUMBER"
} >> "$GITHUB_ENV"
echo "Telegram ${APP_VERSION} (${BUILD_NUMBER})"
- name: Build IPA
run: |
set -euo pipefail
mkdir -p "$BAZEL_CACHE_DIR"
python3 build-system/Make/ImportCertificates.py \
--path build-system/fake-codesigning/certs
# PrivateMusic-style fast path: unsigned/fake-signed IPA, no extensions, no dSYM.
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 \
--skipDsym \
--enableParallelSwiftmoduleGeneration \
--outputBuildArtifactsPath=build/artifacts
- name: Collect artifacts
run: |
set -euo pipefail
OUTPUT_PATH="build/artifacts"
mkdir -p "$OUTPUT_PATH"
if [ ! -f "$OUTPUT_PATH/Telegram.ipa" ]; then
IPA="$(find -L bazel-out -path '*/Telegram/Telegram.ipa' -type f 2>/dev/null | head -1 || true)"
[ -n "$IPA" ] || { echo "::error::Telegram.ipa not found"; exit 1; }
cp "$IPA" "$OUTPUT_PATH/Telegram.ipa"
fi
mv "$OUTPUT_PATH/Telegram.ipa" \
"$OUTPUT_PATH/Telegram-${APP_VERSION}-${BUILD_NUMBER}-unsigned.ipa"
ls -lh "$OUTPUT_PATH"
- name: Upload unsigned IPA
if: success()
uses: actions/upload-artifact@v5
with:
name: Telegram-iOS-${{ env.APP_VERSION }}-${{ env.BUILD_NUMBER }}-unsigned
path: build/artifacts/*.ipa
if-no-files-found: error
retention-days: 14
- 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="build/artifacts/Telegram-${APP_VERSION}-${BUILD_NUMBER}-unsigned.ipa"
test -f "$IPA"
if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then
TAG="${GITHUB_REF_NAME}"
else
TAG="v${APP_VERSION}-${BUILD_NUMBER}"
fi
NOTES="$(mktemp)"
cat > "$NOTES" <<EOF
Telegram iOS ${APP_VERSION} (${BUILD_NUMBER})
Unsigned (fake-codesigned) IPA — extensions and dSYMs skipped for CI speed.
Sign with your own certificate before installing on a device.
Configuration: \`${BUILD_CONFIGURATION}\`
Commit: \`${GITHUB_SHA}\`
EOF
if gh release view "$TAG" >/dev/null 2>&1; then
gh release upload "$TAG" "$IPA" --clobber
gh release edit "$TAG" --title "Telegram ${APP_VERSION} (${BUILD_NUMBER})" --notes-file "$NOTES"
else
gh release create "$TAG" "$IPA" \
--title "Telegram ${APP_VERSION} (${BUILD_NUMBER})" \
--notes-file "$NOTES"
fi