Ansible: GitHubリリースからPythonパッケージをインストールする

## 概要
Ansibleを使って、GitHubリリースにアップロードされているWheelファイルをダウンロードしインストールします。

## 事前準備と前提
[community.generalコレクション](https://docs.ansible.com/ansible/latest/collections/community/general/index.html)を使用するので、インストールされていなければ下記のコマンドでインストールして下さい。

```shell-session
ansible-galaxy collection install community.general
```

また、AnsibleでインストールするPythonパッケージは、下記[Jupyter Notebookの例](https://github.com/jupyter/notebook/releases/tag/v6.4.11)のようにGitHubのリリースに`.whl`ファイルとしてアップロードされているとします。

![Jupyter Notebookの例](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62253/034dd7cb-f15a-c644-1bab-5c11ba70f1be.png)


## 最新リリースの取得
[community.generalコレクション](https://docs.ansible.com/ansible/latest/collections/community/general/index.html)に[github_release](https://docs.ansible.com/ansible/latest/collections/community/general/github_release_module.html#ansible-collections-community-general-github-release-module)というモジュールはあるのですが、v4.8.0時点ではアセットの取得ができません。
代わりに[ansible.builtin.get_url](https://docs.ansible.com/ansible/latest/collections/ansible/builtin/get_url_module.html)モジュールを使って直接[Get the latest release API](https://docs.github.com/ja/rest/releases/releases#get-the-latest-release)にアクセスします。

```yaml
- name: Find the latest version
  ansible.builtin.uri:
    url: "https://api.github.com/repos/{{ org }}/{{ repo }}/releases/latest"
    headers:
      Authorization: "Bearer {{ github_token }}"
      Accept: application/vnd.github.v3+json
  register: info
  no_log: true
```

変数`org`にレポジトリのOrganization名(個人アカウントの場合はユーザ名)、`repo`にリポジトリ名が格納されているとします。

リポジトリがプライベートの場合はAuthorizationヘッダを使った認証が必要です。
変数`github_token`には`repo`権限のあるアクセストークンを格納されているとします。
パブリックレポジトリの場合、Authorizationヘッダは不要です。

APIのレスポンスはJSON形式で返ってくるので、戻り値を格納した変数`info`の`json`属性からアクセスできます。

## Wheelファイルのダウンロードとインストール
GitHubリリースにアップロードされたファイルは、[Get the latest release API](https://docs.github.com/ja/rest/releases/releases#get-the-latest-release)レスポンスのassets属性にリストとして記録されています。

その中から、`.whl`ファイルだけを取得するためにJSONクエリフィルターを使います。
クエリ``assets[?ends_with(name, `.whl`)].{name: name, url: url}``は、`assets`の中から`name`が`.whl`で終わるものだけを抜き出し、`name`と`url`からなるオブジェクトを作成します。
この`name`と`url`は、get_urlモジュールの`dest`と`url`で使用しています。

```yaml
- name: Download the latest package
  ansible.builtin.get_url:
    url: "{{ item.url }}"
    headers:
      Authorization: "Bearer {{ github_token }}"
      Accept: application/octet-stream
    dest: "/tmp/{{ item.name }}"
    mode: "0644"
  loop: "{{ info.json | community.general.json_query(query) }}"
  vars:
    query: "assets[?ends_with(name, `.whl`)].{name: name, url: url}"
  register: download
  no_log: true
```

今回もパブリックリポジトリの場合、Authorizationヘッダは不要です。

:::note warn
`Accept: application/octet-stream`の指定を忘れると、`.whl`ファイルではなくメタデータがJSON形式で返ってきてしまうので注意が必要です。
:::


最後に、ダウンロードしてきた`.whl`ファイルを`pip`を使ってインストールします。

```yaml
- name: Install the package
  ansible.builtin.pip:
    name: "file://{{ item.dest }}"
    state: present
  loop: "{{ download.results }}"
```

assetsがリストのため、このタスクもループ処理になります。

## プレイブック
以上をまとめたプレイブックは次の通りです。`vars`以下の変数を適切に設定してください。 
`github_token`はvaultで暗号化しておくと良いでしょう。


```yaml:gh_package.yaml
- hosts: all 
  become: true
  gather_facts: false
  vars:
    org: org_name
    repo: repo_name
    github_token: personal_access_token
  tasks:
    - name: Find the latest version
      ansible.builtin.uri:
        url: "https://api.github.com/repos/{{ org }}/{{ repo }}/releases/latest"
        headers:
          Authorization: "Bearer {{ github_token }}"
          Accept: application/vnd.github.v3+json
      register: info
      no_log: true

    - name: Download the latest package
      ansible.builtin.get_url:
        url: "{{ item.url }}"
        headers:
          Authorization: "Bearer {{ github_token }}"
          Accept: application/octet-stream
        dest: "/tmp/{{ item.name }}"
        mode: "0644"
      loop: "{{ info.json | community.general.json_query(query) }}"
      vars:
        query: "assets[?ends_with(name, `.whl`)].{name: name, url: url}"
      register: download
      no_log: true

    - name: Install the package
      ansible.builtin.pip:
        name: "file://{{ item.dest }}"
        state: present
      loop: "{{ download.results }}"
```

## 参考
- [community.general.github_release module – Interact with GitHub Releases](https://docs.ansible.com/ansible/latest/collections/community/general/github_release_module.html)
- [ansible.builtin.get_url module – Downloads files from HTTP, HTTPS, or FTP to node](https://docs.ansible.com/ansible/latest/collections/ansible/builtin/get_url_module.html)
- [Get the latest release API](https://docs.github.com/ja/rest/releases/releases#get-the-latest-release)
- [Ansible - Download latest release binary from Github repo](https://stackoverflow.com/questions/50966777/ansible-download-latest-release-binary-from-github-repo)