スクラムチームのプロジェクト管理にはJiraが便利

この記事ははてなエンジニアアドベントカレンダー 2023の5日目の記事です。

maku693です。

私が現在所属しているチームではJiraを使ってチームのタスクを管理しています。

はてなではスクラムやそれをベースにした手法を使ってプロジェクトを管理していることが多いですが、以前所属していたチームでは別のウェブサービスを使っており、ベロシティ計測や中長期の計画に手間がかかると感じていました。

私は今のチームに異動して初めてJiraに触れたのですが、うまくJiraの機能に乗ることでそういった仕事を省力化できることがわかりました。

この記事では私のチームでJiraをどのように活用しているか紹介します。

Jiraの良いところ

Jiraの良いところはスクラムのコンセプトをもとにした機能が豊富な点です。私のチームもスクラムをベースにプロジェクトを管理しているので、他のツールに比べてよりチームに適していると感じました。

Jiraを使っていて特に良いと感じるのは次の3点です。

  • 「バックログ」ボードでの計画がしやすい
  • 「課題タイプ」と「バージョン」でタスクが分類できる
  • 「レポート」機能を使ってプロジェクトの進捗が把握できる

それぞれ詳しく説明します。

「バックログ」ボードでの計画がしやすい

Jiraの「ボード」とはJiraの課題の表示方法の一種です。私のチームでは「バックログ」ボードを使って直近数スプリントの計画を実施しています。

Jiraのバックログボードには「スプリント」と「バックログ」の概念があります。これらはそれぞれスクラムのスプリントバックログとプロダクトバックログに相当するもので、バックログには将来とりかかる予定のタスクを、スプリントにはそのスプリントに手がける予定のタスクを並べられます。

スプリントは好きなだけ用意しておくことができるので、先のスプリントの計画をするだけでなく、まだリファインメントされていない課題を表現するなど、とりあえずタスクを分類する場所として使うこともできます。

ボード上のスプリントには見積もりの合計が表示されます。後ほど説明するスプリントレポートを見ればチームのキャパシティがわかるので、スプリントプランニング時にスプリントバックログの過不足を防げるのが便利です。

「課題タイプ」と「バージョン」によるタスクが分類できる

課題タイプ

Jiraでは課題タイプを任意に追加できますが、私のチームではデフォルトで存在する「エピック」と「ストーリー」をよく利用しています。

「エピック」は「〇〇機能」のような、開発に1ヶ月程度かかるような粒度でバックログを管理するのに利用しています。

「ストーリー」ではエピックを構成するユーザーストーリーを管理しています。ストーリーについては必ずしも「△△できる」のようなユーザーストーリーっぽい書き方のタイトルにしているわけではなく、作業量が多いときは「××のUI」「××バックエンドAPI」などのようにコンポーネントレベルで分割することもあります。タスクの見積もり(ストーリーポイント)はストーリーに記録しています。

また、ストーリー中の細かいタスクは「サブタスク」で管理しています。サブタスクに関してはストーリーを実現するコードを書く際に、Pull Request単位くらいの粒度で分割して利用しています。これに関しては見積もりはしていません。

私のチームでは1チームでいくつかのプロダクトの面倒を見ている都合で、複数のエピックを並行して何スプリントにもまたがって進行させることがあるのですが、単に課題の種別があるだけでなく、エピック - ストーリー - サブタスク という親子関係がスプリントとは別に存在することで、素直にそういった状況を表現できるのが便利なポイントです。

バージョン

「バージョン」は課題タイプやスプリントとはまた別の軸で、これを使うと複数の課題をあるバージョンにまとめられます。

私のチームでは何ヶ月かかけていくつかの機能を開発し、全てが完成してからそれらをまとめてリリースしています。

このようなマイルストーンを管理する際に、一つの大きなエピックを作るのではなく、エピックは機能単位で表現し、バージョンを使ってそれらをまとめられるというのがわかりやすくて気に入っているポイントです。

