원문 캡처 · github.com
원문 캡처 · github.com

rails-preflight는 레거시 Rails 애플리케이션을 정적 분석해 사람이 읽을 수 있는 업그레이드 위험 보고서를 만든다. 자동 마이그레이션이 아니라 업그레이드 계획과 범위 산정을 돕는 도구다.

오픈소스 앱 Mastodon v3.3.0을 Rails 7.0으로 업그레이드하는 사례를 확인한 전체 보고서가 공개돼 있다.

이 도구는 팀이 업그레이드 위험을 과소평가하는 문제를 줄이기 위해 만들어졌다. 호환성을 알 수 없는 비공개 젬, 눈에 잘 띄지 않는 프레임워크 사용 중단 예정 기능, Ruby와 운영체제의 지원 수명 불일치, 뒤늦게 드러나는 Docker 런타임 문제 등을 업그레이드 전에 확인한다. 팀이나 컨설턴트, 코딩 에이전트가 업그레이드를 맡더라도 먼저 위험을 조사하는 용도로 쓰며, 문제를 직접 고치는 도구는 아니다.

검사 범위와 한계

rails-preflight는 다음과 같은 업그레이드 위험 요소를 검사한다.

  • Ruby와 Rails의 호환성, Ruby를 어느 업그레이드 단계에서 올릴지
  • 지원이 종료된 Ruby와 Node 버전
  • 비공개·내부 젬 의존성
  • 고정된 젬의 Rails 요구 버전이 업그레이드를 막는 경우와, 알려진 제한이 있는 젬을 모은 목록
  • 코드와 설정 YAML, 스크립트 및 CI의 rake 작업에서 사용하는 제거·사용 중단 예정 Rails API. 현재 Rails 버전에서 이미 제거된 API도 포함한다.
  • Docker 런타임 위험: 지원이 종료된 기본 이미지, 로캘, tzdata, OpenSSL 불일치
  • 데이터베이스 스키마 위험: 문자 집합과 정수 ID
  • 누락되거나 오래된 config.load_defaults

결과는 엔지니어가 읽고, 엔지니어링 관리자와 기술 리드가 이해하며, 업그레이드 계획 회의에서 사용할 수 있도록 만든 HTML 보고서다. 같은 검사 결과를 Markdown으로는 이슈·PR·코딩 에이전트용 체크리스트로, JSON으로는 CI와 다른 도구용으로도 출력한다. 형식은 출력 형식 안내를 참조하면 된다.

이 도구는 코드를 수정하거나 사용 중단 예정 기능을 자동으로 고치지 않으며, 업그레이드 성공을 보장하지 않는다. 테스트나 CI를 대신하지도 않는다. 한 번의 클릭으로 업그레이드하는 도구가 아니다.

함께 사용할 수 있는 도구로는 대상 Rails 버전에서 작동하는 젬 버전을 확인하는 next_rails와 RailsBump, 보안 문제를 찾는 Brakeman, 여러 사용 중단 예정 기능을 자동 수정하는 rubocop-rails이 있다.

Rails 5.0 이상 애플리케이션의 업그레이드, 보안 목적의 업그레이드 계획, 자원 투입 전 작업 범위 산정, 여러 레거시 Rails 앱 감사에 특히 적합하다. Rails 3과 4는 지원하지 않는다. 업그레이드 전략을 맡은 컨설턴트와 스태프 엔지니어도 대상이다.

설치와 실행

전역으로 설치한 다음 검사할 앱의 경로를 지정한다. 앱 파일을 읽기만 하므로 서버에서 사용하는 오래된 버전을 포함해 어떤 Ruby 버전의 앱이든 검사할 수 있다. 도구를 실행하는 컴퓨터에는 Ruby 2.7 이상이 필요하다.

sh
gem install rails_preflight
rails-preflight /path/to/your/app

