I'm trying to set up a CI/CD pipeline for a flutter desktop app on Github Actions. While the build for windows works fine, the macos job keeps on hanging (I set a timeout at 30 mins). By setting verbose on flutter build and modifying the autogenerated scripts to dump logs in a file I was able to determine the step it's stuck on is the codesign (I manage certificates with fastlane match), but I can't find any errors in the log, and running the same command locally works fine. The job is defined like this:
build_macos:
if: ${{ github.actor != 'github-actions' }}
needs: mac_check
runs-on: macos-latest
env:
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
ASC_KEY_P8: ${{ secrets.ASC_KEY_P8 }}
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
MATCH_STORAGE_MODE: "google_cloud"
GCLOUD_KEY_JSON: ${{ secrets.GCLOUD_KEY_JSON }}
APP_IDENTIFIER: "<APP_IDENTIFIER>"
TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
steps:
- name: Skip if disabled
if: needs.mac_check.outputs.macos_build != 'true'
run: echo "Skipping MacOS Build"
- uses: actions/checkout@v3
if: needs.mac_check.outputs.macos_build == 'true'
- name: Setup Flutter
if: needs.mac_check.outputs.macos_build == 'true'
uses: subosito/flutter-action@v2
with:
flutter-version-file: pubspec.yaml
- name: Enable macOS Desktop
if: needs.mac_check.outputs.macos_build == 'true'
run: flutter config --enable-macos-desktop
- name: Check Flutter installation and path
run: |
which flutter
flutter doctor -v
- name: Print macos_assemble.sh content
run: |
cat /Users/runner/hostedtoolcache/flutter/stable-3.32.5-arm64/packages/flutter_tools/bin/macos_assemble.sh
- name: Clean ephemeral and cache
if: needs.mac_check.outputs.macos_build == 'true'
run: |
flutter clean
rm -rf macos/Flutter/ephemeral
- name: Install dependencies
if: needs.mac_check.outputs.macos_build == 'true'
run: flutter pub get
- name: Install fastlane
if: needs.mac_check.outputs.macos_build == 'true'
run: |
gem install fastlane
which fastlane
- name: Save GCS credentials
if: needs.mac_check.outputs.macos_build == 'true'
run: |
mkdir -p .credentials
echo "${GCLOUD_KEY_JSON}" | base64 --decode > .credentials/credentials.json
echo "GOOGLE_APPLICATION_CREDENTIALS=$PWD/.credentials/credentials.json" >> $GITHUB_ENV
- name: Run Fastlane macos_signing lane
if: needs.mac_check.outputs.macos_build == 'true'
working-directory: macos
run: fastlane macos_signing --verbose
- name: List code signing identities
run: |
security find-identity -v -p codesigning
- name: Build Flutter macOS release
if: needs.mac_check.outputs.macos_build == 'true'
timeout-minutes: 30
continue-on-error: true
run: |
flutter build macos --release --build-number=${{ github.run_number }} -v --split-debug-info=build/debug-info
- name: Show assemble_debug.log
if: always()
run: |
LOG_FILE="${{ github.workspace }}/macos/Flutter/ephemeral/assemble_debug.log"
if [ -f "$LOG_FILE" ]; then
echo "Contents of assemble_debug.log:"
cat "$LOG_FILE"
else
echo "No assemble_debug.log file found."
fi
- name: Run Fastlane release lane
if: needs.mac_check.outputs.macos_build == 'true'
working-directory: macos
run: fastlane upload_macos_build --verbose
- name: Upload macOS installer for gh release
if: github.ref == 'refs/heads/main' && needs.mac_check.outputs.macos_build == 'true'
uses: actions/upload-artifact@v4
with:
name: macos-installer
path: build/macos/Build/Products/Release/*.app
and this is the content of the fastfile (initially I was calling flutter build from there, I put it in a separate step of the build just to see which step was stuck):
default_platform(:macos)
platform :macos do
before_all do
load_asc_api_token
end
desc "Load the App Store Connect API token"
lane :load_asc_api_token do
app_store_connect_api_key(
key_id: ENV["ASC_KEY_ID"],
issuer_id: ENV["ASC_ISSUER_ID"],
key_content: ENV["ASC_KEY_P8"],
is_key_content_base64: true,
in_house: false
)
end
desc "Release a new macOS build to TestFlight"
lane :release_beta do
commit = last_git_commit
puts "*** Starting macOS release for commit(#{commit[:abbreviated_commit_hash]}) ***"
api_key = lane_context[SharedValues::APP_STORE_CONNECT_API_KEY]
sync_code_signing(
api_key: api_key,
app_identifier: "<APP_IDENTIFIER>",
type: "appstore",
readonly: true
)
build_number = ENV["GITHUB_RUN_NUMBER"] || Time.now.strftime("%Y%m%d%H%M")
puts "*** Build Flutter macOS app for build number #{build_number} ***"
Dir.chdir("../..") do
sh("flutter", "build", "macos", "--release", "--build-number=#{build_number}")
end
puts "*** Create .pkg for notarization ***"
app_path = "../../build/macos/Build/Products/Release/ivaservizi.app"
pkg_path = "ivaservizi.pkg"
sh("productbuild", "--component", app_path, "/Applications", pkg_path)
puts "*** Upload to TestFlight (App Store Connect) ***"
upload_to_testflight(
api_key: api_key,
skip_waiting_for_build_processing: true,
pkg: pkg_path
)
end
lane :macos_signing do
api_key = lane_context[SharedValues::APP_STORE_CONNECT_API_KEY]
sync_code_signing(
api_key: api_key,
app_identifier: "<APP_IDENTIFIER>",
type: "appstore",
readonly: true
)
end
desc "Upload a prebuilt macOS app to TestFlight"
lane :upload_macos_build do
app_path = "../../build/macos/Build/Products/Release/ivaservizi.app"
pkg_path = "ivaservizi.pkg"
unless File.exist?(app_path)
UI.user_error!("Missing built app at #{app_path}. Run `flutter build macos` first.")
end
load_asc_api_token
api_key = lane_context[SharedValues::APP_STORE_CONNECT_API_KEY]
puts "*** Create .pkg for notarization ***"
sh("productbuild", "--component", app_path, "/Applications", pkg_path)
puts "*** Upload to TestFlight (App Store Connect) ***"
upload_to_testflight(
api_key: api_key,
skip_waiting_for_build_processing: true,
pkg: pkg_path
)
end
end
I checked the certificates used by the workflow with security find-identity -v -p codesigning
and they seem correct. I'm honestly stumped, I can't see any issues from the workflow log that could at least point me in the right direction.
The issue was with keychain access. I was able to solve it by creating a temporary one and unlocking it. I also had some trouble getting code signing and notarization to work, so here's the full job definition and fastfile for reference:
build_macos:
if: ${{ github.actor != 'github-actions' }}
needs: mac_check
runs-on: macos-latest
env:
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
ASC_KEY_P8: ${{ secrets.ASC_KEY_P8 }}
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
MATCH_STORAGE_MODE: "google_cloud"
GCLOUD_KEY_JSON: ${{ secrets.GCLOUD_KEY_JSON }}
APP_IDENTIFIER: "<APP IDENTIFIER>"
TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
steps:
- name: Skip if disabled
if: needs.mac_check.outputs.macos_build != 'true'
run: echo "Skipping MacOS Build"
- uses: actions/checkout@v3
if: needs.mac_check.outputs.macos_build == 'true'
- name: Setup Flutter
if: needs.mac_check.outputs.macos_build == 'true'
uses: subosito/flutter-action@v2
with:
flutter-version-file: pubspec.yaml
- name: Enable macOS Desktop
if: needs.mac_check.outputs.macos_build == 'true'
run: flutter config --enable-macos-desktop
- name: Check Flutter installation and path
run: |
which flutter
flutter doctor -v
- name: Create temporary keychain
id: create_keychain
if: needs.mac_check.outputs.macos_build == 'true'
run: |
KEYCHAIN_NAME=temp_build.keychain-db
KEYCHAIN_PASSWORD=$(openssl rand -base64 32)
echo "Creating temporary keychain: $KEYCHAIN_NAME"
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_NAME"
security set-keychain-settings -lut 21600 "$KEYCHAIN_NAME"
security default-keychain -s "$KEYCHAIN_NAME"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_NAME"
security list-keychains -d user -s "$KEYCHAIN_NAME"
echo "password=$KEYCHAIN_PASSWORD" >> $GITHUB_OUTPUT
echo "FASTLANE_KEYCHAIN_PATH=$KEYCHAIN_NAME" >> $GITHUB_ENV
echo "FASTLANE_KEYCHAIN_PASSWORD=$KEYCHAIN_PASSWORD" >> $GITHUB_ENV
- name: Print macos_assemble.sh content
run: |
cat /Users/runner/hostedtoolcache/flutter/stable-3.32.5-arm64/packages/flutter_tools/bin/macos_assemble.sh
- name: Clean ephemeral and cache
if: needs.mac_check.outputs.macos_build == 'true'
run: |
flutter clean
rm -rf macos/Flutter/ephemeral
- name: Install dependencies
if: needs.mac_check.outputs.macos_build == 'true'
run: flutter pub get
- name: Install fastlane
if: needs.mac_check.outputs.macos_build == 'true'
run: |
gem install fastlane
which fastlane
- name: Save GCS credentials
if: needs.mac_check.outputs.macos_build == 'true'
run: |
mkdir -p .credentials
echo "${GCLOUD_KEY_JSON}" | base64 --decode > .credentials/credentials.json
echo "GOOGLE_APPLICATION_CREDENTIALS=$PWD/.credentials/credentials.json" >> $GITHUB_ENV
- name: Run Fastlane macos_signing lane
if: needs.mac_check.outputs.macos_build == 'true'
working-directory: macos
env:
FASTLANE_KEYCHAIN_PASSWORD: ${{ needs.build_macos.outputs.keychain_password }}
run: fastlane macos_signing --verbose
- name: Setting up keychain for codesign and productbuild
if: needs.mac_check.outputs.macos_build == 'true'
run: |
security set-key-partition-list -S apple-tool:,apple: -s -k "$FASTLANE_KEYCHAIN_PASSWORD" "$FASTLANE_KEYCHAIN_PATH"
- name: Run Fastlane release lane
if: needs.mac_check.outputs.macos_build == 'true'
working-directory: macos
env:
FASTLANE_KEYCHAIN_PASSWORD: ${{ needs.build_macos.outputs.keychain_password }}
run: fastlane release_beta --verbose
- name: Upload macOS installer for gh release
if: github.ref == 'refs/heads/main' && needs.mac_check.outputs.macos_build == 'true'
uses: actions/upload-artifact@v4
with:
name: macos-installer
path: build/macos/Build/Products/Release/*.app
- name: Delete temporary keychain
if: always() && needs.mac_check.outputs.macos_build == 'true'
run: |
KEYCHAIN_NAME=temp_build.keychain-db
echo "Deleting keychain $KEYCHAIN_NAME"
security delete-keychain "$KEYCHAIN_NAME" || echo "Keychain not found or already deleted"
default_platform(:mac)
platform :mac do
before_all do
load_asc_api_token
end
desc "Load the App Store Connect API token"
lane :load_asc_api_token do
app_store_connect_api_key(
key_id: ENV["ASC_KEY_ID"],
issuer_id: ENV["ASC_ISSUER_ID"],
key_content: ENV["ASC_KEY_P8"],
is_key_content_base64: true,
in_house: false
)
end
desc "Release a new macOS build to TestFlight"
lane :release_beta do
commit = last_git_commit
puts "*** Starting macOS release for commit(#{commit[:abbreviated_commit_hash]}) ***"
api_key = lane_context[SharedValues::APP_STORE_CONNECT_API_KEY]
macos_signing
build_number = ENV["GITHUB_RUN_NUMBER"] || Time.now.strftime("%Y%m%d%H%M")
puts "*** Build Flutter macOS app for build number #{build_number} ***"
Dir.chdir("../..") do
sh("flutter", "build", "macos", "--release", "--build-number=#{build_number}")
end
increment_build_number({build_number:build_number})
build_mac_app(
configuration: "Release",
skip_package_pkg: false,
output_directory: ".build",
# xcodebuild_formatter: "xcbeautify",
silent: true,
export_method: "app-store",
export_options: {
provisioningProfiles: {
"<APP IDENTIFIER>" => "match AppStore <APP IDENTIFIER> macos"
}
}
)
puts "*** Upload to TestFlight (App Store Connect) ***"
upload_to_testflight(
api_key: api_key,
skip_waiting_for_build_processing: true
)
end
lane :macos_signing do
api_key = lane_context[SharedValues::APP_STORE_CONNECT_API_KEY]
keychain_name = ENV["FASTLANE_KEYCHAIN_PATH"] || "login.keychain-db"
keychain_password = ENV["FASTLANE_KEYCHAIN_PASSWORD"] || ""
sync_code_signing(
api_key: api_key,
app_identifier: "<APP IDENTIFIER>",
type: "appstore",
readonly: true,
platform: "macos",
keychain_name: keychain_name,
keychain_password: keychain_password,
additional_cert_types: ["mac_installer_distribution", "developer_id_installer"]
)
end
end