なおバージョンはストーリーに設定しています。こうすることで同じバージョンに紐づくストーリーの見積りの合計がレポートで把握できます。

「レポート」機能を使ってプロジェクトの進捗が把握できる

Jiraが特に秀でていると感じるのはこのレポート機能です。

私のチームではつぎのレポートをよく利用しています。

  • ベロシティグラフ
  • バージョンレポート
  • エピックレポート

私のチームではスプリントプランニングの際に 「ベロシティグラフ」で過去のスプリントのベロシティの実績を確認し、次のスプリントのキャパシティの参考にしたり、「バージョンレポート」「エピックレポート」でバージョン・エピックの開発が締切に間に合うか確認したりといった形で利用しています。

ベロシティグラフ
バージョンレポート

なお、バージョンレポートとエピックレポートは集計対象が異なるだけで見た目はほぼ同じです。

この2つのレポートはいわゆるバーンアップ形式の表現ですが、どちらもバーンダウン形式のものも存在するので、必要に応じてどちらも見れるのが便利です。

内部的にどのような計算式が使われているのか把握していないのですが、バージョンやエピックの完了予定日も表示されるので、これも計画の参考にしています。

また、このレポートは半期や1年などの中長期計画を立てるときにも利用できます。たとえば企画の立ち上げ段階で、開発を実施するかどうかが確定していないタイミングでは、バックログがまだ詳細化できないことが多いと思います。そのような状況でも、レポートを使えば過去のエピックの見積もりや開発期間が簡単にわかるので、同等の規模の機能の見積もりと、直近のチームのベロシティをもとに開発期間の予測を立てられます。

ただし、レポートを正常に運用するには、バックログアイテムの見積もりがある程度正確に行われていたり、スプリントの期間を一定にしていたりと、スクラムのお約束が守られていることが前提になります。そういったスクラムのコンセプトをチームに浸透させておくことも重要かと思います。

おわりに

Jira以外のツールだとタスクを分類する軸が足りなかったり逆に自由度が高すぎて使いづらかったりと不満を感じることが多いのですが、Jiraにはスクラムの概念をもとにモデリングされた機能がビルトインされているので、そういった面での辛さがないのが嬉しいポイントです。

スクラムチームのプロジェクト管理に悩んでいるけど、今までJiraを使ったことがないという方は一度試してみてはいかがでしょうか。

この記事ははてなエンジニアアドベントカレンダー 2023の5日目の記事でした。

明日の担当は id:tomato3713 さんです!

(おまけ)クラウド版Jiraで今回説明したようなプロジェクトをセットアップする方法

この記事を書くにあたって初めて知ったのですが、クラウド版Jiraのプロジェクトには、「チーム管理対象プロジェクト」と「企業管理対象プロジェクト」の2種類があります。今回説明した機能のうち特にレポート周りは「企業管理対象プロジェクト」でしか利用できないものが多いです。

「チーム管理対象プロジェクト」だとエピックやバージョンのレポートが見れない上に、あとからプロジェクトタイプの変換はできないようなので、そのようなレポートが必要な場合は最初から「企業管理対象プロジェクト」として作成するのがおすすめです。

詳しくは次のJiraのヘルプを参考にすると良いでしょう。

チーム管理対象プロジェクトと企業管理対象プロジェクト間の移行 | Jira Software Cloud | Atlassian サポート

少し調べてみたところ、どうやらPremiumやEnterpriseプランではAdvanced Roadmapsという機能が利用でき、これが代替になるようにも見えるのですが、無料プランのアカウントしか持っていないので試せていません。この辺りも機会があれば触ってみたいと思います。

Goのmapをsetのように使う

Go言語の標準ライブラリには他言語のset型のようなものは存在しませんが、単純なものでよければ組み込みのmapをsetのように使えます。

x := map[string]struct{}{} // string型のset相当のmapを作成
x["foobar"] = struct{}{}   // "foobar" を登録
_, ok := x["foobar"]       // ok == true。集合に値が含まれる
_, ok = x["barbaz"]        // ok == false。集合に値は含まれない

