下町柚子黄昏記 by @yuzutas0

したまち発・ゆずたそ作・試行錯誤の瓦礫の記録

各ツールのチケットを全部JIRAにぶっこむスクリプトを作りました

概要

Confluence、Github、JIRA、Redmineに散在しているTodoチケットを1つ残らずJIRAにインポートするスクリプトを作りました。 リポジトリhttps://github.com/yuzutas0/AnyToJira

f:id:yuzutas0:20170412211034p:plain

もくじ

  • 解決したい課題
  • 経緯・背景
  • やったこと
  • 構成
  • コード(一部抜粋)
  • 感想・学び

解決したい課題

某開発チームにてTodoを一元管理できていなかったことが課題となります。

ビジネス観点でやりたいことは施策案件として一覧化されていたのですが、バグ・技術課題については、 過去の開発・運用担当者がそれぞれのツールに起票し、申し送り事項として放置されていました。

経緯・背景

引き継ぎ前の各部隊で使っているツールが異なっていました。

  • 前の製造チーム: Confluenceの表に申し送り事項をメモ。
  • 前の検品チーム: Redmineに不具合を起票。未解決課題はそのまま申し送りに。
  • 前の運用チーム: GithubのIssueに不具合やTodoを起票。未解決課題はそのまま申し送りに。
  • 前の保守チーム: JIRAのチケットとして申し送り事項を起票。

職能別組織として考えると、知識を一元管理するのは自然なので、やむをえないと思います。 分野によって蓄積すべき情報・項目が違うので利用ツールは分かれるでしょう。まさに以下の記事が良い例かと。

tech.mercari.com

ただ、最初は良いのですが、知識の成熟やコモディティ化に伴って、リードタイム短縮を志向した職能横断の動きに変わることがあります。 Dev/Opsを一体化する際に申し送り事項が散在していると、可視化・運用の妨げになってしまいます。 (最初から一元管理できているのが理想ですが実際はなかなか難しい!)

やったこと

申し送り事項をスクリプトで全部JIRAにぶっこみました。

取得元 送り先 データ取得方法
旧JIRA 新JIRA WebAPIへのリクエスト(専用ライブラリ利用)
Confluence 新JIRA 擬似ログイン → 画面情報をスクレイピング
Github 新JIRA WebAPIへのリクエスト(専用ライブラリ利用)
Redmine 新JIRA WebAPIへのリクエスト

構成

シンプルにしています。

  • main : 処理全体を記述したもの
  • lib : main からコールされる
    • 各ツールからのデータ取得処理( main とのI/Oは全て共通)
    • JIRAへの送信処理

f:id:yuzutas0:20170412211049p:plain

コード(一部抜粋)

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

感想・学び

  • 散在していようが、データさえあれば後で一元化できます。後に引き継ぐ人のために、少しでも気になる課題はどこかに書き出すことが大事だと思いました。
  • 利用者の多いグループウェアなら、データをぶっこぬくライブラリやサンプルコードを誰かしら公開しています。ツール採用に悩んだときは、とりあえず有名どころを使えば引き継ぎ観点では問題なさそうです。
  • この手の情報管理を考えると、プロマネディレクション業務でも、プログラミングスキルがあるに越したことはないですね。

そんな感じです。最後まで読んでいただき本当にありがとうございます。