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
506 lines
21 KiB
YAML
506 lines
21 KiB
YAML
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
|