Mapのキーには比較演算子 (==!=) が定義されている型が利用できます *1(comparable*2 に近いが、mapやsliceはキーにできないのでcomparableとは異なる)。

Mapのvalueの型に空struct (struct{}) を使っているので、valueのために余計なメモリを確保せずに済みます。またこうすることでvalueに意味がないことの表明にもなります。

ただし、mapはあくまでmapなので、他言語のsetのような集合どうしの和や積などをとるためのインタフェースは存在しません。そのようなアルゴリズムが必要な場合はおとなしくdeckarep/golang-setのようなライブラリを入れるのがよいでしょう。

このテクニックは id:cockscomb さんのコードをレビューしていて知りました。

Github Actionsで簡単にPuppeteerを使えるaction-puppeteer-scriptを作りました

maku693です。

Github ActionsでPuppeteerを簡単に使えるCustom Actionを作りました。

github.com

最近Github Actions上でブラウザを動かしたくなったのですが、いちいち実行環境を整えるのも面倒なので、サクッとできないものかと調べたところ、意外とPuppeteerをそのまま使えるactionというのは存在しないようだったので、自分で作りました。

使い方はREADMEに書いてありますが、ここでも軽く紹介します。

以下のjobでは、ページのタイトルを取ってきて、それを後続のstepで利用しています。

- id: get-title
  uses: maku693/action-puppeteer-script@v0
  with:
    script: |
      const page = await browser.newPage();
      await page.goto("https://maku693.hatenablog.jp");
      const title = await page.$eval('title', el => el.textContent);
      return title;
- run: echo '${{ steps.get-title.outputs.result }}' #  `"The Third Law"` が出力される

ちゃんと試していないのですが、基本的にほぼ全てのPuppeteerのAPIが使えると思うので、スクリーンショットを撮るとか、ファイルをダウンロード・アップロードするとか、ブラウザが必要な色々な場面の自動化に活用できると思います。

続きを読む

goコマンドに特定のディレクトリを無視させたいときは空のgo.modファイルを置いておく

タイトルでほとんど説明しましたが、Go Modulesが有効な環境で、module-awareなコマンドに特定のディレクトリを無視させるには、無視させたいディレクトリにダミーのgo.modファイルを設置しておくとよいです。ファイルの内容は空で構いません。

Githubにサンプルコードを置いておいたので、参考にしてください。

github.com

解説

パッケージパターンを引数として受け取るgoコマンドのサブコマンドを使うとき、...というワイルドカードを使うと、特定のディレクトリツリー内のパッケージをまとめて指定できて便利です。

例えば

$ go generate ./...

というコマンドは、カレントディレクトリ(.)以下のすべてのパッケージを対象にしてgo generateコマンドを実行します。

これはパッケージがたくさんある場合にひとつずつ指定せずに済むので便利なのですが、この方法だとgoコマンドがすべてのディレクトリツリーを走査してしまうので、実行に時間がかかる場合があります。

例えば、GoとNode.jsのソースコードが同居しているコードベースがあったとき、node_modulesディレクトリにはGoのソースコードは含まれていないので、goコマンドがこのようなディレクトリを見ないようにできると、パフォーマンス上のメリットがあります。

goコマンドは_または.から始まるディレクトリや、testdataというディレクトリは無視する*1ので、ディレクトリをリネームできる場合はそうすればいいのですが、node_modulesのようにディレクトリ名を制御できないこともあります。

これに対処するには、ダミーのgo.modファイルを当該ディレクトリに設置し、goコマンドにそのディレクトリ以下を別のモジュールとして認識させるという方法が使えます。

個人的には筋の良い解決策とは思えないのですが、Goチームとしてはgoコマンドに特定のディレクトリを無視する機能を追加する予定はないようです*2

node_modulesを無視したい場合はpostinstallスクリプトを使う

一般にnode_modulesディレクトリはバージョン管理システムで管理されないため、go.modファイルが必ずnode_modulesに配置されるようにするには一工夫必要です。

