はじめてのCustom Action作成

# 取り組んだきっかけ
先月末から今月の上旬にかけて、Github Actionsに浸かっています。
色々触っている中で、**Custom Action**が気になり始めました。

あるworkflowを作成時、AWS Lambda更新のため、以下を利用しました。

https://github.com/int128/deploy-lambda-action

どんなことをしているか、大雑把にはわかりましたが、
まずは、**一通りリリースまでやってみよう！**
ということで、より初歩的なCustom Actionを探しました。

### Custom Actionとは？
`Custom Action`は、workflow内の`uses:`で記述したアクションのことです。

https://docs.github.com/ja/actions/creating-actions/about-custom-actions

`actions/checkout`がその一例です。
```yaml
steps:
  - uses: actions/checkout@v4
```
https://github.com/actions/checkout

アクションとしての作り方として、以下の3種類あるようです。
- Docker コンテナー(linuxのみ対応)
- JavaScript
- 複合アクション

https://docs.github.com/ja/actions/creating-actions

## 初歩的なCustom Actionをマネる
gitの基本的な操作（Push、PR）はご存知の前提で進めます。

以下の動画が、Custom Action駆け出しの私にはピッタリでした。
この動画に沿って進めて作業をします。
先ほどお話した3種類の作り方の中では、JavaScriptに分類されます。

https://www.youtube.com/watch?v=N26xgQ7kLKo

https://github.com/Balastrong/ga-label-issue

主なスタックですが、TypescriptとJestをです。
前提知識はなくても作業は進められる、と思います。

### 土台作成
Custom Action作成で基本的には必要なファイルから用意します。
##### action.yml、package.json

`action.yml`の記載項目です。
- `name`：作成するアクションの名前を設定します。マーケットプレイスに公開するために、**この値は他の公開されたアクションと重複してはいけません。**
- `description`：何ができるアクションか、説明を記載します。
- `inputs`：実行時にどのようなパラメータを必要とするか記載します。
- `outputs`：以降のworkflowで使用できるパラメータと設定値を記載します。
- `runs`：実行環境と実行ファイルを指定します。


他の項目について、詳細は[こちら](https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions)に記載されています。

```yaml:hello.yml
name: "Label new pull requests Try"
description: "Add a label when a new PR is opened Try."
author: "eno-conan"

runs:
  using: "node16"
  main: "dist/index.js"
```
以下を実行すると、`package.json`が生成されます。
```
npm init -y
```

##### gitignore
この段階では、コマンド実行のみでOKです。
```shell
npx gitignore node
```

##### tsconfig.json
typescriptのインストール・初期化を実施します。
```
npm i -D typescript
npx tsc --init
```
コンパイル設定は以下の通りです。
```json:tsconfig.json
{
  "compilerOptions": {
    "target": "ES2019",
    "module": "commonjs",
    "outDir": "dist",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true
  },
}
```

##### src/index.ts
最初はこれだけでいきます。
```typescript:index.ts
console.log("Hello My World!!");
```
`package.json`を以下のようにして、buildを実行し、jsファイルを生成します。
```json:package.json
"scripts": {
    "build":"tsc",
    "test": "echo \"Error: no test specified\" && exit 1"
}
```
```sh
npm run build
```

##### .github/workflows/XXX.yml
最初は手動でworkflowを実行できるようにします。
<details><summary>ymlの内容</summary>

```yaml
name: "Hello"

on:
    workflow_dispatch:

jobs:
  hello:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: ./
```
</details>

コードをpushします。
`Run workflow`をクリックします。成功が確認できましたら、次に進みましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/69917b61-d980-d3b6-f2ae-ce410d558c72.png)

Github Actionsの動作確認ですが、nektos/actを使うと、ローカルでも行えます。
気になる方は、自分の記事で恐縮ですが、ご覧ください！

https://qiita.com/eno49conan/items/f4c272c650b546bd96a6


