GitHub ActionsでLaTeXをビルドしてGoogle Driveに上げる

これは、[「LOCAL students Advent Calendar 2019」](https://adventar.org/calendars/4020)25日目の記事です

## 前提

- Git の基本的な知識
  - ビルドするLaTeXのファイルがリポジトリにすでにコミットされていること
- Docker の基礎的な知識
  - Docker イメージを使ってビルドができること
- YAML の記法

## GitHub Actions とは

ソフトウェアのビルド・テスト・デプロイの自動化を行うCI/CDサービスの一つです。
こう聞くと難しく聞こえるかもしれませんが、リポジトリに対して行なっていたコマンドラインの手作業(今回では LaTeX のビルドと Google Drive へのアップロード)を自動化できるツールと考えると良いと思います。

## GitHub Actions の概念

GitHub Actions は、ワークフロー・ジョブ・ステップを定義して使います。図に表すとこんな感じです。

![GitHub Actions-Concept(1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159836/df1aad41-07a4-a11d-d9c0-495fc4d4df84.png)


- ワークフロー
    いつこのワークフローを実行するのか指定します。リポジトリにプッシュされた時、毎週午前2時、などなど。
    それぞれのワークフローは並列に実行されます。
    1つ以上のジョブから構成され、1つのワークフローは1つのファイルに記述します。

- ジョブ
    実行環境を Linux(Ubuntu)・Windows・macOS から選択します。
    それぞれのデフォルトでは並列に実行されますが、特定のジョブが終わった後に行う設定もできます。
    実行するステップを順番に定義します。ジョブの中のそれぞれのステップは同じ環境で実行されるので、ステップ中で作成したファイルを用いて次のステップを実行できます。

- ステップ
    何を実行するのか(タスク)を書きます。
    実行するコマンドを書くこともできますが、すでに定義された処理を行うことができます。定義されたものは「アクション」と呼ばれ、GitHub の Marketplace から様々な人が開発したアクションを検索できます。
例えば、リポジトリの clone (チェックアウト)を行うには [GitHub 公式の `actions/checkout`](https://github.com/marketplace/actions/checkout) を使います。 

    この記事は筆者の作った2つのアクションの使い方です。

## 実行の流れ

実行の流れはこんな感じになります。

![GitHub Actions-Example(4).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159836/a946d4df-2878-5289-a57c-4aa485e7c462.png)


## 設定方法


### トークンの作成・GitHub への登録
Google Drive へのアップロードに、CLIツールの [skicka](https://github.com/google/skicka) を使いました。
skicka 用の認証トークンを事前に作る必要があります。

1. Dockerを使える環境で、以下のコマンドを実行します。

    ```sh
    docker run --rm -it --entrypoint "" satackey/skicka sh -c "skicka --no-browser-auth ls && cat /root/.skicka.tokencache.json"
    ```

1. ブラウザで表示されたURLにアクセスします。
1. アクセスを許可し、コード表示されたら、ターミナルに戻り貼り付けます。
  > 2020年1月2日現在、初めてskickaにログインするアカウントで`このアプリでは「Google でログイン」機能が一時的に無効`と表示される問題が発生することがあります。
  > 自分で Google Drive API の Client ID と Client Secret をセットアップし、skicka に設定することで回避できます。~~セットアップの具体的な方法はググってください。~~
  >> 2020年1月3日追記 
  >> Client ID と Client Secret の取得方法を[こちら](https://qiita.com/satackey/items/34c7fc5bf77bd2f5c633)に投稿しました。
  >> トークンの作成方法を[こちら](https://github.com/satackey/action-google-drive#skicka-%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%9F%E3%81%93%E3%81%A8%E3%81%8C%E3%81%AA%E3%81%84%E6%96%B9%E6%89%8B%E9%A0%861%E3%81%AE%E3%83%AD%E3%82%B0%E3%82%A4%E3%83%B3%E6%99%82%E3%81%AE%E5%95%8F%E9%A1%8C%E3%81%AE%E5%9B%9E%E9%81%BF)の `skicka を使ったことがない方・手順1のログイン時の問題の回避` に記載しました
  > また、`satackey/action-google-drive`を使う際にID, Secretの指定が必要になります。`Google Driveへアップロード` の部分のステップの設定の説明を確認してください。
1. GitHub の Secrets に最後の行のJSONを登録します。

  - GitHub のリポジトリを開きます。
 
    <img width="996" alt="スクリーンショット 2020-01-02 11.24.26.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159836/9fff7cbe-71d7-00b9-40ff-c215dcf7c63b.png">

  - Settings を開きます。

    <img width="1009" alt="スクリーンショット 2020-01-02 11.24.50.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159836/c7f4f69c-8127-1a55-0eec-1c8a213a79ab.png">

  - Secretsを開きます。

    <img width="760" alt="スクリーンショット 2020-01-02 11.25.12.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159836/2798e4ec-1b0a-3a7e-8952-064a89be1feb.png">

  - Add a new secret を押し、 Name に `SKICKA_TOKENCACHE_JSON`, Value にターミナルの最後の行に表示されたJSONをコピペします。

    <img width="737" alt="スクリーンショット 2020-01-02 11.28.48.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159836/8737f531-5e93-65e7-4ab1-4e0edd3f0a1e.png">

  - Add secret を押します。

    <img width="745" alt="スクリーンショット 2020-01-02 12.16.20.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159836/d7fc1755-8911-b99b-4575-e373cdec9695.png">


これで準備完了です:thumbsup:

### ワークフローファイルの記述

- GitHub のリポジトリの Actions タブを開きます。

    <img width="698" alt="スクリーンショット 2020-01-02 12.50.26.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159836/c6116f82-9bfc-232f-caa4-df5c241106d8.png">

- Set up this workflow を押します。

    <img width="682" alt="スクリーンショット 2020-01-02 12.52.08.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159836/c0119247-1dfa-a4fe-4754-ccc078b43420.png">
    エディタが表示されましたね。このファイルを編集していきます。

- [**最後にスニペットをまとめたものがあります！！**](#ワークフローファイル全体)

- 12行目以降は例なので消します。
    初期状態で、`実行の流れ`の`ステップ1・チェックアウト`は書かれていますね。


- `latexmkの実行` の部分のステップはこのようになります。

    ```yaml
      # 名前をつけます。ワークフローが実行された時に表示されます。
    - name: Build LaTeX files

      # 使うアクションを公開されているリポジトリを
      # ユーザ名 / リポジトリ名 @ gitのタグ･ブランチ で指定します
      # 今回は筆者の作ったアクションです
      uses: satackey/action-latex@v2

      # アクションの実行に必要なデータを渡します。
      # 必要なものがアクションの作者によって定義されています。
      with:
        # TeXLive･latexmk が入っている、ビルドに使用する Docker イメージを指定します。
        docker-image: paperist/alpine-texlive-ja

        # コンパイルのコマンドです
        build-entrypoint: latexmk

        # コンパイル時の引数を指定します
        build-args: # ここに、いつも latexmk を実行する時に使っているオプションを指定してください

        # ビルドするファイルを1行ごとに指定します。
        # ファイルが置かれたディレクトリに移動してビルドが行われます。
        build-files: |
          hogehoge/main.tex
          fugafuga/main.tex
    ```

- Google Drive にアップロードするファイルが入ったディレクトリを作ります。

    ```yaml
    - run: |
        mkdir upload
        find . -name "*.pdf" | xargs -I{} sh -c 'mkdir -p $(dirname {}); cp "{}" $(dirname "upload/{}")'
    ```
    拡張子が`*.pdf`のファイルが`uploads/`のディレクトリへ、階層を保ってコピーされます。

- ビルドできたので、Google Driveへアップロードと進みたいところですが、Google Drive のフォルダを固定してしまうと、ブランチの中で最後にプッシュされたものしか残りません。`固定の名前+ブランチ名`でアップロードさせるため、ブランチ名を表示するコマンドを実行し、その結果を固定の名前と合わせてアクションに渡します。
    実行結果を以降のステップ使えるようにするには、`::set-output name={名前}::{内容}`の構文で出力します。
    このステップはこのようになります[^extract-branch-name]。

    ```yaml
    - name: Extract branch name
      id: extract_branch
      run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})"
    ```

    `id` を指定したので、以降のアクションで `${{ steps.extract_branch.outputs.branch }}` を記述すると、実行時にこの出力に展開されます。

- `Google Driveへアップロード`のステップはこのようになります。
    
    ```yaml
    - name: Upload to Google Drive
      uses: satackey/action-google-drive@v1
      with:
        # ${{ secrets.登録した名前 }} で secret の内容を参照できます。
        # トークンの作成・GitHub への登録 で登録した文字列を参照しています。
        skicka-tokencache-json: ${{ secrets.SKICKA_TOKENCACHE_JSON }}

        # アップロードするディレクトリを指定します。
        upload-from: ./upload

        # アップロード先のフォルダを指定します。
        # 1つ前のステップの実行結果を参照しています。
        upload-to: /path/to/upload/dir/${{ steps.extract_branch.outputs.branch }}

        # # skickaにログインできなかった人向け
        # # Google の Client ID, Secret を GitHub の Secrets に、
        # # SKICKA_TOKENCACHE_JSON と同様の手順で、以下のように名前を変更して登録してください。
        # google-client-id: ${{ secrets.GOOGLE_CLIENT_ID }}
        # google-client-secret: ${{ secrets.GOOGLE_CLIENT_SECRET }}
    ```

#### ワークフローファイル全体

```yaml
# GitHub リポジトリの Actions ページで表示されるワークフローの名前です。お好きなものに変えてください。
name: Build and upload reports

on: [push]

jobs:
  # ジョブの名前です。お好きな(ry
  build-and-upload:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v2

      # 名前をつけます。ワークフローが実行された時に表示されます。
    - name: Build LaTeX files

      # 使うアクションを公開されているリポジトリを
      # ユーザ名 / リポジトリ名 @ gitのタグ･ブランチ で指定します
      # 今回は筆者の作ったアクションです
      uses: satackey/action-latex@v2

      # アクションの実行に必要なデータを渡します。
      # 必要なものがアクションの作者によって定義されています。
      with:
        # TeXLive･latexmk が入っている、ビルドに使用する Docker イメージを指定します。
        docker-image: paperist/alpine-texlive-ja

        # コンパイルのコマンドです
        build-entrypoint: latexmk

        # コンパイル時の引数を指定します
        build-args: # ここに、いつも latexmk を実行する時に使っているオプションを指定してください

        # ビルドするファイルを1行ごとに指定します。
        # ファイルが置かれたディレクトリに移動してビルドが行われます。
        build-files: |
          hogehoge/main.tex
          fugafuga/main.tex

    - run: |
        mkdir upload
        find . -name "*.pdf" | xargs -I{} sh -c 'mkdir -p $(dirname {}); cp "{}" $(dirname "upload/{}")'
    
    - name: Extract branch name
      id: extract_branch
      run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})"

    - name: Upload to Google Drive
      uses: satackey/action-google-drive@v1
      with:
        # ${{ secrets.登録した名前 }} で secret の内容を参照できます。
        # トークンの作成・GitHub への登録 で登録した文字列を参照しています。
        skicka-tokencache-json: ${{ secrets.SKICKA_TOKENCACHE_JSON }}

        # アップロードするディレクトリを指定します。
        upload-from: ./upload

        # アップロード先のフォルダを指定します。
        # 1つ前のステップの実行結果を参照しています。
        upload-to: /path/to/upload/dir/${{ steps.extract_branch.outputs.branch }}

        # # skickaにログインできなかった人向け
        # # Google の Client ID, Secret を GitHub の Secrets に、
        # # SKICKA_TOKENCACHE_JSON と同様の手順で、以下のように名前を変更して登録してください。
        # google-client-id: ${{ secrets.GOOGLE_CLIENT_ID }}
        # google-client-secret: ${{ secrets.GOOGLE_CLIENT_SECRET }}

```

## 応用編

複数の LaTeX プロジェクトが存在する時、その TeX ファイルを `find` を用いて一覧にし、その結果を LaTeX ビルドのアクションに参照させることで、新しくプロジェクトを追加した時でも、ワークフローファイルを変更せずにコンパイルを行うことができます。以下はすべての `main.tex` をコンパイルさせる例です。[^find-cmd]。
[^find-cmd]: `find` コマンドの `-name` 引数の内容を `*.tex` として、すべての `.tex` ファイルをビルドさせることなども可能です。
GitHub Actions の Outputs の出力時には、改行等のエスケープを行う必要があります[^outputs-escape]。

```yaml
    - name: List TeX files
      id: list-tex-files
      run: |
        SOURCE_FILES=$(find . -name "main.tex")
        SOURCE_FILES="${SOURCE_FILES//'%'/'%25'}"
        SOURCE_FILES="${SOURCE_FILES//$'\n'/'%0A'}"
        SOURCE_FILES="${SOURCE_FILES//$'\r'/'%0D'}"
        echo "::set-output name=tex-files::$SOURCE_FILES"

    - name: Build LaTeX files
      uses: satackey/action-latex@v2
      with:
        docker-image: paperist/alpine-texlive-ja
        build-files: ${{ steps.list-tex-files.outputs.tex-files }}
        # ...その他設定
```


## 参考資料

[GitHub Actionsの中核的概念](https://help.github.com/ja/actions/automating-your-workflow-with-github-actions/core-concepts-for-github-actions)
[Docker コンテナのアクションを作成する](https://help.github.com/ja/actions/automating-your-workflow-with-github-actions/creating-a-docker-container-action)


[^extract-branch-name]: ステップのスニペットを https://stackoverflow.com/questions/58033366/how-to-get-current-branch-within-github-actions より引用

[^outputs-escape]: エスケープ部分のスニペットを https://github.community/t5/GitHub-Actions/set-output-Truncates-Multiline-Strings/td-p/37870 より引用