Compare commits

...

6 Commits

Author SHA1 Message Date
Marcos Rodriguez
a27f7722c4
Update build-mac-catalyst.yml 2026-05-31 18:15:02 -05:00
Marcos Rodriguez
426faaae79
Update Fastfile 2026-05-31 18:09:15 -05:00
Marcos Rodriguez
298c7df42f
Update build-mac-catalyst.yml 2026-05-31 18:01:41 -05:00
Marcos Rodriguez
de186df003
Update build-mac-catalyst.yml 2026-05-31 17:52:40 -05:00
Marcos Rodriguez
e45dfebacc
z 2026-05-31 17:49:50 -05:00
Marcos Rodriguez
cfb1b2a4bc
OPS: Notarize macOS 2026-05-31 17:33:29 -05:00
3 changed files with 169 additions and 67 deletions

View File

@ -2,10 +2,13 @@ name: Build Mac Catalyst
on:
workflow_dispatch:
push:
branches:
- master
pull_request:
branches:
- master
types: [labeled, synchronize]
types: [opened, reopened, synchronize, labeled]
concurrency:
group: catalyst-build-${{ github.event.pull_request.number || github.run_id }}
@ -13,10 +16,6 @@ concurrency:
jobs:
build:
if: >
github.event_name == 'workflow_dispatch' ||
(github.event.action == 'labeled' && (github.event.label.name == 'mac-dmg' || github.event.label.name == 'testflight')) ||
github.event.action == 'synchronize'
runs-on: macos-15
timeout-minutes: 120
@ -29,56 +28,39 @@ jobs:
run: |
LABELS=$(gh api "repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/labels" --jq '.[].name' | tr '\n' ',')
echo "all=${LABELS}" >> $GITHUB_OUTPUT
if [[ "$LABELS" == *"mac-dmg"* ]]; then
echo "has_mac_dmg=true" >> $GITHUB_OUTPUT
else
echo "has_mac_dmg=false" >> $GITHUB_OUTPUT
fi
if [[ "$LABELS" == *"testflight"* ]] && [[ "$LABELS" == *"mac-dmg"* ]]; then
if [[ "$LABELS" == *"testflight"* ]]; then
echo "upload_testflight=true" >> $GITHUB_OUTPUT
else
echo "upload_testflight=false" >> $GITHUB_OUTPUT
fi
echo "Labels on PR: ${LABELS}"
- name: Skip if mac-dmg label not present
if: github.event_name == 'pull_request' && steps.labels.outputs.has_mac_dmg != 'true'
run: |
echo "mac-dmg label not found on PR — skipping build."
exit 0
- name: Checkout project
if: github.event_name == 'workflow_dispatch' || steps.labels.outputs.has_mac_dmg == 'true'
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Setup Node.js
if: github.event_name == 'workflow_dispatch' || steps.labels.outputs.has_mac_dmg == 'true'
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
cache: 'npm'
- name: Setup Xcode
if: github.event_name == 'workflow_dispatch' || steps.labels.outputs.has_mac_dmg == 'true'
uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0
with:
xcode-version: latest
- name: Set up Ruby
if: github.event_name == 'workflow_dispatch' || steps.labels.outputs.has_mac_dmg == 'true'
uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1.306.0
with:
ruby-version: 3.4.9
bundler-cache: true
- name: Install Node modules
if: github.event_name == 'workflow_dispatch' || steps.labels.outputs.has_mac_dmg == 'true'
run: npm ci
- name: Cache CocoaPods
if: github.event_name == 'workflow_dispatch' || steps.labels.outputs.has_mac_dmg == 'true'
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
@ -90,7 +72,6 @@ jobs:
${{ runner.os }}-pods-catalyst-
- name: Install CocoaPods dependencies
if: github.event_name == 'workflow_dispatch' || steps.labels.outputs.has_mac_dmg == 'true'
env:
SKIP_APP_STORE_CONNECT_AUTH: '1'
RCT_USE_RN_DEP: "1"
@ -98,62 +79,74 @@ jobs:
run: bundle exec fastlane ios install_pods
- name: Create temporary keychain for signing
if: (github.event_name == 'workflow_dispatch' || steps.labels.outputs.has_mac_dmg == 'true') && steps.labels.outputs.upload_testflight == 'true'
run: |
security create-keychain -p "${{ secrets.KEYCHAIN_PASSWORD }}" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "${{ secrets.KEYCHAIN_PASSWORD }}" build.keychain
security set-keychain-settings -t 3600 -u build.keychain
- name: Create App Store Connect API Key JSON
env:
APPLE_API_KEY_CONTENT: ${{ secrets.APPLE_API_KEY_CONTENT || secrets.APP_STORE_CONNECT_API_KEY_CONTENT }}
run: |
if [[ -n "${APPLE_API_KEY_CONTENT}" ]]; then
printf '%s' "${APPLE_API_KEY_CONTENT}" > ./appstore_api_key.json
echo "Created appstore_api_key.json"
else
echo "No App Store Connect API key content provided; skipping key file creation."
fi
- name: Build Mac Catalyst app with Fastlane
if: github.event_name == 'workflow_dispatch' || steps.labels.outputs.has_mac_dmg == 'true'
id: build_catalyst
run: bundle exec fastlane ios build_catalyst_app_lane
env:
SKIP_APP_STORE_CONNECT_AUTH: '1'
SKIP_CLEAR_DERIVED_DATA: '1'
CATALYST_SIGNING_IDENTITY: ${{ steps.labels.outputs.upload_testflight == 'true' && secrets.CATALYST_SIGNING_IDENTITY || '' }}
CATALYST_TEAM_ID: ${{ steps.labels.outputs.upload_testflight == 'true' && secrets.CATALYST_TEAM_ID || '' }}
GIT_URL: ${{ steps.labels.outputs.upload_testflight == 'true' && secrets.GIT_URL || '' }}
GIT_ACCESS_TOKEN: ${{ steps.labels.outputs.upload_testflight == 'true' && secrets.GIT_ACCESS_TOKEN || '' }}
CATALYST_SIGNING_IDENTITY: ${{ secrets.CATALYST_SIGNING_IDENTITY || 'Apple Distribution' }}
CATALYST_TEAM_ID: ${{ secrets.CATALYST_TEAM_ID || secrets.ITC_TEAM_ID || secrets.TEAM_ID }}
GIT_URL: ${{ secrets.GIT_URL }}
GIT_ACCESS_TOKEN: ${{ secrets.GIT_ACCESS_TOKEN }}
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
MATCH_READONLY: ${{ steps.labels.outputs.upload_testflight == 'true' && 'false' || 'true' }}
KEYCHAIN_NAME: ${{ steps.labels.outputs.upload_testflight == 'true' && 'build' || '' }}
KEYCHAIN_NAME: build
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
CATALYST_NOTARIZE: ${{ (((secrets.CATALYST_SIGNING_IDENTITY != '' || 'Apple Distribution' != '') && (secrets.CATALYST_TEAM_ID != '' || secrets.ITC_TEAM_ID != '' || secrets.TEAM_ID != '')) && (((secrets.APPLE_API_KEY_CONTENT != '' || secrets.APP_STORE_CONNECT_API_KEY_CONTENT != '') && (secrets.APPLE_API_KEY_ID != '' || secrets.APP_STORE_CONNECT_API_KEY_KEY_ID != '') && (secrets.APPLE_API_ISSUER_ID != '' || secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID != '')) || (secrets.APPLE_ID != '' && (secrets.APPLE_APP_SPECIFIC_PASSWORD != '' || secrets.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD != '')))) && '1' || '0' }}
APPLE_API_KEY_PATH: ./appstore_api_key.json
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID || secrets.APP_STORE_CONNECT_API_KEY_KEY_ID }}
APPLE_API_ISSUER_ID: ${{ secrets.APPLE_API_ISSUER_ID || secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD || secrets.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }}
- name: Upload Mac Catalyst DMG
id: upload_dmg
if: success() && (github.event_name == 'workflow_dispatch' || steps.labels.outputs.has_mac_dmg == 'true')
if: success()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: BlueWallet-Mac-Catalyst
path: ${{ steps.build_catalyst.outputs.catalyst_dmg_path }}
if-no-files-found: warn
- name: Create App Store Connect API Key JSON
if: success() && steps.labels.outputs.upload_testflight == 'true'
run: echo '${{ secrets.APPLE_API_KEY_CONTENT }}' > ./appstore_api_key.json
- name: Upload to TestFlight
if: success() && steps.labels.outputs.upload_testflight == 'true'
if: success() && github.event_name == 'pull_request' && steps.labels.outputs.upload_testflight == 'true'
run: bundle exec fastlane ios upload_catalyst_to_testflight
env:
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
APPLE_API_ISSUER_ID: ${{ secrets.APPLE_API_ISSUER_ID }}
CATALYST_TEAM_ID: ${{ secrets.CATALYST_TEAM_ID }}
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID || secrets.APP_STORE_CONNECT_API_KEY_KEY_ID }}
APPLE_API_ISSUER_ID: ${{ secrets.APPLE_API_ISSUER_ID || secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }}
CATALYST_TEAM_ID: ${{ secrets.CATALYST_TEAM_ID || secrets.ITC_TEAM_ID || secrets.TEAM_ID }}
TEAM_ID: ${{ secrets.TEAM_ID }}
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
LATEST_COMMIT_MESSAGE: ${{ github.event.pull_request.title || 'Manual build' }}
- name: Cleanup App Store Connect API Key JSON
if: always() && steps.labels.outputs.upload_testflight == 'true'
if: always()
run: rm -f ./appstore_api_key.json
- name: Cleanup temporary keychain
if: always() && steps.labels.outputs.upload_testflight == 'true'
if: always()
run: security delete-keychain build.keychain || true
- name: Comment on PR with DMG link
if: success() && github.event_name == 'pull_request' && steps.labels.outputs.has_mac_dmg == 'true'
if: success() && github.event_name == 'pull_request'
env:
GH_TOKEN: ${{ github.token }}
UPLOADED_TO_TF: ${{ steps.labels.outputs.upload_testflight }}
@ -175,7 +168,7 @@ jobs:
if [[ -n "$TF_LINE" ]]; then
printf '%s\n' "${TF_LINE}"
fi
printf '<sub>Built from `%s`"
printf '<sub>Built from `%s`</sub>\n' "${{ github.sha }}"
} >"${COMMENT_FILE}"
gh api "repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments" \

