5
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

iOSアプリの証明書・プロビジョニングプロファイルの有効期限を定期的にチェックする仕組みをfastlaneで実装する

Last updated at Posted at 2018-05-18

概要

iOSアプリの各種証明書とプロビジョニングプロファイルは1年で有効期限が切れるので、その度に更新が必要ですが、1年に1回なのでけっこう忘れやすいのではないかと思います。特にプッシュ通知の証明書とかは普段意識しないので忘れやすいんじゃないでしょうか。実際、飛ばなくなってから期限切れに気づくということがあったため、再発防止のために定期的に有効期限をチェックしてSlackに投げる仕組みをfastaneのlaneとして実装してみました。

これで来年は同じ過ちを犯さないと思います。

実装方法

以下に実装方法をまとめます。

1. 証明書とプロビジョニングプロファイルの情報をDeveloper Portalから取得する

  • fastlane/spaceshipを使う

  • spaceship自体はactionとして用意されていないが、fastlaneに組み込まれていて、sighなどの内部で使われているらしい

  • Spaceship::Portal.loginでログイン(ID/パスはAppfileとFASTLANE_PASSWORDに設定)したあと、Spaceship.provisioning_profile、Spaceship.certificateを呼び出すことでDeveloper PortalのProvisioningProfileとCertificateの情報を取得することができる。

     fastlane_require 'spaceship'
     
     Spaceship::Portal.login
     
     certs = Spaceship.certificate.all
     profiles = Spaceship.provisioning_profile.all
     
    

2. 対象のProvisioningProfileとCertificateの有効期限をチェックして、残り日数に応じて警告を設定する

  • チェック対象とするProvisioningProfileとCertificateに絞る

    • 以下のような形で各タイプに絞った証明書やプロビジョニングプロファイルを取得できる
     Spaceship.certificate.Production
     Spaceship.certificate.ProductionPush
     Spaceship.certificate.VoipPush
     Spaceship.provisioning_profile.AppStore
     Spaceship.provisioning_profile.AdHoc
     ~
    
    • また、certificateとprovisioning_profileは以下のようなプロパティを持っているのでそれらの値でも絞ることができる
      • Spaceship::CertificateのInstance Attribute
        expires, id, name, owner_id, owner_name, owner_type, status, type_display_id

      • Spaceship::ProvisioningProfileのInstance Attribute
        app, certificates, devices, distribution_method, expires, id, managing_app, name, platform, status, type, uuid, version

  • 取得したcertificate、provisioning_profileのexpiresから残り日数を計算する

3. fastlaneのslackアクションでSlackにいい感じで投稿する

  • Slackで3段階のステータスごとに証明書とプロビジョニングプロファイルを並べて表示する

  • fastlane/slackアクションのattachment_propertiesパラメータにattachmentを渡してアクションを実行する。Slack APIのドキュメントを見るとattachmentsのarrayを送ることができるようだったが、fastlane/slackアクションでは配列を渡せないようだったので、attachmentごとにslackアクションを実行するようにした。

     attachments.each do |attachment|
       slack(
         message: "",
         default_payloads: [],
         slack_url: ENV["WEBHOOK_URL"],
         use_webhook_configured_username_and_icon: true,
         attachment_properties: attachment
         )
     end
    
  • attachmentのデータは以下のような項目からなるhash形式にする。fieldsにcertificateとprovisioning_profileの各アイテムについての表示を配列で入れる。

     {
       "color": "#2eb886",
       "pretext": "Optional text that appears above the attachment block",
       "title": "Slack API Documentation",
       "title_link": "https://api.slack.com/",
       "text": "Optional text that appears within the attachment",
       "fields": [
           {
               "title": "Priority",
               "value": "High",
               "short": false
           }
       ],
       ~
     }
    

4. BitriseのScheduled Buildで定期的に有効期限チェックのlaneを実行する

  • Scheduled Buildの設定はBuild一覧からStart/Schedule a Buildを選択すれば設定できる。

コード