대상 Rails 버전을 지정하지 않으면 앱의 현재 버전 다음에 오는 마이너 버전을 검사한다. 예를 들어 5.2에서 6.0으로, 7.1에서 7.2로 이동하는 식이다. 이는 Rails가 권장하는 다음 업그레이드 단계다. 더 큰 버전 도약을 계획할 때는 대상 버전을 지정한다. 경로를 생략하면 현재 디렉터리를 검사한다.

sh
rails-preflight 8.1 /path/to/your/app

비공개 젬 정보는 Gemfile.lock과 Gemfile의 source 블록에서 가져오며, 이 파일들은 실행하지 않고 읽는다. 두 파일 어디에도 젬 출처가 지정되지 않은 경우에만 rubygems.org에 질의한다. Rails에 의존하는 공개 젬의 마지막 릴리스 시점도 rubygems.org에서 확인하고, 대상 Rails 버전보다 새 릴리스가 없는 젬을 보고한다. 이는 해당 젬이 작동하지 않는다는 뜻이 아니라, 대상 버전에서 시험한 사람이 없을 수 있다는 뜻이다. 비공개 젬 이름은 전송하지 않는다. --offline을 지정하면 완전한 정적 검사로 실행한다. 이 경우 출처를 확인할 수 없는 젬은 검사하지 않은 항목으로 표시하고 릴리스 날짜도 확인하지 않는다.

도구가 아는 Rails 버전 범위는 database/compatibility.yml에 지정된 5.0~8.1이다. 기본 대상 버전은 이 목록이 최신인 범위까지만 지원한다.

검사 대상은 다음과 같다.

  • Gemfile과 Gemfile.lock, 또는 BUNDLE_GEMFILE이 가리키는 Gemfile과 그 잠금 파일. 듀얼 부팅 앱에서는 BUNDLE_GEMFILE=Gemfile.next처럼 지정할 수 있다.
  • .ruby-version
  • 있는 경우 Dockerfile
  • 있는 경우 db/schema.rb. db/structure.sql을 사용하는 앱에는 스키마 검사를 건너뛰었다는 안내를 표시한다.
  • config/application.rb와 config/**/*.yml
  • 프로젝트 루트와 각 Packwerk 팩의 app/, config/, db/, lib/, test/, spec/ 아래에 있는 .rb, .erb, .haml, .slim, .rake 파일. 팩 경로 예시는 packs/<name>/app/이다.
  • 제거된 rake 작업을 찾기 위한 스크립트와 CI 설정: bin/, script/, Procfile, Makefile, CI 워크플로

기본으로 생성하는 파일은 rails_preflight_report.html이다. --format markdown 또는 --format json을 지정하면 rails_preflight_report.md 또는 rails_preflight_report.json을 만든다. 자세한 내용은 출력 형식 안내를 참조하면 된다.

보고서와 출력 형식

모든 형식은 같은 검사 결과를 담는다. 각 항목에는 요약 문장만이 아니라 관련 젬과 고정 버전, 충족하지 못한 요구 조건 또는 규칙, 해당 파일·줄 번호·일치한 코드가 포함된다. 근거가 된 릴리스 노트, README, Rails 소스도 함께 제공한다.

형식옵션앱에 기록되는 파일용도
HTML(기본값)기본값rails_preflight_report.html읽기, 계획, 공유
Markdown--format markdownrails_preflight_report.md이슈, PR, 코딩 에이전트
JSON--format jsonrails_preflight_report.jsonCI와 다른 도구

도구가 파일로 쓰는 것은 보고서뿐이다. --stdout을 추가하면 보고서를 표준 출력으로 내보내며 파일은 만들지 않는다. 이때 진행 상황은 표준 오류로 보내 표준 출력에 보고서만 남기므로 파이프나 코딩 에이전트에서 사용할 수 있다. 매 실행은 SSH 세션과 CI 로그에 맞춰 항목 수와 각 차단 항목을 짧게 출력한다.

HTML 보고서는 하나의 독립 파일이며, 열 때 네트워크 요청을 하지 않고 운영체제의 다크 모드를 따르며 A4 용지에 인쇄할 수 있다. Mastodon v3.3.0에서 Rails 7.0으로 옮기는 예시를 볼 수 있다.