View File

@ -25,6 +25,8 @@ def app_store_state_readable(state)
end
require 'securerandom'
require 'json'
require 'base64'
default_platform(:android)
PROJECT_ROOT = File.expand_path("..", __dir__)
@ -437,6 +439,75 @@ platform :ios do
end
end
def match_git_basic_auth_value(token)
return nil if token.nil? || token.empty?
begin
decoded = Base64.strict_decode64(token)
return token if decoded.include?(':')
rescue ArgumentError
# Token is not base64-encoded; encode it below.
end
Base64.strict_encode64("x-access-token:#{token}")
end
def catalyst_notarization_enabled?
ENV.fetch('CATALYST_NOTARIZE', '1') != '0'
end
def notarize_and_staple_catalyst_app!(app_path:, output_dir:)
UI.user_error!("Mac Catalyst app not found for notarization at #{app_path}") unless File.exist?(app_path)
zip_path = File.join(output_dir, 'BlueWallet-Mac-Catalyst-notarization.zip')
FileUtils.rm_f(zip_path)
sh('ditto', '-c', '-k', '--sequesterRsrc', '--keepParent', app_path, zip_path)
api_key_json_path = ENV['APPLE_API_KEY_PATH'] || './appstore_api_key.json'
submitted = false
if File.exist?(api_key_json_path)
begin
key_payload = JSON.parse(File.read(api_key_json_path))
key_id = ENV['APPLE_API_KEY_ID'] || key_payload['key_id']
issuer_id = ENV['APPLE_API_ISSUER_ID'] || key_payload['issuer_id']
private_key = key_payload['key']
if key_id && !key_id.empty? && issuer_id && !issuer_id.empty? && private_key && !private_key.empty?
p8_path = File.join(output_dir, 'notary_api_key.p8')
File.write(p8_path, private_key)
begin
UI.message('Submitting app for notarization with App Store Connect API key...')
sh('xcrun', 'notarytool', 'submit', zip_path, '--key', p8_path, '--key-id', key_id, '--issuer', issuer_id, '--wait')
submitted = true
ensure
FileUtils.rm_f(p8_path)
end
end
rescue JSON::ParserError => ex
UI.important("Could not parse #{api_key_json_path} for notarization: #{ex.message}")
end
end
unless submitted
apple_id = ENV['APPLE_ID'] || ENV['FASTLANE_USER']
app_specific_password = ENV['APPLE_APP_SPECIFIC_PASSWORD'] || ENV['FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD']
team_id = ENV['CATALYST_TEAM_ID'] || ENV['TEAM_ID']
UI.user_error!('Notarization credentials missing. Provide APPLE_API_KEY_PATH (+ APPLE_API_KEY_ID/APPLE_API_ISSUER_ID) or APPLE_ID + APPLE_APP_SPECIFIC_PASSWORD + TEAM_ID/CATALYST_TEAM_ID.') if apple_id.to_s.empty? || app_specific_password.to_s.empty? || team_id.to_s.empty?
UI.message('Submitting app for notarization with Apple ID credentials...')
sh('xcrun', 'notarytool', 'submit', zip_path, '--apple-id', apple_id, '--password', app_specific_password, '--team-id', team_id, '--wait')
end
UI.message('Stapling notarization ticket to app bundle...')
sh('xcrun', 'stapler', 'staple', app_path)
sh('xcrun', 'stapler', 'validate', app_path)
UI.success('Mac Catalyst app notarized and stapled')
ensure
FileUtils.rm_f(zip_path) if defined?(zip_path) && zip_path
end
desc "Register new devices from a file"
lane :register_devices_from_txt do
UI.message("Registering new devices from file...")
@ -492,7 +563,7 @@ platform :ios do
with_retry(3, "Fetching provisioning profile for #{app_identifier}") do
UI.message("Fetching provisioning profile for #{app_identifier}...")
match(
git_basic_authorization: ENV["GIT_ACCESS_TOKEN"],
git_basic_authorization: match_git_basic_auth_value(ENV["GIT_ACCESS_TOKEN"]),
git_url: ENV["GIT_URL"],
type: "appstore",
clone_branch_directly: true,
@ -619,29 +690,53 @@ platform :ios do
ENV['CATALYST_TEAM_ID'] && !ENV['CATALYST_TEAM_ID'].empty?
has_match_creds = ENV['GIT_URL'] && !ENV['GIT_URL'].empty? &&
ENV['GIT_ACCESS_TOKEN'] && !ENV['GIT_ACCESS_TOKEN'].empty?
should_sign = has_signing_creds && has_match_creds && ENV['CATALYST_SKIP_CODESIGNING'] != '1'
should_sign = has_signing_creds && ENV['CATALYST_SKIP_CODESIGNING'] != '1'
should_use_match_profiles = should_sign && has_match_creds && ENV['CATALYST_USE_MATCH'] != '0'
should_notarize = catalyst_notarization_enabled?
signing_identity = ENV['CATALYST_SIGNING_IDENTITY'] || "Apple Distribution"
xcargs_str = "ARCHS=arm64 ONLY_ACTIVE_ARCH=YES"
if should_sign
if should_use_match_profiles
UI.message("Setting up Mac Catalyst provisioning profiles via match...")
team_id = ENV['CATALYST_TEAM_ID']
match_readonly = ENV['MATCH_READONLY'] != 'false'
clone_branch_directly = match_readonly
match_git_auth = match_git_basic_auth_value(ENV['GIT_ACCESS_TOKEN'])
# Create/fetch provisioning profiles for catalyst targets
catalyst_identifiers.each do |app_id|
match(
type: "appstore",
platform: "catalyst",
app_identifier: app_id,
team_id: team_id,
git_url: ENV['GIT_URL'],
git_basic_authorization: ENV['GIT_ACCESS_TOKEN'],
readonly: match_readonly,
clone_branch_directly: true,
keychain_name: ENV['KEYCHAIN_NAME'] || "login",
keychain_password: ENV['KEYCHAIN_PASSWORD'] || ""
)
begin
match(
type: "appstore",
platform: "catalyst",
app_identifier: app_id,
team_id: team_id,
git_url: ENV['GIT_URL'],
git_basic_authorization: match_git_auth,
readonly: match_readonly,
clone_branch_directly: clone_branch_directly,
keychain_name: ENV['KEYCHAIN_NAME'] || "login",
keychain_password: ENV['KEYCHAIN_PASSWORD'] || ""
)
rescue => ex
if !match_readonly && clone_branch_directly
UI.important("match failed for #{app_id} with clone_branch_directly=true: #{ex.message}")
UI.message('Retrying match without clone_branch_directly for writable setup...')
match(
type: "appstore",
platform: "catalyst",
app_identifier: app_id,
team_id: team_id,
git_url: ENV['GIT_URL'],
git_basic_authorization: match_git_auth,
readonly: match_readonly,
keychain_name: ENV['KEYCHAIN_NAME'] || "login",
keychain_password: ENV['KEYCHAIN_PASSWORD'] || ""
)
else
raise ex
end
end
end
xcargs_str += " DEVELOPMENT_TEAM=#{team_id}"
@ -657,12 +752,20 @@ platform :ios do
end
UI.success("Provisioning profiles configured for Mac Catalyst")
elsif should_sign
UI.message('Skipping match profile setup and building without Xcode signing; app will be signed after build')
xcargs_str += " DEVELOPMENT_TEAM=#{ENV['CATALYST_TEAM_ID']}"
xcargs_str += " CODE_SIGN_IDENTITY=- CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO"
else
# Disable code signing entirely so xcodebuild doesn't look for provisioning profiles
xcargs_str += " CODE_SIGN_IDENTITY=- CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO"
UI.message("No signing credentials provided — building without code signing")
end
if should_notarize && !should_sign
UI.user_error!('CATALYST_NOTARIZE is enabled but signing credentials are missing. Set CATALYST_SIGNING_IDENTITY and CATALYST_TEAM_ID, or disable notarization with CATALYST_NOTARIZE=0.')
end
build_app(
scheme: "BlueWallet",
workspace: workspace_path,
@ -670,7 +773,7 @@ platform :ios do
destination: "generic/platform=macOS,variant=Mac Catalyst",
xcargs: xcargs_str,
clean: true,
skip_codesigning: !should_sign,
skip_codesigning: !should_use_match_profiles,
skip_package_ipa: true,
derived_data_path: derived_data_path,
buildlog_path: File.join(project_root, "ios", "build_logs")
@ -715,6 +818,12 @@ platform :ios do
UI.success("App ad-hoc signed")
end
if should_notarize
notarize_and_staple_catalyst_app!(app_path: catalyst_app_path, output_dir: output_dir)
else
UI.message('Skipping notarization (CATALYST_NOTARIZE=0)')
end
dmg_path = File.join(output_dir, "BlueWallet-Mac-Catalyst.dmg")
UI.message("Creating DMG at: #{dmg_path}")

View File

@ -1,5 +1,5 @@
PODS:
- BugsnagReactNative (8.8.1):
- BugsnagReactNative (8.9.0):
- hermes-engine
- RCTRequired
- RCTTypeSafety
@ -29,7 +29,7 @@ PODS:
- hermes-engine/Pre-built (= 250829098.0.10)
- hermes-engine/Pre-built (250829098.0.10)
- lottie-ios (4.6.0)
- lottie-react-native (7.3.7):
- lottie-react-native (7.3.8):
- hermes-engine
- lottie-ios (= 4.6.0)
- RCTRequired
@ -2802,13 +2802,13 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/yoga"
SPEC CHECKSUMS:
BugsnagReactNative: bee770e3f497a8571feb1579bdc083a070bee1f3
BugsnagReactNative: 73ce58aac04585e7cba3081c0abba06d848d62fc
BVLinearGradient: cb006ba232a1f3e4f341bb62c42d1098c284da70
CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99
FBLazyVector: 24e62c765683b8d89006a88a2c8f5cf019f0074d
hermes-engine: 86cdbf283775c54dc008895c3eacd24a1f2a40b4
lottie-ios: 8f959969761e9c45d70353667d00af0e5b9cadb3
lottie-react-native: 26b365c3d5615e87f4db048dcb151de3eb9a8e76
lottie-react-native: ee142214581f3bb68fbda7efcf07b835a189eeda
RCTDeprecation: a4c521821fab57cbb125b36effe84d897d0dfa12
RCTRequired: 9f3a7e5645d4bc3f551593de7550bb66ab6e42bc
RCTSwiftUI: 239ed2eb9e73de5a6f518810630f0c95e01c8702
@ -2817,7 +2817,7 @@ SPEC CHECKSUMS:
React: e2dc35338068bbd299c66f043ae0d7f25de8499e
React-callinvoker: 28b25d21b124c26cebaea713ba7d801b9351dc48
React-Core: 02ed7d2ffb70437bdf2aba074a13078a7b0b9ff0
React-Core-prebuilt: 9e875134f667c471ab68bf9edf1661fa11b86540
React-Core-prebuilt: 77e6ce0d749dda263043e4b099bd12d086f85b4a
React-CoreModules: b3a5a42dadcde3b5d47b325bd912eb2ced89e146
React-cxxreact: fe8f88dda044e5905e99a00f41b7a874c3908716
React-debug: 92944dc4d89f56d640e75498266cbde557a48189
@ -2920,6 +2920,6 @@ SPEC CHECKSUMS:
RNWorklets: dd3b2cb0750090d78d85cd3b3ec0fdbeab5ce118
Yoga: 77dfa8673de2874e1855002ae59c68b8be9b007b
PODFILE CHECKSUM: a6ebefd60fd3fd993430ecd1d3feb222ff502eb0
PODFILE CHECKSUM: 7bf5ce6745c1d4a552afd47cefaf8272077519a4
COCOAPODS: 1.16.2
COCOAPODS: 1.15.2