desc 'Check expirarion date of certificates and provisioning profiles in Developer Center'
desc 'ex: fastlane ios check_expiration_date'
lane :check_expiration_date do
  Spaceship::Portal.login

  certs = Spaceship.certificate.Production.all + Spaceship.certificate.ProductionPush.all + Spaceship.certificate.VoipPush.all
  profiles = Spaceship.provisioning_profile.all.select { |p| p.status != "Invalid" && p.type == "iOS Distribution" }

  attachments = get_attachments(certs, profiles)
  puts attachments

  attachments.each do |attachment|
    slack(
      message: "",
      default_payloads: [],
      slack_url: ENV["WEBHOOK_URL"],
      use_webhook_configured_username_and_icon: true,
      attachment_properties: attachment
      )
  end
end

CERTIFICATE = "証明書"
MOBILEPROVISION = "プロビジョニングプロファイル"
def get_attachments(certificates, profiles)
  cer_profile_list = { CERTIFICATE => certificates, MOBILEPROVISION => profiles }

  attachments = []
  initial_attachment = {
    "pretext" => "📱 CLINICSアプリ(iOS)の証明書・プロビジョニングプロファイル有効期限切れチェック",
    "color"   => "#FFFFFF"
  }
  expired_attachment = {
    "title"  => "以下の#{CERTIFICATE}または#{MOBILEPROVISION}の有効期限が切れています!!",
    "color"  => "#EEEEEE",
    "fields" => []
  }
  
  danger_attachment = {
    "title"  => "以下の#{CERTIFICATE}または#{MOBILEPROVISION}の有効期限がもうすぐ切れます",
    "color"  => "danger",
    "fields" => []
  }

  warning_attachment = {
    "title"  => "以下の#{CERTIFICATE}または#{MOBILEPROVISION}の有効期限が近づいています",
    "color"  => "warning",
    "fields" => []
  }

  other_attachment = {
    "title"  => "その他の#{CERTIFICATE}#{MOBILEPROVISION}",
    "color"  => "good",
    "fields" => []
  }
  last_attachment = {
    "text": "Distributionの更新手順は<https://xxxxx|こちら>、APNS・VoIPの更新手順は<https://xxxxx|こちら>",
    "actions": [
      {
        "type": "button",
        "text": "View Apple Developer Portal",
        "url": "https://developer.apple.com/account/ios/certificate/?teamId=#{ENV["TEAM_ID"]}"
      }
    ],
    "color"   => "#FFFFFF"
  }

  cer_profile_list.each() { |type, items|
    items.each { |item|
      field = get_field(item, type)
      days_left = get_days_left(item.expires.to_time)

      if days_left < 0
        expired_attachment["fields"].push(field)
      elsif days_left < ENV["DANGER_DAY"].to_i
        danger_attachment["fields"].push(field)
      elsif days_left < ENV["WARNING_DAY"].to_i
        warning_attachment["fields"].push(field)
      else
        other_attachment["fields"].push(field)
      end
    }
  }
  attachments.push(initial_attachment)
  attachments.push(expired_attachment) unless expired_attachment["fields"].empty?
  attachments.push(danger_attachment) unless danger_attachment["fields"].empty?
  attachments.push(warning_attachment) unless warning_attachment["fields"].empty?
  attachments.push(other_attachment) unless other_attachment["fields"].empty?
  attachments.push(last_attachment)
end

def get_days_left(expiration_date)
  now = Time.now
  today = Time.local(now.year, now.month, now.day)
  diff = ((expiration_date - today) / 3600 / 24).floor
end

def get_field(item, type)
  if type == CERTIFICATE
    title = "#{item.owner_name} - #{item.name}"
    extension = ".cer"
  elsif type == MOBILEPROVISION
    title = item.name
    extension = ".mobileprovision"
  end
  days_left = get_days_left(item.expires.to_time)
  expires = item.expires.to_time.strftime("%Y/%m/%d")

  return {
    "title" => "#{title}(#{extension})",
    "value" => "残り #{days_left}日 (有効期限:#{expires})"
  }
end

参考にしたもの

Documents

Spaceship

fastlane/slack

SpaceshipとSlack APIの使い方で参考にした記事とソース

5
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?