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 # Quoted so the `matrix.release_only != 'true'` guard compares # string-to-string. A YAML boolean here coerces to a number in # GitHub expressions (true -> 1, 'true' -> NaN), making the guard # always true and building this release-only leg on every PR. 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 }}"