Skip to content

Merge branch 'main' of https://github.com/yingxiaoshuai/FileFlash-Bridge #10

Merge branch 'main' of https://github.com/yingxiaoshuai/FileFlash-Bridge

Merge branch 'main' of https://github.com/yingxiaoshuai/FileFlash-Bridge #10

Workflow file for this run

name: iOS TestFlight
on:
workflow_dispatch:
inputs:
marketing_version:
description: Optional marketing version override, for example 1.0.1
required: false
type: string
push:
tags:
- "ios-v*"
concurrency:
group: ios-testflight-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
permissions:
contents: read
jobs:
upload-testflight:
runs-on: macos-15
timeout-minutes: 60
env:
CI: true
NODE_VERSION: 22.13.0
XCODE_MAJOR_VERSION: "26"
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: npm
cache-dependency-path: package-lock.json
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 2.6.10
bundler-cache: true
- name: Select Xcode ${{ env.XCODE_MAJOR_VERSION }}
shell: bash
run: |
set -euo pipefail
shopt -s nullglob
candidates=(/Applications/Xcode_${XCODE_MAJOR_VERSION}*.app)
if (( ${#candidates[@]} == 0 )); then
echo "No Xcode ${XCODE_MAJOR_VERSION}.x installation found on this runner." >&2
ls -1 /Applications | grep '^Xcode' >&2 || true
exit 1
fi
selected_app="$(
printf '%s\n' "${candidates[@]}" \
| ruby -e 'apps = STDIN.read.lines.map(&:strip).reject(&:empty?); abort("No Xcode candidates found") if apps.empty?; puts apps.max_by { |path| Gem::Version.new(File.basename(path).sub(/^Xcode_/, "").sub(/\.app$/, "")) }'
)"
developer_dir="${selected_app}/Contents/Developer"
echo "DEVELOPER_DIR=${developer_dir}" >> "${GITHUB_ENV}"
"${developer_dir}/usr/bin/xcodebuild" -version
"${developer_dir}/usr/bin/xcodebuild" -showsdks
- name: Install npm dependencies
run: npm ci
- name: Type check
run: npm run typecheck
- name: Run unit tests
run: npm test -- --runInBand
- name: Install CocoaPods dependencies
run: bundle exec pod install --project-directory=ios
- name: Resolve release metadata
id: meta
shell: bash
run: |
set -euo pipefail
build_settings="$(xcodebuild -showBuildSettings \
-workspace ios/FileFlashBridge.xcworkspace \
-scheme FileFlashBridge \
-configuration Release \
-sdk iphoneos)"
bundle_id="$(printf '%s\n' "${build_settings}" | sed -n 's/^[[:space:]]*PRODUCT_BUNDLE_IDENTIFIER = //p' | tail -n1)"
team_id="$(printf '%s\n' "${build_settings}" | sed -n 's/^[[:space:]]*DEVELOPMENT_TEAM = //p' | tail -n1)"
project_version="$(printf '%s\n' "${build_settings}" | sed -n 's/^[[:space:]]*MARKETING_VERSION = //p' | tail -n1)"
if [[ -z "${bundle_id}" || -z "${team_id}" || -z "${project_version}" ]]; then
echo "Failed to resolve Release build settings" >&2
exit 1
fi
version_input="${{ inputs.marketing_version }}"
version="${project_version}"
if [[ "${GITHUB_REF_TYPE:-}" == "tag" ]]; then
ref_name="${GITHUB_REF_NAME}"
if [[ "${ref_name}" =~ ^ios-v([0-9]+(\.[0-9]+){1,2})$ ]]; then
version="${BASH_REMATCH[1]}"
else
echo "Tag must look like ios-v1.0.1" >&2
exit 1
fi
elif [[ -n "${version_input}" ]]; then
version="${version_input}"
fi
if [[ ! "${version}" =~ ^[0-9]+(\.[0-9]+){1,2}$ ]]; then
echo "Marketing version must look like 1.0 or 1.0.1" >&2
exit 1
fi
build_number="${GITHUB_RUN_NUMBER}"
auth_key_path="${RUNNER_TEMP}/AuthKey_${{ secrets.APP_STORE_CONNECT_KEY_ID }}.p8"
echo "bundle_id=${bundle_id}" >> "${GITHUB_OUTPUT}"
echo "team_id=${team_id}" >> "${GITHUB_OUTPUT}"
echo "marketing_version=${version}" >> "${GITHUB_OUTPUT}"
echo "build_number=${build_number}" >> "${GITHUB_OUTPUT}"
echo "auth_key_path=${auth_key_path}" >> "${GITHUB_OUTPUT}"
{
echo "### iOS Upload"
echo ""
echo "- Bundle ID: \`${bundle_id}\`"
echo "- Team ID: \`${team_id}\`"
echo "- Marketing Version: \`${version}\`"
echo "- Build Number: \`${build_number}\`"
} >> "${GITHUB_STEP_SUMMARY}"
- name: Prepare App Store Connect API key
shell: bash
env:
APP_STORE_CONNECT_PRIVATE_KEY: ${{ secrets.APP_STORE_CONNECT_PRIVATE_KEY }}
run: |
set -euo pipefail
if [[ -z "${APP_STORE_CONNECT_PRIVATE_KEY}" ]]; then
echo "Missing APP_STORE_CONNECT_PRIVATE_KEY secret" >&2
exit 1
fi
key_content="${APP_STORE_CONNECT_PRIVATE_KEY//$'\r'/}"
# Support raw multiline PEM, secrets pasted with literal \n escapes,
# and Base64-encoded PEM content.
if [[ "${key_content}" == *"BEGIN PRIVATE KEY"* ]]; then
if [[ "${key_content}" == *"\\n"* ]]; then
printf '%b' "${key_content}" > "${{ steps.meta.outputs.auth_key_path }}"
else
printf '%s\n' "${key_content}" > "${{ steps.meta.outputs.auth_key_path }}"
fi
else
printf '%s' "${key_content}" | base64 --decode > "${{ steps.meta.outputs.auth_key_path }}"
fi
if [[ ! -s "${{ steps.meta.outputs.auth_key_path }}" ]]; then
echo "APP_STORE_CONNECT_PRIVATE_KEY decoded to an empty file" >&2
exit 1
fi
if ! grep -q "BEGIN PRIVATE KEY" "${{ steps.meta.outputs.auth_key_path }}"; then
echo "APP_STORE_CONNECT_PRIVATE_KEY is not a valid .p8 PEM document" >&2
exit 1
fi
chmod 600 "${{ steps.meta.outputs.auth_key_path }}"
- name: Create export options
shell: bash
run: |
cat > "${RUNNER_TEMP}/ExportOptions.plist" <<'PLIST'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>destination</key>
<string>upload</string>
<key>manageAppVersionAndBuildNumber</key>
<false/>
<key>method</key>
<string>app-store-connect</string>
<key>signingStyle</key>
<string>automatic</string>
<key>teamID</key>
<string>${{ steps.meta.outputs.team_id }}</string>
<key>stripSwiftSymbols</key>
<true/>
<key>uploadSymbols</key>
<true/>
</dict>
</plist>
PLIST
- name: Archive iOS app
shell: bash
run: |
set -o pipefail
xcodebuild \
-workspace ios/FileFlashBridge.xcworkspace \
-scheme FileFlashBridge \
-configuration Release \
-sdk iphoneos \
-destination "generic/platform=iOS" \
-archivePath "${RUNNER_TEMP}/FileFlashBridge.xcarchive" \
-authenticationKeyPath "${{ steps.meta.outputs.auth_key_path }}" \
-authenticationKeyID "${{ secrets.APP_STORE_CONNECT_KEY_ID }}" \
-authenticationKeyIssuerID "${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}" \
-allowProvisioningUpdates \
COMPILER_INDEX_STORE_ENABLE=NO \
CODE_SIGN_STYLE=Automatic \
DEVELOPMENT_TEAM="${{ steps.meta.outputs.team_id }}" \
MARKETING_VERSION="${{ steps.meta.outputs.marketing_version }}" \
CURRENT_PROJECT_VERSION="${{ steps.meta.outputs.build_number }}" \
clean archive | tee "${RUNNER_TEMP}/archive.log"
- name: Export and upload to TestFlight
shell: bash
run: |
set -o pipefail
xcodebuild \
-exportArchive \
-archivePath "${RUNNER_TEMP}/FileFlashBridge.xcarchive" \
-exportPath "${RUNNER_TEMP}/export" \
-exportOptionsPlist "${RUNNER_TEMP}/ExportOptions.plist" \
-authenticationKeyPath "${{ steps.meta.outputs.auth_key_path }}" \
-authenticationKeyID "${{ secrets.APP_STORE_CONNECT_KEY_ID }}" \
-authenticationKeyIssuerID "${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}" \
-allowProvisioningUpdates | tee "${RUNNER_TEMP}/export.log"
- name: Upload release logs
if: failure()
uses: actions/upload-artifact@v4
with:
name: ios-testflight-logs
path: |
${{ runner.temp }}/archive.log
${{ runner.temp }}/export.log
${{ runner.temp }}/ExportOptions.plist