ããã«ã¡ã¯ãã¨ã³ã¸ãã¢ã®ããã¾ãã§ãã
å»å¹´ã®å¤é ã«ãªã¼ãã¿ã¤ã ã®è¨æ¸¬ãå§ãã¦ãããæ¯ãè¿ãã§è¯ãæ°ã¥ããå¾ãããããã«ãªã£ãããªã¼ãã¿ã¤ã ãæ¸ããã¢ã¯ã·ã§ã³ãçã¾ãããã¨è¯ããã¨ããããããã£ãã®ã§ãä»åã¯ãã®ç´¹ä»ããããã¨æãã¾ãã
ãªã¼ãã¿ã¤ã ã®å®ç¾©
ãLeanã¨DevOpsã®ç§å¦ãã§ã¯ããªã¼ãã¿ã¤ã ããã³ã¼ãã®ã³ãããããæ¬çªç¨¼åã¾ã§ã®æè¦æéãã¨ãã¦å®ç¾©ãã¦ãã¾ãã
ç§ãã¡ã®ãã¼ã ã®ãªãã¸ããªã§ã¯ãã©ã³ãæ¦ç¥ã¨ãã¦GitHub Flowãæ¡ç¨ãã¦ãããmainã¸ã®ãã¼ã¸ã¨æ¬çªç¨¼åã®ã¿ã¤ãã³ã°ãè¿ãããããPRãopenãã¦ãããã¼ã¸ããã¾ã§ã®æéãããªã¼ãã¿ã¤ã ã¨ãã¦å®ãã¦è¨æ¸¬ãã¾ããã
ãªã¼ãã¿ã¤ã è¨æ¸¬ãå§ããåæ©
ç§ãã¡ã®ãã¼ã ã§ã¯ããã¼ã ã®ã¹ãã¼ãããã¾ãåºã¦ããªãæ°ããããã¨ããæ¼ ç¶ã¨ãã課é¡æãããã¾ããããããã課é¡æã¯ããã¤ã¤ããã§ã¯ã©ããããã¨è¨ãããã¨å ·ä½çãªã¢ã¯ã·ã§ã³ãåºã«ããç¶æ ãç¶ãã¦ãã¾ããã
ããã§ããªã¼ãã¿ã¤ã ã測ããã¨ã§ããã«ããã¯ã«ãªã£ã¦ããç®æãæ確ã«ããããªããªã®ã¹ãã¼ããä¸ãããã¨æãè¨æ¸¬ãå§ãããã¨ã«ãã¾ããã
è¨æ¸¬æ¹æ³
è¨æ¸¬æ¹æ³ã«ã¤ãã¦ã¯ãå¾åã«èª¬æããéãèªååã§ãã¦ããªããªã©ã®æ¹åç¹ãããã¤ãããã¾ãããä¸ä¾ã¨ãã¦èª°ãã®åèã«ãªãã°ã¨æãç´¹ä»ãã¾ãã
ç§ãã¡ã®ãã¼ã ã§ã¯GitHub Projectsã使ã£ã¦PRãissueã®ç®¡çããã¦ãããã¯ãã¼ãºããã¢ã¤ãã ã¯å®äºã¬ã¼ã³ã«ç½®ãããã«ãã¦ãã¾ããã
åç´ã«ç®¡è½ãªãã¸ããªã®ãªã¼ãã¿ã¤ã ãè¨æ¸¬ããã ãã ã¨ãè¤æ°ãã¼ã ã使ããªãã¸ããªã®å ´åã«ä»ãã¼ã ã®çµæãæ··ãã£ã¦ãã¾ãã¾ããããã§ããGitHub Projectä¸ã®å®äºã¬ã¼ã³ã«ããã¢ã¤ãã ãããªã¼ãã¿ã¤ã è¨æ¸¬ã®å¯¾è±¡ã¨ããè¨æ¸¬ããã¢ã¤ãã ã¯åé¤ãããããªéç¨ã«ãã¾ããã
å®è£
ã¯ã·ã§ã«ã¹ã¯ãªããã¨Rubyã§è¡ãã¾ããã
ã·ã§ã«ã¹ã¯ãªããä¸ã§GitHub APIãå©ãã¦å®äºã¬ã¼ã³ã®ã¢ã¤ãã ãåå¾ããRubyã使ã£ã¦ãªã¼ãã¿ã¤ã ãè¨ç®ãã¦åºåãã¦ãã¾ãã
å½åã¯ãå®äºã¬ã¼ã³ã«ããã¢ã¤ãã ãåå¾ããã ããªãGitHub APIã«å°ç¨ã®ã¯ã¨ãªããããããã¨èãã¦ãã¾ãããããã®ãããªã¯ã¨ãªã¯è¦å½ãããªãã£ããããä¸è¨ã®ããã«å®è£ ãã¾ããã
- GitHub CLIã使ã£ã¦GitHub APIãå©ããprojectã®idãåå¾ãã
- ãªãã¸ããªãåæãã¦foræã§åããä¸è¨ãç¹°ãè¿ã
- GitHub APIã®search queryã使ã£ã¦ç´è¿ã§ã¯ãã¼ãºããPR/issueã100件åå¾ãã
- åå¾ããã¢ã¤ãã ã®ãã¡ã対象ã®project idã®å®äºã¬ã¼ã³ã«åå¨ãããã®ãJSONãã¡ã¤ã«ã®æ«å°¾ã«è¿½å
- JSONãã¡ã¤ã«å ã®åã¢ã¤ãã ã®ãªã¼ãã¿ã¤ã ãè¨ç®ãã¦åºå
å®è£ ã¯ä¸è¨ã®éãã§ãã
ã·ã§ã«ã¹ã¯ãªããã使ã£ãå®è£
(1, 2)
#!/bin/bash PROJECT_NUMBER="1" # GitHub Projectsã®URLã«è¨è¼ããã¦ããæ°å¤ gh api graphql -f query=' query ($org: String!, $project_number: Int!) { organization(login: $org) { projectV2(number: $project_number) { id } } }' -f org="classi" -F project_number=$PROJECT_NUMBER >project.json PROJECT_ID=$(jq -r '.data.organization.projectV2.id' project.json) repositories=( # è¨æ¸¬å¯¾è±¡ã®ãªãã¸ããªåãæ ¼ç´ãã ) echo -n "[]" >completed_items.json for ((i = 0; i < ${#repositories[@]}; i++)); do search_query="repo:classi/${repositories[i]} is:closed sort:updated-desc" gh api graphql -f query=' query ($search_query: String!) { search(type: ISSUE, first: 100, query: $search_query) { nodes { ... on PullRequest { title url repository { name } assignees(first: 10) { nodes { login } } labels(first: 10) { nodes { name } } createdAt closedAt projectItems(first: 10) { nodes { id project { id } fieldValues(first: 10) { nodes { ... on ProjectV2ItemFieldSingleSelectValue { field { ... on ProjectV2SingleSelectField { name } } name } } } } } } } } }' -f search_query="$search_query" >search_result.json # GitHub Projectä¸ã®"å®äº"ã¬ã¼ã³ã«ããã¢ã¤ãã ãtempã«æ ¼ç´ temp=$( jq --arg project_id "$PROJECT_ID" ' .data.search.nodes[] | select(. != {}) | select(.projectItems.nodes[] | .project.id == $project_id) | select(.projectItems.nodes[] | .fieldValues.nodes[] | select(.field.name == "Status").name | contains("â å®äº")) ' search_result.json | jq -cs . ) # tempé åã®å¤ãcompleted_items.jsonã®é åã®æ«å°¾ã«è¿½å ãã completed_items=$(jq -s '.[0] + .[1]' completed_items.json <(echo "$temp")) echo "$completed_items" >completed_items.json done bundle exec ruby format_pr_infos.rb
Rubyã使ã£ãå®è£
(3)
- format_pr_infos.rb
require "json" require "./completed_item" rows = '' File.open("completed_items.json") do |f| completed_items = JSON.load(f) completed_items.each do |completed_item| next if completed_item.empty? item = CompletedItem.new(completed_item) rows+="#{item.title}\t" rows+="#{item.repository}\t" rows+="#{item.assignees}\t" rows+="#{item.labels}\t" rows+="#{item.lead_time}\t" rows+="#{item.formatted_lead_time}\t" rows+="#{item.created_at}\t" rows+="#{item.closed_at}\t" rows+="#{item.url}\n" end end puts rows
- completed_item.rb
require 'time' class CompletedItem def initialize(item) @item = item end def title @item['title'] end def repository @item['repository']['name'] end def created_at Time.parse(@item['createdAt']).getlocal("+09:00").strftime("%Y-%m-%d %H:%M:%S") end def closed_at Time.parse(@item['closedAt']).getlocal("+09:00").strftime("%Y-%m-%d %H:%M:%S") end def lead_time "#{format("%02d", hour)}:#{format("%02d", minute_per_hour)}" end def formatted_lead_time "#{format("%03d", date)}æ¥ #{format("%02d", hour_per_day)}æé #{format("%02d", minute_per_hour)}å" end def assignees assignees_nodes = @item['assignees']['nodes'] assignees_nodes.map{ |node| node['login'] }.join(',') end def labels labels_nodes = @item['labels']['nodes'] labels_nodes.map { |node| node['name'] }.join(',') end def url @item['url'] end private def time @time ||= (Time.parse(closed_at) - Time.parse(created_at)).to_i end def date @date ||= (time / (60 * 60 * 24)).floor end def hour @hour ||= (time / (60 * 60)).floor end def minute @minute ||= (time / 60).floor end def hour_per_day @hour_per_day ||= hour - date * 24 end def minute_per_hour @minute_per_hour ||= minute - hour * 60 end end
åºåããå 容ã¯ãæ¯ãè¿ããããããããã«Googleã¹ãã¬ããã·ã¼ãä¸ã«è²¼ãä»ãã¦è¨é²ããããã«ãã¾ããã ã¹ãã¬ããã·ã¼ãã®ãã£ã«ã¿æ©è½ã使ãã°ãä»»æã®æéãæ å½è ãªã©ã§æ¡ä»¶ãçµãè¾¼ã¿ããã®æ¡ä»¶ã®ãªã¼ãã¿ã¤ã ã®å¹³åå¤ãä¸å¤®å¤ãç®åºã§ãã¾ãã
æ¯é±ããªã¼ãã¿ã¤ã ãè¨é²ããéã«ãã®é±ã§çµãè¾¼ãã ãã£ã«ã¿ãä½æãããã¨ã§ãå¾ããç°¡åã«è¦è¿ããããã«ãã¾ããã
ãã¼ã ã®å¤å
ãªã¼ãã¿ã¤ã ãè¨æ¸¬ãã¦ããä¸è¨ã®ãããªè¯ãå¤åãèµ·ãã¾ããã
PRã®è³ªãæèããããã«ãªã£ã
ãªã¼ãã¿ã¤ã ãè¨æ¸¬ãããã¨ã§ãPRã®ç²åº¦ã大ããå ´åãè¤éãªå ´åã«ãªã¼ãã¿ã¤ã ãããªã伸ã³ãã¨ãããã¨ããããã¾ããã
ç²åº¦ã®å¤§ããPRã¯è¦ãç®æãå¤ãã®ã§ãå½ç¶ãã®åã¬ãã¥ã¼ã«ãæéããããã¾ãã
ãã ãããã ãã§ã¯ãªãã¬ãã¥ã¼èªä½ãå¾åãã«ããã¦ãããã¨ãåå ã®ä¸ã¤ã§ããã
ã¬ãã¥ã¢ã¼ããã¬ãã¥ã¼ãã¥ããããã¨æããPRã®ã¬ãã¥ã¼ãå¾åãã«ãã¦ãããããã«ããããã«ãªã¼ãã¿ã¤ã ã伸ã³ã¦ãã¾ããã
ãã®æ°ã¥ãã«ãããã¬ãã¥ã¼ãã¥ãããªã¨æãã¦ãå¾åãã«ãããã¾ãã¯PRã®è³ªã«é¢ããææãç©æ¥µçã«ããããã«ãªã£ãã¨æãã¾ãã
ã¾ããPRãä½ãå´ãã¬ãã¥ã¼ããããPRãæèããããã«ãªã£ã¦ããããã¼ã ã¡ã³ãã¼ã«å¯¾ãã¦ãªã¼ãã¿ã¤ã è¨æ¸¬åå¾ã§ã©ããªã¢ã¯ã·ã§ã³ãåãããã«ãªã£ããã¢ã³ã±ã¼ããã¨ã£ãã¨ãããPRãå°ããä½ãããã«ãªã£ãããdescriptionãç´°ããè¨è¿°ããããã«ãªã£ããã¨ãã声ãå¯ãããã¾ããã
ãã¢/ã¢ãä½æ¥ãå¢ãã
ä¸è¨ã¯PRèªä½ã®åé¡ã«ãããã®ã§ããããPRèªä½ã«åé¡ã¯ãªãã¦ãåæã®ç解ã大å¤ã ã£ããããã¼ã ã¡ã³ãã¼ã®è¦æåéãéã¾ã£ã¦ããå¤æ´ã ã£ããããã¨ãªã¼ãã¿ã¤ã ã伸ã³ã¦ãã¾ãã¾ãã
ãããã£ãPRããã£ãå ´åã¯ãã¬ãã¥ã¢ã¼å´ããPRä½æè ã«ãã¢ã¬ãã¥ã¼ãå¼ã³ããã¦ãªã¼ãã¿ã¤ã ã®ç縮ã«åªãã¾ããã
ãã¢ã¬ãã¥ã¼ãå¼ã³ãããã¨ãç§ããã®PRããããã£ã¦ãªãã®ã§åå ãããã§ããã¨ãããããªå£°ãä¸ãã£ã¦3~4人ã®ã¢ãã¬ãã¥ã¼ã«ãªããã¨ããããPRä½æè ã¨ã¬ãã¥ã¢ã¼éã®ç¥èå·®ãç´ æ©ãåãããã¨ãã§ããã®ã§ããªãä½æãè¯ãã£ãã§ãã
éããæã«é£ãããã ã¨æããPRã¯ãã¬ãã¥ã¼ãå¾åãã«ããã®ã§ã¯ãªãããã¢ã¬ãã¥ã¼ãã£ã³ã¹ã ã¨æãããã«ãªãã¾ããã
æ¯ãè¿ãä¼ã§è¯ãæ°ã¥ããå¾ãããããã«ãªã£ã
ä¸è¿°ããå¤åã¯ãããããã¼ã ã®æ¯ãè¿ãä¼ã®ä¸ã§çã¾ããã¢ã¯ã·ã§ã³ã§ãã
ããã«ããã¯ãæ確ã«ãªã£ããã¨ã§ãä»ã¾ã§ææ§ã ã£ãåé¡ç¹ãæãããããªããæ¯ãè¿ãä¼ãæ´»çºã«ãªã£ãããã«æãã¾ãã
å ·ä½çã«ã¯ãæ¯ãè¿ãä¼ã®ä¸ã§ä¸è¨ã®ãããªåé¡ç¹ã«æ°ã¥ãã¾ããã
- ãã¼ã ã¡ã³ãã¼ã®è¦æé åãéã¾ã£ã¦ããPRã¯æããã«ãªã¼ãã¿ã¤ã ã伸ã³ã¦ãã
- åãåãã対å¿ãå¤ãé±ã¯ãªã¼ãã¿ã¤ã ã伸ã³ããã
- ã¿ã¹ã¯ãæ±ãããã¦ã¬ãã¥ã¼ã«æãåããªããªã£ã¦ãã
ç¹ã«è¨æ¸¬ãå§ãã¦ä¸ã¶æãããã¯ãæ¯ãè¿ãä¼ã®ãã³ã«ãã¼ã ãæ¹åãã¦ãã£ã¦ããå®æãããããã¼ã ã§åãã¢ããã¼ã·ã§ã³ã«ãªã£ã¦ãã¾ããã
ç¾ç¶ã®åé¡ç¹
ãªã¼ãã¿ã¤ã ãè¨æ¸¬ãããã¨èªä½ã¯è¯ããã¨ã¥ãããªã®ã§ãããä»ã®è¨æ¸¬æ¹æ³ã«ã¯ä¸è¨ã®ãããªåé¡ç¹ãããã¾ãã
- æåå®è¡ãé¢åèã
- ä»ãã¼ã ã§æµç¨ãã¥ãã
æ¯ãè¿ãä¼ã®åã«æåã§å®è¡ãã¦ãGoogleã¹ãã¬ããã·ã¼ãä¸ã«è²¼ãä»ãã¦ããã£ã«ã¿ä½æãã¦â¦ã¨ãããã¨ãæ¯é±ãã£ã¦ãããããªãé¢åã§ãã æåãªã®ã§ãªããã¹ãèµ·ãããã¨ããããä¸åº¦éå»ã«ä½æãããã£ã«ã¿ãå ¨é¨åé¤ãã¦ãã¾ã£ããã¨ãããã¾ããã
ã¾ãããGitHub Projectã®å®äºã¬ã¼ã³ãåç §ãããã¨ããèªåãã¡ã®ãã¼ã ã®éç¨æ¹æ³ã«ä¾åããæ¹æ³ã«ãã¦ãã¾ã£ããããä¼¼ããããªéç¨ããã¦ãããã¼ã ã§ã¯ãªãã¨çä¼¼ããã®ãé£ãããªã£ã¦ãã¾ãã
ã§ã¯ã©ãããã®ã
ãåã ã®ãã¼ã ããªã¼ãã¿ã¤ã ãè¨æ¸¬ãã¦ãä»ãã¼ã ã¸ã®æ¨ªå±éãé£ãããã¨ããåé¡ç¹ã¯ä»¥åããåå¨ãããããæè¿ãã¼ã¿ãã©ãããã©ã¼ã ãã¼ã ã横æçã«Four Keysã®å¯è¦åãããããã®åºç¤ãä½ã£ã¦ããã¾ããï¼ ãã®åºç¤ãå©ç¨ããã°ä¸è¿°ããåé¡ç¹ã¯å ¨ã¦è§£æ±ºã§ããããªã®ã§ãå¾ã ã«ç§»è¡ãé²ãããã¨èãã¦ãã¾ãã
ä»å¾ãã¾ã移è¡ã§ããã°ã¾ãç´¹ä»ãã¾ãã®ã§ããæå¾ ãã ããï¼
çµã³
以ä¸ãç§ã®ãã¼ã ã§è¡ãªã£ã¦ãããªã¼ãã¿ã¤ã ã®è¨æ¸¬æ¹æ³ã¨ãè¨æ¸¬ãããã¨ã§èµ·ãããã¼ã ã®å¤åã«ã¤ãã¦ã®ç´¹ä»ã§ããã
è¨æ¸¬ã®ä»çµã¿ãæ´ããã¾ã§ã«ãããã³ã¹ãã«å¯¾ãã¦å¾ãããæ°ã¥ãã¯å¤§ããã£ãã®ã§ãè²»ç¨å¯¾å¹æãé«ããã£ã¦è¯ãã£ããªã¨æãã¾ããã
å°ãã§ããªã¼ãã¿ã¤ã ãè¨æ¸¬ãããã¨æã£ã¦ããæ¹ã®å©ãã«ãªãã°å¹¸ãã§ãã ããã¾ã§èªãã§ããã ããããã¨ããããã¾ããã