### githubとの連携実装
必要なライブラリをインストールします。
```shell
npm install @actions/core @actions/github
npm install -D ts-node
```

##### src/index.ts更新
適切なパラメータが与えられれば、
パラメータ`label`の値で、PRのLabelsにその値を設定するworkflowです。

**更新される箇所（PR画面）**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/36e780cb-d446-9168-6164-3b2ee8cc0813.png)

<details><summary>index.tsの内容</summary>

```typescript:index.ts
import { getInput, setFailed } from "@actions/core";
import { context, getOctokit } from "@actions/github";

export async function run() {
    const token = getInput("gh-token");
    const label = getInput("label");

    const octokit = getOctokit(token);
    const pullRequest = context.payload.pull_request;

    try {
        if (!pullRequest) {
            throw new Error("This action can only be run on Pull Requests");
        }

        await octokit.rest.issues.addLabels({
            owner: context.repo.owner,
            repo: context.repo.repo,
            issue_number: pullRequest.number,
            labels: [label],
        });
    } catch (error) {
        setFailed((error as Error)?.message ?? "Unknown error");
    }
}

if (!process.env.JEST_WORKER_ID) {
    run();
}
```
</details>

##### .github/workflows/XXX.yml更新
PRが`opened`または`reopened`をトリガーに、実行します。
```yaml
name: "Hello"

on:
  pull_request:
    types: [opened, reopened]
jobs:
  hello:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: ./
        with:
          gh-token: ${{ secrets.GITHUB_TOKEN }}
          label: "needs-review"
```
build、pushと行います。main以外のブランチでテキトーにファイル作成して、
PRを作成します。workflowが実行されますが、失敗します。
> Error: Cannot find module '@actions/core'

##### @vercel/ncc導入

動画内の説明より
> TypeScriptでコードを書いている場合、tscコンパイル後のコードは自動的にはNode.js互換形式にはなりません。

これを解決するために、`@vercel/ncc`を使います。

https://www.npmjs.com/package/@vercel/ncc

**nccはTypeScriptのコードをCommonJSに変換して1つのファイルにbundleしてくれます。import/export文を解決し、Node.jsのモジュールシステムと互換性を持たせる**ことで、先ほどのエラーを解消します。
```
npm i @vercel/ncc
```
##### `package.json`、`tsconfig.json`、`.gitignore`更新
```diff_json:package.json
- "build": "tsc",
+ "build": "tsc && ncc build lib/index.js",
```
```diff_json:tsconfig.json
- "outDir": "dist",
+ "outDir": "lib",
```
```diff:.gitignore
.pnp.*

+ lib
```
**`lib`を追加した理由（動画内の説明より）**
> ビルド結果のアーティファクトであり、ソースコードではありません。
Gitではソースコードのみ追跡するのが、望ましいはずです。
`dist`が公開されるべき最終成果物で、`lib`は中間生成物的な位置づけです。
Gitに含める必要性が低いため除外します。

これでbuild、pushで完成！といきたいですが、
もう1つ修正が必要です。

##### リポジトリの権限変更
push後、PRを`closed`→`reopened`として、workflowを確認すると、
以下エラーが表示されます。
> Resource not accessible by integration

特定のLabelsを付与する、つまり**書き込み（更新）権限が必要**です。
Settings（※）から以下画像の通り修正し、保存して完了です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/762cba0b-2bfb-c762-d82e-707331a2b650.png)
※以下URLの一番下の項目
`https://github.com/{username}/{repo_name}/settings/actions`

### テストコード
必要なライブラリをインストールします。
```shell
npm i -D jest ts-jest @types/jest
```
動画では、Github Copilotでテストコードを生成していました。
以下のテストを実施しています。
- should throw an error if not run on a pull request
    - PRで実行されてない場合にエラーを出力すること
- should add label to the pull request
    - PRでlabelが設定されていること
