* ci: gate TestFlight behind opt-in toggle + Xcode 27 build fixes TestFlight is now opt-in (default OFF). A workflow_dispatch boolean `enable_testflight` (or the `ENABLE_TESTFLIGHT` repo variable) drives a `prepare.outputs.testflight_enabled` flag that gates the macOS app-store-pkg build leg and both testflight_ios/testflight_macos upload legs. Missing Apple signing secrets no longer fail the normal DMG/IPA release path (package-macos-app-store-pkg.sh hard-exits without them). Xcode 27 build compatibility: - Align Apple deployment targets so no pod sits below the app minimum (Xcode 27 rejects this): macOS pods + RunnerTests -> 15.6, iOS pods -> 15.5 to match the Runner targets. - Add a `lipo` shim (scripts/xcode-tools/lipo) wired onto PATH in the iOS/macOS build phases; Xcode 27 only accepts one `-verify_arch` architecture per call while Flutter passes them all at once. - macOS project hygiene: correct PrivacyInfo.xcprivacy path, set app display name + LSApplicationCategoryType. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test: make temp-dir cleanup resilient to concurrent-write races The assistant execution target tests deleted their temp HOME/workspace dirs with a raw recursive delete in addTearDown. A background flush (e.g. controller dispose still persisting state) can keep writing into the dir while the delete walks it, so the delete races and fails with "Directory not empty" (errno 39), failing the test on CI. Route all unguarded teardown deletes through the existing _resilientDelete helper (re-check existence + retry), and harden that helper so its final fallback never re-throws — a temp-dir cleanup failure must never fail a test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Haitao Pan <manbuzhe2009@qq.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
410 lines
17 KiB
YAML
410 lines
17 KiB
YAML
name: Build and Release XWorkmate Packages
|
|
|
|
env:
|
|
VAULT_ADDR: https://vault.svc.plus
|
|
FLUTTER_VERSION: 3.41.4
|
|
|
|
on:
|
|
push:
|
|
branches:
|
|
- main
|
|
- "release/**"
|
|
tags:
|
|
- "v*"
|
|
paths-ignore:
|
|
- "README.md"
|
|
pull_request:
|
|
paths:
|
|
- "lib/**"
|
|
- "assets/**"
|
|
- "android/**"
|
|
- "ios/**"
|
|
- "macos/**"
|
|
- "linux/**"
|
|
- "windows/**"
|
|
- "rust/**"
|
|
- "test/**"
|
|
- "scripts/**"
|
|
- "pubspec.*"
|
|
- "Makefile"
|
|
- ".github/actions/setup-flutter-sdk/action.yml"
|
|
- ".github/workflows/build-and-release.yml"
|
|
workflow_dispatch:
|
|
inputs:
|
|
enable_testflight:
|
|
description: "Build & upload TestFlight (macOS/iOS App Store) artifacts"
|
|
type: boolean
|
|
default: false
|
|
|
|
permissions:
|
|
contents: read
|
|
id-token: write
|
|
|
|
concurrency:
|
|
group: build-and-release-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
jobs:
|
|
prepare:
|
|
if: ${{ github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main' || (github.event_name == 'pull_request' && github.base_ref == 'main') }}
|
|
runs-on: ubuntu-22.04
|
|
needs:
|
|
- verify
|
|
permissions:
|
|
contents: write
|
|
outputs:
|
|
should_release: ${{ steps.flags.outputs.should_release }}
|
|
testflight_enabled: ${{ steps.flags.outputs.testflight_enabled }}
|
|
release_tag: ${{ steps.meta.outputs.release_tag }}
|
|
release_title: ${{ steps.meta.outputs.release_title }}
|
|
release_notes: ${{ steps.meta.outputs.release_notes }}
|
|
steps:
|
|
- name: Checkout source
|
|
uses: actions/checkout@v7
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Determine release mode
|
|
id: flags
|
|
shell: bash
|
|
env:
|
|
ENABLE_TESTFLIGHT_INPUT: ${{ github.event.inputs.enable_testflight }}
|
|
ENABLE_TESTFLIGHT_VAR: ${{ vars.ENABLE_TESTFLIGHT }}
|
|
run: |
|
|
if [[ "${GITHUB_REF:-}" == refs/tags/v* || "${GITHUB_EVENT_NAME:-}" == "workflow_dispatch" || "${GITHUB_REF:-}" == "refs/heads/main" ]]; then
|
|
echo "should_release=true" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "should_release=false" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
# TestFlight is opt-in (default OFF). Enabled only when explicitly
|
|
# requested via the workflow_dispatch input or the ENABLE_TESTFLIGHT
|
|
# repo/org variable. Keeps missing Apple signing secrets from failing
|
|
# the normal DMG/IPA release path.
|
|
if [[ "${ENABLE_TESTFLIGHT_INPUT:-}" == "true" || "${ENABLE_TESTFLIGHT_VAR:-}" == "true" ]]; then
|
|
echo "testflight_enabled=true" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "testflight_enabled=false" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
- name: Compute release metadata
|
|
id: meta
|
|
shell: bash
|
|
run: bash ./scripts/ci/compute_release_metadata.sh
|
|
|
|
verify:
|
|
runs-on: ubuntu-22.04
|
|
steps:
|
|
- name: Checkout source
|
|
uses: actions/checkout@v7
|
|
|
|
- name: Set up Flutter SDK
|
|
uses: ./.github/actions/setup-flutter-sdk
|
|
with:
|
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
|
|
|
- name: Install Linux dependencies
|
|
shell: bash
|
|
run: bash ./scripts/ci/setup_platform_deps.sh linux
|
|
|
|
- name: Run Flutter verification suite
|
|
shell: bash
|
|
run: bash ./scripts/ci/run_flutter_ci_suite.sh
|
|
|
|
build:
|
|
if: ${{ github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main' || (github.event_name == 'pull_request' && github.base_ref == 'main') }}
|
|
name: Build ${{ matrix.platform }} ${{ matrix.package }}
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- platform: linux
|
|
arch: amd64
|
|
package: deb-rpm
|
|
runs_on: ubuntu-22.04
|
|
artifact_name: build-linux-amd64-deb-rpm
|
|
artifact_paths: |
|
|
dist/linux/*.deb
|
|
dist/linux/*.rpm
|
|
- platform: windows
|
|
arch: amd64
|
|
package: msi
|
|
runs_on: windows-2022
|
|
artifact_name: build-windows-amd64-msi
|
|
artifact_paths: |
|
|
dist/windows/*.msi
|
|
dist/windows/*.zip
|
|
- platform: macos
|
|
arch: arm64
|
|
package: dmg
|
|
runs_on: macos-14
|
|
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
|
|
runs_on: macos-14
|
|
artifact_name: build-ios-arm64-ipa
|
|
artifact_paths: |
|
|
dist/ios/*.ipa
|
|
dist/ios/*.zip
|
|
- platform: android
|
|
arch: arm64
|
|
package: apk
|
|
runs_on: ubuntu-22.04
|
|
artifact_name: build-android-arm64-apk
|
|
artifact_paths: |
|
|
dist/android/*.apk
|
|
runs-on: ${{ matrix.runs_on }}
|
|
needs:
|
|
- prepare
|
|
env:
|
|
PLATFORM: ${{ matrix.platform }}
|
|
ARCH: ${{ matrix.arch }}
|
|
SHOULD_RELEASE: ${{ needs.prepare.outputs.should_release }}
|
|
steps:
|
|
- name: Checkout source
|
|
uses: actions/checkout@v7
|
|
|
|
# Secrets are loaded per-platform so a missing/extra field for one OS
|
|
# family never fails the matrix legs of the others (vault-action's
|
|
# ignoreNotFound does NOT suppress field-level "No match data" errors).
|
|
- name: Load Vault secrets (Apple)
|
|
id: vault_apple
|
|
if: ${{ (matrix.platform == 'macos' || matrix.platform == 'ios') && (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 XWORKMATE_SIGN_IDENTITY | XWORKMATE_SIGN_IDENTITY ;
|
|
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
|
|
|
|
- name: Load Vault secrets (Windows)
|
|
id: vault_windows
|
|
if: ${{ matrix.platform == 'windows' && (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 WINDOWS_PFX_BASE64 | WINDOWS_PFX_BASE64 ;
|
|
kv/data/github-actions/xworkmate-app WINDOWS_PFX_PASSWORD | WINDOWS_PFX_PASSWORD ;
|
|
kv/data/github-actions/xworkmate-app WINDOWS_CODESIGN_SUBJECT | WINDOWS_CODESIGN_SUBJECT
|
|
|
|
- name: Load Vault secrets (Android)
|
|
id: vault_android
|
|
if: ${{ matrix.platform == 'android' && (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 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 ;
|
|
kv/data/github-actions/xworkmate-app ANDROID_KEY_PASSWORD | ANDROID_KEY_PASSWORD
|
|
|
|
- name: Export signing secrets
|
|
shell: bash
|
|
run: |
|
|
{
|
|
echo "XWORKMATE_SIGN_IDENTITY=${{ steps.vault_apple.outputs.XWORKMATE_SIGN_IDENTITY }}"
|
|
echo "APPLE_CERT_P12_BASE64=${{ steps.vault_apple.outputs.APPLE_CERT_P12_BASE64 }}"
|
|
echo "APPLE_CERT_PASSWORD=${{ steps.vault_apple.outputs.APPLE_CERT_PASSWORD }}"
|
|
echo "APPLE_PROVISION_PROFILE_BASE64=${{ steps.vault_apple.outputs.APPLE_PROVISION_PROFILE_BASE64 }}"
|
|
echo "APPLE_MAC_PROVISION_PROFILE_BASE64=${{ steps.vault_apple.outputs.APPLE_MAC_PROVISION_PROFILE_BASE64 }}"
|
|
echo "APPLE_KEYCHAIN_PASSWORD=${{ steps.vault_apple.outputs.APPLE_KEYCHAIN_PASSWORD }}"
|
|
echo "APPLE_EXPORT_METHOD=${{ steps.vault_apple.outputs.APPLE_EXPORT_METHOD }}"
|
|
echo "WINDOWS_PFX_BASE64=${{ steps.vault_windows.outputs.WINDOWS_PFX_BASE64 }}"
|
|
echo "WINDOWS_PFX_PASSWORD=${{ steps.vault_windows.outputs.WINDOWS_PFX_PASSWORD }}"
|
|
echo "WINDOWS_CODESIGN_SUBJECT=${{ steps.vault_windows.outputs.WINDOWS_CODESIGN_SUBJECT }}"
|
|
echo "ANDROID_KEYSTORE_BASE64=${{ steps.vault_android.outputs.ANDROID_KEYSTORE_BASE64 }}"
|
|
echo "ANDROID_KEYSTORE_PASSWORD=${{ steps.vault_android.outputs.ANDROID_KEYSTORE_PASSWORD }}"
|
|
echo "ANDROID_KEY_ALIAS=${{ steps.vault_android.outputs.ANDROID_KEY_ALIAS }}"
|
|
echo "ANDROID_KEY_PASSWORD=${{ steps.vault_android.outputs.ANDROID_KEY_PASSWORD }}"
|
|
} >> "$GITHUB_ENV"
|
|
|
|
- name: Set up Flutter SDK
|
|
uses: ./.github/actions/setup-flutter-sdk
|
|
with:
|
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
|
|
|
- name: Set up Java 17 for Android
|
|
if: ${{ matrix.platform == 'android' }}
|
|
uses: actions/setup-java@v5
|
|
with:
|
|
distribution: temurin
|
|
java-version: "17"
|
|
|
|
- name: Preflight platform lane
|
|
id: preflight
|
|
shell: bash
|
|
run: bash ./scripts/ci/platform_preflight.sh "$PLATFORM" "$SHOULD_RELEASE"
|
|
|
|
- name: Install platform dependencies
|
|
if: ${{ steps.preflight.outputs.should_build_platform == 'true' }}
|
|
shell: bash
|
|
run: bash ./scripts/ci/setup_platform_deps.sh "$PLATFORM"
|
|
|
|
- name: Install Go
|
|
if: ${{ matrix.platform == 'macos' && steps.preflight.outputs.should_build_platform == 'true' }}
|
|
uses: actions/setup-go@v6
|
|
with:
|
|
go-version: "1.24.1"
|
|
|
|
- name: Build platform artifacts
|
|
if: ${{ steps.preflight.outputs.should_build_platform == 'true' && (matrix.release_only != 'true' || env.SHOULD_RELEASE == 'true') && (matrix.package != 'app-store-pkg' || needs.prepare.outputs.testflight_enabled == 'true') }}
|
|
shell: bash
|
|
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' && (matrix.release_only != 'true' || env.SHOULD_RELEASE == 'true') && (matrix.package != 'app-store-pkg' || needs.prepare.outputs.testflight_enabled == 'true') }}
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: ${{ matrix.artifact_name }}
|
|
path: ${{ matrix.artifact_paths }}
|
|
if-no-files-found: error
|
|
|
|
remote_contract:
|
|
name: Test - remote provider contract
|
|
runs-on: ubuntu-22.04
|
|
needs:
|
|
- build
|
|
# Test-stage quality gate: runs between build and release.
|
|
# continue-on-error keeps it skippable so a failure never blocks release.
|
|
continue-on-error: true
|
|
if: ${{ github.event_name != 'push' && github.event_name != 'pull_request' }}
|
|
steps:
|
|
- name: Checkout source
|
|
uses: actions/checkout@v7
|
|
|
|
- name: Load Vault secrets
|
|
id: vault
|
|
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 REVIEW_ACCOUNT_LOGIN_PASSWORD | REVIEW_ACCOUNT_LOGIN_PASSWORD
|
|
|
|
- name: Export remote contract secrets
|
|
run: echo "REVIEW_ACCOUNT_LOGIN_PASSWORD=${{ steps.vault.outputs.REVIEW_ACCOUNT_LOGIN_PASSWORD }}" >> "$GITHUB_ENV"
|
|
|
|
- name: Verify accounts to bridge provider contract
|
|
shell: bash
|
|
env:
|
|
REVIEW_ACCOUNT_BASE_URL: ${{ vars.REVIEW_ACCOUNT_BASE_URL }}
|
|
REVIEW_ACCOUNT_LOGIN_NAME: ${{ vars.REVIEW_ACCOUNT_LOGIN_NAME }}
|
|
run: bash ./scripts/ci/verify_remote_provider_contract.sh
|
|
|
|
release:
|
|
# always() so release waits for the remote_contract gate to finish but is
|
|
# 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' }}
|
|
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:
|
|
- prepare
|
|
- build
|
|
- remote_contract
|
|
steps:
|
|
- name: Checkout source
|
|
uses: actions/checkout@v7
|
|
|
|
- name: Load App Store Connect secrets
|
|
id: vault
|
|
if: ${{ matrix.target != 'github_release' && needs.prepare.outputs.testflight_enabled == 'true' && (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 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' && needs.prepare.outputs.testflight_enabled == 'true' }}
|
|
run: |
|
|
{
|
|
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:
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
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' && needs.prepare.outputs.testflight_enabled == 'true' }}
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: ${{ matrix.artifact_name }}
|
|
path: ${{ matrix.artifact_path }}
|
|
|
|
- name: Upload to TestFlight
|
|
if: ${{ matrix.target != 'github_release' && needs.prepare.outputs.testflight_enabled == 'true' }}
|
|
shell: bash
|
|
run: bash ./scripts/ci/testflight_upload.sh "${{ matrix.testflight_platform }}" "${{ matrix.artifact_path }}"
|