リリースノート、チェンジログを自動で生成したいのでlerna-changelogを使った

デプロイする前にリリースタグを打つ運用をしているんですが、リリースノートに書く内容を自分で集めるのも面倒なので、みんな自動化してるやろと思って調べてみました。

conventional-changelog/conventional-changelog がポピュラーだけど、今回はコミットの列挙じゃなくてGitHubのプルリクエストを列挙したかったので要件に合わず。

tl;dr

lerna-changelogの方が動作がキビキビしてていいけど、PRにタグを打つのも面倒。

なので、下記のPRのforkをビルドしてnpmで公開しました。

www.npmjs.com

github-changelog-generator

github.com

  • Ruby
  • PRからCHANGELOG.mdを自動で生成してくれる
  • ラベルがなければ全部のPRを載せてくれる
  • ラベルが打ってあると、enhancement, bug fixなどでカテゴリを分けてくれる
  • 起動すると全部のPRを取得するので遅い
    • キャッシュはするけど、それでも遅い

lerna-changelog

github.com

上記のforkでmasterのHEADを取り込んでやるのが良さそう。やってみる。

github.com

やってみた。GitHub Packagesで公開してみましたが、利用する各位が.npmrcで利用するnpmリポジトリの設定とパーソナルアクセストークンの発行が必要なんですね。それだと使いにくいので結局npmでスコープつけて公開にしました。

www.npmjs.com

PRを閲覧する関係で、GitHubのPersonal Access Tokenが必要(必要権限はrepo)なので、 $ export GITHUB_AUTH=... で登録し、package.jsonにこんな設定を入れると、

  "changelog": {
    "labels": {
      "unlabeled": "PRs",
      "dependencies": "Dependabot"
    },
    "wildcardLabel": "unlabeled"
  }

こんな感じで標準出力に出してくれるので、 PRsとDependabotの所だけリリースタグを打つときのリリースノートに入れるのが便利かなと。

yarn run v1.22.5
$ /home/runner/work/sandbox/sandbox/node_modules/.bin/lerna-changelog

## Unreleased (2021-03-09)

