概要
Confluence、Github、JIRA、Redmineに散在しているTodoチケットを1つ残らずJIRAにインポートするスクリプトを作りました。 リポジトリ→ https://github.com/yuzutas0/AnyToJira
もくじ
- 解決したい課題
- 経緯・背景
- やったこと
- 構成
- コード(一部抜粋)
- 感想・学び
解決したい課題
某開発チームにてTodoを一元管理できていなかったことが課題となります。
ビジネス観点でやりたいことは施策案件として一覧化されていたのですが、バグ・技術課題については、 過去の開発・運用担当者がそれぞれのツールに起票し、申し送り事項として放置されていました。
経緯・背景
引き継ぎ前の各部隊で使っているツールが異なっていました。
- 前の製造チーム: Confluenceの表に申し送り事項をメモ。
- 前の検品チーム: Redmineに不具合を起票。未解決課題はそのまま申し送りに。
- 前の運用チーム: GithubのIssueに不具合やTodoを起票。未解決課題はそのまま申し送りに。
- 前の保守チーム: JIRAのチケットとして申し送り事項を起票。
職能別組織として考えると、知識を一元管理するのは自然なので、やむをえないと思います。 分野によって蓄積すべき情報・項目が違うので利用ツールは分かれるでしょう。まさに以下の記事が良い例かと。
ただ、最初は良いのですが、知識の成熟やコモディティ化に伴って、リードタイム短縮を志向した職能横断の動きに変わることがあります。 Dev/Opsを一体化する際に申し送り事項が散在していると、可視化・運用の妨げになってしまいます。 (最初から一元管理できているのが理想ですが実際はなかなか難しい!)
やったこと
申し送り事項をスクリプトで全部JIRAにぶっこみました。
取得元 | 送り先 | データ取得方法 |
---|---|---|
旧JIRA | 新JIRA | WebAPIへのリクエスト(専用ライブラリ利用) |
Confluence | 新JIRA | 擬似ログイン → 画面情報をスクレイピング |
Github | 新JIRA | WebAPIへのリクエスト(専用ライブラリ利用) |
Redmine | 新JIRA | WebAPIへのリクエスト |
構成
シンプルにしています。
main
: 処理全体を記述したものlib
:main
からコールされる- 各ツールからのデータ取得処理(
main
とのI/Oは全て共通) - JIRAへの送信処理
- 各ツールからのデータ取得処理(
コード(一部抜粋)
JIRAに送るところ
require 'jira-ruby' class JiraSender def initialize @client = JIRA::Client.new(JIRA_COMMON.options) @project_id = @client.Project.find(ENV['JIRA_PROJECT_NAME']).id end def send_jira(summary, description) issue = @client.Issue.build response = issue.save( fields: { summary: summary, description: DESC_PREFIX + description }.merge(fields) ) puts response unless response end end
JIRAから取得するところ
require 'jira-ruby' class JiraReceiver URL_PREFIX = "#{ENV['JIRA_HOST']}#{ENV['JIRA_CONTEXT_PATH']}/browse/" class << self def issues client = JIRA::Client.new(JIRA_COMMON.options) issues = {} client.Issue.jql(ENV['JIRA_JQL']).each do |issue| issues[issue.summary] = URL_PREFIX + issue.key end issues end end end
Confluenceから取得するところ
require 'mechanize' require 'open-uri' require 'kconv' require 'oauth' class Confluence CONFLUENCE_XPATH_PREFIX = '//*[@id="main-content"]/div/table/tbody/tr[' CONFLUENCE_XPATH_SUFFIX = "]/td[#{ENV['CONFLUENCE_TABLE_COLUMN']}]" CONFLUENCE_PAGE = "#{ENV['CONFLUENCE_URL']}pages/viewpage.action?pageId=#{ENV['CONFLUENCE_PAGE']}" class << self def issues issues = {} titles.each { |title| issues[title] = CONFLUENCE::CONFLUENCE_PAGE } issues end private def titles result, agent = io_variables agent.get(ENV['CONFLUENCE_URL'] + 'login.action') do |page| login(page) contents, start_row, end_row = crawl_variables(agent) start_row.upto(end_row) do |index| scraped = scrape(contents, index) result << scraped unless scraped.empty? end end result.compact.reject(&:empty?) end def login(page) page.form_with(name: 'loginform') do |form| form.os_username = ENV['JIRA_MAILADDRESS'] form.os_password = ENV['JIRA_PASSWORD'] end.submit end def scrape(contents = '', index = 0) confluence_xpath = CONFLUENCE_XPATH_PREFIX + index.to_s + CONFLUENCE_XPATH_SUFFIX return '' if contents.xpath(confluence_xpath).empty? unless contents.xpath(confluence_xpath).xpath('.//li').empty? return scrape_content_with_list(contents, confluence_xpath) end contents.xpath(confluence_xpath).text.to_s end end end
Githubから取得するところ
require 'octokit' class Github URL = 'https://github.com/' REPOSITORIES = ENV['GITHUB_REPOSITORIES'].split(',') class << self def issues issues = {} client = Octokit::Client.new(access_token: ENV['GITHUB_TOKEN']) REPOSITORIES.each do |repo| responses(client, repo).each do |issue| issues[issue.title] = issue.html_url end end issues end private def responses(client, repository) responses, next_response = call_api(client, repository) paging(responses, next_response) pr_prefix = "#{URL}#{ENV['GITHUB_ORGANIZATION']}/#{repository}/pull/" responses.reject do |response| response.html_url.start_with? pr_prefix end end def call_api(client, repository) responses = [] repository_path = "#{ENV['GITHUB_ORGANIZATION']}/#{repository}" responses.concat client.issues(repository_path) next_response = client.last_response.rels[:next] [responses, next_response] end end end
Redmineから取得するところ
require 'open-uri' require 'kconv' require 'json' class Redmine URL_PREFIX = "#{ENV['REDMINE_URL']}/projects/#{ENV['REDMINE_PROJECT_NAME']}/issues.json?key=#{ENV['REDMINE_API_KEY']}&query_id=#{ENV['REDMINE_QUERY_ID']}" LIMIT_SIZE = 100 class << self def issues pages = request['total_count'].to_i / LIMIT_SIZE + 1 issues = {} pages.times do |page| this_page = page + 1 items = request(LIMIT_SIZE, this_page)['issues'] items.each { |issue| issues[issue['subject']] = url_of(issue) } end issues end private def request(limit = 1, page = 1) request = "#{URL_PREFIX}&limit=#{limit}&page=#{page}" response = open(request, &:read).toutf8 JSON.parse(response) end def url_of(issue) "#{ENV['REDMINE_URL']}issues/#{issue['id']}" end end end
感想・学び
- 散在していようが、データさえあれば後で一元化できます。後に引き継ぐ人のために、少しでも気になる課題はどこかに書き出すことが大事だと思いました。
- 利用者の多いグループウェアなら、データをぶっこぬくライブラリやサンプルコードを誰かしら公開しています。ツール採用に悩んだときは、とりあえず有名どころを使えば引き継ぎ観点では問題なさそうです。
- この手の情報管理を考えると、プロマネやディレクション業務でも、プログラミングスキルがあるに越したことはないですね。
そんな感じです。最後まで読んでいただき本当にありがとうございます。