ワークアラウンドとしては、package.jsonpostinstallスクリプトに空のgo.modファイルを作成するようなスクリプトを指定するとよいでしょう(とりあえずnode_modules生成後に自動でgo.modを配置するという用途にはこれで十分ではあるものの、GoのツールチェインのためのファイルをNode.jsの仕組みで生成させるのはいかがなものか……とも思いますが)。

2022年3月現在、Docker Desktop for Macでbind mount volumeにcached, delegatedオプションを指定する意味はない

以前、Docker Desktop for Macのbind mount volumeにはcacheddelegatedというパフォーマンスチューニング用のオプションがあったのですが、現在はもはや指定しても何も起きないようです*1。かなり前からそうだった*2ようなのですが、全く知りませんでした。

フレンドリーな単語を出力する friendly-words-cli というコマンドラインツールを作りました

1ヶ月くらい前の話なのですが、friendly-words-cliというコマンドラインツールを作りました。

github.com

趣味でコードを書くにあたってプロジェクト名を決めたいとき、ランダムな英単語を組み合わせた文字列が欲しいときのワンライナー - The Third Lawのように決めていたのですが、この方法には公の場で使うのは憚られそうな単語が入っていることがあるという欠点があったので、代替を探していました。

あるときp5.js Web Editorを使う機会があり、これに実装されているプロジェクト名を決める仕組みを流用できないかと調べてみたところ、glitchdotcom/friendly-wordsというNPMパッケージを見つけたので、これを使ったコマンドラインツールを作りました。

プロジェクト名を決めたいというのが個人的な使い道なのですが、ランダムな単語が欲しいシチュエーションは他にもあると思うので、コマンドとしては単語の出力に専念するようなインタフェースにしてあります。

$ friendly-words
comfort

p5.js Web EditorGlitchのプロジェクト名のような単語が欲しい場合は、以下のように使うとよいでしょう。

$ echo "$(friendly-words -l predicates)-$(friendly-words -l objects)"
flax-tangerine

直接ディレクトリを作りたい場合はこんな感じで使えます。

$ mkdir "$(friendly-words -l predicates)-$(friendly-words -l objects)"
$ ls
plucky-bacon

ぜひお使いください。

github.com

Dataloaderの元になったHaskellのHaxlというライブラリがおもしろい

GraphQL APIを作るとき、素朴に実装するとN+1問題が発生しがちなので、これを解消するためにDataloaderという仕組みも実装するのが一般的です。

少し前にHaskell Dayというイベントの発表資料をレビューする機会があり*1、Dataloader的な設計はGraphQL以前から知られていて、Facebookで開発されたHaxlというHaskellのライブラリに由来するということを知りました*2

ちなみにレビューした発表はこちらです(この発表をネタにして記事を書いていいですか?と発表者の@nakaji_dayoさんに聞いたところ、快諾いただけた上にこのブログ記事の内容もレビューしていただけました!ありがとうございます)。

www.youtube.com
speakerdeck.com

このHaxlですが、他言語で実装された一般的なDataloaderにはない特徴があります。

よくあるDataloaderには、RDBMSへのクエリやWeb APIの呼び出しなどのリモートへのリクエストのバッチ化や、その結果をキャッシュしておく機能があります。Haxlではこれに加えて、ApplicativeDoというGHC拡張*3を用いることで、データの取得を暗黙に並行化できます。

たとえば、データAとデータBが必要なとき、AとBに依存がなければ、ユーザーが何も指定しなくても、Aの取得とBの取得を順番に実行するのではなく、同時に実行してくれるのです。

これは特定の構文(といっても特殊なものではなく、Haskellでは一般的なdo構文という書き方)を使ってHaxlを呼び出すコードを書くと、コンパイラがコンパイル時に処理を並行に実行できるようにしてくれるという方法で実現されています。より詳しく知りたい方は、ぜひ冒頭で紹介した発表を見てください。

