mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-10-09 11:51:48 +00:00
feat: signed macOS and iOS releases from one credentials-driven script
This commit is contained in:
37
.github/workflows/build-artifacts.yml
vendored
37
.github/workflows/build-artifacts.yml
vendored
@@ -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
7
.gitignore
vendored
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
239
ios/Gemfile.lock
239
ios/Gemfile.lock
@@ -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
|
||||||
|
|||||||
@@ -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
277
tool/release.sh
Executable 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
266
tool/secrets.sh
Executable 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
|
||||||
Reference in New Issue
Block a user