GitHub Action の Private / Public リポジトリの制限

GitHub Actionsの支払いについて - GitHub ヘルプ

Public リポジトリ

GitHub Actions の制限はない

Private リポジトリ

GitHub Actions の実行時間とストレージに制限がある。

アカウントに存在するすべての Private リポジトリの実行時間、ストレージ使用量の合算が制限の対象。個々のリポジトリごとに制限が計算されるわけではない。

  • Free アカウントの場合
    • ストレージ : 500 MB
    • 実行時間 : 2,000分(月)
  • Pro アカウントの場合
    • ストレージ : 1 GB
    • 実行時間 : 3,000分(月)

Private リポジトリにおける GitHub の Actions の使用状況(利用時間とストレージ使用量)はユーザアカウントから確認できる。

GitHub Actions の使用状況を表示する - GitHub ヘルプ

制限を超えて使用したい場合は、料金支払いが必要になる。

まとめ

  • Public リポジトリ
    • Action の実行時間、ストレージは気にしなくて良い
  • Private リポジトリ
    • Action の実行時間、ストレージを気にする必要がある
    • 実行時間を気にするならば Action 内の処理で適切にタイムアウト時間を設定する必要もある
    • タイムアウト待ち時間で、実行時間が思わず消費される可能性があるからだ

GitHub ワークフローで環境変数、シークレットな情報を使用する

これは 2020/05 時点の情報をもとにした記事なので、最新では事情が変わっている可能性を留意すること。

ワークフロー内で使用できる環境変数

環境変数の利用 - GitHub ヘルプ

たとえば GITHUB_REPOSITORY(所有者とリポジトリの名前 octocat/Hello-World など)がある。

API キーなどシークレット情報の扱い

暗号化されたシークレットの作成と保存 - GitHub ヘルプ

ある Web API を呼び出すのに必要な API キーがあったとして、それをコード中にそのまま記載してしまうと API キーの漏洩になってしまう。これを避けるため、シークレット情報は GitHub に設定して秘匿して使用することができる。

シークレット情報は Organization / Repository ごとに設定することができる。

設定したシークレット情報は、以下のようにワークフローで参照することができる。

steps:
  - shell: bash
    env:
      SUPER_SECRET: ${{ secrets.SuperSecret }}
    run: |
      example-command "$SUPER_SECRET"

GitHub ワークフローを使って、プロジェクトを定期実行し、成果物をデプロイする(Python)

GitHub ワークフローを使ってプロジェクトの定期実行→成果物をデプロイする方法を調べたのでまとめる。

これは 2020/05 時点の情報をもとにした記事なので、最新では事情が変わっている可能性を留意すること。

