fluttermacosgithub-actionsfastlanefastlane-match

flutter build macos --release hangs on codesign when it runs on github actions


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.


Solution

  • 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