Gemfile.lockをもとに特定のバージョンのRubyで利用できないgemの一覧を出す

Railsアプリなど、Bundlerでgemの依存管理をしているプロジェクトでRubyのアップデートをしようとするとき、不幸なことにgemが全体的に古いことがある。

また、gemspecでrequired_ruby_versionというメタデータでRubyのバージョンの範囲を設定していると、その範囲のRubyの環境下でしかそのgemはインストールできない。よくある例として、次のようにRuby 2系でしか使えないようにしているものがある。

Gem::Specification.new do |spec|
  spec.required_ruby_version = "~> 2.0"
end

このような状況下でRubyだけアップデートするとgemをインストールできないことがある。

こうなると、まずgemをアップデートしていく必要がある。この作業の範囲がどれぐらいか把握するために、Gemfile.lockに載っているgemのうち、特定のバージョンのRubyだとrequired_ruby_versionを満たせないgemの一覧を出せるようなスクリプトを書いた。

# unusable_gems.rb

require 'bundler'

def resolve_gemspec_path(spec)
  # nokogiriはarchとosをgemspecのファイル名に持つ場合がある
  if spec.name == 'nokogiri'
    native_gem_path = Bundler.specs_path.join("#{spec.name}-#{spec.version}-#{Gem::Platform.local.cpu}-#{Gem::Platform.local.os}.gemspec")
    return native_gem_path if native_gem_path.exist?
  end

  case spec.source
  when Bundler::Source::Rubygems
    # default gem
    default_gem_path = Pathname.new(Gem.default_specifications_dir).join("#{spec.name}-#{spec.version}.gemspec")
    return default_gem_path if default_gem_path.exist?

    # ふつうのgem
    Bundler.specs_path.join("#{spec.name}-#{spec.version}.gemspec")
  when Bundler::Source::Git
    # git sourceからインストールしたgem
    Bundler.bundle_path.join("#{spec.source.install_path}", "#{spec.name}.gemspec")
  else
    warn "failed to resolve: #{spec.inspect}"
    nil
  end
end

gemspec_paths = Bundler::LockfileParser.new(Bundler.default_lockfile.read).specs.map { |spec|
  path = resolve_gemspec_path(spec)

  unless path&.exist?
    warn "gemspec not found: #{path}"
    next nil
  end

  path
}.compact

gemspecs = gemspec_paths.map { |path| Gem::Specification.load(path.to_s) }

checked_ruby_version = Gem::Version.create(ARGV[0])
puts gemspecs.reject { |gemspec| gemspec.required_ruby_version.satisfied_by?(checked_ruby_version) }.map { |gemspec| "#{gemspec.name} (#{gemspec.version})" }

例として、まず次のGemfileでbundle installしておく。

source "https://rubygems.org"

gem "activerecord", "7.0.4.2" # required_ruby_version = ">= 2.7.0"

Gemfile.lockが存在するディレクトリで次のように実行する。かなり古いバージョン2.6.0、2.7.0を例として使う。

$ ruby unusable_gems.rb 2.6.0
activemodel (7.0.4.2)
activerecord (7.0.4.2)
activesupport (7.0.4.2)
nokogiri (1.14.1)
oauth (1.1.0)
oauth-tty (1.0.5)

$ ruby unusable_gems.rb 2.7.0
# 2.7.0だと使えないgemがないので結果は空

たとえば間接依存のoauth 1.1.0も https://github.com/oauth-xx/oauth-ruby/blob/v1.1.0/oauth.gemspec#L36 を見るとわかるように2.7.0以上の制限がある。

注意点としては、今回必要だったユースケースではgemのソースの種類としてパス (Bundler::Source::Path) などはなかったので入れていない。また、nokogiriのような特殊なパターンもまだあるかもしれない。

TerraformでRoute 53のゾーンに付属するNS/SOAレコードをインポートする

TerraformでRoute 53の設定をインポートするときに、aws_route53_recordのドキュメントを読んでいた。このドキュメントにはRoute 53のゾーンを作るときに自動で作られるNSレコードとSOAレコードの管理方法について記述がある。しかし、コード例にはNSレコードについての記載しかない。

https://registry.terraform.io/providers/hashicorp/aws/4.53.0/docs/resources/route53_record#ns-and-soa-record-management

また、

Enabling the allow_overwrite argument will allow managing these records in a single Terraform run without the requirement for terraform import

とあるが、ゾーン作成時に一緒に作成されたNS/SOAレコードをインポートしたいだけのときは合わないユースケースに見えた(自分が把握できていないユースケースがあるのかもしれない)。

これらのNS/SOAレコードをTerraformで管理するには、あらかじめaws_route53_record.example_nsaws_route53_record.example_soa(名前は例)をインポートする。

$ terraform import aws_route53_record.example_ns XXXXXXXXXXXXX_example.com_NS
$ terraform import aws_route53_record.example_soa XXXXXXXXXXXXX_example.com_SOA

そのうえで、リソースを次のように定義して、terraform planで差分が出ないようにする。

resource "aws_route53_zone" "example" {
  name = "example.com"
}

resource "aws_route53_record" "example_ns" {
  zone_id = aws_route53_zone.example.id
  name    = "example.com"
  records = [
    "${aws_route53_zone.example.name_servers[0]}.",
    "${aws_route53_zone.example.name_servers[1]}.",
    "${aws_route53_zone.example.name_servers[2]}.",
    "${aws_route53_zone.example.name_servers[3]}.",
  ]
  ttl  = 86400
  type = "NS"
}

resource "aws_route53_record" "example_soa" {
  zone_id = aws_route53_zone.example.id
  name    = "example.com"
  records = [
    "${aws_route53_zone.example.primary_name_server}. awsdns-hostmaster.amazom.com 1 7200 900 1209600 86400"
  ]
  ttl  = 900
  type = "SOA"
}

GitHub Actionsで"Files changed"のファイルを取得する

GitHub ActionsでPRの"Files changed"タブと同じファイルの内容を取得する方法*1

- uses: actions/checkout@v3
  with:
    # マージベースの探索でコミットをさかのぼるために全コミットを取得しておく
    fetch-depth: 0
- run: |
    # ... でPRのFiles changedと同じ差分
    git diff origin/main...HEAD

    # ファイル名のリストだけ
    git diff --name-only origin/main...HEAD

    # 必要に応じて追加、変更したファイルだけなど
    git diff --diff-filter=AM origin/main...HEAD

やっていることは次と同じ。

# fast-forwardでない可能性があるのでマージベースを見つけておく
merge_base_sha=$(git merge-base origin/main HEAD)

git diff $merge_base_sha HEAD

たぶん実用的にはchanged-filesアクションを使えばよいが、とりあえずインラインで書きたい、なんらかの理由で外部アクションに依存したくないという時用。

*1:2022-11-23に ... を使う方法を追記