JiraのIssueを作成するURLを編集するWebアプリを作った

# 概要

* [Jira](https://www.atlassian.com/ja/software/jira)のissueを作成するURLを編集する静的Webアプリを作った
* アプリの使い方を説明する
* アプリの構成を説明する

# Jiraのissueを作成するURLとは

JiraにはURLからissueを作成する機能が存在します。[^jira]
参考: [How to create issues using direct HTML links in Jira Server](https://confluence.atlassian.com/jirakb/how-to-create-issues-using-direct-html-links-in-jira-server-159474.html)

[^jira]: リンク先に書いてあるとおり、この機能は現在非推奨です。便利なのになぁ

以下のようなURLにアクセスすると、issueを作成するページが表示されます。
`[JIRA BASE URL]/secure/CreateIssueDetails!init.jspa?[ARGUMENTS]`

この時クエリパラメータに文字列を渡すと、issueページを表示した時に要約や説明、ラベル等に予めテキストを設定できます。
つまり、issueのテンプレートのように扱うことができます。[^komatta]（以後テンプレートURLと表記）

[^komatta]: issueの要約欄に「テンプレート」等書いてissueに直接記入しないでissueをクローンさせることでテンプレートとして活用したりしていたのですが、この方法だと度々テンプレートissueに誤って記入される事故が発生して困っていました

これが大変便利で、テンプレートURLをブラウザのブックマークに保存しておけば自分だけのテンプレートになりますし、
手順書等にテンプレートURLを記載しておくだけで、誰でもある程度体裁の整ったissueを起票できるようになります。

# テンプレートURLの悩み

一つだけ悩みがありました。
それはクエリパラメータを変更するのが大変なことです。

クエリパラメータは当然パーセントエンコーディングされてるので、テンプレートURLを直接編集するのは至難の業です。

テンプレートURLを新規に生成する時は、単純なスクリプトを作って生成するだけで良いので簡単だったのですが
すでにあるテンプレートURLをちょっとだけ変更したい時が面倒でした。

Webの画面でちょろっと変更して保存できたら良いのになぁ...
と思ったので自分で作りました。

# 成果物

jira_issue_url_generator という静的Webアプリです。GitHubPages上で動作させてます。

https://jiro4989.github.io/jira_issue_url_generator/

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69665/594a45cf-792a-e49b-304c-ae84decb38d5.png)

ソースコードは以下。

https://github.com/jiro4989/jira_issue_url_generator

# 使い方

画面一番上の入力欄にテンプレートURLを貼り付けて「Apply」ボタンを押します。
すると、それ以降の入力欄にクエリパラメータがデコードされて設定されます。
あとは編集したい項目を編集します。

編集結果は随時 OUTPUT のURLに反映されるので、そのURLをコピーして好きに使えばOKです。

# アプリの構成

React.js + TypeScript で作りました。

この程度のアプリなら素のHTMLと素のJavaScriptだけでも良かったのですが、以下の理由でこの構成にしました。

1. あんまりReact.js書いたことなかったので勉強したかった
1. 単体テスト、自動テストをちゃんと整備したかった。素のHTML+JSだと面倒そう
1. ちゃんと型を明確にしたかった

フロントエンドのフレームワークだとVue.jsやNuxt.jsもありますけれど、React.jsの方が好みなのでReact.jsにしました。

# やったこと

## 実装

create-react-app でひな形を生成して、あとは普通にTypeScriptで実装しました。

実装に際して、inputがいくつも出てくるので共通のUIコンポーネントを作成して使い回すように実装しました。

CSSは殆ど書いたこと無いので悪戦苦闘しながら書きました。
おしゃれなUI[^oshare]にするつもりは毛頭無いので、殺風景すぎないけれど、でもHTML構造が複雑になりすぎない程度を目指して調整しました。

[^oshare]: そもそもおしゃれなUIが分からない。作る技術も持ってないし、今回のアプリの目的でもないのでやる理由も無い

ボタンをおしゃれに表現するためにaタグをdivタグでくるんでボタンを表現する手法がWebにいっぱいあって、
「タグの本来の用途と違うくない？めっちゃ嫌だなぁ」って思いながら僕はbuttonタグでボタンを作りました。

## テスト

単体テスト、UIテストも書きました。
趣味でGoで何か作ることが多いので、Goのテーブルドリブンテストと同じ感じで単体テストを書くようにしたら結構良かったです。

こんな感じで、テストケース違いをObjectの配列でひたすら列挙してループでテストを回します。
これは僕の好みですが、テーブルドリブンテストを書く時は以下の説明を必ずテストケースのパラメータに書くようにします。

1. 何のテストをするのか
1. 正常系か異常系か（例外を返すか）

```typescript
describe('generateURL', () => {
  const testCases = [
    {
      description: '正常系: すべてのフィールドがクエリパラメータになる',
      inJira: new Jira(origin, 1, 2, 'summary', 'desc', 4, ['hello', 'world']),
      want: `${baseURL}?pid=1&issuetype=2&priority=4&summary=summary&description=desc&labels=hello&labels=world`,
    },
    {
      description:
        '正常系: ラベルが1つだけ設定されてる場合はクエリパラメータも1つだけになる',
      inJira: new Jira(origin, 1, 2, 'summary', 'desc', 4, ['hello']),
      want: `${baseURL}?pid=1&issuetype=2&priority=4&summary=summary&description=desc&labels=hello`,
    },
    {
      description: '正常系: ラベルが無い場合はクエリパラメータも無い',
      inJira: new Jira(origin, 1, 2, 'summary', 'desc', 4, []),
      want: `${baseURL}?pid=1&issuetype=2&priority=4&summary=summary&description=desc`,
    },
    {
      description: '正常系: descriptionが無い場合はクエリパラメータも無い',
      inJira: new Jira(origin, 1, 2, 'summary', '', 4, []),
      want: `${baseURL}?pid=1&issuetype=2&priority=4&summary=summary`,
    },
    {
      description: '正常系: マルチバイト文字はパーセントエンコーディングされる',
      inJira: new Jira(origin, 1, 2, 'あ', 'い', 4, []),
      want: `${baseURL}?pid=1&issuetype=2&priority=4&summary=%E3%81%82&description=%E3%81%84`,
    },
    {
      description: '正常系: &も=もパーセントエンコーディングされる',
      inJira: new Jira(origin, 1, 2, 'a=1&a=2', 'b=1&b=2', 4, []),
      want: `${baseURL}?pid=1&issuetype=2&priority=4&summary=a%3D1%26a%3D2&description=b%3D1%26b%3D2`,
    },
  ]

  for (const testCase of testCases) {
    const { description, inJira, want } = testCase

    test(description, () => {
      const got = inJira.generateURL()
      expect(got).toEqual(want)
    })
  }
})
```

## コードフォーマット

コードスタイルを統一したかったのでprettierを入れました。
Goを書くときは`go fmt`を絶対にかけるので、同様にReact.jsにもフォーマッタが欲しかった。

[Gitのpre-commit hook](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks)にもprettierを入れましたけれど、念の為CIでもコードスタイルチェックするようにしました。

## CIで自動テスト

GitHub Actionsで自動テストするようにしました。
以下の全てがパスしないとダメにしました。

1. アプリのビルドが通る
1. コードスタイルチェックが通る
1. 単体テストが通る

node_modulesのインストールで1分くらい待たされるのが若干長く感じたのでキャッシュするようにもしました。

## テストカバレッジの取得と可視化

せっかくだったのでテストカバレッジも取って[Codecov](https://about.codecov.io)で見れるようにしました。
カバレッジは79％までいきました。
せっかくなので100%を目指したかったですが、主要どころは網羅したので良しとしました。あと飽きた。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69665/c1399e0d-061e-9ea7-4a08-a207e82c62a8.png)


React.jsのアプリでUIテストを書いたのは初めてだったのですが、UIテストのカバレッジも取れて便利ですね。
`react-scripts test --coverage --watchAll=false` とするだけでテストカバレッジを取ってくれて、
GitHub Actions側に以下の設定をするだけでCodecovにカバレッジレポートをアップロードできました。

```yml
      - name: Upload test coverage to Codecov
        uses: codecov/codecov-action@v2.1.0
```

## Pagesの更新の自動化

GitHubPagesの更新もCIからやるようにして、Gitのタグを打つだけでPagesが更新されるようにしました。

## 依存パッケージの更新の自動化

[Dependabot](https://docs.github.com/ja/code-security/supply-chain-security/managing-vulnerabilities-in-your-projects-dependencies/configuring-dependabot-security-updates)も入れてNode.jsの依存パッケージの最新版が公開されたら自動でアップグレードするMRを作成できるようにしました。
パッチバージョンの場合はCIがパスすると自動マージします。

## Node.jsランタイムの新バージョンの自動検知

アプリのNode.jsのバージョンは[nodenv](https://github.com/nodenv/nodenv)で固定しているのですが、ランタイム自体の更新も自動化したくなりました。
そこでDependabotでnode_modulesの更新を自動化するのと同様に、Node.jsランタイムの更新も自動化しました。

やり方としてはまず、ベースイメージがnodeのDockerfileを作成します。

```dockerfile
FROM node:17.4.0-alpine

WORKDIR /app
ENTRYPOINT ["npm"]
```

次にdependabotの設定でエコシステムに`docker`を追加します。

```yaml
  - package-ecosystem: "docker"
    directory: "/"
    schedule:
      interval: "daily"
    assignees:
      - "jiro4989"
```

最後にCI側もDockerコンテナ経由でテストを実行します。
package.jsonのscriptsにdocker経由でテストを実行するスクリプトを定義して、CI側から呼び出します。

```package.json
  "scripts": {
    // 省略
    "test-coverage": "react-scripts test --coverage --watchAll=false",
    "build-docker": "docker build -t ci .",
    "docker-run": "docker run -v \"$PWD:/app\" -t ci",
    "format": "prettier --write 'src/**/*.ts' 'src/**/*.tsx'",
    "format-check": "prettier --check 'src/**/*.ts' 'src/**/*.tsx'"
  },
```

```.github/workflows/test.yml
---
name: test

"on":
  push:
    paths-ignore:
      - 'LICENSE'
      - 'README.*'
    branches:
      - main
  pull_request:
    paths-ignore:
      - 'LICENSE'
      - 'README.*'

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Cache
        id: cache
        uses: actions/cache@v2
        with:
          path: |
            node_modules
          key: ${{ runner.os }}-${{ hashFiles('Dockerfile') }}-${{ hashFiles('package-lock.json') }}
      - name: Build docker image
        run: npm run build-docker
      - name: Install dependencies
        run: npm run docker-run -- ci
        if: steps.cache.outputs.cache-hit != 'true'
      - name: Check code style
        run: npm run docker-run -- run format-check
      - name: Test
        run: npm run docker-run -- run test-coverage
      - name: Build
        run: npm run docker-run -- run build
```

これでnodeの新バージョンのDockerイメージが公開されたらPRが自動で作成されて、新バージョンのランタイムでテストが走ります。
あとはPRに合わせて .node-version を更新します。
.node-versionを上げる部分は手動です。

# 感想

仕事ではインフラやCI、監視系ばかり触っていてフロントエンド事情には疎いのですが、
こうしてアプリのコードを書くとNode.jsはエコシステムが充実しててすごいなぁと感じました。

アプリ自体は大したものではないですが、とにかく思いつく限りの自動化をできて満足です。
アプリが一通り完成したのも多少嬉しかったですが、やっぱり裏側の仕組み考えてる方が自分には合ってることがわかりました。

# まとめ

以下の内容について書きました。

* Jiraのissueを作成するURLを編集する静的Webアプリを作った
* アプリの使い方を説明する
    * JiraのURLを貼り付けて画面上でinputの値を編集するだけです
* アプリの構成、やったことを説明する
    * React.js + TypeScript構成
    * 自動テストの整備
    * 依存パッケージ、ランタイム更新の自動化

以上です。