この嬉しさを実感するには、実際にコードをみた方が早いでしょう。

AとBの取得を並行化したい場合、JavaScriptではPromise.all、Goではgoroutineとsync.WaitGroupを使うなど、人間が手で依存を解決して、並行に実行されるコードを書く必要があります。

// 並行に取得しない場合
async function numCommonFrends(a, b) {
  const fa = await friendsOf(a);
  const fb = await friendsOf(B);
  return intersect(fa, fb).length;
}
// 並行に取得する場合
async function numCommonFrends(a, b) {
  const [fa, fb] = await Promise.all([friendsOf(a), friendsOf(b)]);
  return intersect(fa, fb).length;
}
// 並行に取得しない場合
func NumCommonFrends(a, b *User) int {
	fa := friendsOf(a)
	fb := friendsOf(a)
	return len(intersect(fa, fb))
}
// 並行に取得する場合
func NumCommonFrends(a, b *User) int {
	var fa, fb []*User
	var wg sync.WaitGroup
	wg.Add(2)
	go func() {
		fa = friendsOf(a)
		wg.Done()
	}()
	go func() {
		fb = friendsOf(b)
		wg.Done()
	}()
	wg.Wait()
	return len(intersect(fa, fb))
}

ApplicativeDoとHaxlを使うと、この地味で面倒な作業をコンパイラがやってくれるわけです。

-- 何もしなくても並行に処理が実行される
numCommonFriends a b = do
    fa <- friendsOf a
    fb <- friendsOf b
    return (length(intersect fa fb))

私は普段ウェブアプリケーションの開発ではJavaScript (TypeScript) やGoを書いていて、Haskellを使うことは全くありませんが、こういうところを見るとHaskellは進んでるな〜と感じます。

こうして見比べてみると、Goのコードの冗長さが目立ちますね……。

*1:Haskellにはあまり詳しくないので、主にGraphQL面のレビューをしました。

*2:調べたらオリジナルのdataloaderのREADMEにも書いてありました。

*3:GHCはGlasgow Haskell CompilerというHaskellコンパイラのデファクトスタンダードで、GHC拡張とはコンパイラのFeature Flagを切り替えてHaskellの言語機能を拡張できる仕組みです。

Stylelintで無効化したいルールにはnullを指定する

タイトルでほぼ全部説明しましたが、Stylelintを使う時、既存のconfigで指定されているルールを完全にオフにするには null を使います。

例えば、stylelint-config-sass-guidelinesstylelint-config-property-sort-order-smacssを組み合わせて使いたい時、プロパティの並び順のルールがそれぞれ異なるので、単に組み合わせるだけだとルールがコンフリクトしてしまうのですが、不要なルールをオフにすると解決できます。

{
  "extends": [
    "stylelint-config-sass-guidelines",
    "stylelint-config-property-sort-order-smacss"
  ],
  "rules": {
    "order/properties-alphabetical-order": null
  }
}

このように設定すると、基本的にはstylelint-config-sass-guidelinesを使いつつ、プロパティの並び順についてはstylelint-config-property-sort-order-smacssのものを使う、という設定が可能です。

オフにするとき何を指定したらいいのか分からず、false や空配列などを指定していましたが、うまく動かなかったのでStylelintのソースコードをみたところ、 nullを指定するという仕組みだというのを発見しました。

github.com

よくみると null だけでなく [null] でもいいみたいですね。

BuildKitがオンの時 docker build 中の標準出力をそのままホストに出力したい場合は --progress=plain オプションを使う

タイトルで全て説明していますが、BuildKitがオンの環境で、docker build 中の標準出力の内容をビルドする側で全部確認したい場合は --progress=plain オプションが使えます。

$ docker build --progress=plain .

docker build するとき、BuildKitが有効化されていると、デフォルトではすでに完了したステップのstdoutが隠れてしまいます。

いつからか自分の手元でも BuildKit が有効になっていたのですが、イメージのビルド時に何が起きていたか調べたくなり困ったので、ログを出す方法を調べてみたところオプションがありました。

