Madogiwa Blog

主に技術系の学習メモに使っていきます。

GitHub上のPRがクローズされるまでの時間を計測するMEMO

開発チームの健全性とかを定量的に計測する指標としてFour Keysがあると思うのですが、この辺の変更のリードタイムを表す数値として厳密では無いですが、PRが作成されてからクローズされるまでの時間を簡単に計測できないかなと思い調べてみたら結構すぐ取れそうだったのでメモ🗒

cloud.google.com

GitHub上のPRがクローズされるまでの時間を計測する

やり方は簡単でGitHubAPIでPRのリストを取得できるので、それを使って取得してPRの作成日時とクローズされた日時の差分を計算します。 ※ページングとかを考慮すると実装が複雑になるのと全部のPRを計算しなくても良いのでparametersで指定可能な直近100件で計測します。

その後は好きに計算すればいいのですが、今回はパーセンタイルを指定して出力するようにしてみました。

以下はOktokitを使って実装してみたサンプルです。

const { Octokit } = require("@octokit/core");

const repoOwner = "owner name";
const repoName = "repo name";
const repoType = "private or public";
const percentile = 50;
const baseBranch = "master";
const token = process.env.GITHUB_TOKEN;

// NOTE: initialize OktoKit
const octokit = new Octokit({ auth: token });

// main
const dateHourDiff = (before, after) => (after - before) / 1000 / 60 / 60;
const getPulls = async () => {
  return await octokit.request(
    "GET /repos/{owner}/{repo}/pulls?state=closed&per_page=100&sort=created&direction=desc&base={base}",
    {
      owner: repoOwner,
      repo: repoName,
      type: repoType,
      base: baseBranch,
    }
  );
};

try {
  (async () => {
    const res = await getPulls();
    const result = res.data.map((pr) => {
      return {
        number: pr.number,
        title: pr.title,
        created_at: pr.created_at,
        merged_at: pr.merged_at,
        closed_at: pr.closed_at,
        duration_hour: dateHourDiff(
          new Date(pr.created_at),
          new Date(pr.closed_at)
        ),
      };
    });
    durations = result.map((r) => r.duration_hour).sort();
    percentile_index = Math.floor(durations.length * (percentile / 100.0));
    console.log("durations", durations[percentile_index]);
  })();
} catch (error) {
  console.error(error);
}

実行すると以下のような感じです。

$ node index.js
durations 32.27638888888889

APIの詳細な使用は以下に記載されています。

docs.github.com

Github Actionにしてみる

任意のタイミングでリポジトリ上から計測できると便利そうなので以下を参考にGitHub Actionにしてみます。

docs.github.com

const core = require("@actions/core");
const github = require("@actions/github");

// NOTE: configure parameters
const repoOwner = github.context.repo.owner;
const repoName = github.context.repo.repo;
const repoType = core.getInput("repo-type");
const percentile = core.getInput("percentile");
const baseBranch = core.getInput("base-branch");
const token = core.getInput("repo-token");

// NOTE: initialize OktoKit
const octokit = github.getOctokit(token);

// main
const dateHourDiff = (before, after) => (after - before) / 1000 / 60 / 60;
const getPulls = async () => {
  return await octokit.request(
    "GET /repos/{owner}/{repo}/pulls?state=closed&per_page=100&sort=created&direction=desc&base={base}",
    {
      owner: repoOwner,
      repo: repoName,
      type: repoType,
      base: baseBranch,
    }
  );
};

try {
  (async () => {
    const res = await getPulls();
    const result = res.data.map((pr) => {
      return {
        number: pr.number,
        title: pr.title,
        created_at: pr.created_at,
        merged_at: pr.merged_at,
        closed_at: pr.closed_at,
        duration_hour: dateHourDiff(
          new Date(pr.created_at),
          new Date(pr.closed_at)
        ),
      };
    });
    const durations = result.map((r) => r.duration_hour).sort((a, b) => a - b);
    const percentile_index = Math.floor(durations.length * (percentile / 100.0));
    core.setOutput("duration", durations[percentile_index]);
  })();
} catch (error) {
  core.setFailed(error.message);
}

使うには以下のような感じです。

on: workflow_dispatch

jobs:
  pr_close_duration:
    runs-on: ubuntu-latest
    name: A job calc close PR duration
    steps:
      - name: Checkout
        uses: actions/checkout@v2
      - name: run pr-close-duration
        uses: ./pr-close-duration
        id: pr-close-duration
        with:
          base-branch: main
          repo-type: private
          percentile: 50
          repo-token: ${{ secrets.MY_GITHUB_TOKEN }}
      - name: Get the output time
        run: echo "The duration was ${{ steps.pr-close-duration.outputs.duration }} hour."

以下のような感じで見れます。

