feat: signed macOS and iOS releases from one credentials-driven script

This commit is contained in:
Janez Troha
2026-09-18 10:38:41 +02:00
parent 7924803351
commit de321be0e5
7 changed files with 802 additions and 107 deletions

View File

@@ -261,7 +261,23 @@ jobs:
- name: Build macOS release - name: Build macOS release
run: flutter build macos --release run: flutter build macos --release
# A release gets the signed, notarized DMG; an ordinary push keeps the
# quick unsigned one, because notarization means waiting on Apple.
- name: Build, sign and notarize DMG
if: github.event_name == 'release'
env:
MACOS_CERT_P12: ${{ secrets.MACOS_CERT_P12 }}
MACOS_CERT_PASSWORD: ${{ secrets.MACOS_CERT_PASSWORD }}
MACOS_SIGN_ID: ${{ secrets.MACOS_SIGN_ID }}
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
ASC_KEY_P8: ${{ secrets.ASC_KEY_P8 }}
run: |
./tool/release.sh macos
cp dist/*-macos.dmg .
- name: Create DMG - name: Create DMG
if: github.event_name != 'release'
run: | run: |
RAW_TAG="${{ github.event.release.tag_name || github.ref_name }}" RAW_TAG="${{ github.event.release.tag_name || github.ref_name }}"
TAG="${RAW_TAG//\//-}" TAG="${RAW_TAG//\//-}"
@@ -329,6 +345,27 @@ jobs:
echo "IOS_RUNNER_ZIP=${RUNNER_ZIP_NAME}" >> "$GITHUB_ENV" echo "IOS_RUNNER_ZIP=${RUNNER_ZIP_NAME}" >> "$GITHUB_ENV"
ls -lah "${IPA_NAME}" "${RUNNER_ZIP_NAME}" ls -lah "${IPA_NAME}" "${RUNNER_ZIP_NAME}"
# The unsigned artifacts above are for anyone who wants the binary. On a
# release the same commit is signed and sent to TestFlight, through the
# ios/fastlane lanes `make release-ios` uses.
- name: Set up Ruby
if: github.event_name == 'release'
uses: ruby/setup-ruby@v1
with:
ruby-version: "3.3"
working-directory: ios
bundler-cache: true
- name: Sign and upload to TestFlight
if: github.event_name == 'release'
env:
IOS_CERT_P12: ${{ secrets.IOS_CERT_P12 }}
IOS_CERT_PASSWORD: ${{ secrets.IOS_CERT_PASSWORD }}
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
ASC_KEY_P8: ${{ secrets.ASC_KEY_P8 }}
run: ./tool/release.sh ios
- name: Upload iOS artifact - name: Upload iOS artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:

7
.gitignore vendored
View File

@@ -97,3 +97,10 @@ worker/dist/
# Dart code coverage # Dart code coverage
coverage/ coverage/
lcov.info lcov.info
# Release tooling (tool/release.sh, tool/secrets.sh)
dist/
tool/.release.env
ios/vendor/bundle/
ios/.bundle/
ios/fastlane/report.xml

View File

@@ -1,3 +1,6 @@
# fastlane drives the iOS release: archive, export, TestFlight upload.
# Pinned through Gemfile.lock so CI ships what a laptop tested. 2.240+ is
# required: older fastlane needs the abbrev gem, which Ruby 4 no longer bundles.
source "https://rubygems.org" source "https://rubygems.org"
gem "fastlane" gem "fastlane", ">= 2.240"

View File

@@ -1,114 +1,116 @@
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
CFPropertyList (3.0.9) CFPropertyList (3.0.8)
abbrev (0.1.2) abbrev (0.1.2)
addressable (2.9.0) addressable (2.9.0)
public_suffix (>= 2.0.2, < 8.0) public_suffix (>= 2.0.2, < 8.0)
artifactory (3.0.17) artifactory (3.0.17)
atomos (0.1.3) atomos (0.1.3)
aws-eventstream (1.3.2) aws-eventstream (1.4.0)
aws-partitions (1.1109.0) aws-partitions (1.1287.0)
aws-sdk-core (3.224.1) aws-sdk-core (3.257.0)
aws-eventstream (~> 1, >= 1.3.0) aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0) aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9) aws-sigv4 (~> 1.9)
base64 base64
bigdecimal
jmespath (~> 1, >= 1.6.1) jmespath (~> 1, >= 1.6.1)
logger logger
aws-sdk-kms (1.101.0) rexml (~> 3.4, >= 3.4.2)
aws-sdk-core (~> 3, >= 3.216.0) aws-sdk-kms (1.132.0)
aws-sdk-core (~> 3, >= 3.256.0)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.188.0) aws-sdk-s3 (1.232.1)
aws-sdk-core (~> 3, >= 3.224.1) aws-sdk-core (~> 3, >= 3.256.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.5)
aws-sigv4 (1.11.0) aws-sigv4 (1.12.1)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4) babosa (1.0.4)
base64 (0.3.0) base64 (0.3.0)
benchmark (0.5.0)
bigdecimal (4.1.3)
cgi (0.5.2)
claide (1.1.0) claide (1.1.0)
colored (1.2) colored (1.2)
colored2 (3.1.2) colored2 (3.1.2)
commander (4.6.0) commander (4.6.0)
highline (~> 2.0.0) highline (~> 2.0.0)
csv (3.3.5) csv (3.3.6)
declarative (0.0.20) declarative (0.0.20)
digest-crc (0.7.0) digest-crc (0.7.0)
rake (>= 12.0.0, < 14.0.0) rake (>= 12.0.0, < 14.0.0)
domain_name (0.5.20190701) domain_name (0.6.20260907)
unf (>= 0.0.5, < 1.0.0)
dotenv (2.8.1) dotenv (2.8.1)
emoji_regex (3.2.3) emoji_regex (3.2.3)
excon (0.109.0) erb (6.0.7)
faraday (1.10.5) excon (1.7.1)
faraday-em_http (~> 1.0) logger
faraday-em_synchrony (~> 1.0) faraday (2.14.4)
faraday-excon (~> 1.1) faraday-net_http (>= 2.0, < 3.5)
faraday-httpclient (~> 1.0) json
faraday-multipart (~> 1.0) logger
faraday-net_http (~> 1.0)
faraday-net_http_persistent (~> 1.0)
faraday-patron (~> 1.0)
faraday-rack (~> 1.0)
faraday-retry (~> 1.0)
ruby2_keywords (>= 0.0.4)
faraday-cookie_jar (0.0.8) faraday-cookie_jar (0.0.8)
faraday (>= 0.8.0) faraday (>= 0.8.0)
http-cookie (>= 1.0.0) http-cookie (>= 1.0.0)
faraday-em_http (1.0.0) faraday-follow_redirects (0.5.0)
faraday-em_synchrony (1.0.1) faraday (>= 1, < 3)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-multipart (1.2.0) faraday-multipart (1.2.0)
multipart-post (~> 2.0) multipart-post (~> 2.0)
faraday-net_http (1.0.2) faraday-net_http (3.4.4)
faraday-net_http_persistent (1.2.0) net-http (~> 0.5)
faraday-patron (1.0.0) faraday-retry (2.4.0)
faraday-rack (1.0.0) faraday (~> 2.0)
faraday-retry (1.0.4)
faraday_middleware (1.2.1)
faraday (~> 1.0)
fastimage (2.4.1) fastimage (2.4.1)
fastlane (2.229.0) fastlane (2.240.1)
CFPropertyList (>= 2.3, < 4.0.0) CFPropertyList (>= 2.3, < 5.0.0)
abbrev (~> 0.1.2) abbrev (~> 0.1)
addressable (>= 2.8, < 3.0.0) addressable (>= 2.9.0, < 3.0.0)
artifactory (~> 3.0) artifactory (~> 3.0)
aws-sdk-s3 (~> 1.0) aws-sdk-s3 (~> 1.197)
babosa (>= 1.0.3, < 2.0.0) babosa (>= 1.0.3, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0) base64 (~> 0.2)
benchmark (>= 0.1.0)
bundler (>= 2.4.0, < 5.0.0)
cgi (~> 0.4)
colored (~> 1.2) colored (~> 1.2)
commander (~> 4.6) commander (~> 4.6)
csv (~> 3.3) csv (~> 3.3)
dotenv (>= 2.1.1, < 3.0.0) dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 4.0) emoji_regex (>= 0.1, < 4.0)
excon (>= 0.71.0, < 1.0.0) excon (>= 0.71.0, < 2.0.0)
faraday (~> 1.0) faraday (~> 2.7)
faraday-cookie_jar (~> 0.0.6) faraday-cookie_jar (~> 0.0.8)
faraday_middleware (~> 1.0) faraday-follow_redirects (~> 0.3)
faraday-multipart (~> 1.0)
faraday-retry (~> 2.0)
fastimage (>= 2.1.0, < 3.0.0) fastimage (>= 2.1.0, < 3.0.0)
fastlane-sirp (>= 1.0.0) fastlane-sirp (>= 1.1.0)
gh_inspector (>= 1.1.2, < 2.0.0) gh_inspector (>= 1.1.2, < 2.0.0)
google-apis-androidpublisher_v3 (~> 0.3) google-apis-androidpublisher_v3 (~> 0.99)
google-apis-playcustomapp_v1 (~> 0.1) google-apis-playcustomapp_v1 (~> 0.1)
google-cloud-env (>= 1.6.0, < 2.0.0) google-cloud-env (>= 1.6.0, < 2.4.0)
google-cloud-storage (~> 1.31) google-cloud-storage (~> 1.31)
highline (~> 2.0) highline (~> 2.0)
http-cookie (~> 1.0.5) irb (>= 1.8)
json (< 3.0.0) json (< 3.0.0)
jwt (>= 2.1.0, < 3) jwt (>= 2.10.3, < 4)
logger (>= 1.6, < 2.0)
mini_magick (>= 4.9.4, < 5.0.0) mini_magick (>= 4.9.4, < 5.0.0)
multi_json (~> 1.12)
multipart-post (>= 2.0.0, < 3.0.0) multipart-post (>= 2.0.0, < 3.0.0)
mutex_m (~> 0.3.0) mutex_m (~> 0.3)
naturally (~> 2.2) naturally (~> 2.2)
nkf (~> 0.2)
optparse (>= 0.1.1, < 1.0.0) optparse (>= 0.1.1, < 1.0.0)
ostruct (>= 0.1.0)
plist (>= 3.1.0, < 4.0.0) plist (>= 3.1.0, < 4.0.0)
rubyzip (>= 2.0.0, < 3.0.0) rubyzip (>= 3.4.0, < 4.0.0)
security (= 0.1.5) security (~> 0.3)
simctl (~> 1.6.3) simctl (~> 1.6.3)
terminal-notifier (>= 2.0.0, < 3.0.0) terminal-notifier (>= 2.0.0, < 3.0.0)
terminal-table (~> 3) terminal-table (~> 4)
tty-screen (>= 0.6.3, < 1.0.0) tty-screen (>= 0.6.3, < 1.0.0)
tty-spinner (>= 0.8.0, < 1.0.0) tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0) word_wrap (~> 1.0.0)
@@ -117,100 +119,133 @@ GEM
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
fastlane-sirp (1.1.0) fastlane-sirp (1.1.0)
gh_inspector (1.1.3) gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.54.0) google-apis-androidpublisher_v3 (0.108.0)
google-apis-core (>= 0.11.0, < 2.a) google-apis-core (>= 0.15.0, < 2.a)
google-apis-core (0.11.3) google-apis-core (1.2.5)
addressable (~> 2.5, >= 2.5.1) addressable (~> 2.9)
googleauth (>= 0.16.2, < 2.a) faraday (~> 2.13)
httpclient (>= 2.8.1, < 3.a) faraday-follow_redirects (~> 0.3)
mini_mime (~> 1.0) googleauth (~> 1.14)
mini_mime (~> 1.1)
multi_json (~> 1.11)
representable (~> 3.0) representable (~> 3.0)
retriable (>= 2.0, < 4.a) retriable (>= 3.1, < 5.0)
rexml google-apis-iamcredentials_v1 (0.28.0)
google-apis-iamcredentials_v1 (0.17.0) google-apis-core (>= 0.15.0, < 2.a)
google-apis-core (>= 0.11.0, < 2.a) google-apis-playcustomapp_v1 (0.18.0)
google-apis-playcustomapp_v1 (0.13.0) google-apis-core (>= 0.15.0, < 2.a)
google-apis-core (>= 0.11.0, < 2.a) google-apis-storage_v1 (0.67.0)
google-apis-storage_v1 (0.32.0) google-apis-core (>= 0.15.0, < 2.a)
google-apis-core (>= 0.11.0, < 2.a) google-cloud-core (1.9.0)
google-cloud-core (1.6.1)
google-cloud-env (>= 1.0, < 3.a) google-cloud-env (>= 1.0, < 3.a)
google-cloud-errors (~> 1.0) google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0) google-cloud-env (2.3.1)
faraday (>= 0.17.3, < 3.0) base64 (~> 0.2)
google-cloud-errors (1.3.1) faraday (>= 1.0, < 3.a)
google-cloud-storage (1.37.0) google-cloud-errors (1.7.0)
google-cloud-storage (1.62.0)
addressable (~> 2.8) addressable (~> 2.8)
digest-crc (~> 0.4) digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1) google-apis-core (>= 0.18, < 2)
google-apis-storage_v1 (~> 0.1) google-apis-iamcredentials_v1 (~> 0.18)
google-apis-storage_v1 (>= 0.42)
google-cloud-core (~> 1.6) google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a) googleauth (~> 1.9)
mini_mime (~> 1.0) mini_mime (~> 1.0)
googleauth (1.8.1) google-logging-utils (0.2.0)
faraday (>= 0.17.3, < 3.a) googleauth (1.17.4)
jwt (>= 1.4, < 3.0) faraday (>= 1.0, < 3.a)
multi_json (~> 1.11) google-cloud-env (~> 2.2)
google-logging-utils (~> 0.1)
jwt (>= 1.4, < 4.0)
os (>= 0.9, < 2.0) os (>= 0.9, < 2.0)
pstore (~> 0.1)
signet (>= 0.16, < 2.a) signet (>= 0.16, < 2.a)
highline (2.0.3) highline (2.0.3)
http-cookie (1.0.8) http-cookie (1.1.6)
domain_name (~> 0.5) domain_name (~> 0.5)
httpclient (2.9.0) io-console (0.9.4)
mutex_m irb (1.18.0)
pp (>= 0.6.0)
prism (>= 1.3.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
jmespath (1.6.2) jmespath (1.6.2)
json (2.7.6) json (2.21.2)
jwt (2.10.3) jwt (3.3.0)
base64 base64
logger (1.7.0) logger (1.7.0)
mini_magick (4.13.2) mini_magick (4.13.2)
mini_mime (1.1.5) mini_mime (1.1.5)
multi_json (1.15.0) multi_json (1.21.2)
multipart-post (2.4.1) multipart-post (2.4.1)
mutex_m (0.3.0) mutex_m (0.3.0)
nanaimo (0.4.0) nanaimo (0.4.0)
naturally (2.3.0) naturally (2.3.0)
net-http (0.9.1)
uri (>= 0.11.1)
nkf (0.3.0)
optparse (0.8.1) optparse (0.8.1)
os (1.1.4) os (1.1.4)
ostruct (0.6.3)
plist (3.7.2) plist (3.7.2)
public_suffix (5.1.1) pp (0.6.4)
prettyprint
prettyprint (0.2.0)
prism (1.9.0)
pstore (0.2.1)
public_suffix (7.0.5)
rake (13.4.2) rake (13.4.2)
rbs (4.2.0)
logger
prism (>= 1.6.0)
tsort
rdoc (8.0.0)
erb
prism (>= 1.6.0)
rbs (>= 4.0.0)
tsort
reline (0.7.0)
io-console (~> 0.5)
representable (3.2.0) representable (3.2.0)
declarative (< 0.1.0) declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0) trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0) uber (< 0.2.0)
retriable (3.8.0) retriable (4.2.0)
rexml (3.4.4) rexml (3.4.4)
rouge (3.28.0) rouge (3.28.0)
ruby2_keywords (0.0.5) rubyzip (3.6.0)
rubyzip (2.4.1) security (0.3.0)
security (0.1.5) signet (0.22.0)
signet (0.18.0)
addressable (~> 2.8) addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a) faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0) jwt (>= 1.5, < 4.0)
multi_json (~> 1.10)
simctl (1.6.10) simctl (1.6.10)
CFPropertyList CFPropertyList
naturally naturally
terminal-notifier (2.0.0) terminal-notifier (2.0.0)
terminal-table (3.0.2) terminal-table (4.0.0)
unicode-display_width (>= 1.1.1, < 3) unicode-display_width (>= 1.1.1, < 4)
trailblazer-option (0.1.2) trailblazer-option (0.1.2)
tsort (0.2.0)
tty-cursor (0.7.1) tty-cursor (0.7.1)
tty-screen (0.8.2) tty-screen (0.8.2)
tty-spinner (0.9.3) tty-spinner (0.9.3)
tty-cursor (~> 0.7) tty-cursor (~> 0.7)
uber (0.1.0) uber (0.1.0)
unf (0.2.0) unicode-display_width (3.2.0)
unicode-display_width (2.6.0) unicode-emoji (~> 4.1)
unicode-emoji (4.2.0)
uri (1.1.1)
word_wrap (1.0.0) word_wrap (1.0.0)
xcodeproj (1.27.0) xcodeproj (1.28.1)
CFPropertyList (>= 2.3.3, < 4.0) CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3) atomos (~> 0.1.3)
base64
claide (>= 1.0.2, < 2.0) claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1) colored2 (~> 3.1)
nanaimo (~> 0.4.0) nanaimo (~> 0.4.0)
nkf
rexml (>= 3.3.6, < 4.0) rexml (>= 3.3.6, < 4.0)
xcpretty (0.4.1) xcpretty (0.4.1)
rouge (~> 3.28.0) rouge (~> 3.28.0)
@@ -222,7 +257,7 @@ PLATFORMS
ruby ruby
DEPENDENCIES DEPENDENCIES
fastlane fastlane (>= 2.240)
BUNDLED WITH BUNDLED WITH
2.4.22 2.4.22

View File

@@ -13,6 +13,9 @@
# Uncomment the line if you want fastlane to automatically update itself # Uncomment the line if you want fastlane to automatically update itself
# update_fastlane # update_fastlane
require "shellwords"
require "tmpdir"
default_platform(:ios) default_platform(:ios)
platform :ios do platform :ios do
@@ -20,6 +23,66 @@ platform :ios do
File.expand_path("../..", __dir__) File.expand_path("../..", __dir__)
end end
# tool/release.sh exports an App Store Connect key; with it these lanes run
# with nobody at the keyboard, and without it they fall back to the
# interactive Apple ID login they have always used.
def asc_key
return nil unless ENV["ASC_KEY_ID"] && ENV["ASC_KEY_P8"]
app_store_connect_api_key(
key_id: ENV.fetch("ASC_KEY_ID"),
issuer_id: ENV.fetch("ASC_ISSUER_ID"),
key_content: ENV.fetch("ASC_KEY_P8"),
is_key_content_base64: true,
in_house: false,
)
end
# pubspec's +build is a floor, not the last word: TestFlight knows what it
# has already accepted, and a duplicate number is refused outright. So a
# release no longer depends on remembering `make bump` first — bump when the
# version should change, and the build number takes care of itself.
def next_build_number(key)
return ENV.fetch("RELEASE_BUILD", "1").to_i if key.nil?
latest = latest_testflight_build_number(
api_key: key,
app_identifier: "com.meshcore.sar.meshcoreSarApp",
version: ENV["RELEASE_VERSION"],
initial_build_number: 0,
)
[ENV.fetch("RELEASE_BUILD", "0").to_i, latest + 1].max
end
# gym has no api_key option — it shells out to xcodebuild, so the key goes in
# as authentication flags pointing at a file. Only the archive needs them:
# gym adds the same flags to the export itself, and xcodebuild rejects a
# repeated -authenticationKeyPath.
def with_archive_args(build_number = nil)
return yield(nil) unless ENV["ASC_KEY_ID"] && ENV["ASC_KEY_P8"]
path = File.join(Dir.tmpdir, "AuthKey_#{ENV.fetch('ASC_KEY_ID')}.p8")
File.binwrite(path, ENV.fetch("ASC_KEY_P8").unpack1("m"))
File.chmod(0o600, path)
args = [
"-allowProvisioningUpdates",
"-authenticationKeyPath", Shellwords.escape(path),
"-authenticationKeyID", Shellwords.escape(ENV.fetch("ASC_KEY_ID")),
"-authenticationKeyIssuerID", Shellwords.escape(ENV.fetch("ASC_ISSUER_ID")),
]
# A build setting on the command line outranks any xcconfig, so the number
# the caller decided survives the archive regenerating Generated.xcconfig.
if ENV["RELEASE_VERSION"]
args << "FLUTTER_BUILD_NAME=#{ENV.fetch('RELEASE_VERSION')}"
args << "FLUTTER_BUILD_NUMBER=#{build_number}" if build_number
end
begin
yield(args.join(" "))
ensure
File.delete(path) if File.exist?(path)
end
end
def beta_app_info def beta_app_info
{ {
"en-US" => { "en-US" => {
@@ -41,9 +104,12 @@ platform :ios do
desc "Push a new release build to the App Store" desc "Push a new release build to the App Store"
lane :release do lane :release do
increment_build_number(xcodeproj: "Runner.xcodeproj") key = asc_key
build_app(workspace: "Runner.xcworkspace", scheme: "Runner") build = next_build_number(key)
UI.message("building #{ENV['RELEASE_VERSION'] || 'pubspec version'} (#{build})")
with_archive_args(build) { |args| build_app(workspace: "Runner.xcworkspace", scheme: "Runner", xcargs: args) }
upload_to_testflight( upload_to_testflight(
api_key: key,
skip_waiting_for_build_processing: true, skip_waiting_for_build_processing: true,
localized_app_info: beta_app_info, localized_app_info: beta_app_info,
localized_build_info: beta_build_info, localized_build_info: beta_build_info,
@@ -53,9 +119,12 @@ platform :ios do
desc "Push a new beta build to TestFlight" desc "Push a new beta build to TestFlight"
lane :beta do lane :beta do
increment_build_number(xcodeproj: "Runner.xcodeproj") key = asc_key
build_app(workspace: "Runner.xcworkspace", scheme: "Runner") build = next_build_number(key)
UI.message("building #{ENV['RELEASE_VERSION'] || 'pubspec version'} (#{build})")
with_archive_args(build) { |args| build_app(workspace: "Runner.xcworkspace", scheme: "Runner", xcargs: args) }
upload_to_testflight( upload_to_testflight(
api_key: key,
skip_waiting_for_build_processing: true, skip_waiting_for_build_processing: true,
localized_app_info: beta_app_info, localized_app_info: beta_app_info,
localized_build_info: beta_build_info, localized_build_info: beta_build_info,
@@ -71,6 +140,7 @@ platform :ios do
desc "Upload App Store screenshots" desc "Upload App Store screenshots"
lane :store_assets do lane :store_assets do
upload_to_app_store( upload_to_app_store(
api_key: asc_key,
skip_binary_upload: true, skip_binary_upload: true,
skip_metadata: true, skip_metadata: true,
skip_screenshots: false, skip_screenshots: false,

277
tool/release.sh Executable file
View File

@@ -0,0 +1,277 @@
#!/usr/bin/env bash
# Builds, signs and ships MeshCore SAR for macOS and iOS.
#
# ./tool/release.sh macos # → dist/meshcore-sar-v<version>-macos.dmg
# ./tool/release.sh ios # → TestFlight, through the lanes in ios/fastlane
# ./tool/release.sh all
# ./tool/release.sh macos --dry-run # resolve credentials + version, build nothing
#
# Credentials come from the environment, so the same script runs on a laptop and
# in CI with nothing changed. Locally, put them in tool/.release.env (gitignored,
# sourced automatically); in GitHub Actions they arrive as repository secrets.
#
# MACOS_CERT_P12 base64 of the "Developer ID Application" .p12
# MACOS_CERT_PASSWORD password for that .p12
# MACOS_SIGN_ID e.g. "Developer ID Application: Name (JND55328G8)"
# IOS_CERT_P12 base64 of the "Apple Distribution" .p12
# IOS_CERT_PASSWORD password for that .p12
# ASC_KEY_ID App Store Connect API key id
# ASC_ISSUER_ID App Store Connect issuer id
# ASC_KEY_P8 base64 of the AuthKey_<ASC_KEY_ID>.p8
# APPLE_TEAM_ID optional, defaults to JND55328G8
# IOS_PROFILE optional base64 .mobileprovision; without it the API
# key fetches the profile via -allowProvisioningUpdates
#
# One App Store Connect key covers both halves: notarytool notarizes the DMG
# with it and altool uploads the IPA with it. The certificates never touch the
# login keychain — they are imported into a throwaway keychain that the exit
# trap deletes along with the decoded private key, whether the build succeeds or
# fails.
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
APP_DIR="$ROOT_DIR"
DIST="$APP_DIR/dist"
ENV_FILE="$APP_DIR/tool/.release.env"
say() { printf '→ %s\n' "$*"; }
die() { printf 'error: %s\n' "$*" >&2; exit 1; }
# ---------------------------------------------------------------- arguments --
COMMAND=""
VERSION=""
BUILD_NUMBER=""
DRY_RUN=0
usage() {
sed -n '2,31p' "${BASH_SOURCE[0]}" | sed 's/^#\{1\} \{0,1\}//'
exit "${1:-0}"
}
[ $# -gt 0 ] || usage 1
case "$1" in
macos|ios|all) COMMAND="$1"; shift ;;
-h|--help) usage ;;
*) die "unknown command '$1' (expected macos, ios or all)" ;;
esac
while [ $# -gt 0 ]; do
case "$1" in
--version) VERSION="${2:-}"; shift 2 ;;
--build) BUILD_NUMBER="${2:-}"; shift 2 ;;
--dry-run) DRY_RUN=1; shift ;;
-h|--help) usage ;;
*) die "unknown option '$1'" ;;
esac
done
# -------------------------------------------------------------- credentials --
if [ -f "$ENV_FILE" ]; then
say "credentials from tool/.release.env"
set -a; # shellcheck disable=SC1090
. "$ENV_FILE"; set +a
fi
APPLE_TEAM_ID="${APPLE_TEAM_ID:-JND55328G8}"
# Names every missing variable at once — a build that dies on the fourth secret
# after twelve minutes of compiling is a waste of a coffee break.
require() {
local missing=()
for name in "$@"; do
[ -n "${!name:-}" ] || missing+=("$name")
done
if [ ${#missing[@]} -gt 0 ]; then
printf 'error: missing credentials: %s\n' "${missing[*]}" >&2
printf ' set them in %s or in the environment\n' "${ENV_FILE#"$ROOT_DIR"/}" >&2
exit 1
fi
}
case "$COMMAND" in
macos) require MACOS_CERT_P12 MACOS_CERT_PASSWORD MACOS_SIGN_ID ASC_KEY_ID ASC_ISSUER_ID ASC_KEY_P8 ;;
ios) require IOS_CERT_P12 IOS_CERT_PASSWORD ASC_KEY_ID ASC_ISSUER_ID ASC_KEY_P8 ;;
all) require MACOS_CERT_P12 MACOS_CERT_PASSWORD MACOS_SIGN_ID \
IOS_CERT_P12 IOS_CERT_PASSWORD ASC_KEY_ID ASC_ISSUER_ID ASC_KEY_P8 ;;
esac
# ------------------------------------------------------------------ version --
# pubspec.yaml carries the version `make bump` wrote. Its +build is a starting
# floor only: the iOS lane asks TestFlight what it has already accepted and
# takes whichever number is higher, so a release never needs a bump first.
if [ -z "$VERSION" ]; then
VERSION="$(awk -F'[ +]' '/^version:/ {print $2}' "$APP_DIR/pubspec.yaml")"
fi
if [ -z "$BUILD_NUMBER" ]; then
BUILD_NUMBER="$(awk -F'+' '/^version:/ {print $2}' "$APP_DIR/pubspec.yaml")"
fi
[ -n "$BUILD_NUMBER" ] || BUILD_NUMBER=0
# ------------------------------------------------------------------ flutter --
# mise.toml pins the SDK this project builds with; a bare `flutter` on PATH is
# some other version.
cd "$APP_DIR"
if command -v mise >/dev/null 2>&1 && [ -f "$ROOT_DIR/mise.toml" ]; then
flutter() { mise exec -- flutter "$@"; }
else
command -v flutter >/dev/null 2>&1 || die "flutter not found (install mise, or put flutter on PATH)"
fi
# ------------------------------------------------------- keychain + api key --
KEYCHAIN=""
WORK=""
KEYCHAINS_BEFORE=""
cleanup() {
if [ -n "$KEYCHAIN" ]; then
security delete-keychain "$KEYCHAIN" 2>/dev/null || true
# Putting the search list back matters on a laptop, where the list the
# build borrowed is the one the rest of the session depends on.
if [ -n "$KEYCHAINS_BEFORE" ]; then
# shellcheck disable=SC2086
security list-keychains -d user -s $KEYCHAINS_BEFORE 2>/dev/null || true
fi
fi
[ -n "$WORK" ] && rm -rf "$WORK" || true
}
trap cleanup EXIT
# Imports a .p12 into a keychain created for this run only. The partition list
# is what stops codesign from popping a UI prompt on a headless runner.
open_keychain() {
[ -n "$KEYCHAIN" ] && return 0
KEYCHAIN="meshcore-release.keychain"
local pw; pw="$(uuidgen)"
security delete-keychain "$KEYCHAIN" 2>/dev/null || true
security create-keychain -p "$pw" "$KEYCHAIN"
security set-keychain-settings -lut 21600 "$KEYCHAIN"
security unlock-keychain -p "$pw" "$KEYCHAIN"
KEYCHAINS_BEFORE="$(security list-keychains -d user | tr -d '"' | tr '\n' ' ')"
# shellcheck disable=SC2086
security list-keychains -d user -s "$KEYCHAIN" $KEYCHAINS_BEFORE
KEYCHAIN_PW="$pw"
}
import_cert() {
local b64="$1" password="$2" label="$3"
open_keychain
printf '%s' "$b64" | base64 --decode > "$WORK/cert.p12"
security import "$WORK/cert.p12" -k "$KEYCHAIN" -P "$password" \
-T /usr/bin/codesign -T /usr/bin/security >/dev/null
security set-key-partition-list -S apple-tool:,apple:,codesign: \
-s -k "$KEYCHAIN_PW" "$KEYCHAIN" >/dev/null
rm -f "$WORK/cert.p12"
say "imported $label certificate"
}
WORK="$(mktemp -d -t meshcore-release)"
KEY_FILE="$WORK/AuthKey_${ASC_KEY_ID}.p8"
printf '%s' "$ASC_KEY_P8" | base64 --decode > "$KEY_FILE"
chmod 600 "$KEY_FILE"
# altool looks the key up by id in a directory; notarytool takes the path.
export API_PRIVATE_KEYS_DIR="$WORK"
# -------------------------------------------------------------------- macos --
build_macos() {
say "macOS $VERSION ($BUILD_NUMBER)"
import_cert "$MACOS_CERT_P12" "$MACOS_CERT_PASSWORD" "Developer ID"
flutter build macos --release \
--build-name="$VERSION" --build-number="$BUILD_NUMBER"
local app
app="$(find "$APP_DIR/build/macos/Build/Products/Release" -maxdepth 1 -name '*.app' | head -1)"
[ -n "$app" ] || die "no .app in build/macos/Build/Products/Release"
# Sign inside out: nested code first, then the bundle. --force drops the
# entitlements Xcode baked in, so hand them back on the outer signature or the
# sandboxed app launches without network access.
say "signing $(basename "$app")"
while IFS= read -r nested; do
codesign --force --options runtime --timestamp \
--sign "$MACOS_SIGN_ID" "$nested"
done < <(find "$app/Contents" -depth \( -name '*.framework' -o -name '*.dylib' \) -print 2>/dev/null)
codesign --force --options runtime --timestamp \
--entitlements "$APP_DIR/macos/Runner/Release.entitlements" \
--sign "$MACOS_SIGN_ID" "$app"
codesign --verify --strict --verbose=2 "$app"
mkdir -p "$DIST"
local dmg="$DIST/meshcore-sar-v$VERSION-macos.dmg"
say "packaging $(basename "$dmg")"
rm -f "$dmg"
hdiutil create -volname "MeshCore SAR" -srcfolder "$app" -ov -format UDZO "$dmg" >/dev/null
say "notarizing (this waits on Apple)"
xcrun notarytool submit "$dmg" \
--key "$KEY_FILE" --key-id "$ASC_KEY_ID" --issuer "$ASC_ISSUER_ID" --wait
xcrun stapler staple "$app"
xcrun stapler staple "$dmg"
xcrun stapler validate "$dmg"
spctl --assess --type exec -vv "$app"
say "done: ${dmg#"$ROOT_DIR"/}"
}
# ---------------------------------------------------------------------- ios --
build_ios() {
say "iOS $VERSION ($BUILD_NUMBER)"
import_cert "$IOS_CERT_P12" "$IOS_CERT_PASSWORD" "Apple Distribution"
if [ -n "${IOS_PROFILE:-}" ]; then
local dir="$HOME/Library/MobileDevice/Provisioning Profiles"
mkdir -p "$dir"
printf '%s' "$IOS_PROFILE" | base64 --decode > "$dir/meshcore-release.mobileprovision"
say "installed provisioning profile"
fi
# --config-only just refreshes Generated.xcconfig; the Flutter build itself
# happens inside the Xcode build phase when fastlane archives. This is the
# sequence `make release-ios` has always used.
flutter build ios --release --no-codesign --config-only \
--build-name="$VERSION" --build-number="$BUILD_NUMBER"
[ -f "$APP_DIR/ios/Gemfile.lock" ] || die "run 'bundle install' in ios/ first"
say "archiving and uploading through the ios/fastlane lanes"
mkdir -p "$DIST"
# fastlane refuses to handle non-ASCII metadata without a UTF-8 locale, and
# a CI runner's locale is whatever the image felt like.
export LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8
export FASTLANE_SKIP_UPDATE_CHECK=1
export ASC_KEY_ID ASC_ISSUER_ID ASC_KEY_P8 APPLE_TEAM_ID
export RELEASE_VERSION="$VERSION" RELEASE_BUILD="$BUILD_NUMBER"
(cd "$APP_DIR/ios" && bundle exec fastlane ios release)
say "done: $VERSION ($BUILD_NUMBER) is on TestFlight"
}
# --------------------------------------------------------------------- main --
if [ "$DRY_RUN" -eq 1 ]; then
if [ -n "${IOS_PROFILE:-}" ]; then profile_note="supplied"; else profile_note="fetched with -allowProvisioningUpdates"; fi
cat <<SUMMARY
→ dry run, nothing will be built
command $COMMAND
version $VERSION ($BUILD_NUMBER)
team $APPLE_TEAM_ID
api key $ASC_KEY_ID (issuer ${ASC_ISSUER_ID:0:8}…)
sign id ${MACOS_SIGN_ID:-–}
profile $profile_note
output ${DIST#"$ROOT_DIR"/}
SUMMARY
exit 0
fi
case "$COMMAND" in
macos) build_macos ;;
ios) build_ios ;;
all) build_macos; build_ios ;;
esac

266
tool/secrets.sh Executable file
View File

@@ -0,0 +1,266 @@
#!/usr/bin/env bash
# Publishes the signing credentials this repo needs into GitHub Actions secrets.
#
# ./tool/secrets.sh # export identities, push every secret
# ./tool/secrets.sh --dry-run # do everything except the push
# ./tool/secrets.sh --asc-key ~/Downloads/AuthKey_ABC123.p8 \
# --asc-issuer 69a6de80-… --asc-key-id ABC123
# ./tool/secrets.sh list # what the repo has now, and what's local
# ./tool/secrets.sh env # write the same values to tool/.release.env
#
# The certificates come out of the login keychain live: there is no .p12 lying
# around to lose, and no step where a private key sits in Downloads. macOS will
# ask you to allow the export once per key — that prompt is the point.
#
# What ends up in the repo (see tool/release.sh for what reads them):
# MACOS_CERT_P12 / MACOS_CERT_PASSWORD Developer ID Application identity
# MACOS_SIGN_ID its exact codesign name
# IOS_CERT_P12 / IOS_CERT_PASSWORD Apple Distribution identity
# ASC_KEY_ID / ASC_ISSUER_ID / ASC_KEY_P8 App Store Connect key, if given
#
# `env` sends that same set to tool/.release.env instead of to GitHub, which is
# how a release runs on this machine: one export, two possible sinks.
#
# The App Store Connect key is not a keychain identity, so it comes from
# --asc-key/--asc-issuer/--asc-key-id or the matching environment variables.
# Leave them out and the three secrets are skipped, the certificates still go.
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
APP_DIR="$ROOT_DIR"
TEAM="${APPLE_TEAM_ID:-JND55328G8}"
say() { printf '→ %s\n' "$*"; }
die() { printf 'error: %s\n' "$*" >&2; exit 1; }
# ---------------------------------------------------------------- arguments --
COMMAND="push"
DRY_RUN=0
REPO=""
MACOS_IDENTITY=""
IOS_IDENTITY=""
ASC_KEY_FILE="${ASC_KEY_FILE:-}"
ASC_KEY_ID="${ASC_KEY_ID:-}"
ASC_ISSUER_ID="${ASC_ISSUER_ID:-}"
ENV_FILE="$APP_DIR/tool/.release.env"
usage() {
sed -n '2,25p' "${BASH_SOURCE[0]}" | sed 's/^#\{1\} \{0,1\}//'
exit "${1:-0}"
}
if [ $# -gt 0 ]; then
case "$1" in
push|list|env) COMMAND="$1"; shift ;;
esac
fi
while [ $# -gt 0 ]; do
case "$1" in
--dry-run) DRY_RUN=1; shift ;;
--repo) REPO="${2:-}"; shift 2 ;;
--macos-identity) MACOS_IDENTITY="${2:-}"; shift 2 ;;
--ios-identity) IOS_IDENTITY="${2:-}"; shift 2 ;;
--asc-key) ASC_KEY_FILE="${2:-}"; shift 2 ;;
--asc-key-id) ASC_KEY_ID="${2:-}"; shift 2 ;;
--asc-issuer) ASC_ISSUER_ID="${2:-}"; shift 2 ;;
-h|--help) usage ;;
*) die "unknown option '$1'" ;;
esac
done
[ "$COMMAND" = "env" ] || command -v gh >/dev/null 2>&1 || die "gh not found (brew install gh)"
command -v openssl >/dev/null 2>&1 || die "openssl not found"
if [ -z "$REPO" ]; then
REPO="$(git -C "$ROOT_DIR" remote get-url origin 2>/dev/null \
| sed -E 's#^git@github\.com:##; s#^https://github\.com/##; s#\.git$##')"
fi
[ -n "$REPO" ] || die "no repo: pass --repo owner/name"
WORK="$(mktemp -d -t meshcore-secrets)"
trap 'rm -rf "$WORK"' EXIT
# ---------------------------------------------------------------- identities --
# `security find-identity` prints one line per usable identity; we want the
# named kind issued to this team, and we want to fail loudly on ambiguity rather
# than sign a release with whichever one sorted first.
find_identity() {
local kind="$1" rows hashes
# Each row is "<sha1> <name>". Xcode installs a copy of a certificate every
# time it fetches one, so the same identity turns up several times; that is
# not ambiguity. Only distinct certificates are.
rows="$(security find-identity -v -p codesigning \
| sed -n 's/^ *[0-9]*) \([0-9A-F]*\) "\(.*\)"$/\1 \2/p' \
| grep " $kind:" | grep "($TEAM)" || true)"
[ -n "$rows" ] || die "no \"$kind\" identity for team $TEAM in the keychain"
hashes="$(printf '%s\n' "$rows" | cut -d' ' -f1 | sort -u)"
if [ "$(printf '%s\n' "$hashes" | wc -l)" -gt 1 ]; then
printf 'error: several different "%s" certificates for team %s:\n' "$kind" "$TEAM" >&2
printf '%s\n' "$rows" | sort -u | sed 's/^/ /' >&2
die "pick one with --macos-identity / --ios-identity"
fi
printf '%s' "$(printf '%s\n' "$rows" | head -1 | cut -d' ' -f2-)"
}
[ -n "$MACOS_IDENTITY" ] || MACOS_IDENTITY="$(find_identity 'Developer ID Application')"
[ -n "$IOS_IDENTITY" ] || IOS_IDENTITY="$(find_identity 'Apple Distribution')"
if [ "$COMMAND" = "list" ]; then
say "repo $REPO"
gh secret list --repo "$REPO" || true
printf '\n'
say "local identities (team $TEAM)"
printf ' macOS %s\n iOS %s\n' "$MACOS_IDENTITY" "$IOS_IDENTITY"
exit 0
fi
# ------------------------------------------------------------------- export --
# `security export` has no way to name a single identity, so everything comes out
# in one bundle and openssl splits it back apart. A cert and its key share a
# localKeyID inside the bundle, which is what pairs them here.
BUNDLE="$WORK/all.p12"
BUNDLE_PW="$(uuidgen)"
BAGS="$WORK/bags.pem"
say "exporting identities from the login keychain (allow the prompt)"
security export -k "$HOME/Library/Keychains/login.keychain-db" \
-t identities -f pkcs12 -P "$BUNDLE_PW" -o "$BUNDLE" \
|| die "export refused — the prompt needs Allow, not Deny"
openssl pkcs12 -in "$BUNDLE" -passin "pass:$BUNDLE_PW" -nodes -legacy -out "$BAGS" 2>/dev/null \
|| openssl pkcs12 -in "$BUNDLE" -passin "pass:$BUNDLE_PW" -nodes -out "$BAGS" \
|| die "openssl could not read the exported bundle"
rm -f "$BUNDLE"
# Apple's intermediates ride along in the .p12 so the runner can build a chain
# to the root without having to already trust the right CA.
CHAIN="$WORK/chain.pem"
: > "$CHAIN"
for ca in "Apple Worldwide Developer Relations" "Developer ID Certification Authority"; do
security find-certificate -a -c "$ca" -p >> "$CHAIN" 2>/dev/null || true
done
# Pulls one identity out of the bundle: the cert bag with this friendlyName, and
# the key bag carrying the same localKeyID.
split_identity() {
local name="$1" out_cert="$2" out_key="$3"
BAGS="$BAGS" NAME="$name" CERT="$out_cert" KEY="$out_key" python3 - <<'PY'
import os, re, sys
bags = open(os.environ["BAGS"]).read()
blocks = re.findall(r"Bag Attributes.*?-----END [A-Z ]+-----\n", bags, re.S)
def attr(block, key):
m = re.search(rf"^\s*{key}:\s*(.+)$", block, re.M)
return m.group(1).strip() if m else None
want = os.environ["NAME"]
cert = next((b for b in blocks
if "BEGIN CERTIFICATE" in b and attr(b, "friendlyName") == want), None)
if cert is None:
sys.exit(f"no certificate named {want!r} in the exported bundle")
key_id = attr(cert, "localKeyID")
key = next((b for b in blocks
if "PRIVATE KEY" in b and attr(b, "localKeyID") == key_id), None)
if key is None:
sys.exit(f"{want!r} has no private key in the keychain — it cannot sign")
pem = lambda b: b[b.index("-----BEGIN"):]
open(os.environ["CERT"], "w").write(pem(cert))
open(os.environ["KEY"], "w").write(pem(key))
PY
}
# Repacks one identity into its own .p12 and prints "<base64> <password>".
pack_identity() {
local name="$1"
local cert="$WORK/leaf.pem" key="$WORK/leaf.key" p12="$WORK/leaf.p12"
local pw; pw="$(uuidgen)"
split_identity "$name" "$cert" "$key"
# An empty -certfile is an error rather than a no-op, so only pass it when
# the intermediates were actually found. -legacy keeps the encryption to what
# macOS `security import` reads without argument on every runner image.
local chain=()
if [ -s "$CHAIN" ]; then chain=(-certfile "$CHAIN"); fi
openssl pkcs12 -export -legacy -out "$p12" -inkey "$key" -in "$cert" \
${chain[@]+"${chain[@]}"} -name "$name" -passout "pass:$pw" 2>/dev/null \
|| openssl pkcs12 -export -out "$p12" -inkey "$key" -in "$cert" \
${chain[@]+"${chain[@]}"} -name "$name" -passout "pass:$pw"
printf '%s %s' "$(base64 < "$p12" | tr -d '\n')" "$pw"
rm -f "$cert" "$key" "$p12"
}
say "packing ${MACOS_IDENTITY}"
read -r MACOS_CERT_P12 MACOS_CERT_PASSWORD <<<"$(pack_identity "$MACOS_IDENTITY")"
say "packing ${IOS_IDENTITY}"
read -r IOS_CERT_P12 IOS_CERT_PASSWORD <<<"$(pack_identity "$IOS_IDENTITY")"
# ---------------------------------------------------------- app store connect --
ASC_KEY_P8=""
if [ -n "$ASC_KEY_FILE" ]; then
[ -f "$ASC_KEY_FILE" ] || die "no such key file: $ASC_KEY_FILE"
ASC_KEY_P8="$(base64 < "$ASC_KEY_FILE" | tr -d '\n')"
# AuthKey_ABC123.p8 names the key it holds; take the id from the filename
# unless one was given, because that is one fewer thing to mistype.
if [ -z "$ASC_KEY_ID" ]; then
base="$(basename "$ASC_KEY_FILE")"; base="${base%.p8}"
ASC_KEY_ID="${base#AuthKey_}"
fi
[ -n "$ASC_ISSUER_ID" ] || die "--asc-key needs --asc-issuer too"
fi
# --------------------------------------------------------------------- push --
set_secret() {
local name="$1" value="$2"
[ -n "$value" ] || return 0
if [ "$DRY_RUN" -eq 1 ]; then
printf ' %-20s %s bytes\n' "$name" "${#value}"
return 0
fi
if [ "$COMMAND" = "env" ]; then
# Single-quoted so base64 padding and the spaces in an identity name stay
# literal; the only character that could break out is escaped.
printf "%s='%s'\n" "$name" "${value//\'/\'\\\'\'}" >> "$ENV_FILE"
printf ' %-20s written\n' "$name"
return 0
fi
# Through stdin, never as an argument: arguments are readable in `ps`.
printf '%s' "$value" | gh secret set "$name" --repo "$REPO"
printf ' %-20s set\n' "$name"
}
if [ "$DRY_RUN" -eq 1 ]; then
say "dry run — $REPO would receive:"
elif [ "$COMMAND" = "env" ]; then
say "writing ${ENV_FILE#"$ROOT_DIR"/}"
# Created empty and locked down before anything is appended: the private keys
# must never exist in a world-readable file, not even for an instant.
rm -f "$ENV_FILE"
install -m 600 /dev/null "$ENV_FILE"
printf '# Written by tool/secrets.sh — read by tool/release.sh. Not in git.\n' >> "$ENV_FILE"
else
say "pushing to $REPO"
fi
set_secret MACOS_CERT_P12 "$MACOS_CERT_P12"
set_secret MACOS_CERT_PASSWORD "$MACOS_CERT_PASSWORD"
set_secret MACOS_SIGN_ID "$MACOS_IDENTITY"
set_secret IOS_CERT_P12 "$IOS_CERT_P12"
set_secret IOS_CERT_PASSWORD "$IOS_CERT_PASSWORD"
set_secret ASC_KEY_ID "$ASC_KEY_ID"
set_secret ASC_ISSUER_ID "$ASC_ISSUER_ID"
set_secret ASC_KEY_P8 "$ASC_KEY_P8"
if [ -z "$ASC_KEY_P8" ]; then
printf '\nnote: no App Store Connect key given, so ASC_KEY_ID, ASC_ISSUER_ID and\n'
printf ' ASC_KEY_P8 were left alone. Add them with:\n'
printf ' ./tool/secrets.sh --asc-key AuthKey_XXX.p8 --asc-issuer <uuid>\n'
fi