ä¼å¡äºæ¥é¨ãµã¼ãã¹éçºã°ã«ã¼ãé·ã®æç°ã§ãã
ç§ã¯2015å¹´1æããä¼å¡äºæ¥é¨ã§ãµã¼ãã¹éçºã¨ã³ã¸ãã¢ããã£ã¦ãã¾ããã2014å¹´4æã¾ã§ã¯æè¡é¨éçºåºç¤ã°ã«ã¼ã㧠Web ãµã¼ãã¹éçºãå éãããæ§ã ãªåãçµã¿ãå®æ½ãã¦ãã¾ãããæ¬ç¨¿ã§ã¯ãéçºåºç¤ã°ã«ã¼ãæ代ã«ç§ãåãçµãã éçºè ãã¹ãã®å¤±æã追跡ããããããåãçµã¿ã«ã¤ãã¦èª¬æãã¾ãã
ã¯ãã¯ãããã® Web ãµã¼ãã¹éçºã¨ CI
ã¯ãã¯ãããã®ãµã¼ãã¹éçºã¯ã大ããã¦ã5åãããã®å°ããªãã¼ã ãä¸ã¤ã®æ©è½ãæ å½ãã¾ããããããå¤æ°ã®ãã¼ã ã1ã¤ã®å¤§ã㪠Rails ã¢ããªã±ã¼ã·ã§ã³ãåæã«å¤æ´ããã®ãç¹å¾´ã§ã *1ã
Web ãµã¼ãã¹éçºãå éãã工夫ã«ã¯æ§ã ãªæ¹åæ§ãèãããã¾ãããããã§ã¯ãã¯ãã¯ãããã®ãããªã¹ã¿ã¤ã«ã§ã® Web ãµã¼ãã¹éçºãå éããããã«éçºè ãã¹ããä½å¦ã«åæ»ã«ããããèãã¾ãã
å³: ãªã ãã³ã¹
ã¯ãã¯ãããã§ã¯ãªã ãã³ã¹ã¨å¼ã°ãã CI ã·ã¹ãã ããããCI ã§ã®ãã¹ãããã¹ãããªãã¸ã§ã³ã ãããããã¤ã許ããã¾ãã
ãµã¼ãã¹éçºã¯ãããã¤ãã¦ãããæ¬çªã§ããéçºä¸ã®ãµã¼ãã¹ãã¦ã¼ã¶ã«åºãã¦ã使ããæ¹ãåæãã¦æ¹åãã¦ãããµã¤ã¯ã«ãä½åº¦ãåãã«ã¯ãä½åº¦ããããã¤ããå¿ è¦ãããã¾ãã CI ã§ã®ãã¹ããåæ»ã«æåãç¶ãããã¨ãé«éãªãµã¼ãã¹éçºã®èã§ãã
ã³ããã㨠CI ã®ç£è¦
ã¯ãã¯ãããã§ã¯ãéçºè ã¯åºæ¬çã«èªåã®ã³ããããèªå㧠master ãã©ã³ãã«ãã¼ã¸ãããããã¤ãèªåã§ããã¾ãããã®ãããéçºè ã¯èªåã®ã³ãããããã¼ã¸ãããå¾ã« CI ã§èµ°ããã¹ãã®çµæã«ã¯ååæ°ãä»ãã¦ãã¾ãã
CI ã§ã®ãã¹ãçµæã¯ãã£ããã«éç¥ããã¾ãããã®ãããªç°å¢ã§ã¯ãéçºè ã¯ãèªåã®ã³ãããã CI ã§ãã¹ãããã¦ããã¨ãã¯ããã¤ããããã£ããã«æ³¨ç®ãã¦å¤±æã«ããåå¿ã§ããããæºåãã¦ãã¾ãããã®æä¸ã¯ãã¤ãããéçºã«éä¸ã§ãã¾ãããéçºã«éä¸ãã¦ãã¾ãã¨ããã¹ãã®å¤±æã«ããåå¿ã§ããªãããã§ãã
ãã®ãããªç¶æ³ã解æ¶ãéçºè ãéçºã«éä¸ã§ããããã«ããããã2012å¹´ã« jenkins-hipchat-publisher ãã©ã°ã¤ã³ããã¼ã¹ã«ãCI ã§ãã¹ãã失æããã¨ãã«ã³ããããã人ããã£ããéç¥ã§èªåã¡ã³ã·ã§ã³ãããã©ã°ã¤ã³ããå½æéçºåºç¤ã°ã«ã¼ãã«æå±ãã¦ãã id:sora_h ã«ãã£ã¦éçºããã¾ãã*2ããã®ãã©ã°ã¤ã³ã«ããéç¥ã®æ§åã以ä¸ã«ç¤ºãã¾ãã
å³: ãã¹ã失æéç¥ã§ã®ã¡ã³ã·ã§ã³
ãã®ãããªã¡ã³ã·ã§ã³éç¥ããããããã§ããã£ããã«å¼µãä»ãã¦ããªãéçºè ã§ããã¹ãã®å¤±æã«æ°ä»ãããããªãã¾ãã
ãã£ããã§ã®å¤±æéç¥ããªããã«ãã
ãã¹ãã®å¤±æéç¥ããã£ããã«æµããã¨ãã®éçºè ã®åããè¦ã¦ã¿ã¾ããããéç¥ã§ã¡ã³ã·ã§ã³ãããéçºè 㯠CI ã®ãã¹ãå®è¡ãã°ã確èªãã¾ããã©ã®ãã¹ãã失æããããææ¡ãã¦æ¬¡ã®è¡åã«ç§»ãããã§ãããã®ã¨ããä¸å³ã§ç¤ºã4ã¤ã®å ´åã«åããã¾ãã
å³: ãã¹ã失ææã®è¡å4ãã¿ã¼ã³
失æãèªåã®å¤æ´ã«é¢ä¿ããå ´åã¯ããã£ããã§ä¿®æ£ä¸ã§ããæ¨ãå ±åãããã¹ãããã¡ãã¨éãããã«ä¿®æ£ãã¾ã (å³ã®å·¦ä¸ Case 1) ã
失æãèªåã®å¤æ´ã¨ç¡é¢ä¿ã§ããå ´åã¯2ã¤ã«åããã¾ããå³ã®å³ä¸ Case 2 ã¯ãèªåã®ã³ããããåå ã§ä»äººã®ãã¹ãã失æããã¦ãã¾ã£ãå ´åã§ãããã®å ´åã¯ãåå ã調æ»ããããã«ã失æãããã¹ãã®é¢ä¿è ã git blame ã§èª¿ã¹ã¦ãã£ããã§è³ªåããããä¿®æ£ã移è²ããããã¾ãããã®ä½æ¥ã¯ãCI ã®å®è¡ãã°ã¨æå ã®ã¿ã¼ããã«ã¨ãè¡ãæ¥ããå¿ è¦ãããå°å³ã§é¢åãªä½æ¥ã§ãã
ä»äººã®ã³ãããã«ãã£ã¦èªåãæ¸ãããã¹ãã失æããå ´åãããã¾ã (å³ã®å·¦ä¸ Case 3)ã ãã®å ´åã¯ãèªåãæ¸ãããã¹ãã®å 容ãééã£ã¦ãããä¸å®å ¨ã ã£ããããã®ã§ããã¹ããèªåã§ä¿®æ£ããå¿ è¦ãããã¾ããããããèªåã§ã¯ããã«å¤±æã«æ°ä»ãã¾ããã
ãã®ããã«ãCI ã§ãã¹ãã失æããå¾ã«èµ·ããè¡åã«ã¯ãCI ã®å¤±æã«æ³¨ç®ãã¦ããªãä»äººãå·»ãè¾¼ãå¿ è¦ãããå ´åã®æ¹ãå¤ããããã¦ããã®å¯¾è±¡è 㯠git blame ã§èª¿ã¹ãå¿ è¦ãããã¾ãããã®å·¥ç¨ã¯ããµã¼ãã¹éçºãé 延ããã大ããªè¦å ã§ãã
ãããæ¹åããããã以ä¸ã«ç¤ºãæ°ããéç¥ãå°å ¥ãã¾ããã
å³: ãã¹ã失æã®éç¥å®å ¨ç
1è¡ç®ãæ°ããéç¥ã§ããä¸ã®2è¡ã¯å ã»ã©ãè¦ããã jenkins-hipchat-publisher ãã©ã°ã¤ã³ã«ããéç¥ã§ãã
ãã®éç¥ã®å 容ã¯ãrspec ããã°ã®æå¾ã«åºåãã¦ãããã失æãã examples ãåå®è¡ããã³ãã³ãã©ã¤ã³ã¨ã»ã¨ãã©åãã§ããéãã¯ã以ä¸ã®è¦ç´ ãå ãã£ã¦ãããã¨ã§ãã
- ããã¡ã¤ã«å:è¡çªå·ãã®é¨åã GitHub Enterprise ã¸ã®ãªã³ã¯ã«ãªã£ã¦ãã (ãã¡ããã該å½è¡ã¸ã®ç´ãªã³ã¯)
- ãã®è¡ã®æçµæ´æ°ãªãã¸ã§ã³ (git blame ã®çµæã§ããã¡ãã GHE ã¸ã®ãªã³ã¯ã«ãªã£ã¦ã)
- ãã®è¡ãæå¾ã«å¤æ´ãã人ã¨ææ (ããã git blame ã®çµæ)
ãããã®æ å ±ããã£ããã«æµãã¦ããã ãã§ã失æãã example ãããã«èª¿ã¹ããã¾ãã失æãèªåã®å¤æ´ã¨ç´æ¥é¢ä¿ãªããããªã¨ãã§ããgit blame ãããªãã§é¢ä¿è ãããå¼ã¹ã¾ããéçºè ã¯ãèªåãå¿ è¦ãªã¨ãã ããã¹ãã®å¤±æãã°ãè¦ã«è¡ãã°è¯ãããæå ã§åå®è¡ãããå ´åããã£ããã§éç¥ããã rspec ã®ã³ãã³ãã©ã¤ã³ã端æ«ã«ã³ããããã ãã§ãã
ã¾ã¨ã
æ¬ç¨¿ã§ã¯ãCI ã§å¤±æãããã¹ãã«ã¤ãã®æ å ±ããã£ããã«éç¥ãããã¨ã§ãéçºè ãã¹ãã®å¤±æã追跡ããããããæ¹æ³ã«ã¤ãã¦èª¬æãã¾ããã
æå¾ã«ããã®éç¥å 容ãçæããã¹ã¯ãªãããç´¹ä»ãã¾ãããã®ã¹ã¯ãªããã¯ãæ¨æºå ¥åã« rspec ããã°ã®æå¾ã«åºåãã rspec ã³ãã³ãã®ãªã¹ããä¸ããããäºãåæã«æ¸ããã¦ãã¾ããã³ãã³ãã©ã¤ã³å¼æ°ã§ã欲ãããã©ã¼ããã (html, json, plain-text) 㨠git ã®ãã©ã³ãåãä¸ãã¾ãã
#! /usr/bin/env ruby require 'pathname' require 'time' require 'rubygems' require 'bundler/setup' require 'action_view' include ActionView::Helpers::DateHelper GHE_REPOSITORY_ROOT = ENV["GHE_REPOSITORY_ROOT"] def short_ref(ref) `git show --oneline #{ref}`.each_line.first.split(/ /)[0] end format = ARGV[0] branch = ARGV[1] root_dir = Pathname.pwd app = root_dir.basename entries = $stdin.read.lines.map { |line| rspec, filename, lineno, description = line.chomp.sub(/\s*#\s*?(.*)$/, "\\1").split(/[ :]/, 4) next nil unless rspec && filename && lineno description ||= '' spec_real_path = Dir.chdir(File.dirname filename) { Pathname.pwd.join(File.basename filename).relative_path_from(root_dir) } [ filename, lineno ].tap do |ary| blame = `git blame -w -l #{spec_real_path} #{additional_argument}` hash, author, timestamp = blame.match(/^([0-9a-fA-F]+)\s+(?:\S+\s+)?\(([-+=^:;<>_@\.0-9A-Za-z ]+?)\s+(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\s+[-+\d]+)\s+#{lineno}\)/m)[1,3] relative_timestamp = time_ago_in_words(Time.parse(timestamp)) ary << hash << "#{author}, #{relative_timestamp} ago" << spec_real_path << description end }.compact case format when 'html' if branch entries.each do |filename, lineno, hash, info, spec_real_path, description| message = "rspec " path = root_dir.join(spec_real_path).relative_path_from(root_dir.parent) message << %Q[<a href="#{GHE_REPOSITORY_ROOT}/blob/#{branch}/#{path}\#L#{lineno}">#{filename}:#{lineno}</a>] message << %Q[ \# (<a href="#{GHE_REPOSITORY_ROOT}/commit/#{hash}">#{short_ref(hash)}</a>) #{info}<br />] puts message end end when 'json' require 'json' failures = entries.map do |filename, lineno, hash, info, spec_real_path, description| path = root_dir.join(spec_real_path).relative_path_from(root_dir.parent) { :file => filename, :line => lineno, :commit => hash, :description => description, :real_path => path, } end payload = {:failures => failures} payload.merge!(:build_url => ENV["BUILD_URL"]) if ENV["BUILD_URL"] payload.merge!(:build => ENV["BUILD_NUMBER"]) if ENV["BUILD_NUMBER"] puts payload.to_json else entries.each do |filename, lineno, hash, info, spec_real_path, description| puts "rspec #{filename}:#{lineno} \# (#{hash}) #{info}" end end
*1:Akira Matsuda. The recipe for the worlds largest rails monolith. Ruby on Ales 2015
*2:ãã®ãã©ã°ã¤ã³ã¯ç¤¾å ãµã¼ãã¹ããæ å ±ãåå¾ããå¿ è¦ããããããªã¼ãã³ã½ã¼ã¹ã«ãã¦ã¾ãã