f:id:madogiwa0124:20220327131219p:plain

GitHub Actionは以下で公開してみました。

github.com

おしまい。

package.jsonで複数バージョンの同じライブラリを管理する方法MEMO

既存のコンポーネントはVue2を使いつつ、新規のコンポーネントはVu3を使いたいみたいなケースがあると思うんですが、そういう時にaliasを使用するといい感じに実現できそうだったのでメモ🗒

やり方

各種パッケージマネージャーのalias機能を使用します。 alias機能を使ったinstall方法はパッケージマネージャー毎に異なるので以下別セクションで公式ページから引用したものを記載しています。

インストールすると以下のような形で別名でpackageを管理することができます。

  "dependencies": {
    "vue": "^3.2.31",
    "vue-v2": "npm:vue@2.6.14"

実際に以下のような形でimportするpackageを切り替えることができます。

import { createApp } from "vue";
import Vue from "vue-v2";

Yarn

$ yarn add <alias-package>@npm:<package>

This will install a package under a custom alias. Aliasing, allows multiple versions of the same dependency to be installed, each referenced via the alias-package name given. yarn add | Yarn

npm

$ npm install <alias>@npm:<name>

Install a package under a custom alias. Allows multiple versions of a same-name package side-by-side npm-install | npm Docs

おわりに

段階的にバージョンを上げたりするときに便利ですね📦✨

参考

dev.to

Ruby on Rails: AikotobaというEmail/Passwordによるシンプルな認証ライブラリを作成しました

認証周りの勉強がてらAikotobaというEmail/Passwordによるシンプルな認証ライブラリを作成しましたので使い方とかをメモしておきます。

rubygems.org

Aikotobaとは?

Rails engineを使ったEmail/Passwordによるシンプルな認証ライブラリを作成できるライブラリです。

github.com

使い方は簡単で以下をGemfileに追記し、bundle install後に

gem 'aikotoba'

Rails engineをmountし、

Rails.application.routes.draw do
  mount Aikotoba::Engine => "/"
end

これだけでシンプルなEmail/Passwordによる会員登録及び認証の機能が使えるようになります。

f:id:madogiwa0124:20220312105558p:plain

f:id:madogiwa0124:20220312105610p:plain

あとは以下のような感じで認証用のHelperやログイン状態での認可用のメソッドを生やすとControllerやViewで分岐したり色々できるようになります。

class ApplicationController < ActionController::Base
  include Aikotoba::Authenticatable

  # NOTE: You can implement the get authenticated account process as follows.
  alias_method :current_account, :aikotoba_current_account
  helper_method :current_account

  # NOTE: You can implement the authorization process as follows
  def authenticate_account!
    return if current_account
    redirect_to aikotoba.new_session_path, flash: {alert: "Oops. You need to Signed up or Signed in." }
  end
end

またオプションで以下のような機能も利用することができます。

  • アカウント作成後にメール配信してEmailの確認を行う(DeviseのConfirmableっぽいやつ)
  • 指定回数ログインに失敗した場合にアカウントをロックしアンロック用のURLを配信する(DeviseのLockableっぽいやつ)
  • パスワード変更用のURLを配信しパスワード変更を行う(DeviseのRecoverableっぽいやつ)

詳しい使い方はREADMEを参照してください。

github.com

作成したモチベーション

作成したモチベーションは以下の用な感じです。

  • 単純に認証周りの考慮すべきセキュリティ等々の実装の勉強をしたかった。
  • 認証用のライブラリ、汎用性を考慮してる反面どれも実装が複雑で難しくなにか起きたときにモンキーパッチを当てて対応する等が難しかったので、Rails engineを使ったガッツリRailsに依存したシンプルな実装のライブラリがほしかった。
  • 既存の認証用のライブラリだとbcryptが使われていることが多いかモダンなHashアルゴリズムを使いたかった(Argon2)
  • Devise等デフォルトだとガッツリUserといったサービスのコアなドメインで認証用のロジックが含まれてしまうので、それを別のモデルにできるような設計にしたかった。
  • 複数DB対応をデフォルトで考慮したい。

おわりに

個人的には割と読みやすくシンプルに実装できたのと勉強にもなり個人のWebサービスにも導入できたので満足しました🥳

Ruby on Rails: Rollbarに連携する時に属性値等々をマスキングするMEMO

エラー通知サービスにRollbarを使っているのですが、ActiveRecord::RecordInvalid等の例外が発生したりするとActiveRecordインスタンスの属性等が表示されてしまいものによっては連携したくない情報がRollbarに連携されてしまう可能性があります。

Rollarのコード上の設定をいじると、この辺をいい感じにマスキングできることを知ったのでメモ🗒

rollbar.com

Rollbar連携前にマスキングする方法

設定は簡単でinitializer等のRollbar.configureの中でconfig.scrub_fieldsconfig.scrub_headersにフィルタリングしたいkeyを設定してあげればフィルタリングして連携してくれます。 以下の例ではRailsfilter_parametersと同様のマスキングを行うようにしています。

Rollbar.configure do |config|
  # NOTE: 秘密にした方が良い情報はマスキングする
  # https://docs.rollbar.com/docs/ruby#scrubbing-items
  config.scrub_fields |= Rails.application.config.filter_parameters
end

一応デフォルトでも一定フィルタリングしてくれるようです。

By default, the notifier will "scrub" the following fields from payloads before sending to Rollbar :passwd :password :password_confirmation :secret :confirm_password :secret_token https://docs.rollbar.com/docs/ruby#scrubbing-items

※コードを見る限り正規表現でマッチすればフィルタリングしてくれるっぽい

Rollbar便利ですね✨

Terraform Provider Herokuをv4系からv5系にアップグレードするMEMO🗒

Terraform v1系に対応したTerraform Provider Heroku v5がリリースされていたので個人のサービスをアップグレードしてみたので、その辺りの手順とかをメモ🗒

github.com

Terraform v1系、Terraform Provider Heroku v5系にアップグレード

Terraform Provider Heroku v5系はTerraform v1系に対応しているので.terraform-versionを以下に変更し、Terraform v1系の最新版を利用するようにします。

1.1.6

元々Terraform Provider HerokuはTerraform v0.12系に対応していたのをTerraform v1系に上げるにあたって、 providerのバージョンの指定方法も変更になっているようなのでそのあたりの修正を行います。

provider "heroku" {
--  version = "4.6.0"
}

terraform {
++  required_providers {
++    heroku = {
++      source  = "heroku/heroku"     
++      version = "5.0.1"
++    }
++  }

以下のバージョンアップ後のProvierのインストール等を実行します。

terraform init --reconfigure

このときInvalid legacy provider addressのようなエラーが発生したら以下のコマンドを実行し、registryのパスを修正します。

terraform state replace-provider 'registry.terraform.io/-/heroku' 'registry.terraform.io/heroku/heroku'

成功したら以下のドキュメントに沿ってコードを修正していきます。

github.com

修正が完了したら以下のようにplanに無視できない差分が出ないことを確認し、applyします。

terraform plan
terraform apply

Terraform Cloudを使用していてplan時にバージョン差異的なエラーが発生した場合には以下のドキュメントを参考にTerraform Cloud側もTerraformのバージョンを変更します。

learn.hashicorp.com

terraform applyterraform refreshでも良いらしいがTerraform Cloud側の問題によりv0.12系だとterraform refreshが使えないっぽい(?) 🤔

github.com

とりあえず、上記のような手順で無事にアップデートできました🙌

参考

zenn.dev

Ruby on Rails: Active Record Encryptionを使って属性値を暗号化するメモ

Ruby on Rails 7から導入されたActive Record Encryptionを使ってみたところ大分良さそうだったので使い方とかをメモしてきます🗒

Active Record Encryptionとは?

Active Record Encryptionは、Rails 7から導入されたActive Recordの新機能です。

特定の属性値をシンプルなDSLで暗号化して扱うことができます。

guides.rubyonrails.org

Active Record Encryptionを使ってみる

Active Record Encryptionの初期設定

Active Record Encryptionで暗号化を行うのに必要な秘密情報を以下のコマンドで生成しcredentialsに設定します。

bin/rails app:db:encryption:init

Add this entry to the credentials of the target environment:

active_record_encryption:
  primary_key: EGY8WhulUOXixybod7ZWwMIL68R9o5kC
  deterministic_key: aPA5XyALhf75NNnMzaspW7akTfZp0lPY
  key_derivation_salt: xEY0dt6TZcAMg52K7O84wYzkjvbA62Hz

これだけで使う準備はOKです🙆‍♂️

Active Record Encryptionで暗号化してみる

例えばユーザー作成後にトークンを送信して存在確認したい時に以下のような実装を行なったとします。

class Account::ConfirmationToken < ApplicationRecord
  belongs_to :account
  validates :token, presence: true
  validates :expired_at, presence: true

  scope :active, ->(now: Time.current) { where("expired_at >= ?", now) }

  after_initialize do |record|
    record.token ||= SecureRandom.urlsafe_base64(32)
    record.expired_at ||= 1.day.ago
  end
end

今回の例ではトークンの有効期限は1日でありハッシュ化は行わないという判断をしていますが、 DBに存在する値が何かしらの理由により漏れた場合に不正に存在確認がされてしまう恐れがあります。

このようなリスクを軽減するために以下のようにencrypts :nameのような形式で記載するだけで暗号化して保存することができます。 ※deterministic: true決定論的暗号化を使用するフラグ値です。一意制約をDBに設定するケース等、同一の値を常に同一の暗号化結果とするためにはdeterministic: trueを設定します。

class Account::ConfirmationToken < ApplicationRecord
  belongs_to :account
  validates :token, presence: true
  validates :expired_at, presence: true

  encrypts :token, deterministic: true

  scope :active, ->(now: Time.current) { where("expired_at >= ?", now) }

  after_initialize do |record|
    record.token ||= SecureRandom.urlsafe_base64(32)
    record.expired_at ||= 1.day.ago
  end
end

以下のように${name}_before_type_castで実際にDBに保存されている値を閲覧できますが、アプリケーション内で使う分には暗号化されていることを気にせず利用することができます。

Account::ConfirmationToken.create(account: @account, token: "generated_token_1")
token = Account::ConfirmationToken.find_by(token: "generated_token_1")
token.token
# => "generated_token_1"
token.token_before_type_cast
# => "{\"p\":\"4P8OBmTH31wihhJq++M1iuU=\",\"h\":{\"iv\":\"Wox4jyGiHXpaaEXN\",\"at\":\"4FZ5dHQFKGnBJacTK0PttA==\"}}"

1行追加するだけで利用できて非常に便利ですね✨

Tips: 暗号化に必要な秘密情報をconfigで管理する

Active Record Encryptionは非常に便利なのですがcredentialsに暗号化に必要な秘密情報を格納する方式ではテスト環境等で少し扱いが困るかなと思ったのですが、 以下のようにconfigでも管理できるのでテスト環境とか開発環境ではconfingの値を使ってcredencialsは修正しなくても使えるようにも出来ます。

module Dummy
  class Application < Rails::Application
    config.active_record.encryption.primary_key = "foo"
    config.active_record.encryption.deterministic_key = "bar"
    config.active_record.encryption.key_derivation_salt = "baz"
  end
end

6 Configuration

  • primary_key: The key or lists of keys used to derive root data-encryption keys. The way they are used depends on the key provider configured. It's preferred to configure it via a credential > active_record_encryption.primary_key.
  • deterministic_key: The key or list of keys used for deterministic encryption. It's preferred to configure it via a credential active_record_encryption.deterministic_key.
  • key_derivation_salt: The salt used when deriving keys. It's preferred to configure it via a credential active_record_encryption.key_derivation_salt. Active Record Encryption — Ruby on Rails Guides

参考

techracho.bpsinc.jp

Ruby on Rails: Request specでresponse bodyのBOM付きCSVを検証する。

BOM付きのCSVダウンロード系の機能の検証をRequest specでresponse bodyのBOM付きCSVを検証する時に若干ハマったのでやり方をメモ🗒

BOM付きのCSVを検証する時の注意点

当たり前といえば当たり前ですが、encoding: 'bom|utf-8'を指定しないとパース時にエラー(Illegal quoting in line 1. CSV::MalformedCSVErrorのような)になるので注意が必要です。

stackoverflow.com

さらにパースできても以下のようにBOMが入っているのでkeyのマッチができなくなります。※ rows['column']が一見keyが一致しているように見えてもBOMによりnilが返却されます。

github.com

実運用ではエクセル等で文字化けせずに利用できて便利ですが、テストコードでBOM付きCSVを扱うのは大分大変です。

いい感じに検証する

上記の通りBOM付きCSVをそのまま扱うのは厳しいのでテストコードではBOMを削除して扱います。 response.body.delete("\uFEFF")でBOMを削除し、削除した文字列をそのままCSV.parseでパースして中身を検証しています。

describe 'GET /records.csv' do
  before { get records_path(format: :csv) }

  it '成功すること' do
    expect(response).to have_http_status(:ok)
  end

  it '適切なCSVが出力されること' do
    rows = CSV.parse(response.body.delete("\uFEFF"), headers: true).map(&:to_h)
    expect(rows.length).to eq 2
    expect(rows.last)).to eq(
      {
        'column1' => 'value1',
        'column2' => 'value2',
      }
    )
  end
end

※以下の通りBOMは容認されているが推奨されているものではんく削除によるリスクはほぼ無いものと判断しました。

UTF-8文字コードとしてASCIIを前提としたプログラムでもおよそ支障なく動作するように設計されているが、BOMによって正常に処理できなくなる場合がある。Unicodeの規格において、UTF-8においてBOMは容認されるが、必須でも勧められるものでもないとされている[5]。また、データベースやメモリにロードするデータなど、内部的なデータ形式では、プログラムの性能や効率の観点から普通BOMは用いられない。 バイト順マーク - Wikipedia

参考

qiita.com