Code CommitにpushされたらコミットのリンクをBacklogに記載を自動化

## はじめに
Backlogのgitではなく、Code Commitでソースを管理している場合にどのチケットに関するコミットか？分からなくなる
そこでBacklogにコミットのリンクを自動で記載する仕組みを構築したのでその備忘録を残す

https://github.com/yuta-katayama-23/codecommit-backlog

## 全体構成
自動的にコミットハッシュをBacklogチケット（Backlog的にはissue）に紐づけるロジックとしては、
コミットメッセージから正規表現でコミットハッシュを紐づけるべきBacklogのissueを探すキーワードを抽出し、そのキーワードをissueの件名に含むものを探し出しそのissueにコミットハッシュを紐づける
[^条件]: projectId や issueType

具体的には、

1. 開発者のCode CommitへのpushをCloud Watch Eventで捕捉しLambda関数を呼び出す
1. Cloud Watch EventのEventオブジェクトの内容にはコミットメッセージがないので、AWS SDK（今回はJavaScript）でCode Commitからコミットメッセージを取得
1. BacklogのWeb APIでBacklog上のissueをある条件で絞り込み、コミットメッセージに含まれるキーワードで検索する対象になるissueを取得する
1. 取得したissueの中からコミットメッセージのissueとの紐づきを探すためのキーワードが件名に含まれるissueを特定する
1. 該当のissueにコミットハッシュを書き込む

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/c5bb0519-2782-d334-867b-b4e957534118.png)

## Backlogにコメントを記載するLambdaを作成
### Lambdaをローカルでもテスト可能に
`lambda-local`というパッケージを使うと簡単

https://github.com/ashiina/lambda-local

### Cloud Watch Eventから渡ってくるEventオブジェクトの中身を把握する
Code Commitへのpushの際には、Cloud Watch Eventsの`referenceUpdated`というタイプのイベントが発火する
そのEventオブジェクトの中身としては以下

```json
{
  "version": "0",
  "id": "xxxxxxxxxxxxxxxxxxxxxx",
  "detail-type": "CodeCommit Repository State Change",
  "source": "aws.codecommit",
  "account": "xxxxxxxxxx",
  "time": "2021-08-18T07:09:12Z",
  "region": "ap-northeast-1",
  "resources": [
    "arn:aws:codecommit:ap-northeast-1:xxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxx"
  ],
  "detail": {
    "callerUserArn": "arn:aws:iam::xxxxxxxxxx:user/xxxxxxxxxx",
    "commitId": "xxxxxxxxxxxxxxxxxxxxxx",
    "event": "referenceUpdated",
    "oldCommitId": "xxxxxxxxxxxxxxxxxxxxxx",
    "referenceFullName": "refs/heads/main",
    "referenceName": "main",
    "referenceType": "branch",
    "repositoryId": "xxxxxxxxxxxxxxxxxxxxxx",
    "repositoryName": "xxxxxxxxxxxxxxxxxxxxxx"
  }
}
```