docs.docker.com

BUILDKIT_PROGRESS 環境変数を使ってデフォルトでplainにすることもできるようです。

stackoverflow.com

golang-migrateを使っている環境でRailsのdb/structure.sql相当のファイルを作成する方法

仕事で携わっているウェブアプリケーションでは、DBのマイグレーションにgolang-migrateを使っています。

DBをマイグレーションするだけのツールとしてはこれで十分なのですが、現在のスキーマダンプ、Railsでいうところのdb/structure.sqlをダンプする機能がなくて困っていたので、それ相当のファイルを作成する方法を考えました。

スキーマダンプには

  • DBスキーマの信頼できる情報源となり、現在のスキーマを概観できる
  • ダンプにスキーマのバージョンが記録されていて、バージョン管理システムの異なるブランチで同時に作業したとき、両方をマージしようとするとコンフリクトするので不整合に気づきやすい

などの利点があり*1、Railsのようなフレームワークを使っていなくとも、これに相当するファイルがあると便利です。

ファイルを作成する手順としては、DBからスキーマとそのバージョンを取得し、ファイルに書き出すだけです。

仕事ではMySQLを使っているので、bashとMySQLのコマンドラインツールを使った例を示します。ツールは例示したものである必要はなく、スキーマとそのバージョンが取得できればやり方はなんでも構いません。他の言語・DBでも同じ考え方でスキーマダンプを作成できるでしょう。

DBからスキーマのバージョンを取得してファイルに書き出す

まずはスキーマのバージョンを取得します。golang-migrateはDBがMySQLの場合、デフォルトではschema_migrationsテーブルにバージョンを記録するので、それをmysqlコマンドで取得します*2

$ echo "/* Schema version: $(
mysql \
  --host 127.0.0.1 \
  --port 13306 \
  --user root \
  --password=root \
  --skip-column-names \
  --silent \
  --execute "SELECT version FROM database.schema_migrations"
) */" > structure.sql

mysqlコマンドは単に --execute オプションを指定するだけだと、罫線を使った表を出力しますが、今回はカラム名は必要なく、SELECTした値だけが欲しいので、 --silent も指定しています。

なお、パスワードを直接指定しているのはあくまで例示のためで、実際には環境変数などを介して渡すのが良いでしょう(次に紹介するmysqldumpも同様です)。

DBからスキーマを取得してファイルに追記する

次にDBスキーマをダンプします。mysqldumpを使って、先ほどバージョンを出力したファイルにスキーマを追記します。

mysqldump \
  --host 127.0.0.1 \
  --port 13306 \
  --user root \
  --password=root \
  --compact \
  --no-data \
  --single-transaction \
  --skip-dump-date \
  database \
  >> structure.sql

database はダンプしたいデータベースの名前です。

mysqldumpはデフォルトだとテーブルの行や様々なコメントなどを出力しますが、今回はスキーマだけ出力できればいいので、 --compact --no-data オプションを使って CREATE TABLE 文だけが出力されるようにします。

また、--single-transaction を指定してトランザクション内でダンプすることで、仮にダンプ中にスキーマが変更されても出力結果が変わらないようにします。

--skip-dump-date オプションは、スキーマの変更以外でダンプファイルが変更されないように、ダンプした日付の出力をスキップする設定です。

これらのコマンドをシェルスクリプトにしておけば、いつでも誰でも手元のスキーマをファイルにダンプできるようになります。

さらに、ダンプをソースコードリポジトリにコミットしておいて、CIでもスキーマダンプを生成し、コミット済みのものと差分があったら失敗するようにしておけば、マイグレーションの適用漏れなどのも不整合も発生しにくくなるでしょう。

*1:Active Record マイグレーション - Railsガイド

*2:migrateコマンドでも取得できるのですが、ダンプを出力する環境でmigrateとMySQLコマンドラインツールを両方インストールするのが面倒だったので、mysqlコマンドを使います