보고서는 앱과 대상 버전, 단계 수, Ruby 업그레이드 시점, 차단 항목 수를 한 문장으로 판정해 보여준다. 이어서 항목을 링크한 개수와 함께 네 가지 상태를 표시한다. ‘이미 문제 있음’은 현재 Rails보다 앞서 제거된 API나 마지막 지원 Rails 버전을 지난 젬을 뜻한다. 해당 코드가 실행되면 실패하거나 아예 실행되지 않을 수 있다. ‘차단 항목’은 업그레이드가 작동하기 전에 반드시 고쳐야 하는 문제로, 너무 오래된 Ruby나 여전히 사용 중인 제거된 API 등이 해당한다. ‘수정할 항목’은 업그레이드 과정에서 경고나 오류를 일으킬 수 있는 사용 중단 예정 기능과 뒤처진 설정이며, 지금 또는 이후 단계에서 처리할 수 있다. ‘확인하지 못함’은 비공개 젬이나 누락된 파일처럼 도구가 검증하지 못한 항목으로, 수동 검토가 필요하다.

계획 부분은 Rails가 한 번에 한 마이너 버전씩 올리도록 권장하는 방식에 따라 각 단계를 보여주고, 단계별 차단 항목을 표시한다. 대상 버전보다 뒤의 버전은 흐리게 표시하며, 같은 검사에서 발견한 해당 버전의 제거 대상 코드 수를 보여준다. 단계별 작업, 필요한 Ruby 버전, 상대적인 작업 난이도도 표시한다. 난이도는 실제 작업량이 아니라 그 단계에서 가장 어려운 단일 수정의 수준이며, 검사 항목별로 낮음·중간·높음 중 하나로 정한다. 각 결과에 표시되는 발생 횟수는 실제 발견 건수를 보여준다.

세부 단계에서는 어느 단계에도 속하지 않는 항목을 먼저 ‘시작 전’에 표시하고, 각 단계별 카드에 필요한 Ruby 버전 범위와 수정할 API·젬을 담는다. 각 항목은 Rails 릴리스 노트나 젬 소스로 연결된다. 발생 횟수를 펼치면 앱과 테스트에서 찾은 항목을 구분해 파일, 줄 번호, 코드 조각을 볼 수 있다. 대상 버전보다 뒤에서 제거될 코드는 버전별로 ‘앞으로’ 항목에 표시하며, 지금 당장 처리할 필요는 없다. 보고서가 확인하지 못하는 항목에는 비공개 젬, 이름만 알려진 젬, 동작 변경, 여러 줄에 걸친 코드, 테스트 범위가 포함되며, 대신 취할 조치도 안내한다. 마지막에는 각 섹션의 신뢰 수준과 근거가 프로젝트 파일, 패턴 일치, 주요 입력 누락 중 어디에 해당하는지 표시한다.

Markdown 형식은 --format markdown으로 앱에 rails_preflight_report.md를 기록한다. 코딩 에이전트가 표준 출력에서 읽도록 할 수도 있다.

sh
rails-preflight --format markdown --stdout 7.2 /path/to/your/app

Markdown은 단계별 체크리스트이며 각 항목 아래에 file:line과 일치한 줄을 표시한다. 이슈나 PR에 그대로 붙여 넣을 수 있다. 보고서에는 코딩 에이전트가 한 단계씩 진행하고 각 단계 뒤에 테스트를 실행하며, ‘앞으로’와 ‘확인하지 못함’ 항목은 건드리지 않도록 안내하는 메모가 들어간다. 같은 Mastodon 검사에서 나온 전체 예시의 일부는 다음과 같다.

markdown
## Step 3: 6.1 to 7.0

Needs Ruby 2.7.0–3.2: 2.7.2 works.

