GitHub Actions のアーティファクトを楽に見れるようにしてみた


こちらは、[GitHub Actions Advent Calendar 2020](https://qiita.com/advent-calendar/2020/github-actions) 18日目の記事になります。

# はじめに
GitHub Actions を使っていて、アーティファクト（成果物）をリポジトリのタブから
パイプラインを選択して探すの面倒だと思ったことはないでしょうか？

自分もAndroidの開発でアーティファクトにアップロードしたアプリを
インストールするのが大変だと感じていました。

なので今回は、Github Actions でアップロードしたアーティファクトを
簡単にダウンロードできる Webアプリ を作ってみました。
（といっても、1ページの簡単なものですが）


# 作成したもの
<img width=400 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109303/3cf8c935-a150-690f-8660-72d89d0a3e6e.png">
[![TakenokoTech/Unitool - GitHub](https://gh-card.dev/repos/TakenokoTech/apricot.svg)](https://github.com/TakenokoTech/apricot)
Webページ：https://takenokotech.github.io/apricot

# システム構成図

フロント部分は Next.js を使ってみました。
![graph.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109303/6a5253ca-db15-4ec1-487d-ec03f5f83ca1.png)

# シーケンス
シーケンスと使用したAPIを紹介します。
<img width=600 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109303/cdf8b149-b0f0-2390-22e6-33c40a6700bd.png">

## 認可〜トークン取得
https://docs.github.com/ja/free-pro-team@latest/developers/apps/authorizing-oauth-apps
トークン取得のPOSTは、CORSを回避するために CORS Anywhere を用いてプロキシしました。

```typescript
export async function getToken(clientId: string, clientSecret: string, redirectUri: string, oauthScope: string, code: string = null): Promise<string> {
  if (!code) {
    window.location.href = `https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${redirectUri}&scope=${oauthScope}`;
    return null;
  }

  const accessTokenUrl = "https://cors-anywhere.herokuapp.com/https://github.com/login/oauth/access_token";
  const response = await fetch(`${accessTokenUrl}?client_id=${clientId}&client_secret=${clientSecret}&code=${code}&redirect_uri=${redirectUri}`, {
    method: "POST",
    headers: {
      Accept: "application/json",
    },
  });

  const { access_token, scope, token_type } = await response.json();
  return access_token
}
```

## ユーザー情報取得
https://docs.github.com/en/free-pro-team@latest/rest/reference/users#get-the-authenticated-user
ユーザー情報に紐づく様々な公開データが取れます。

```typescript
export async function getUser(accesstoken: string): Promise<User> {
  const apiUrl = "https://api.github.com/user";
  const resp = await fetch(apiUrl, {
    headers: {
      Authorization: `token ${accesstoken}`,
      Accept: "application/vnd.github.v3+json",
    },
  });

  // repos_urlは、下のリポジトリ取得で使われるURLと同じ
  const { login, repos_url } = await resp.json();
  return login
```

## リポジトリ取得
https://docs.github.com/en/free-pro-team@latest/rest/reference/repos#list-organization-repositories
ユーザーが公開しているリポジトリが見えます。

```typescript
export async function getRepos(userName: string): Promise<RepoItem[]> {
  const url = `https://api.github.com/users/${userName}/repos?per_page=100`;
  const resp = await fetch(url);
  const json = await resp.json();
  const repos =
    json.map((repo) => ({
      name: repo.name,
      full_name: repo.full_name,
      html_url: repo.html_url,
    })) || [];
  return repos;
}
```

## ワークフロー取得
https://docs.github.com/en/free-pro-team@latest/rest/reference/actions#list-workflow-runs-for-a-repository
コミットログなどが取れます。

```typescript
export async function getWorkflow(userName: string, repoName: string): Promise<WorkflowItem[]> {
  const url = `https://api.github.com/repos/${userName}/${repoName}/actions/runs`;
  const resp = await fetch(url);
  const json = await resp.json();
  const workflows = json.workflow_runs?.map((workflow) => workflow) || [];
  return workflows;
}
```

## アーティファクト取得
https://docs.github.com/en/free-pro-team@latest/rest/reference/actions#list-artifacts-for-a-repository
アーティファクトURLはワークフローから取得できます。
この`getArtifactUrl`ではファイルを取得するための期限つきURLが発行されるので、そちらを使ってファイルを取得します。

```typescript
export async function getArtifactUrl(user: string, accesstoken: string, artifactUrl: string): Promise<string> {
  const response = await fetch(artifactUrl, {
    headers: {
      Authorization: `token $accesstoken`,
      Accept: "application/vnd.github.v3+json",
      "Content-Type": "application/zip",
      "User-Agent": user,
    },
  });
  return response.url;
}

// getArtifactUrlで取得したURLを使ってファイルをダウンロードする。
const url = await getArtifactUrl(this.state.artifactUrl);
const link = document.createElement("a");
link.download = this.state.name;
link.href = url;
link.click();
```



# まとめ
以上、今回作った Apricot の技術解説でした。
ご覧いただきありがとうございました。