From a9e7a6fa9eb9cb1b3f64d0dd125608d20bc44228 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Mon, 29 Jun 2026 14:27:49 +0800 Subject: [PATCH] ci(release): add TestFlight release matrix * chore(release): bump version to 1.1.5+2 * chore(release): bump build metadata for 1.1.5+2 * ci(release): add TestFlight release matrix --------- Co-authored-by: Haitao Pan --- .github/workflows/build-and-release.yml | 86 ++++++++++++++++++++-- scripts/ci/build_matrix_artifacts.sh | 22 ++++-- scripts/ci/testflight_upload.sh | 82 +++++++++++++++++++++ scripts/package-macos-app-store-pkg.sh | 95 +++++++++++++++++++++++++ 4 files changed, 277 insertions(+), 8 deletions(-) create mode 100755 scripts/ci/testflight_upload.sh create mode 100755 scripts/package-macos-app-store-pkg.sh diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 2d266e90..abc8548e 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -122,6 +122,14 @@ jobs: artifact_name: build-macos-arm64-dmg artifact_paths: | dist/macos/*.dmg + - platform: macos + arch: arm64 + package: app-store-pkg + release_only: true + runs_on: macos-14 + artifact_name: build-macos-arm64-pkg + artifact_paths: | + dist/macos-app-store/*.pkg - platform: ios arch: arm64 package: ipa @@ -166,8 +174,12 @@ jobs: kv/data/github-actions/xworkmate-app APPLE_CERT_P12_BASE64 | APPLE_CERT_P12_BASE64 ; kv/data/github-actions/xworkmate-app APPLE_CERT_PASSWORD | APPLE_CERT_PASSWORD ; kv/data/github-actions/xworkmate-app APPLE_PROVISION_PROFILE_BASE64 | APPLE_PROVISION_PROFILE_BASE64 ; + kv/data/github-actions/xworkmate-app APPLE_MAC_PROVISION_PROFILE_BASE64 | APPLE_MAC_PROVISION_PROFILE_BASE64 ; kv/data/github-actions/xworkmate-app APPLE_KEYCHAIN_PASSWORD | APPLE_KEYCHAIN_PASSWORD ; kv/data/github-actions/xworkmate-app APPLE_EXPORT_METHOD | APPLE_EXPORT_METHOD ; + kv/data/github-actions/xworkmate-app APP_STORE_CONNECT_API_KEY_ID | APP_STORE_CONNECT_API_KEY_ID ; + kv/data/github-actions/xworkmate-app APP_STORE_CONNECT_ISSUER_ID | APP_STORE_CONNECT_ISSUER_ID ; + kv/data/github-actions/xworkmate-app APP_STORE_CONNECT_API_KEY_P8_BASE64 | APP_STORE_CONNECT_API_KEY_P8_BASE64 ; kv/data/github-actions/xworkmate-app ANDROID_KEYSTORE_BASE64 | ANDROID_KEYSTORE_BASE64 ; kv/data/github-actions/xworkmate-app ANDROID_KEYSTORE_PASSWORD | ANDROID_KEYSTORE_PASSWORD ; kv/data/github-actions/xworkmate-app ANDROID_KEY_ALIAS | ANDROID_KEY_ALIAS ; @@ -183,8 +195,12 @@ jobs: echo "APPLE_CERT_P12_BASE64=${{ steps.vault.outputs.APPLE_CERT_P12_BASE64 }}" echo "APPLE_CERT_PASSWORD=${{ steps.vault.outputs.APPLE_CERT_PASSWORD }}" echo "APPLE_PROVISION_PROFILE_BASE64=${{ steps.vault.outputs.APPLE_PROVISION_PROFILE_BASE64 }}" + echo "APPLE_MAC_PROVISION_PROFILE_BASE64=${{ steps.vault.outputs.APPLE_MAC_PROVISION_PROFILE_BASE64 }}" echo "APPLE_KEYCHAIN_PASSWORD=${{ steps.vault.outputs.APPLE_KEYCHAIN_PASSWORD }}" echo "APPLE_EXPORT_METHOD=${{ steps.vault.outputs.APPLE_EXPORT_METHOD }}" + echo "APP_STORE_CONNECT_API_KEY_ID=${{ steps.vault.outputs.APP_STORE_CONNECT_API_KEY_ID }}" + echo "APP_STORE_CONNECT_ISSUER_ID=${{ steps.vault.outputs.APP_STORE_CONNECT_ISSUER_ID }}" + echo "APP_STORE_CONNECT_API_KEY_P8_BASE64=${{ steps.vault.outputs.APP_STORE_CONNECT_API_KEY_P8_BASE64 }}" echo "ANDROID_KEYSTORE_BASE64=${{ steps.vault.outputs.ANDROID_KEYSTORE_BASE64 }}" echo "ANDROID_KEYSTORE_PASSWORD=${{ steps.vault.outputs.ANDROID_KEYSTORE_PASSWORD }}" echo "ANDROID_KEY_ALIAS=${{ steps.vault.outputs.ANDROID_KEY_ALIAS }}" @@ -220,12 +236,12 @@ jobs: go-version: "1.24.1" - name: Build platform artifacts - if: ${{ steps.preflight.outputs.should_build_platform == 'true' }} + if: ${{ steps.preflight.outputs.should_build_platform == 'true' && (matrix.release_only != 'true' || env.SHOULD_RELEASE == 'true') }} shell: bash - run: bash ./scripts/ci/build_matrix_artifacts.sh "$PLATFORM" "$ARCH" "$SHOULD_RELEASE" + run: bash ./scripts/ci/build_matrix_artifacts.sh "$PLATFORM" "$ARCH" "${{ matrix.package }}" "$SHOULD_RELEASE" - name: Upload build artifacts - if: ${{ steps.preflight.outputs.should_build_platform == 'true' }} + if: ${{ steps.preflight.outputs.should_build_platform == 'true' && (matrix.release_only != 'true' || env.SHOULD_RELEASE == 'true') }} uses: actions/upload-artifact@v7 with: name: ${{ matrix.artifact_name }} @@ -273,7 +289,23 @@ jobs: # never blocked by it being skipped (e.g. push events) or failing. # build/prepare must still genuinely succeed. if: ${{ always() && needs.prepare.outputs.should_release == 'true' && needs.prepare.result == 'success' && needs.build.result == 'success' }} - runs-on: ubuntu-22.04 + strategy: + fail-fast: false + matrix: + include: + - target: github_release + runs_on: ubuntu-22.04 + - target: testflight_ios + runs_on: macos-14 + artifact_name: build-ios-arm64-ipa + artifact_path: release-artifacts/build-ios-arm64-ipa + testflight_platform: ios + - target: testflight_macos + runs_on: macos-14 + artifact_name: build-macos-arm64-pkg + artifact_path: release-artifacts/build-macos-arm64-pkg + testflight_platform: macos + runs-on: ${{ matrix.runs_on }} permissions: contents: write needs: @@ -284,12 +316,46 @@ jobs: - name: Checkout source uses: actions/checkout@v7 + - name: Load App Store Connect secrets + id: vault + if: ${{ matrix.target != 'github_release' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} + uses: hashicorp/vault-action@v4 + with: + url: ${{ env.VAULT_ADDR }} + method: jwt + role: github-actions-xworkmate-app + jwtGithubAudience: vault + ignoreNotFound: true + secrets: | + kv/data/github-actions/xworkmate-app APPLE_CERT_P12_BASE64 | APPLE_CERT_P12_BASE64 ; + kv/data/github-actions/xworkmate-app APPLE_CERT_PASSWORD | APPLE_CERT_PASSWORD ; + kv/data/github-actions/xworkmate-app APPLE_MAC_PROVISION_PROFILE_BASE64 | APPLE_MAC_PROVISION_PROFILE_BASE64 ; + kv/data/github-actions/xworkmate-app APPLE_KEYCHAIN_PASSWORD | APPLE_KEYCHAIN_PASSWORD ; + kv/data/github-actions/xworkmate-app APP_STORE_CONNECT_API_KEY_ID | APP_STORE_CONNECT_API_KEY_ID ; + kv/data/github-actions/xworkmate-app APP_STORE_CONNECT_ISSUER_ID | APP_STORE_CONNECT_ISSUER_ID ; + kv/data/github-actions/xworkmate-app APP_STORE_CONNECT_API_KEY_P8_BASE64 | APP_STORE_CONNECT_API_KEY_P8_BASE64 + + - name: Export App Store Connect secrets + if: ${{ matrix.target != 'github_release' }} + run: | + { + echo "APPLE_CERT_P12_BASE64=${{ steps.vault.outputs.APPLE_CERT_P12_BASE64 }}" + echo "APPLE_CERT_PASSWORD=${{ steps.vault.outputs.APPLE_CERT_PASSWORD }}" + echo "APPLE_MAC_PROVISION_PROFILE_BASE64=${{ steps.vault.outputs.APPLE_MAC_PROVISION_PROFILE_BASE64 }}" + echo "APPLE_KEYCHAIN_PASSWORD=${{ steps.vault.outputs.APPLE_KEYCHAIN_PASSWORD }}" + echo "APP_STORE_CONNECT_API_KEY_ID=${{ steps.vault.outputs.APP_STORE_CONNECT_API_KEY_ID }}" + echo "APP_STORE_CONNECT_ISSUER_ID=${{ steps.vault.outputs.APP_STORE_CONNECT_ISSUER_ID }}" + echo "APP_STORE_CONNECT_API_KEY_P8_BASE64=${{ steps.vault.outputs.APP_STORE_CONNECT_API_KEY_P8_BASE64 }}" + } >> "$GITHUB_ENV" + - name: Download all artifacts + if: ${{ matrix.target == 'github_release' }} uses: actions/download-artifact@v8 with: path: release-artifacts - name: Upload assets to GitHub Release + if: ${{ matrix.target == 'github_release' }} shell: bash run: bash ./scripts/ci/github_release_upload.sh release-artifacts env: @@ -297,3 +363,15 @@ jobs: RELEASE_TAG: ${{ needs.prepare.outputs.release_tag }} RELEASE_TITLE: ${{ needs.prepare.outputs.release_title }} RELEASE_NOTES: ${{ needs.prepare.outputs.release_notes }} + + - name: Download TestFlight artifact + if: ${{ matrix.target != 'github_release' }} + uses: actions/download-artifact@v8 + with: + name: ${{ matrix.artifact_name }} + path: ${{ matrix.artifact_path }} + + - name: Upload to TestFlight + if: ${{ matrix.target != 'github_release' }} + shell: bash + run: bash ./scripts/ci/testflight_upload.sh "${{ matrix.testflight_platform }}" "${{ matrix.artifact_path }}" diff --git a/scripts/ci/build_matrix_artifacts.sh b/scripts/ci/build_matrix_artifacts.sh index b078ffdf..f300bcf7 100755 --- a/scripts/ci/build_matrix_artifacts.sh +++ b/scripts/ci/build_matrix_artifacts.sh @@ -6,7 +6,8 @@ cd "$repo_root" eval "$(python3 "$repo_root/scripts/ci/build_version.py" --format shell)" platform="${1:?platform is required}" arch="${2:?arch is required}" -should_release="${3:-false}" +package_kind="${3:-}" +should_release="${4:-false}" flutter pub get @@ -15,9 +16,22 @@ case "$platform" in bash ./scripts/package-linux.sh ;; macos) - bash ./scripts/package-flutter-mac-app.sh - mkdir -p dist/macos - find dist -maxdepth 1 -name '*.dmg' -exec mv {} dist/macos/ \; + case "$package_kind" in + dmg) + bash ./scripts/package-flutter-mac-app.sh + mkdir -p dist/macos + find dist -maxdepth 1 -name '*.dmg' -exec mv {} dist/macos/ \; + ;; + app-store-pkg) + bash ./scripts/package-macos-app-store-pkg.sh + mkdir -p dist/macos-app-store + find dist -maxdepth 1 -name '*.pkg' -exec mv {} dist/macos-app-store/ \; + ;; + *) + echo "Unsupported macOS package kind: $package_kind" >&2 + exit 1 + ;; + esac ;; windows) flutter build windows --release \ diff --git a/scripts/ci/testflight_upload.sh b/scripts/ci/testflight_upload.sh new file mode 100755 index 00000000..6de435e5 --- /dev/null +++ b/scripts/ci/testflight_upload.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +set -euo pipefail + +platform="${1:?platform is required}" +artifact_root="${2:?artifact root is required}" + +required_vars=( + APP_STORE_CONNECT_API_KEY_ID + APP_STORE_CONNECT_ISSUER_ID + APP_STORE_CONNECT_API_KEY_P8_BASE64 +) + +missing=() +for var_name in "${required_vars[@]}"; do + if [[ -z "${!var_name:-}" ]]; then + missing+=("$var_name") + fi +done + +if [[ "${#missing[@]}" -gt 0 ]]; then + echo "Missing App Store Connect secrets: ${missing[*]}" >&2 + exit 1 +fi + +if ! command -v xcrun >/dev/null 2>&1; then + echo "xcrun is required to upload TestFlight artifacts." >&2 + exit 1 +fi + +apple_decode_base64() { + if base64 --help 2>&1 | grep -q -- '--decode'; then + base64 --decode + else + base64 -D + fi +} + +tmp_dir="$(mktemp -d "${RUNNER_TEMP:-/tmp}/xworkmate-testflight.XXXXXX")" +cleanup() { + rm -rf "$tmp_dir" +} +trap cleanup EXIT + +private_keys_dir="$tmp_dir/private_keys" +mkdir -p "$private_keys_dir" + +p8_path="$private_keys_dir/AuthKey_${APP_STORE_CONNECT_API_KEY_ID}.p8" +printf '%s' "$APP_STORE_CONNECT_API_KEY_P8_BASE64" | apple_decode_base64 > "$p8_path" + +case "$platform" in + ios) + artifact_file="$(find "$artifact_root" -type f -name '*.ipa' | head -n 1)" + ;; + macos) + artifact_file="$(find "$artifact_root" -type f -name '*.pkg' | head -n 1)" + ;; + *) + echo "Unsupported TestFlight platform: $platform" >&2 + exit 1 + ;; +esac +if [[ -z "$artifact_file" ]]; then + echo "No ipa/pkg artifact found under $artifact_root" >&2 + exit 1 +fi + +export API_PRIVATE_KEYS_DIR="$private_keys_dir" + +if [[ "$platform" == "ios" ]]; then + xcrun altool \ + --upload-app \ + -f "$artifact_file" \ + --api-key "$APP_STORE_CONNECT_API_KEY_ID" \ + --api-issuer "$APP_STORE_CONNECT_ISSUER_ID" \ + --show-progress +else + xcrun altool \ + --upload-package "$artifact_file" \ + --api-key "$APP_STORE_CONNECT_API_KEY_ID" \ + --api-issuer "$APP_STORE_CONNECT_ISSUER_ID" \ + --show-progress +fi diff --git a/scripts/package-macos-app-store-pkg.sh b/scripts/package-macos-app-store-pkg.sh new file mode 100755 index 00000000..5eff39fa --- /dev/null +++ b/scripts/package-macos-app-store-pkg.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +DIST_DIR="$ROOT_DIR/dist/macos-app-store" +APP_NAME="${APP_NAME:-XWorkmate}" +APP_STORE_DEFINE="${APP_STORE_DEFINE:---dart-define=XWORKMATE_APP_STORE=${XWORKMATE_APP_STORE:-true}}" +source "$ROOT_DIR/scripts/ci/apple_signing.sh" +APPLE_SIGNING_CLEANUP_COMMANDS=() +trap apple_run_cleanup EXIT + +required_vars=( + APPLE_CERT_P12_BASE64 + APPLE_CERT_PASSWORD + APPLE_MAC_PROVISION_PROFILE_BASE64 + APPLE_KEYCHAIN_PASSWORD +) + +missing=() +for var_name in "${required_vars[@]}"; do + if [[ -z "${!var_name:-}" ]]; then + missing+=("$var_name") + fi +done + +if [[ "${#missing[@]}" -gt 0 ]]; then + echo "Missing macOS TestFlight signing secrets: ${missing[*]}" >&2 + exit 1 +fi + +eval "$(python3 "$ROOT_DIR/scripts/ci/build_version.py" --format shell)" +app_version="$DISPLAY_VERSION" +app_build="$BUILD_NUMBER" +BUILD_DATE_LINE="$(sed -n 's/^build-date:[[:space:]]*//p' "$ROOT_DIR/pubspec.yaml" | head -n 1)" +BUILD_ID_LINE="$(sed -n 's/^build-id:[[:space:]]*//p' "$ROOT_DIR/pubspec.yaml" | head -n 1)" +GIT_BUILD_DATE="$(cd "$ROOT_DIR" && git show -s --format=%cs HEAD 2>/dev/null || true)" +GIT_BUILD_COMMIT="$(cd "$ROOT_DIR" && git rev-parse --short HEAD 2>/dev/null || true)" +app_build_date="${GIT_BUILD_DATE:-${BUILD_DATE_LINE:-unknown}}" +app_build_commit="${GIT_BUILD_COMMIT:-${BUILD_ID_LINE:-unknown}}" + +tmp_dir="$(mktemp -d "${RUNNER_TEMP:-/tmp}/xworkmate-macos-app-store.XXXXXX")" +cleanup() { + rm -rf "$tmp_dir" +} +trap cleanup EXIT + +apple_setup_signing_keychain + +apple_decode_base64() { + if base64 --help 2>&1 | grep -q -- '--decode'; then + base64 --decode + else + base64 -D + fi +} + +profile_dir="$HOME/Library/MobileDevice/Provisioning Profiles" +profile_path="$profile_dir/xworkmate-macos.mobileprovision" +mkdir -p "$profile_dir" +printf '%s' "$APPLE_MAC_PROVISION_PROFILE_BASE64" | apple_decode_base64 > "$profile_path" +apple_register_cleanup "rm -f \"$profile_path\"" + +mkdir -p "$DIST_DIR" +archive_path="$tmp_dir/$APP_NAME.xcarchive" +export_options_path="$tmp_dir/ExportOptions.plist" +sed "s|\${EXPORT_METHOD}|app-store|g" "$ROOT_DIR/ios/ExportOptions.plist" > "$export_options_path" + +flutter pub get +flutter build macos --release \ + --build-name="$PLATFORM_RELEASE_VERSION" \ + --build-number="$app_build" \ + --dart-define="XWORKMATE_DISPLAY_VERSION=$app_version" \ + --dart-define="XWORKMATE_BUILD_NUMBER=$app_build" \ + --dart-define="XWORKMATE_BUILD_DATE=$app_build_date" \ + --dart-define="XWORKMATE_BUILD_COMMIT=$app_build_commit" \ + "$APP_STORE_DEFINE" + +xcodebuild archive \ + -workspace "$ROOT_DIR/macos/Runner.xcworkspace" \ + -scheme Runner \ + -configuration Release \ + -archivePath "$archive_path" \ + DEVELOPMENT_TEAM="N3G9T67W78" + +xcodebuild -exportArchive \ + -archivePath "$archive_path" \ + -exportPath "$DIST_DIR" \ + -exportOptionsPlist "$export_options_path" + +if ! compgen -G "$DIST_DIR/*.pkg" >/dev/null; then + echo "No macOS TestFlight pkg was produced under $DIST_DIR" >&2 + exit 1 +fi + +echo "macOS TestFlight pkg: $(find "$DIST_DIR" -maxdepth 1 -name '*.pkg' | head -n 1)"