- [ ] **Blocker:** annotate 3.1.1 requires activerecord >= 3.2, < 7.0, so it doesn't install on Rails 7.0. Upgrade it to a release that allows 7.0, or replace it.
- [ ] **Blocker:** `ActionMailer::DeliveryJob` and `ActionMailer::Parameterized::DeliveryJob` were removed in Rails 7.0. Mail is delivered by `ActionMailer::MailDeliveryJob`. ([source](https://guides.rubyonrails.org/7_0_release_notes.html#action-mailer-removals))
 - `config/initializers/delivery_job.rb:1` `ActionMailer::DeliveryJob.class_eval do`
 - `spec/models/user_spec.rb:178` `expect { user.send_confirmation_instructions }.to have_enqueued_job(ActionMailer::DeliveryJob)`

JSON 형식은 --format json으로 앱에 rails_preflight_report.json을 기록하며, CI와 다른 도구에서 사용할 수 있다. --stdout을 지정하면 파이프로 연결할 수 있다.

sh
rails-preflight --format json --stdout 7.2 /path/to/your/app | jq '.counts'

JSON 최상위에는 schema, tool, app, current_rails, target_rails, ruby, offline, verdict, counts가 들어간다. 검사 결과는 첫 단계 전 항목을 담는 before, 시작 버전과 대상 버전, Ruby 안내, 검사 결과를 담는 단계별 steps, 제거 버전을 기준으로 묶은 ahead, 확인할 수 없는 항목을 담는 cant_see에 나뉜다. 타임스탬프는 포함하지 않으므로 같은 앱에서는 같은 출력이 나온다. 스키마 1은 1.0 출시 전까지 바뀔 수 있으며, 바뀌면 번호가 올라간다. 같은 실행의 일부를 줄인 전체 예시는 다음과 같다.

json
{
 "from": "6.1",
 "version": "7.0",
 "findings": [
 {
 "section": "Gem Compatibility",
 "kind": "blocker",
 "message": "annotate 3.1.1 requires activerecord >= 3.2, < 7.0, so it doesn't install on Rails 7.0. ...",
 "removed_in": "7.0",
 "fix_effort": "medium",
 "gem": "annotate",
 "version": "3.1.1",
 "requires": "activerecord >= 3.2, < 7.0"
 },
 {
 "section": "Deprecation Warnings",
 "kind": "blocker",
 "message": "'ActiveModel::Errors#keys', '#values', '#to_h', '#slice!' and '#to_xml' were removed in Rails 7.0. ...",
 "rule": "errors_hash_methods",
 "removed_in": "7.0",
 "fix_effort": "low",
 "confidence": "medium",
 "source": "https://guides.rubyonrails.org/7_0_release_notes.html#active-model-removals",
 "files": [
 { "file": "lib/mastodon/accounts_cli.rb", "line": 106, "snippet": "user.errors.to_h.each do |key, error|", "test": false }
 ]
 }
 ]
}

kind은 broken, blocker, to_fix, tip, unknown 중 하나다. 각 결과에는 해당되는 정보만 담긴다. 젬 결과에는 gem, version, requires, 코드 결과에는 rule, confidence, files, 확인하지 못한 항목에는 names가 포함된다.

CI와 프로젝트 정보

--fail-on blockers는 업그레이드를 막거나 이미 문제가 있는 항목이 있으면 종료 코드 1을 반환한다. --fail-on broken은 이미 문제가 있는 항목이 있을 때만 종료 코드 1을 반환한다. 두 옵션 모두 어떤 출력 형식과도 함께 쓸 수 있다. CI에서 --offline을 사용하면 완전한 정적 검사로 실행한다.

sh
rails-preflight --offline --format json --fail-on blockers 7.2

현재 기능과 향후 계획의 자세한 목록은 ROADMAP.md에서 확인할 수 있다. 이 도구는 완전성보다 명료함, 거짓 확신보다 솔직함, 자동화보다 계획 지원을 우선한다. 개발 환경에서는 다음 명령으로 의존성을 설치하고 테스트를 실행한다. CI는 Ruby 2.7부터 4.0까지 각 버전에서 테스트를 실행한다. 라이선스는 MIT다.

sh
bundle install
bundle exec rake