#### PRs
* [#6](https://github.com/shrkw/sandbox/pull/6) escape line feed character ([@shrkw](https://github.com/shrkw))

#### Dependabot
* [#100](https://github.com/shrkw/sandbox/pull/100) Bump @types/react from 17.0.2 to 17.0.3 ([@dependabot-preview[bot]](https://github.com/apps/dependabot-preview))

#### Committers: 1
- Hiroyuki Shirakawa ([@shrkw](https://github.com/shrkw))
- [@dependabot-preview[bot]](https://github.com/apps/dependabot-preview)
Done in 0.83s.

タグを打ったあとに、 $ yarn run lerna-changelog --from=v1 --to=v2 に実行するのでも良い。

将棋の子 を読んだ

将棋の子 (講談社文庫)

将棋の子 (講談社文庫)

これはノンフィクションなんだろうか。取材はしてるんだろうけど想像で描く場面ばかりだから、要出典とか独自の解釈とかのタグがいっぱい付くべき文章ばかりだった。登場している人たちは勝手に解釈されて文句はないのかな。

全体通して、センチメンタルが強すぎるというか、書いている対象ではなく書いている自分自身への陶酔が強すぎのように感じた。昭和の著者のノンフィクションという感じ。

Railsのコントローラでの例外は直接ハンドリングしなくてもいい / 例外処理の方針

以下は静的なレスポンスを返す場合での方針です。動的なレスポンスならこの限りではありません。

基本方針

  1. 400番台以降のレスポンス(以下、例外レスポンス)を返したい場合でもコントローラであっても直接renderせず、例外をraiseする
  2. application.rb の config.action_dispatch.rescue_responses に例外とステータスコードマッピングを書いていく

こんな感じ。

  config.action_dispatch.rescue_responses = {
    "ActiveRecord::RecordNotFound" => :not_found
  }

デフォルトではこんな設定。 rails/exception_wrapper.rb at master · rails/rails · GitHub

rescue_fromは基本使わなくていい。

このやり方だとenvironmentごとに振る舞いがかわる。これはRails 6.0.3でのデフォルト設定での場合。

  • development
    • 例外がハンドリングされ、そのままRailsの詳細なエラーの画面として表示される
  • test
    • 詳細なデバッグ情報がHTTPレスポンスに出力されつつ、そのまま例外がスローされる(画面やレスポンスが描画されない)
  • production

このままだとrequest spec等のテストのときに、期待するようなレスポンスコードの変換がされないので具合が悪い。それについては下記で補足する。

また、デフォルトでのexceptions_appの仕組みに則って描画されるので、ActionDispatch::PublicExceptions によって描画され、JSONの場合は rack/utils.rb に定義されているメッセージが返却される。ここは変更できないので、変更したい場合はexceptions_appを差し替えるか別の仕組みを利用することになる。

例外の定義

上記のやり方だとカスタム例外を定義していかないといけないが、処理を振り分けするだけのためにStandardErrorにラベル付けする程度の例外を定義しなければならず、中身が1行だけのファイルが増えて煩雑。 なので、こういう定義でラクをするのはどうだろうか。

app/models/exceptions.rb

module Exceptions
  class NotFound < StandardError; end
  class Unauth < StandardError; end
end

なぜそうするか

開発中は、例外レスポンスは想定外な状況で起きることもあるので、発生した場所が分かった方がいい場合もある。

なぜそうなるのか

config.consider_all_requests_local, config.action_dispatch.show_exceptionsの二つの設定が影響し、ActionDispatch::ShowExceptions, ActionDispatch::DebugExceptions の二つのRack Middlewareが操作されている。

ざっくりいうと、 config.consider_all_requests_local がtrueならActionDispatch::DebugExceptionsがデバッグ画面を表示し、config.action_dispatch.show_exceptions がtrueなら、ActionDispatch::ShowExceptionsが例外をハンドリングしてユーザー向けの描画をしている。

config.consider_all_requests_local: このフラグがtrueの場合、どのような種類のエラーが発生した場合にも詳細なデバッグ情報がHTTPレスポンスに出力され、アプリケーションの実行時コンテキストがRails::Infoコントローラによって/rails/info/propertiesに出力されます。このフラグはdevelopmentモードとtestモードではtrue、productionモードではfalseに設定されます。もっと細かく制御したい場合は、このフラグをfalseに設定してから、コントローラでlocal_request?メソッドを実装し、エラー時にデバッグ情報を出力したいリクエストをそこで指定してください。

ActionDispatch::ShowExceptions: アプリケーションから返されるすべての例外をrescueし、リクエストがローカルであるかconfig.consider_all_requests_localがtrueに設定されている場合に適切な例外ページを出力します。config.action_dispatch.show_exceptionsがfalseに設定されていると、常に例外が出力されます。

ref. Rails アプリケーションを設定する - Railsガイド

上記の環境ごとの設定と見比べると分かってくる。

  • development
    • 例外がハンドリングされず、そのままRailsの詳細なエラーの画面として表示される
    • config.consider_all_requests_local: true
    • config.action_dispatch.show_exceptions: true
  • test
    • 詳細なデバッグ情報がHTTPレスポンスに出力されつつ、そのまま例外がスローされる(画面やレスポンスが描画されない)
    • config.consider_all_requests_local: true
    • config.action_dispatch.show_exceptions: false
  • production

refs.

テストどうするか

Rails の exceptions_app によるエラーページの表示をテストする - Qiita に書いているとおり、要は、request specなどでは本番と同じ設定になっていればいい。ということなので、rspecではこんな感じで、実行時に設定を変更する。

shared_context 'Show Exceptions', show_exceptions: true do
  around(:each) do |example|
    show_detailed_exceptions = Rails.application.env_config['action_dispatch.show_detailed_exceptions']
    show_exceptions          = Rails.application.env_config['action_dispatch.show_exceptions']

    Rails.application.env_config['action_dispatch.show_detailed_exceptions'] = false
    Rails.application.env_config['action_dispatch.show_exceptions']          = true

    example.run

    Rails.application.env_config['action_dispatch.show_detailed_exceptions'] = show_detailed_exceptions
    Rails.application.env_config['action_dispatch.show_exceptions']          = show_exceptions
  end
end

# `show_exceptions`メタを付けて実行する 
describe 'Get /hoge', show_exceptions: true do
  ...
end

2020年のRuby/RailsのJSONシリアライザは何を使うべきか問題

よくわからなかったので整理してみる。

候補

tl;dr

基本はjbuilderでいいと思う。 パフォーマンスが気になるならActiveModelSerializersか、Jb。ただ、どちらにせよメンテは心配。

Jbuilder

  • https://github.com/rails/jbuilder
  • HEYのGemfile見る限り、DHHは使っているようだ
  • テンプレート評価が遅いとか、パーシャル使うとpartの評価でN+1問題とか、パフォーマンス関連の課題があったみたいだけどどうなんだろう。コード見る限り変わってないような。
  • 開発は継続されている

サンプルコード

json.extract! @post, :id, :title, :content, :published_at
json.author do
  if @post.anonymous?
    json.null! # or json.nil!
  else
    json.first_name @post.author_first_name
    json.last_name @post.author_last_name
  end
end

ActiveModelSerializers

  • https://github.com/rails-api/active_model_serializers
  • Star: 5000
  • masterは2018年から開発が止まっているけど、 0-10-stable ブランチは年初にRuby2.7のサポートが入っている
  • Starも多し定番だった。メンテの頻度下がってるけど今も使ってる人は多いと思う

サンプルコード

class PostSerializer < ActiveModel::Serializer
  attributes :title, :body

  has_many :comments
  has_one :author
end

リアライザは自動で適用されるのでコントローラは変更不要。

class PostsController < ApplicationController

  def show
    @post = Post.find(params[:id])
    render json: @post
  end
end

jsonapi-rb

サンプルコード

class SerializablePost < JSONAPI::Serializable::Resource
  type 'posts'

  attributes :title, :body

  attribute :date do
    @object.created_at
  end

  belongs_to :author

  has_many :comments do
    data do
      @object.published_comments
    end
  end
end

あんまり使いやすそうな気がしない。

JSON:API Serializer

サンプルコード

class MovieSerializer
  include JSONAPI::Serializer

  set_type :movie  # optional
  set_id :owner_id # optional
  attributes :name, :year
  has_many :actors
  belongs_to :owner, record_type: :user
  belongs_to :movie_type
end
json_string = MovieSerializer.new(movie).serializable_hash.to_json
{
  "data": {
    "id": "3",
    "type": "movie",
    "attributes": {
      "name": "test movie",
      "year": null
    },
    "relationships": {
      "actors": {
        "data": [
          {
            "id": "1",
            "type": "actor"
          },
          {
            "id": "2",
            "type": "actor"
          }
        ]
      },
      "owner": {
        "data": {
          "id": "3",
          "type": "user"
        }
      }
    }
  }
}

Blueprinter

サンプルコード

# app/blueprints/todo_blueprint.rb
class TodoBlueprint < Blueprinter::Base
  identifier :id
    
  view :normal do
    fields :name, :due_at, :completed_at
  end

  view :extended do
    include_view :normal
    fields :description, :created_at, :updated_at
  end
end
# app/controllers/api/todos_controller.rb
module Api
  class TodosController < ApplicationController
    def index
      todos = TodoBlueprint.render Todo.all, view: :normal
      render json: todos
    end

    def show
      todo = TodoBlueprint.render Todo.find(params[:id]), view: :extended
      render json: todo
    end
  end
end
[
  {
    "id":1,
    "completed_at":null,
    "due_at":"2018-03-01 23:09:53 UTC",
    "name":"todo0"
  },
  {
    "id":2,
    "completed_at":null,
    "due_at":"2018-03-02 23:09:53 UTC",
    "name":"todo1"
  },
  ...
]

Jb

  • https://github.com/amatsuda/jb
  • Star: 948
  • わりあい安定してメンテされているが、amatsudaが一人でメンテしている感じ。個人のネームスペースだし
  • jbuilderみたいにビューファイルを書くタイプ
    • なので乗り換えは割とやりやすい

サンプルコード

# app/views/messages/show.json.jb

json = {
  content: format_content(@message.content),
  created_at: @message.created_at,
  updated_at: @message.updated_at,
  author: {
    name: @message.creator.name.familiar,
    email_address: @message.creator.email_address_with_name,
    url: url_for(@message.creator, format: :json)
  }
}

if current_user.admin?
  json[:visitors] = calculate_visitors(@message)
end

json[:comments] = @message.comments.map do |comment|
  {
    content: comment.content,
    created_at: comment.created_at
  }
end

json[:attachments] = @message.attachments.map do |attachment|
  {
    filename: attachment.filename,
    url: url_for(attachment)
  }
end

json

JSON APIどうなのか

https://jsonapi.org/

ページングとかの表現とか、みんなバラバラになるから大統一フォーマットを作ろうというので制定されている。 気持ちはわからないでもないけど、この汎用的な、冗長な表現が天下をとるとも思えないんだよなあ。 見れば見るほど気持ちはわかるんだけど、もっと気軽にやりたいんだという。実は僕が知らないだけで、みんなこれ使ってたりする?

こういうレスポンスになる。

{
  "data": {
    "id": "3",
    "type": "movie",
    "attributes": {
      "name": "test movie",
      "year": null
    },
    "relationships": {
      "actors": {
        "data": [
          {
            "id": "1",
            "type": "actor"
          },
          {
            "id": "2",
            "type": "actor"
          }
        ]
      },
      "owner": {
        "data": {
          "id": "3",
          "type": "user"
        }
      }
    }
  }
}

2020.06のReactでReduxは必要なのか

基本的にはstateはコンポネントに閉じ込められて使うべきで外に影響が染み出していくのはよくない。なので、useStateでシンプルに使うだけなのが一番良い。hooksがあるのでいろんなことがとてもシンプルにできるようになった。 とはいえ、遠いコンポネントと連携させたいケースや、認証状態のようなグローバルで共有したいというものがあるケースも理解できる。あとはキャッシュとか。

新しく入った開発プロジェクトでReactを使っているので、そういう時にどういう選択肢があるのかを整理してみた。

React使うのは3年ぶりくらい。前に使ったときはflux-utils, flow, immutable.jsとかで満足していた。今回のも、なるべく長期で育てていきたいプロジェクト。

今回の結論

頻繁に更新される情報やキャッシュしたいものはRedux ToolKitで書いたReduxで管理して、システムグローバルな参照しかしない情報はContextの利用という合わせ技がいいかなと思っている。

候補

  • グローバルな状態を使わない
    • 多くの階層を経由してpropsを渡すのを避けたいだけならコンポネントコンポジションを使えばいい [ コンテクスト – React]
    • websocketとかで更新情報を配布するようにしておいてメッセージを受けたらAPIからデータ取得するようにすれば、コンポネントを跨いだ更新、参照は不要になる
    • Pros.
      • すでにsocket使っているなら一方向の循環があると言えるので要素を増やさないで済む
    • Cons.
      • 認証状態の共有とかではそうもいかないので別途の方法が必要
      • キャッシュとかは別途考えが必要
      • socketサーバがいないと動かないので準備が増える
  • built-inのContextを使う
    • Pros.
      • Reactにbuilt-inな機能なので追加モジュール不要、メンテナンスも安心
      • シンプルなので挙動が想像しやすい
    • Cons.
      • reducer的な高レベルのファンクションを提供できない
        • useReducerで取得できるstate, dispatchを丸ごとContextに突っ込むというやり方もできるけど
      • カジュアルに更新するものをContextに入れるのは設計、パフォーマンスの両面でしんどい
  • Contextの薄いラッパーでFlux風
    • mizchiのちょっとしたスニペット [React Context を用いた簡易 Flux - mizdev]
    • Pros.
      • contextをちょっと使いやすくする薄いラッパーなので見通しがいいし自前でメンテナンスもできる
      • 参照、更新がやりやすい
    • Cons.
      • 普通にContext使うだけでもいいかもしれない。好みの問題
  • Redux Toolkit(RTK)
    • [Redux Toolkit | Redux Toolkit]
    • Reduxの使いやすくしたツールキット。vanillaなReduxはほとんどのケースで触らなくていいんじゃないか
    • createSlice, createAsyncThunkでかなり幸せになれる
    • Pros.
      • Reduxの実績はたくさん
      • UIと状態を切り離せるので、キャッシュとして利用することもできる
      • 型付けがこなれているのでdispatchするにしても dispatch(actions.fetch()) のように文字列を使わずに実行できる
    • Cons.
      • ReduxはFluxアーキテクチャの具現化を目的にしているのでグローバルな状態管理という目線で見ると、多少やりすぎ感は残る。RTKとRedux hooksで昔よりかなりマシになっているけど
  • reactn
  • recoil
    • [Recoil]
    • Pros.
      • facebookプロダクトなので筋は良さそう
    • Cons.
      • experimentalなので使うにはちょっと早い

NulabのbacklogからJiraへのポーティングのやり方

途中参加したプロジェクトでbacklogを利用していたけど、GitHubとの連携とかも含めて、やはりJiraのほうが洗練されていてやりやすいので、移行した。なので、課題をポーティングしたのでやり方を書いておきます。

要件

  • IDの連番はbacklogのときと同じ数字にしたい
    • 探しやすいしみやすい
    • 後述のリンクの作成もできるし
    • Jiraのプロジェクトは新規で作成して課題が何もない状態にしています
  • コメントも綺麗に入れたい
  • もとのbacklogへのリンクも入れたい
    • 最初のコメントとして登録する
  • backlogで本文、コメントに書いてリンクにしていたものはJiraの関連リンクとして入れたい
  • 今までマイルストンだったのはそのままリリースバージョンに移行したい
  • 添付ファイルがエクスポートできなかったので、手動でいい

やり方

backlogから課題一覧をCSVでダウンロードしている前提で、そのCSVへ手を加えていきます。

  1. (オプショナル) エンコーディングスキームをUTF-8に変える
  2. (オプショナル) IDでソート
  3. 名前がyamadaみたいな形式で入っているので、Atlassianアカウントに登録されているメールアドレスに置換する
    1. 人数多ければスクリプトでやる
  4. コメントのカラム分割
    1. 2020/04/24 13:16 shirakawa.hiroyuki@example.com¥¥nFoobar みたいな形式で入っているので、 2020/04/24 13:16; shirakawa.hiroyuki@example.com;¥¥nFoobar みたいな形式に置換する。こうすると、コメントの日時、投稿者が正しく保存される
    2. "(\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}) ([a-z\.]+@velc\.co\.jp) , $1; $2;みたいな正規表現を使うと便利
  5. backlogの課題へのリンクがあった方が安心感があるのでキーを元にしてリンクのカラムを追加する
    1. ここからはGoogle Spreadsheetを使うのが便利
  6. backlogとJiraで課題キーの連番が揃っていた方がスッキリするので、課題キーのカラムを追加する
    1. Google Spreadsheetのconcatで適宜 =concat('FOO-', $A2) みたいな。
  7. backlogの課題キーのリンクをJiraの関連リンクとして扱いたいので、適当なスクリプトで抽出する。適当なやつを補足に記載
    1. スプレッドシートに貼り付けて、ヘッダを追加
  8. 改行が¥¥nになっているので、\nに置換する

コツなど

  • インポート設定はダウンロードできるので、保存しておくと便利。試行錯誤がしやすい
  • すでに存在する課題キーで登録しようとするとわりと不安定なので、インポートを試す前に全部消した方がいい。消すと安定して動く

補足

リンク抽出スクリプト。不完全かも。

import csv
import re

reg = re.compile(r"(ISSUE_KEY-\d+)")


def find_key(src):
    return reg.findall(src)


def read():
    result = []
    with open("src.csv") as csvfile:
        reader = csv.DictReader(csvfile)
        max = 0
        for row in reader:
            temp = find_key(row["詳細"])
            for i in range(1, 10): # コメントカラムの数まで
                temp += find_key(row[f"コメント{i}"])
            t2 = list(set(temp))
            if max < len(t2):
                max = len(t2)
            print(f"{row["課題キー"]}\t{"\t".join(t2)}")
            result.append(t2)
    print(max)


def main():
    read()


if __name__ == "__main__":
    main()

MacBook ProにつなぐモニターはTS3 Plus経由で接続することに決めた

前回の https://shrkw.hatenablog.com/entry/2020/03/29/100000

Thunderbolt3のモニターを物色していたんんだけど、id:tnamaoにThunderbolt3ドックという存在を教えてもらい、

CalDigitのTS3 Plusを買ったら欲しかったものが全部揃って価格も抑えられて大満足だった。 https://www.caldigit.com/ja/ts3-plus-ja/

TB3ドックについて調べているとHP Thunderbolt3ドック 120W G2というのが見つかって、税抜き16800円という安さ。TS3 Plusは税込で33000円とかだから、半額ぐらいで買えるので検討してたんだけど、不安定というレビューとファームウェア更新したら安定したというレビューと、それでも相性問題が厳しいというレビューがあって、手を出すのをやめた。 https://jp.ext.hp.com/accessories/business/thunderbolt3_120w_g2/

要件にしていた、4K 60Hz, PD 60W, USB 3.1 Gen 1、ケーブル一本でMacと接続、は全て満たした上で、合わせてHP 27f 4K ディスプレイを3.5万くらいで買って、トータル7万に収まってコスパも一番よかった。 https://jp.ext.hp.com/monitors/personal/hp27f_4k/