xworkmate-app/.github/workflows/build-and-release.yml
Haitao Pan 898b723780
ci: load Vault secrets per-platform in build matrix (#43)
The build matrix loaded every signing secret in one shared block for all
platforms. vault-action's ignoreNotFound only suppresses path-level 404s,
not field-level "No match data" errors, so a single missing field failed
every leg — including linux/windows/android that need no Apple secrets.

Split the load into per-OS-family steps gated by matrix.platform (Apple
for macos/ios, Windows, Android); linux requests nothing. Add shell: bash
to the Export step (its `{ … } >> $GITHUB_ENV` brace syntax is bash-only
and would fail under the default pwsh on windows).

Co-authored-by: Haitao Pan <manbuzhe2009@qq.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 15:45:07 +08:00

328 lines
12 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:
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 }}
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
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
- 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: 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_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' }}
shell: bash
run: bash ./scripts/ci/build_matrix_artifacts.sh "$PLATFORM" "$ARCH" "$SHOULD_RELEASE"
- name: Upload build artifacts
if: ${{ steps.preflight.outputs.should_build_platform == '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' || github.event.pull_request.head.repo.full_name == github.repository) }}
steps:
- name: Checkout source
uses: actions/checkout@v7
- name: Load Vault secrets
id: vault
if: ${{ 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 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' }}
runs-on: ubuntu-22.04
permissions:
contents: write
needs:
- prepare
- build
- remote_contract
steps:
- name: Checkout source
uses: actions/checkout@v7
- name: Download all artifacts
uses: actions/download-artifact@v8
with:
path: release-artifacts
- name: Upload assets to 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 }}