はじめて Advent Calendar に参加することになりました。
キャッチーで面白いことを書いたほうがいいんだろうなと思いつつ、めちゃ地味な内容になってしまいました笑
前提
割とながくなっちゃったので、右の目次を活用してもらえると幸いです!
目標
- GitHub Actions で
.ipa
を生成して App Store Connect へアップロード - App Store Connect API を活用
- オリジナルの Actions を作成して Marketplace に公開
- セキュリティ的にも、精神衛生的にも、git管理下にファイルを増やしたくない
環境
- fastlane は使用しない
- CocoaPods / Carthage 両方使用
- Xcode 11.1
- 複数 Target あり ( 今回は Today Extension )
- Manual Signing ( Automatic Signing に関して後述します )
注意
当記事内で P8 や P12 ファイルなどの秘密鍵、パスワード等を扱います。
扱いに注意し、自己責任でお願いします。
コード
下記ファイルを .github/workflows/
内に置きます。
下に各解説を記しておきます。コメントのアルファベットは解説と一致しています。
※ name
や on
などご自分でいじってください。
on: push
jobs:
main:
runs-on: macOS-latest
env:
KEYCHAIN: '/Library/Keychains/System.keychain'
ASC_KEY_ID: AAAAAAAAAA
ASC_ISSUER_ID: 00000000-0000-0000-0000-000000000000
steps:
# A. Setup
- name: Setup | Checkout
uses: actions/checkout@v1
- name: Setup | Xcode 11.1
run: sudo xcode-select --switch /Applications/Xcode_11.1.app
- name: Setup | App Store Connect API
id: asc
uses: yuki0n0/action-appstoreconnect-token@v1.0
with:
key id: ${{ env.ASC_KEY_ID }}
issuer id: ${{ env.ASC_ISSUER_ID }}
key: ${{ secrets.P8_APPSTORECONNECT_API }}
# B. Cache
- name: Cache | cocoapods
uses: actions/cache@v1
with:
path: Pods
key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }}
restore-keys: ${{ runner.os }}-pods-
- name: Cache | carthage
uses: actions/cache@v1
with:
path: Carthage
key: ${{ runner.os }}-carthage-${{ hashFiles('**/Cartfile.resolved') }}
restore-keys: ${{ runner.os }}-carthage-
# C. Install
- name: Install | cocoapods
run: pod install --repo-update
- name: Install | carthage
env:
GITHUB_ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: carthage bootstrap --platform iOS --cache-builds
# D. Keychain
- name: Keychain | cer
env:
CERTIFICATE_ID: "AAAAAAAAAA"
run: |
JSON=`curl -sS -H "Authorization:Bearer ${{ steps.asc.outputs.token }}" https://api.appstoreconnect.apple.com/v1/certificates/$CERTIFICATE_ID?fields[certificates]=certificateContent`
echo $JSON | jq -r .data.attributes.certificateContent > ios_distribution.cer.txt
base64 --decode ios_distribution.cer.txt > ios_distribution.cer
sudo security import ios_distribution.cer -k $KEYCHAIN -T /usr/bin/codesign
- name: Keychain | p12
run: |
echo "${{ secrets.P12_BASE64 }}" > ios_distribution.p12.txt
base64 --decode ios_distribution.p12.txt > ios_distribution.p12
sudo security import ios_distribution.p12 -k $KEYCHAIN -P ${{ secrets.P12_PASSWORD }} -T /usr/bin/codesign
# E. Build
- name: Build | increment build number
env:
APP_ID: "0000000000"
run: |
VERSION=`sed -n '/MARKETING_VERSION/{s/MARKETING_VERSION = //;s/;//;s/^[[:space:]]*//;p;q;}' *.xcodeproj/project.pbxproj`
JSON=`curl -sS -H "Authorization:Bearer ${{ steps.asc.outputs.token }}" https://api.appstoreconnect.apple.com/v1/preReleaseVersions?filter[app]=$APP_ID&filter[version]=$VERSION&limit=1`
PRE_RELEASE_VERSION_ID=`echo $JSON | jq -r .data[0].id`
JSON=`curl -sS -H "Authorization:Bearer ${{ steps.asc.outputs.token }}" https://api.appstoreconnect.apple.com/v1/builds?filter[preReleaseVersion]=$PRE_RELEASE_VERSION_ID&sort=-version&limit=1`
BUILD_NUMBER=`echo $JSON | jq -r .data[0].attributes.version`
agvtool new-version $(( BUILD_NUMBER + 1 ))
- name: Build | provisioning profile
run: |
JSON=`curl -sSg -H "Authorization:Bearer ${{ steps.asc.outputs.token }}" https://api.appstoreconnect.apple.com/v1/profiles?filter[name]=<name>`
LEN=`echo $JSON | jq .data | jq length`
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
for i in `seq 0 $(($LEN - 1))`; do
uuid=`echo $JSON | jq -r .data[$i].attributes.uuid`
echo $JSON | jq -r .data[$i].attributes.profileContent > $uuid.txt
base64 --decode $uuid.txt > ~/Library/MobileDevice/Provisioning\ Profiles/$uuid.mobileprovision
done
- name: Build | xcodebuild archive
run: set -o pipefail && xcodebuild -workspace <workspace>.xcworkspace -scheme <scheme> -configuration Release archive -archivePath ./archive | xcpretty
- name: Build | xcodebuild export
run: set -o pipefail && xcodebuild -exportArchive -archivePath ./archive.xcarchive -exportPath ./build -exportOptionsPlist ./<project>/exportOptions.plist | xcpretty
# F. Upload
- name: Upload | altool
run: |
mkdir ~/private_keys; echo "${{ secrets.P8_APPSTORECONNECT_API }}" > ~/private_keys/AuthKey_$ASC_KEY_ID.p8
xcrun altool --upload-app -f ./build/<project>.ipa -t ios --apiKey $ASC_KEY_ID --apiIssuer $ASC_ISSUER_ID
解説
準備
API 用の各種キー取得
appstoreconnect.apple.com/access/api で各種キーを取得します。
Issuer ID
キーID
と、AuthKey_<キーID>.p8
ファイルを取得できます。
秘密鍵の登録
github.com/:user/:repository/settings/secrets ページで登録します。
P8ファイルの中身をコピペして、今回は P8_APPSTORECONNECT_API
という名前にします。
.gitignore へ追記
下記を追記しておきます。
*.xcarchive/
build/
A. Setup
Xcode のバージョン指定
現在のデフォルトバージョンは 11.1 ですが、予期せぬ変更に備えて指定するようにしておく。
sudo xcode-select --switch /Applications/Xcode_11.1.app
デフォルトのバージョンと、その他のインストールされているバージョンとそのパスは下記に書いてあります。
https://help.github.com/en/actions/automating-your-workflow-with-github-actions/software-installed-on-github-hosted-runners#macos-1015
App Store Connect API の token 取得
この部分に関して、オリジナルの Actions を作成しました。
詳しくは後述します。
他の step から ${{ steps.asc.outputs.token }}
で API token にアクセスできます。
B. Cache
キャッシュできるようになったのは先月(2019/11)頃の話です。
( GitHub Actions 自体の beta が取れたのも同じ頃。)
さすがにできないと他の CI から乗り換えられませんね。carthage
とか時間かかり過ぎちゃう。
ご丁寧に、様々な環境でのキャッシュアクションの記述例が下記に載っています。
https://github.com/actions/cache/blob/master/examples.md
C. Install
Carthage
GitHub の API 制限に引っかかるので、環境変数 GITHUB_ACCESS_TOKEN
を用意しておきます。
こういうときに ${{ secrets.GITHUB_TOKEN }}
で簡単に取得できるから嬉しいですね。
参考: http://yudoufu.hatenablog.jp/entry/2016/06/09/011754
D. Keychain
下記で指定するものはどちらも Type: iOS Distribution
です。
証明書 Certificate
App Store Connect API を使用して取得します。
取得できるデータは、base64エンコードされているものなので、デコードして、Keychain に登録します。
ドキュメントには記載が無い感じがしますが、jq
はインストール済みなようです。
CERTIFICATE_ID
は、証明書のページに移動してURLから見つけることが出来ます。
https://developer.apple.com/account/resources/certificates/download/:id
秘密鍵 P12
1. P12ファイルを書き出す
書き出す際にパスワードを指定するので忘れないように。書き出し方は複数あります。
イ. キーチェーンアクセス から
キーチェーンアクセス.app を開いて、該当の証明書を右クリックし、cerとp12ファイルを書き出します。
ロ. Xcode から
Preferences > Accounts > 該当のアカウント > ManageCertificates > 該当の証明書を右クリック > Export Certificate
2. P12 を Base64 エンコードする
$ base64 <filepath>
でエンコードしてコピーしておきます。
3. Secrets に P12 と パスワード を登録
https://github.com/:user/:repository/settings/secrets のページで登録します。
ここでの名称は P12_BASE64
, P12_PASSWORD
とします。
ここで登録した値は、GitHub Actions 上からは ${{ secrets.<変数名> }}
でアクセスできます。
E. Build
Build番号 をインクリメント
同一番号では App Store Connect にアップロードできないため、インクリメントします。
現在の Version の最大の Build番号 を App Store Connect API から取得し、+1する段取りです。
すんなりとれなかったので下記のような手順になっています。
- 現在の Version を取得
※ Xcode 11 から変更があったため、agvtool
では素直に取れませんでした。
※ 参照: https://stackoverflow.com/questions/56722677/how-to-read-current-app-version-in-xcode-11-with-script - 現在の Version の App Store Connect 上での ID を取得する
/v1/preReleaseVersions と filter を駆使します。 - その ID をもとに Build番号 の最大値を取得します
/v1/builds と filter
と sort を駆使します。 -
agvtool
コマンドを利用し、インクリメントした Build番号 に変更
Build番号 のインクリメントに関しては、考察を後述します。
Provisioning Profile を取得
App Store Connect API 経由で Profile を取得します。
Profile が 1 つの場合は /v1/profiles/{id} で直接指定すればいいと思います。
複数必要な場合は /v1/profiles を使用して filter
などを使いましょう。
証明書の際と同様に、取得された値は Base64 エンコードされているので、デコードしてファイルに落とし込みます。
ファイルは ~/Library/MobileDevice/Provisioning Profiles/
へ置きます。
xcodebuild でビルド
1. xcpretty
xcpretty
はインストール済みなのですぐ使用できます。
ちゃんと出力結果をパイプしましょう。公式ドキュメントにも CI 上での利用注意 が書いてあります。
2. exportOptionPlist について
必ず plist ファイルを用意しなければなりません。
指定する値の説明は $ xcodebuild -help
で全部出てきます。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>app-store</string>
<key>provisioningProfiles</key>
<dict>
<key>com.sample.subdomain</key>
<string>PP Name or UUID</string>
<key>com.sample.subdomain.subsub</key>
<string>PP Name or UUID</string>
</dict>
</dict>
</plist>
F. Upload
App Store Connect にビルドをアップロードする方法は3種類用意されています。
そのなかでCLIから使用できる (xcrun) altool
を使用してアップロードします。その際の認証方法が2種類。
ユーザ名 と パスワード
-u ユーザ名 -p パスワード
で認証します。
1つ突っかかるのが、開発者アカウントは2段階認証を必須化されているはずなので、ただパスワードを指定するだけでは出来ないと思います。
appleid.apple.com で「App用パスワード」を生成するのがよくある解決策かなと思います。
APIキー (今回はこちら)
--apiKey <キーID> --apiIssuer <IssuerID>
で認証します。
App Store Connect API と同様の jwt 認証です。
$ xcrun altool -h
抜粋
--apiKey <api_key>
This option will search the following directories in sequence for a private key file with the name of 'AuthKey_<api_key>.p8': './private_keys', '~/private_keys', '~/.private_keys', and '~/.appstoreconnect/private_keys'.
とのことなので、今回は ~/private_keys
以下に AuthKey_<api_key>.p8
を用意します。
オリジナル Action
なんども App Store Connect API の token を使用する場面があり、オリジナルのアクションにしてみました。
ついでにマーケットプレイスに公開してみました。
https://github.com/marketplace/actions/app-store-connect-api-token
コード
必要最低限のコードです。最終的なものはソースコードを御覧ください。
name: 'App Store Connect API Token'
description: 'App Store Connect API token generator.'
inputs:
issuer id:
description: 'UUID. Can get from App Store Connect.'
required: true
key id:
description: 'Key ID. Can get from App Store Connect.'
required: true
key:
description: 'P8 private key. Can get from App Store Connect.'
required: true
outputs:
token:
description: 'Generated token to use App Store Connect API.'
runs:
using: 'node12'
main: 'main.js'
const jwt = require('jsonwebtoken')
const core = require('@actions/core')
const keyId = process.env.INPUT_KEY_ID
const issuerId = process.env.INPUT_ISSUER_ID
const key = process.env.INPUT_KEY
const payload = { iss: issuerId, aud: 'appstoreconnect-v1', exp: Math.floor(Date.now() / 1000) + 20 * 60 }
const signedToken = jwt.sign(payload, key, { algorithm: 'ES256', keyid: keyId })
core.setOutput('token', signedToken)
ポイント
ドキュメントも整ってるし、日本語訳されてるし、簡単なので、そんな解説することはないです。少しだけ書いておきます。
A. 実行環境
基本 JavaScript で書きます。Linux 環境からのみ Dockerコンテナ のアクションを使用できます。
B. Inputs の値の使用方法
2種類の取得方法があります。
-
INPUT_<変数名>
という名称で環境変数が生成されているのでそれを使用。
変数名はすべて大文字、空白は_
に変換されています。 -
@actions/core
パッケージを使用して取得。
core.getInput('key id')
C. Marketplace へ公開
新しい release を作成する際に、Publish this Action to the GitHub Marketplace
にチェックを入れるだけで簡単に公開できます。
action.yml
で、Marketplace 上で表示されるアイコンを簡易的にカスタマイズできます。
補足 / Tips
A. Automatic Signing
当記事はすべて Manual Signing の設定として書いています。
もし Automatic Signing をオンにしたまま にしたい場合は、下記のような変更が必要です。
1. Keychain に development
の 証明書 / 秘密鍵 も登録しなくてはならない
Automatic Signing の場合、build/archive は Development 証明書 + Provisioning Profile で署名しなくてはならないためです。
また、export の際には Distribution 証明書 + Provisioning Profile で署名します。(配布前提)
つまり、Keychain には Development 証明書 / Development 秘密鍵 / Distribution 証明書 / Distribution 秘密鍵 4つ登録しなくてはなりません。
参照: https://qiita.com/SCENEE/items/c170bca6b8e8bcb2769f
2. Provisioning Profile を持っていく
ローカルの Xcode で自動生成された Profile を、CI に持っていかなくてはなりません。
つまり、App Store Connect API からの DL はできません。
もし Profile をリポジトリ内で管理する場合は git 管理下にファイルも増えてしまいます。
一方で、exportOptions.plist
での Profile を指定する必要はなくなります。
参照: https://stackoverflow.com/a/39598052
B. Build番号 のインクリメントについて
今回は「App Store Connect API を活用する」という名目があったので上記のように行いましたが、冗長ですね。
参考までに別の選択肢を書いておきます。
- UNIXTIME を使用する
秒単位でビルドが重複することは考えづらいので、基本的には値がかぶらないことが期待される。
ただ工夫次第だろうけど値が大きくなりすぎる。 - agvtool でインクリメント
$ agvtool next-version -all
- git リビジョン情報を使用
$ git rev-list HEAD | wc -l | tr -d ' '
これいいね。
参照: https://qiita.com/aqubi/items/3e4e14c1fdb19d7f7879
C. exportOptions で使う plist について
今回作成したファイルくらいのデータ量なら defaults
, plutil
, PlistBuddy
あたりを使ってコマンドライン上からファイル生成しちゃいたい。
D. App Store Connect API で証明書取得に関して
Xcode 11 から使える新しい証明書のタイプ Apple Development / Distribution
がありますが、
現状では /v1/certificates を通じて取得できないと思います。
少なくとも自分は取得できませんでした。
参照: https://forums.developer.apple.com/thread/123302
E. GitHub Actions で環境変数の扱い
とある step
内で環境変数を定義しても、別の step
からは参照できません。
これは最初わりと厄介に感じます。
また、外部アクションを使用する際の step.with.*
変数に環境変数を渡したいときは、下記のようにしなければいけません。
- uses: yuki0n0/action-appstoreconnect-token@v1.0
with:
key: $VAR # NG
key: ${{ env.VAR }} # OK
F. 成功 / 失敗 通知
成功 / 失敗 の通知があるといいですね。
下記例は LINE Notify ですが、Slack なり何なりお好きにどうぞ。
- if: success()
run: curl -H "Authorization: Bearer <token>" -d "message=成功!" https://notify-api.line.me/api/notify
- if: failure()
run: curl -H "Authorization: Bearer <token>" -d "message=失敗〜" https://notify-api.line.me/api/notify
後記
今 Apple 関連なら Swift UI のこととか書いたら面白そうなんですけどね。
地味な内容で申し訳ないです!誰かの参考になれば...!
いいね、コメント、お待ちしてます!