目的

  • GitHub ワークフロー / アクションの意味を理解
  • GitHub リポジトリのプロジェクトを定期実行する(Python
    • 定期実行時の成果物をデプロイする

GitHub ワークフロー / アクションの意味

ワークフロー

ワークフローを設定する - GitHub ヘルプ

ワークフローとは、GitHubで任意のプロジェクトをビルド、テスト、パッケージ、リリース、またはデプロイするためにリポジトリで設定できる、カスタムの自動プロセスです。 ワークフローを使用すると、豊富なツールとサービスでソフトウェア開発のライフサイクルを自動化できます。

「ワークフロー」に記述した内容で、ビルドやテスト、リリース、デプロイが自動化できるようになる。

ワークフロー自体は .github/workflow の下に yml ファイルで記述する。

アクション

GitHub Actionsについて - GitHub ヘルプ

GitHub Actionsは、コードを保存するのと同じ場所でソフトウェア開発のワークフローを自動化し、プルリクエストやIssueで協力することを支援します。 個々のタスクを書き、アクションを呼び出し、それらを組み合わせてカスタムのワークフローを作成できます。

「アクション」は「ワークフロー」を構成する単位となる。

どんなアクションがあるかは Markerplace で確認できる。

GitHub Marketplace · Actions to improve your workflow · GitHub

たとえば git リポジトリからチェックアウトするアクション「Checkout v2」がある。

Checkout · Actions · GitHub Marketplace · GitHub

この Checkout をワークフロー内で使用するには、以下のように記述する。

name: workflow-name

# リポジトリに Push したタイミングでワークフローを実行する
on: [push]


jobs:
  build:
    name: workflow-job
    runs-on: ubuntu-latest

    steps:
    # リポジトリをチェックアウトする
    - name: Checkout
      uses: actions/checkout@v2

    # チェックアウトされたリポジトリで、ビルド、テスト、リリース、デプロイ、などをする。

その他

GitHub リポジトリのプロジェクトを定期実行する(Python

今回のサンプル

GitHub - daisuke-t-jp/jma-hazard-xml2json

今回作成したリポジトリはこれになる。(Python プロジェクト)

この Python スクリプト気象庁で提供されている災害情報(XML)を取得して JSON フォーマットに変換して、ファイルに保存する、ということやっている。

気象庁 | 気象庁防災情報XMLフォーマット形式電文(PULL型)

requirements.txt

Python スクリプトに必要な外部パッケージを requirements.txt ファイルを作成し、そこに記載する。

今回は XMLPython の辞書として扱うために xmltodict を requirements.txt に追加する。

xmltodict

ワークフローを作成する

ローカルでスクリプトの動作が確認出来たら GitHub ワークフローを作成して GitHub 上でも動作するようにする。

今回のワークフローは以下の機能にする

  • 定期実行したい(10分に1回)
  • 成果物である JSON ファイルを保存したい

yaml で記載したワークフローは以下になる。

# ワークフローの名前
name: cron-deploy

# ワークフローの実行タイミング
# この場合は 10 分に 1 回
#
# cron のフォーマットは以下が参考になる
# http://manpages.ubuntu.com/manpages/trusty/ja/man5/crontab.5.html
on:
  schedule:
    - cron:  '*/10 * * * *'

# ジョブの記述
jobs:
  # job_id は "build"
  build:
    # ジョブの名前(GitHub 上で表示される)
    name: deploy
    # Ubuntu で実行する
    # https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idruns-on
    runs-on: ubuntu-latest

    # ジョブのステップ
    steps:
    - name: Checkout
      # チェックアウト
      uses: actions/checkout@v2
    - name: Setup Python 3.8
      # Python のセットアップ
      uses: actions/setup-python@v1
      with:
        python-version: 3.8
    - name: Install dependencies
      # Python 外部パッケージをインストール
      run: |
        python -m pip install --upgrade pip
        pip install -r requirements.txt
    - name: Run script
      # Python スクリプトを実行する
      run: |
        python jma-hazard-xml2json.py
    - name: Deploy
      # gh-pages ブランチに成果物をデプロイする
      uses: peaceiris/actions-gh-pages@v3
      with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./json


このワークフローを GitHub にコミットすると、ただしく10分ごとに gh-pages ブランチにデプロイされることが確認出来た。(前のファイルは上書きされる)

f:id:daisuke-t-jp:20200527152552p:plain:w500

DuckDuckGo iOS アプリの「Find In Page(ページ内検索)」で入力文字の候補が選択できない問題を修正

DuckDuckGo Privacy Browser

DuckDuckGo Privacy Browser

DuckDuckGoiOS アプリで「Find In Page(ページ内検索)」で日本語が入力できない現象があったので、原因を調べて修正した。

DuckDuckGoiOS / Android 共にオープンソースなので、第三者でも実際にコードを動かして確認することができる。

https://github.com/duckduckgo/


問題の原因は、ページ内検索をする時に

  1. TextField に入力された文字で「ページ内検索」を実行する
  2. ページ内検索の結果を WebView に反映
  3. TextField に入力された文字を、再度 TextField に設定する(表示された検索結果と入力された文字を同期、確定)

ということになっていたので、このシーケンスでは、たとえば「鴨」をページ内検索したい場合

  1. 「か」と入力する。「か」でページ内検索を実行する
  2. 「か」のページ内検索を WebView に反映
  3. TextField に「か」を設定する

になるため

  • 「か」の候補である「鴨」を選択できない
  • 「かも」と続けて入力した時の候補である「鴨」を選択できない(「か」「も」でそれぞれ確定されるため)

という現象が発生していた。

最初は日本語の問題に見えていたが、強制的に入力文字が「確定」されてしまうため、「候補」が選べない問題だった。(つまり日本語以外にもこの問題は影響している)


https://github.com/duckduckgo/iOS/pull/608

この問題の修正を PullRequest を送って Merge された。


f:id:daisuke-t-jp:20200518182028p:plain:w600

修正後
AppStore のレビューでこの改善に気づいている人がいて、よかったなと。。

iOS の TensorFlow サンプルを試してみる

TensorFlow(テンソルフロー) や iOS でのサンプル実行の方法を調べたメモ。



https://www.tensorflow.org/lite?hl=ja

iOS で TensorFlow を動かすには TensorFlow Lite を使用すると良い。
TensorFlow Lite はモバイルや組み込みデバイス向けにチューニングされた TensorFlow のバージョンである。

CocoaPods で導入するにはこうする。

platform :ios, '13.0'

target 'TargetName' do
  use_frameworks!

  pod 'TensorFlowLiteSwift'

end



https://www.tensorflow.org/lite/examples?hl=ja

実際にどんなことができるか、どんな動きになるかを試したい場合は、ここに各パターン(画像分類やセグメンテーションなど)のサンプルアプリのリンク(GitHub へのリンク)がある。

しかし GitHub 上にはあるが、上記のサンプルのコレクションページにはない物もあるので GitHub の examples を直接見た方が良いだろう。

https://github.com/tensorflow/examples/tree/master/lite/examples



AI / 機械学習というジャンルでは「推論結果を出したい」の前に、その推論に使用するモデルを用意する必要があるので骨が折れそうなイメージがあるが TensorFlow では、学習ずみのモデルを公開しているのでそれを使用することができる。

https://tfhub.dev/

ちなみに Apple の CoreML で TensorFlow のモデルを使用したい場合は、 CoreML 形式にコンバートされたモデルが Apple のサイトにあるので、ダウンロードできる。

https://developer.apple.com/machine-learning/models/



実際にサンプルを試すには各サンプルのルートで pod install してからワークスペースを実行するだけで OK。

たとえば以下のような物がある。


ImageSegmentation

画像に含まれる物体を分解できる。 人と背景や犬などが画像上のどのピクセルなのかを特定できる。

f:id:daisuke-t-jp:20200515172424p:plain:w250



StyleTransfer

ソースとなる画像にスタイルを適用することができる。

たとえば ソースの写真がこれで
f:id:daisuke-t-jp:20200515195125p:plain:w200


適用するスタイル画像にこれを選ぶと(ゴッホの星月夜)
f:id:daisuke-t-jp:20200515195131p:plain:w200


スタイル適用された画像はこう出力される。(ちょっとゴッホぽくなった)
f:id:daisuke-t-jp:20200515195137p:plain:w200



という感じにサンプルとコードを見ている中でいくつか気になる点が出てきたので、プルリクエストを送ってみた。

https://github.com/tensorflow/examples/pull/214 https://github.com/tensorflow/examples/pull/215 https://github.com/tensorflow/examples/pull/216 https://github.com/tensorflow/examples/pull/217

内容は、ダークモード時に UI が一部見えなくなる、不要なメモリ確保をしている、など。

無事プルリクエストはマージされたので、今後サンプルから学ぶ人のちょっとした足しになれば、いいなぁ、、、と。

それと、「不要なメモリ確保を削除」の PR が受け入れられたということは自分の TensorFlow Lite の認識(の一部)が、正解とズレていなかった証明になるので、安心した。。

pod install と update の違い。あと deintegrate

pod install / update をいままで何となく使っていたので、違いを調べる。

https://guides.cocoapods.org/terminal/commands.html

コマンドのマニュアル↑をみると

  • install
    • Podfile.lock に記載されているバージョンで、依存関係をインストールする
  • update
    • 古い依存関係を更新し、あたらしい Podifle.lock を作成する

となる。

使いわけとしては Podfile.lock がプロジェクトにあり、そこに書かれているライブラリのバージョンにて開発する場合は pod install をする。 もし、ライブラリのバージョンを更新したい場合は pod update を使用する。という感じになる。

実際の使い分けのケースとしては、複数メンバで開発している場合だと、開発時のライブラリバージョンを統一したい時は、リポジトリに Podfile.lock を追加し、各自 pod install でライブラリをインストールする流れにすると、バージョン差異の問題が起きない、ということができる。


あと pod deintegrate というコマンドもあるが、これを実行すると CocoaPods の統合を解除することができる。

具体的には

  • Xcode のプロジェクト(xcodeproj)から CocoaPods の記載を削除する
  • Podfile, Podfile.lock は残る

という感じなので、リポジトリに push したタイミングで自動ビルド(pod install から始まる)をしたい時は deintegrate された状態で、バージョン管理していくといいかもしれない。

SwiftUI で GoogleAdMob バナーを表示する

SwiftUI で GoogleAds のバナー広告を表示するサンプルを作った。




方法としてはまず GADBannerView を表示する UIViewController を用意する。

class GADBannerViewController: UIViewController {
    
    let bannerView = GADBannerView(adSize: kGADAdSizeBanner)
    
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // This app using sample id.
        // https://developers.google.com/admob/ios/test-ads#sample_ad_units
        bannerView.adUnitID = "ca-app-pub-3940256099942544/2934735716"
        
        bannerView.delegate = self
        bannerView.translatesAutoresizingMaskIntoConstraints = false
        bannerView.rootViewController = self
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        bannerView.load(GADRequest())
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        
        bannerView.removeFromSuperview()
    }
    
}



// MARK: - GADBannerViewDelegate
extension GADBannerViewController: GADBannerViewDelegate {
    func adViewDidReceiveAd(_ bannerView: GADBannerView) {
        
        guard let viewController = bannerView.rootViewController else {
            return
        }
        
        guard bannerView.superview == nil else {
            return
        }
        
        
        viewController.view.addSubview(bannerView)
        viewController.view.bringSubviewToFront(bannerView)
        
        viewController.view.addConstraints([
            NSLayoutConstraint(item: bannerView,
                               attribute: .bottom,
                               relatedBy: .equal,
                               toItem: viewController.view.safeAreaLayoutGuide,
                               attribute: .bottom,
                               multiplier: 1,
                               constant: 0),
            NSLayoutConstraint(item: bannerView,
                               attribute: .centerX,
                               relatedBy: .equal,
                               toItem: viewController.view,
                               attribute: .centerX,
                               multiplier: 1,
                               constant: 0)
        ])
        
    }
}


次に UIViewController はそのままだと SwiftUI で使用できないので SwiftUI 用に UIViewControllerRepresentable でビューコントローラーを wrap する。

struct GADBannerViewControllerRepresentable: UIViewControllerRepresentable {
    
    typealias UIViewControllerType = GADBannerViewController
    static let adsBannerSize: CGSize = CGSize(width: 320, height: 50)
    
    
    func makeUIViewController(context: Context) -> UIViewControllerType {
        return .init()
    }
    
    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
    }
    
}


最後に UIViewControllerRepresentable を SwiftUI の View に配置すれば、広告が表示される。

struct ContentView: View {
    var body: some View {
        
        NavigationView() {
            VStack {
                Spacer()
                
                Text("SwiftUI GoogleAdsSample")
                
                Spacer().frame(height: 30)
                
                NavigationLink(destination: ContentView2()) {
                    Text("Link to Next View.")
                }
                
                Spacer()
                
                GADBannerViewControllerRepresentable().frame(width: GADBannerViewControllerRepresentable.adsBannerSize.width,
                                                             height:GADBannerViewControllerRepresentable.adsBannerSize.height,
                                                             alignment: .center)
            }
            .navigationBarHidden(false)
            .padding()
        }

    }
}