源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,505 @@
|
||||
name: Mobile Internal Build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
platform:
|
||||
description: Mobile platform to build
|
||||
required: true
|
||||
type: choice
|
||||
default: android
|
||||
options:
|
||||
- android
|
||||
- ios
|
||||
- all
|
||||
publish_android:
|
||||
description: Publish the Android APK and latest.json to the public release feed
|
||||
required: true
|
||||
type: boolean
|
||||
default: false
|
||||
android_version_name:
|
||||
description: Stable Android version name when publishing, such as 0.1.1
|
||||
required: true
|
||||
type: string
|
||||
default: 0.1.0
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: ${{ inputs.publish_android && 'mobile-android-release-publish' || format('mobile-internal-build-{0}-{1}', inputs.platform, github.ref) }}
|
||||
cancel-in-progress: ${{ !inputs.publish_android }}
|
||||
|
||||
jobs:
|
||||
android:
|
||||
name: Build internal Android APK
|
||||
if: inputs.platform == 'android' || inputs.platform == 'all'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .node-version
|
||||
cache: pnpm
|
||||
cache-dependency-path: pnpm-lock.yaml
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.26.0"
|
||||
cache-dependency-path: |
|
||||
go.work
|
||||
go.work.sum
|
||||
packages/agent/daemon/go.mod
|
||||
packages/agent/daemon/go.sum
|
||||
packages/device-link/go.mod
|
||||
packages/device-link/go.sum
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "17"
|
||||
|
||||
- name: Set up Android SDK
|
||||
uses: android-actions/setup-android@v3
|
||||
|
||||
- name: Install Android build dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
sdkmanager --install \
|
||||
"platform-tools" \
|
||||
"platforms;android-36" \
|
||||
"build-tools;36.0.0" \
|
||||
"ndk;27.3.13750724"
|
||||
|
||||
- name: Install workspace dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Prepare stable Android signing key
|
||||
shell: bash
|
||||
env:
|
||||
ANDROID_RELEASE_KEYSTORE_BASE64: ${{ secrets.ANDROID_RELEASE_KEYSTORE_BASE64 }}
|
||||
TUTTI_ANDROID_RELEASE_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_RELEASE_KEYSTORE_PASSWORD }}
|
||||
TUTTI_ANDROID_RELEASE_KEY_ALIAS: ${{ secrets.ANDROID_RELEASE_KEY_ALIAS }}
|
||||
TUTTI_ANDROID_RELEASE_KEY_PASSWORD: ${{ secrets.ANDROID_RELEASE_KEY_PASSWORD }}
|
||||
run: |
|
||||
if [[ -z "${ANDROID_RELEASE_KEYSTORE_BASE64}" ||
|
||||
-z "${TUTTI_ANDROID_RELEASE_KEYSTORE_PASSWORD}" ||
|
||||
-z "${TUTTI_ANDROID_RELEASE_KEY_ALIAS}" ||
|
||||
-z "${TUTTI_ANDROID_RELEASE_KEY_PASSWORD}" ]]; then
|
||||
echo "Stable Android release signing secrets are required." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
signing_dir="${RUNNER_TEMP}/tutti-android-signing"
|
||||
keystore_path="${signing_dir}/release.keystore"
|
||||
mkdir -p "${signing_dir}"
|
||||
printf '%s' "${ANDROID_RELEASE_KEYSTORE_BASE64}" | base64 --decode > "${keystore_path}"
|
||||
chmod 600 "${keystore_path}"
|
||||
test -s "${keystore_path}"
|
||||
keytool -list \
|
||||
-keystore "${keystore_path}" \
|
||||
-storepass "${TUTTI_ANDROID_RELEASE_KEYSTORE_PASSWORD}" \
|
||||
-alias "${TUTTI_ANDROID_RELEASE_KEY_ALIAS}" \
|
||||
>/dev/null
|
||||
echo "TUTTI_ANDROID_RELEASE_KEYSTORE_PATH=${keystore_path}" >> "${GITHUB_ENV}"
|
||||
|
||||
- name: Build Android release APK
|
||||
env:
|
||||
TUTTI_ANDROID_RELEASE_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_RELEASE_KEYSTORE_PASSWORD }}
|
||||
TUTTI_ANDROID_RELEASE_KEY_ALIAS: ${{ secrets.ANDROID_RELEASE_KEY_ALIAS }}
|
||||
TUTTI_ANDROID_RELEASE_KEY_PASSWORD: ${{ secrets.ANDROID_RELEASE_KEY_PASSWORD }}
|
||||
TUTTI_ANDROID_VERSION_CODE: ${{ github.run_number }}
|
||||
TUTTI_ANDROID_VERSION_NAME: ${{ inputs.android_version_name }}
|
||||
run: |
|
||||
pnpm --filter @tutti-os/mobile android:aar
|
||||
cd apps/mobile/android
|
||||
./gradlew app:assembleRelease
|
||||
|
||||
- name: Verify signed Android APK
|
||||
shell: bash
|
||||
run: |
|
||||
signed_apk="apps/mobile/android/app/build/outputs/apk/release/app-release.apk"
|
||||
expected_certificate="apps/mobile/android/release-certificate.pem"
|
||||
build_tools="${ANDROID_HOME}/build-tools/36.0.0"
|
||||
|
||||
test -f "${signed_apk}"
|
||||
"${build_tools}/zipalign" -c -p 4 "${signed_apk}"
|
||||
verification_output="$(
|
||||
"${build_tools}/apksigner" verify --verbose --print-certs "${signed_apk}"
|
||||
)"
|
||||
printf '%s\n' "${verification_output}"
|
||||
|
||||
expected_digest="$(
|
||||
keytool -printcert -file "${expected_certificate}" |
|
||||
sed -n 's/^[[:space:]]*SHA256: //p' |
|
||||
tr -d ':' |
|
||||
tr '[:upper:]' '[:lower:]'
|
||||
)"
|
||||
actual_digest="$(
|
||||
printf '%s\n' "${verification_output}" |
|
||||
sed -n 's/^Signer #1 certificate SHA-256 digest: //p'
|
||||
)"
|
||||
if [[ -z "${expected_digest}" || "${actual_digest}" != "${expected_digest}" ]]; then
|
||||
echo "Android APK signing certificate does not match the pinned release certificate." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Generate APK checksum
|
||||
working-directory: apps/mobile/android/app/build/outputs/apk/release
|
||||
run: sha256sum app-release.apk > SHA256SUMS
|
||||
|
||||
- name: Validate Android release publishing configuration
|
||||
if: inputs.publish_android && (inputs.platform == 'android' || inputs.platform == 'all')
|
||||
env:
|
||||
AWS_REGION: ${{ vars.AWS_REGION }}
|
||||
TUTTI_ARTIFACTS_AWS_ROLE_ARN: ${{ vars.TUTTI_ARTIFACTS_AWS_ROLE_ARN }}
|
||||
TUTTI_MOBILE_RELEASE_ASSETS_BASE_URL: ${{ vars.TUTTI_MOBILE_RELEASE_ASSETS_BASE_URL }}
|
||||
TUTTI_MOBILE_RELEASE_ASSETS_S3_BUCKET: ${{ vars.TUTTI_MOBILE_RELEASE_ASSETS_S3_BUCKET }}
|
||||
TUTTI_MOBILE_RELEASE_ASSETS_S3_PREFIX: ${{ vars.TUTTI_MOBILE_RELEASE_ASSETS_S3_PREFIX }}
|
||||
TUTTI_ANDROID_VERSION_NAME: ${{ inputs.android_version_name }}
|
||||
run: |
|
||||
for name in AWS_REGION TUTTI_ARTIFACTS_AWS_ROLE_ARN TUTTI_MOBILE_RELEASE_ASSETS_BASE_URL TUTTI_MOBILE_RELEASE_ASSETS_S3_BUCKET TUTTI_MOBILE_RELEASE_ASSETS_S3_PREFIX; do
|
||||
if [[ -z "${!name:-}" ]]; then
|
||||
echo "Missing required Android release publishing variable: ${name}" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
if [[ ! "${TUTTI_ANDROID_VERSION_NAME}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "android_version_name must be stable semver x.y.z." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Build Android release pointer
|
||||
if: inputs.publish_android && (inputs.platform == 'android' || inputs.platform == 'all')
|
||||
env:
|
||||
TUTTI_ANDROID_VERSION_CODE: ${{ github.run_number }}
|
||||
TUTTI_ANDROID_VERSION_NAME: ${{ inputs.android_version_name }}
|
||||
TUTTI_MOBILE_RELEASE_ASSETS_BASE_URL: ${{ vars.TUTTI_MOBILE_RELEASE_ASSETS_BASE_URL }}
|
||||
run: |
|
||||
node tools/scripts/build-mobile-release-latest.mjs \
|
||||
--apk apps/mobile/android/app/build/outputs/apk/release/app-release.apk \
|
||||
--base-url "${TUTTI_MOBILE_RELEASE_ASSETS_BASE_URL}" \
|
||||
--output mobile-release-latest.json \
|
||||
--tag "tutti-mobile-v${TUTTI_ANDROID_VERSION_NAME}" \
|
||||
--version-code "${TUTTI_ANDROID_VERSION_CODE}" \
|
||||
--version-name "${TUTTI_ANDROID_VERSION_NAME}"
|
||||
|
||||
- name: Configure AWS credentials for Android release
|
||||
if: inputs.publish_android && (inputs.platform == 'android' || inputs.platform == 'all')
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
aws-region: ${{ vars.AWS_REGION }}
|
||||
role-to-assume: ${{ vars.TUTTI_ARTIFACTS_AWS_ROLE_ARN }}
|
||||
|
||||
- name: Prevent Android release rollback
|
||||
if: inputs.publish_android && (inputs.platform == 'android' || inputs.platform == 'all')
|
||||
env:
|
||||
TUTTI_MOBILE_RELEASE_ASSETS_S3_BUCKET: ${{ vars.TUTTI_MOBILE_RELEASE_ASSETS_S3_BUCKET }}
|
||||
TUTTI_MOBILE_RELEASE_ASSETS_S3_PREFIX: ${{ vars.TUTTI_MOBILE_RELEASE_ASSETS_S3_PREFIX }}
|
||||
TUTTI_ANDROID_VERSION_CODE: ${{ github.run_number }}
|
||||
TUTTI_ANDROID_VERSION_NAME: ${{ inputs.android_version_name }}
|
||||
run: |
|
||||
prefix="${TUTTI_MOBILE_RELEASE_ASSETS_S3_PREFIX%/}"
|
||||
pointer_path="s3://${TUTTI_MOBILE_RELEASE_ASSETS_S3_BUCKET}/${prefix}/latest.json"
|
||||
candidate_tag="tutti-mobile-v${TUTTI_ANDROID_VERSION_NAME}"
|
||||
if aws s3 cp "${pointer_path}" current-mobile-release-pointer.json 2> current-mobile-pointer-download.err; then
|
||||
export RELEASE_VERSION_CODE="${TUTTI_ANDROID_VERSION_CODE}"
|
||||
export RELEASE_TAG="${candidate_tag}"
|
||||
node <<'NODE'
|
||||
const fs = require("node:fs");
|
||||
const current = JSON.parse(fs.readFileSync("current-mobile-release-pointer.json", "utf8"));
|
||||
const currentCode = Number(current.versionCode);
|
||||
const candidateCode = Number(process.env.RELEASE_VERSION_CODE);
|
||||
if (!Number.isSafeInteger(currentCode) || currentCode <= 0) {
|
||||
throw new Error("Existing Android release pointer has an invalid versionCode.");
|
||||
}
|
||||
if (!Number.isSafeInteger(candidateCode) || candidateCode <= 0) {
|
||||
throw new Error("Candidate Android release has an invalid versionCode.");
|
||||
}
|
||||
if (candidateCode < currentCode) {
|
||||
throw new Error(`Refusing to move Android release from versionCode ${currentCode} back to ${candidateCode}.`);
|
||||
}
|
||||
if (current.tag === process.env.RELEASE_TAG && candidateCode > currentCode) {
|
||||
throw new Error(`Android version ${process.env.RELEASE_TAG} already has a published release.`);
|
||||
}
|
||||
if (candidateCode === currentCode && current.tag !== process.env.RELEASE_TAG) {
|
||||
throw new Error(`Android versionCode ${candidateCode} is already published by ${current.tag}.`);
|
||||
}
|
||||
NODE
|
||||
elif ! grep -Eq "(404|NoSuchKey|Not Found)" current-mobile-pointer-download.err; then
|
||||
cat current-mobile-pointer-download.err >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Publish Android release assets and pointer
|
||||
if: inputs.publish_android && (inputs.platform == 'android' || inputs.platform == 'all')
|
||||
env:
|
||||
TUTTI_MOBILE_RELEASE_ASSETS_S3_BUCKET: ${{ vars.TUTTI_MOBILE_RELEASE_ASSETS_S3_BUCKET }}
|
||||
TUTTI_MOBILE_RELEASE_ASSETS_S3_PREFIX: ${{ vars.TUTTI_MOBILE_RELEASE_ASSETS_S3_PREFIX }}
|
||||
TUTTI_ANDROID_VERSION_NAME: ${{ inputs.android_version_name }}
|
||||
run: |
|
||||
prefix="${TUTTI_MOBILE_RELEASE_ASSETS_S3_PREFIX%/}"
|
||||
checksum_path="apps/mobile/android/app/build/outputs/apk/release/SHA256SUMS"
|
||||
apk_sha256="$(awk 'NR == 1 { print $1 }' "${checksum_path}")"
|
||||
if [[ ! "${apk_sha256}" =~ ^[a-f0-9]{64}$ ]]; then
|
||||
echo "Generated Android APK checksum is invalid." >&2
|
||||
exit 1
|
||||
fi
|
||||
remote_dir="s3://${TUTTI_MOBILE_RELEASE_ASSETS_S3_BUCKET}/${prefix}/tutti-mobile-v${TUTTI_ANDROID_VERSION_NAME}/${apk_sha256}"
|
||||
pointer_path="s3://${TUTTI_MOBILE_RELEASE_ASSETS_S3_BUCKET}/${prefix}/latest.json"
|
||||
preflight_immutable() {
|
||||
local source_path="$1"
|
||||
local destination_path="$2"
|
||||
local comparison_path="${RUNNER_TEMP}/$(basename "${destination_path}").published"
|
||||
local error_path="${comparison_path}.err"
|
||||
|
||||
if aws s3 cp "${destination_path}" "${comparison_path}" 2> "${error_path}"; then
|
||||
if ! cmp --silent "${source_path}" "${comparison_path}"; then
|
||||
echo "Refusing to overwrite immutable Android release asset: ${destination_path}" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Immutable Android release asset already exists with identical content: ${destination_path}"
|
||||
return 0
|
||||
fi
|
||||
if grep -Eq "(404|NoSuchKey|Not Found)" "${error_path}"; then
|
||||
return 10
|
||||
else
|
||||
cat "${error_path}" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
upload_immutable() {
|
||||
local source_path="$1"
|
||||
local destination_path="$2"
|
||||
local content_type="$3"
|
||||
aws s3 cp "${source_path}" "${destination_path}" \
|
||||
--cache-control "public, max-age=31536000, immutable" \
|
||||
--content-type "${content_type}"
|
||||
}
|
||||
|
||||
apk_needs_upload=false
|
||||
if preflight_immutable \
|
||||
apps/mobile/android/app/build/outputs/apk/release/app-release.apk \
|
||||
"${remote_dir}/app-release.apk"; then
|
||||
:
|
||||
else
|
||||
status="$?"
|
||||
if [[ "${status}" -eq 10 ]]; then apk_needs_upload=true; else exit "${status}"; fi
|
||||
fi
|
||||
checksum_needs_upload=false
|
||||
if preflight_immutable "${checksum_path}" "${remote_dir}/SHA256SUMS"; then
|
||||
:
|
||||
else
|
||||
status="$?"
|
||||
if [[ "${status}" -eq 10 ]]; then checksum_needs_upload=true; else exit "${status}"; fi
|
||||
fi
|
||||
|
||||
if [[ "${apk_needs_upload}" == true ]]; then
|
||||
upload_immutable \
|
||||
apps/mobile/android/app/build/outputs/apk/release/app-release.apk \
|
||||
"${remote_dir}/app-release.apk" \
|
||||
"application/vnd.android.package-archive"
|
||||
fi
|
||||
if [[ "${checksum_needs_upload}" == true ]]; then
|
||||
upload_immutable "${checksum_path}" "${remote_dir}/SHA256SUMS" "text/plain"
|
||||
fi
|
||||
aws s3 cp mobile-release-latest.json "${pointer_path}" \
|
||||
--cache-control "public, max-age=60" \
|
||||
--content-type "application/json"
|
||||
|
||||
- name: Upload internal Android artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: tutti-mobile-internal-${{ github.sha }}
|
||||
path: |
|
||||
apps/mobile/android/app/build/outputs/apk/release/app-release.apk
|
||||
apps/mobile/android/app/build/outputs/apk/release/SHA256SUMS
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
ios:
|
||||
name: Build and upload iOS TestFlight
|
||||
if: inputs.platform == 'ios' || inputs.platform == 'all'
|
||||
runs-on: macos-26
|
||||
timeout-minutes: 60
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .node-version
|
||||
cache: pnpm
|
||||
cache-dependency-path: pnpm-lock.yaml
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.26.0"
|
||||
cache-dependency-path: |
|
||||
go.work
|
||||
go.work.sum
|
||||
packages/agent/daemon/go.mod
|
||||
packages/agent/daemon/go.sum
|
||||
packages/device-link/go.mod
|
||||
packages/device-link/go.sum
|
||||
|
||||
- name: Install workspace dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Prepare App Store Connect authentication
|
||||
shell: bash
|
||||
env:
|
||||
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
|
||||
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
||||
APPLE_API_KEY_P8_BASE64: ${{ secrets.APPLE_API_KEY_P8_BASE64 }}
|
||||
IOS_DEVELOPMENT_TEAM: ${{ vars.IOS_DEVELOPMENT_TEAM }}
|
||||
run: |
|
||||
if [[ -z "${APPLE_API_ISSUER}" ||
|
||||
-z "${APPLE_API_KEY_ID}" ||
|
||||
-z "${APPLE_API_KEY_P8_BASE64}" ||
|
||||
-z "${IOS_DEVELOPMENT_TEAM}" ]]; then
|
||||
echo "Apple API key secrets and IOS_DEVELOPMENT_TEAM are required." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
private_key_dir="${RUNNER_TEMP}/tutti-ios-private-keys"
|
||||
private_key_path="${private_key_dir}/AuthKey_${APPLE_API_KEY_ID}.p8"
|
||||
mkdir -p "${private_key_dir}"
|
||||
node -e \
|
||||
"require('node:fs').writeFileSync(process.argv[1], Buffer.from(process.env.APPLE_API_KEY_P8_BASE64, 'base64'))" \
|
||||
"${private_key_path}"
|
||||
chmod 600 "${private_key_path}"
|
||||
|
||||
echo "APPLE_API_KEY_PATH=${private_key_path}" >> "${GITHUB_ENV}"
|
||||
echo "IOS_DEVELOPMENT_TEAM=${IOS_DEVELOPMENT_TEAM}" >> "${GITHUB_ENV}"
|
||||
|
||||
- name: Build iOS framework and install pods
|
||||
run: pnpm --filter @tutti-os/mobile ios:pods
|
||||
|
||||
- name: Archive iOS app
|
||||
shell: bash
|
||||
env:
|
||||
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
|
||||
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
||||
run: |
|
||||
archive_path="${RUNNER_TEMP}/TuttiMobile.xcarchive"
|
||||
xcodebuild \
|
||||
-workspace apps/mobile/ios/TuttiMobile.xcworkspace \
|
||||
-scheme TuttiMobile \
|
||||
-configuration Release \
|
||||
-destination "generic/platform=iOS" \
|
||||
-archivePath "${archive_path}" \
|
||||
-allowProvisioningUpdates \
|
||||
-authenticationKeyPath "${APPLE_API_KEY_PATH}" \
|
||||
-authenticationKeyID "${APPLE_API_KEY_ID}" \
|
||||
-authenticationKeyIssuerID "${APPLE_API_ISSUER}" \
|
||||
DEVELOPMENT_TEAM="${IOS_DEVELOPMENT_TEAM}" \
|
||||
CODE_SIGN_STYLE=Automatic \
|
||||
CURRENT_PROJECT_VERSION="${GITHUB_RUN_NUMBER}.${GITHUB_RUN_ATTEMPT}" \
|
||||
clean archive
|
||||
|
||||
echo "IOS_ARCHIVE_PATH=${archive_path}" >> "${GITHUB_ENV}"
|
||||
|
||||
- name: Export App Store Connect IPA
|
||||
shell: bash
|
||||
env:
|
||||
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
|
||||
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
||||
run: |
|
||||
export_dir="${RUNNER_TEMP}/tutti-ios-export"
|
||||
export_options="${RUNNER_TEMP}/tutti-ios-export-options.plist"
|
||||
artifact_dir="${RUNNER_TEMP}/tutti-ios-artifact"
|
||||
|
||||
plutil -create xml1 "${export_options}"
|
||||
plutil -insert destination -string export "${export_options}"
|
||||
plutil -insert method -string app-store-connect "${export_options}"
|
||||
plutil -insert signingStyle -string automatic "${export_options}"
|
||||
plutil -insert teamID -string "${IOS_DEVELOPMENT_TEAM}" "${export_options}"
|
||||
plutil -insert manageAppVersionAndBuildNumber -bool false "${export_options}"
|
||||
plutil -insert uploadSymbols -bool true "${export_options}"
|
||||
|
||||
xcodebuild \
|
||||
-exportArchive \
|
||||
-archivePath "${IOS_ARCHIVE_PATH}" \
|
||||
-exportPath "${export_dir}" \
|
||||
-exportOptionsPlist "${export_options}" \
|
||||
-allowProvisioningUpdates \
|
||||
-authenticationKeyPath "${APPLE_API_KEY_PATH}" \
|
||||
-authenticationKeyID "${APPLE_API_KEY_ID}" \
|
||||
-authenticationKeyIssuerID "${APPLE_API_ISSUER}"
|
||||
|
||||
ipa_path="$(find "${export_dir}" -maxdepth 1 -name '*.ipa' -print -quit)"
|
||||
test -n "${ipa_path}"
|
||||
mkdir -p "${artifact_dir}"
|
||||
cp "${ipa_path}" "${artifact_dir}/tutti-mobile-testflight.ipa"
|
||||
|
||||
- name: Verify TestFlight IPA
|
||||
shell: bash
|
||||
run: |
|
||||
artifact_dir="${RUNNER_TEMP}/tutti-ios-artifact"
|
||||
verification_dir="${RUNNER_TEMP}/tutti-ios-verification"
|
||||
mkdir -p "${verification_dir}"
|
||||
unzip -q "${artifact_dir}/tutti-mobile-testflight.ipa" -d "${verification_dir}"
|
||||
app_path="$(find "${verification_dir}/Payload" -maxdepth 1 -name '*.app' -print -quit)"
|
||||
test -n "${app_path}"
|
||||
test -f "${app_path}/embedded.mobileprovision"
|
||||
test -f "${app_path}/main.jsbundle"
|
||||
codesign --verify --deep --strict "${app_path}"
|
||||
(
|
||||
cd "${artifact_dir}"
|
||||
shasum -a 256 tutti-mobile-testflight.ipa > SHA256SUMS
|
||||
)
|
||||
|
||||
- name: Upload to TestFlight
|
||||
shell: bash
|
||||
env:
|
||||
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
|
||||
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
||||
run: |
|
||||
upload_dir="${RUNNER_TEMP}/tutti-ios-upload"
|
||||
upload_options="${RUNNER_TEMP}/tutti-ios-upload-options.plist"
|
||||
|
||||
plutil -create xml1 "${upload_options}"
|
||||
plutil -insert destination -string upload "${upload_options}"
|
||||
plutil -insert method -string app-store-connect "${upload_options}"
|
||||
plutil -insert signingStyle -string automatic "${upload_options}"
|
||||
plutil -insert teamID -string "${IOS_DEVELOPMENT_TEAM}" "${upload_options}"
|
||||
plutil -insert manageAppVersionAndBuildNumber -bool false "${upload_options}"
|
||||
plutil -insert uploadSymbols -bool true "${upload_options}"
|
||||
|
||||
xcodebuild \
|
||||
-exportArchive \
|
||||
-archivePath "${IOS_ARCHIVE_PATH}" \
|
||||
-exportPath "${upload_dir}" \
|
||||
-exportOptionsPlist "${upload_options}" \
|
||||
-allowProvisioningUpdates \
|
||||
-authenticationKeyPath "${APPLE_API_KEY_PATH}" \
|
||||
-authenticationKeyID "${APPLE_API_KEY_ID}" \
|
||||
-authenticationKeyIssuerID "${APPLE_API_ISSUER}"
|
||||
|
||||
- name: Upload TestFlight IPA artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: tutti-mobile-ios-testflight-${{ github.sha }}
|
||||
path: |
|
||||
${{ runner.temp }}/tutti-ios-artifact/tutti-mobile-testflight.ipa
|
||||
${{ runner.temp }}/tutti-ios-artifact/SHA256SUMS
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
Reference in New Issue
Block a user