・参考：[referenceUpdated イベント](https://docs.aws.amazon.com/ja_jp/codecommit/latest/userguide/monitoring-events.html#referenceUpdated)

### Code Commitからコミットメッセージを取得
[AWS SDK for JavaScript v3](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/index.html)を使う（正確にはその中の[@aws-sdk/client-codecommit](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-codecommit/index.html#aws-sdkclient-codecommit)を使う）
`@aws-sdk/client-codecommit`には[GetCommitCommand](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-codecommit/classes/getcommitcommand.html)があり、これを使うと`commitId`と`repositoryName`から[コミット情報](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-codecommit/modules/commit.html)が取得できる

基本的にAWS SDKを使う際には、認証が必要になりそれは`AWS.config`に色々設定をする必要があるが、環境変数にCredentialとなるもの（AWS access key ID、AWS secret access keyなど）を定義しておけば勝手に読み込んでくれるのでその方式を採用する
※Lambdaにdeployした後は[LambdaのIAMロール　Lambda実行時のAWS_ACCESS_KEY_IDとかは何になる？](https://qiita.com/yuta-katayama-23/items/d942e62cd2f7d7ff53dd)に書いたように、実行ロールが関係してくるのでそこはまた設定する必要はあるが、今時点では気にしないでOK

・参考：[SDK バージョン 3 開発者ガイド　環境変数から Node.js への認証情報のロード](https://docs.aws.amazon.com/ja_jp/sdk-for-javascript/v3/developer-guide/loading-node-credentials-environment.html)

| AWS.config の Key | 環境変数の Key        | 説明                                                                   |
| ----------------- | --------------------- | ---------------------------------------------------------------------- |
| accessKeyId       | AWS_ACCESS_KEY_ID     | your AWS access key ID                                                 |
| secretAccessKey   | AWS_SECRET_ACCESS_KEY | your AWS secret access key                                             |
| sessionToken      | AWS_SESSION_TOKEN     | (AWS.Credentials) the optional AWS session token to sign requests with |

### コミットハッシュを記載するissueを特定する
Backlogの構成として、

- スペース(spaceId)：プロジェクト(projectId)＝1：N
- プロジェクト(projectId)：課題(issue)＝1：N

というような構成になっている
どのプロジェクトのどの課題とCode Commitのコミットハッシュが紐づくのか？が分かっている場合と分かっていない場合とを想定して以下のような実装にする

#### 環境変数`BACKLOG_PJ_ID`で対象のプロジェクトの指定を可能に
`BACKLOG_PJ_ID`が

- 指定：指定されたプロジェクトのみをコミットハッシュ書き込み対象のissueを検索する対象とする
- 未指定：スペース内の全てのプロジェクトをコミットハッシュ書き込み対象のissueを検索する対象とする

実装としては以下

```js
let pjIds = [];
if (process.env.BACKLOG_PJ_ID) pjIds.push(process.env.BACKLOG_PJ_ID);
else {
   const pjs = await axios.get(`/projects?apiKey=${process.env.BACKLOG_API_KEY}`, config);
   pjs.data.forEach(pj => pjIds.push(pj.id));
}
```

・参考：[プロジェクト一覧の取得](https://developer.nulab.com/ja/docs/backlog/api/2/get-project-list)

#### 環境変数`BACKLOG_ISSUE_TYPE_NAME`で対象のissue typeを指定可能に
次に、[環境変数`BACKLOG_PJ_ID`で対象のプロジェクトの指定を可能に](#環境変数backlog_pj_idで対象のプロジェクトの指定を可能に)で取得したプロジェクトIDを使ってそのプロジェクトにある課題（issue）を取得する
が、この時、`BACKLOG_ISSUE_TYPE_NAME`が

- 指定：指定された課題種別のみのissueをコミットハッシュ書き込み対象のissueを検索する対象とする
- 未指定：プロジェクト内の全てのissueをコミットハッシュ書き込み対象のissueを検索する対象とする
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/4958ce11-6a52-e529-c7e8-25776b7c0899.png)

実装としては以下

```js
let issues = [];
for (const pjId of pjIds) {
   const fectedIssues = await axios.get(`/issues?apiKey=${process.env.BACKLOG_API_KEY}&projectId[]=${pjId}`, config);
   if (process.env.BACKLOG_ISSUE_TYPE_NAME) {
      fectedIssues.data
         .filter(issue => issue.issueType.name === process.env.BACKLOG_ISSUE_TYPE_NAME)
         .map(issue => reObj(issue))
         .forEach(issue => issues.push(issue));
   } else fectedIssues.data.map(issue => reObj(issue)).forEach(issue => issues.push(issue));
}
```

※今回for-ofを使っているがそれは、forEachは[同期関数を期待するから](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach)
ちなみに、forEach の中身は`while`になっている

・参考：[課題一覧の取得](https://developer.nulab.com/ja/docs/backlog/api/2/get-issue-list)

#### コミットハッシュ書き込み対象のissueを特定
Backlogのissueの件名にあるキーワードが含まれているか？でissueを特定すればいいので、実装としては以下

```js
const issueIds = issues
   .filter(issue => issue.summary.includes(ticketSearchKey))
   .map(issue => issue.id);
```

#### コミットハッシュをBacklogのissueに書き込む
複数のissueがヒットする事ももしかしたらBacklogの運用上あり得るかもしれないので実装は以下のようにした

```js
const message = "Commit Message";
const hash = "Code Commit CommitHash";
const link = `[${input.commitId}](https://${process.env.AWS_REGION}.console.aws.amazon.com/codesuite/codecommit/repositories/${input.repositoryName}/commit/${input.commitId}?region=${process.env.AWS_REGION})`;
const data = querystring.stringify({
   content: `## ${message}\n${commitMsg}\n## ${hash}\n${link}`
});

let resultMap = {};
for (const [index, issueId] of issueIds.entries()) {
   const result = await axios.post(`/issues/${issueId}/comments?apiKey=${process.env.BACKLOG_API_KEY}`, data, config);
   resultMap["index"] = index;
   resultMap["result"] = {
      status: result.status,
      statusText: result.statusText,
      id: result.data.id,
      content: result.data.content
      }
}
```

・参考：[課題コメントの追加](https://developer.nulab.com/ja/docs/backlog/api/2/add-comment)

### まとめると・・・
[はじめに](#はじめに)のGitHubのリンクのようなソースコードになる
実行結果としては以下のような感じ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/9109f86f-5a6d-1fb6-5f7b-f55c17cdf938.png)

## おまけ
### Cloud Watch Eventの設定方法
以下のように設定する
※「一致したイベントの一部」でJSONPath形式（ex $.detail）でEventオブジェクトの中身の一部のみをLambdaに渡すとかもできるが、今回はやっていない
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/6f696e56-f860-5c84-d100-900427214ef6.png)