xworkmate-app/.github/workflows/build-and-release.yml
Haitao Pan 01515f95ca
ci: TestFlight opt-in toggle + Xcode 27 build fixes (#54)
* 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>
2026-06-30 07:27:09 +08:00

417 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_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 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' && needs.prepare.outputs.testflight_enabled == 'true' }}
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:
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 }}"