- should handle error and set failed
    - Error時にステータスが失敗になること

詳細は以下の通りです。

https://github.com/Balastrong/ga-label-issue/blob/main/src/__tests__/index.test.ts

### Market Placeに公開
青い部分のボタンをクリックします。規約の承認後、必要事項の設定をします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/dc59d788-543c-ecca-373c-99086a0de8e6.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/b1d1e8c9-b7df-dc77-6ce1-719f0a6d0efa.png)

`See list of avaliable XXX`というリンクから、IconとColorを選んで、`action.yml`に設定します。
```diff:action.yml
runs:
  using: "node16"
  main: "dist/index.js"

+ branding:
+  icon: "anchor"
+  color: "blue"
```
Push後、`README.md`を追加して、Commitします。

以下状態になったら、`Publish Release`をクリックします。
二段階認証の対応があるので、それが終われば、リリース完了です！！！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/8f41a60d-2a0a-8b4f-c964-eb804a52b4ce.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/eaed81c7-4b49-cfce-2140-0c15b892f1fd.png)

赤線部分、`MarketPlace`をクリックすると、以下のように表示されます。
また、`Use latest Version`をクリックすると、workflowでの記述を内容をコピーできます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/0f9bafb9-be01-0295-c4d5-73db1871826b.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/1cd56089-65d2-102b-028c-45781169a486.png)

### 他リポジトリから利用
リリースできました。
他リポジトリから利用してみます。新しい/既存リポジトリはお任せします。
`name: Label new pull requests Try`の部分に、作成したCustomアクションの名前とパラメータを設定します。

```yaml
name: act-sample

on:
  # push:
  pull_request:
    types: [opened, reopened]

jobs:
  echo_text:
    runs-on: ubuntu-latest
    steps:
      - name: echo Hello world
        run: echo "Hello world!!"
      - name: Label new pull requests Try
        uses: eno-conan/ga-label-issue-try@v1.0.0
        with:
          label: "needs-infra-member-review"
          gh-token: ${{secrets.GITHUB_TOKEN}}
```
PRを作成して実行すると、`label`の文言がLabelsに設定できました！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/23c84203-ff1e-c0e8-2e26-850b061e6a38.png)

### 残り作業と補足
この後、huskyやLinterの設定などを行う部分が残っていますが、この記事ではその内容はスキップします。

また、作業が一通り終わってから気付いたのですが、テンプレートが用意されていました。（JS/TS）

https://docs.github.com/ja/actions/creating-actions/creating-a-javascript-action#template-repositories-for-creating-javascript-actions

## LICENSE周りの理解
動画では登場しませんでしたが、より本格的になったときに、避けて通れない内容です。しかし、全く触れてきておらず、マズイかも・・といった気持ちも持ちつつこの文章を書いています。
公式ドキュメントや皆さまが書かれた記事の情報をお借りして、超基本的な部分だけかもしれませんが、学んでいる最中です。。。

https://docs.github.com/ja/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/licensing-a-repository

https://gigazine.net/news/20180607-choose-a-license/

> 自分のソースコードを広く提供し、自由に利用してもらいたいという場合は「MIT License」が最適だとのこと。

確かに、`MIT License`は見覚えがあります。

https://future-architect.github.io/articles/20200821/

> OSSライセンスとは、OSSの使用許諾条件のことであり、著作権に基づいてOSSの利用条件を定義するものとなります。逆に言えばOSSとは、OSSライセンスを遵守することを条件に使用が許諾されていると言うことができます。

# まとめと今後
初歩的な内容ですしたが、Custom Actionの公開、利用まで実施できました。
以前npmのライブラリ公開の初歩を学んだときがありましたが、
何かを便利に柔軟に、といった考え方に基づいて自作するのは、面白いと感じます。

今後は、OSSライセンスの話も勉強しながら、
githubのPRに関係する別のActionや、AWSサービスが関係するActionの作成をしていきたいと思います！