スタディサプリ Product Team Blog

株式会社リクルートが開発するスタディサプリのプロダクトチームのブログです

monorepo で Cursor Project Rule を扱う Tips

こんにちは、@chaspy です。開発部長をしています。本日は小ネタで失礼します。

私たちは普段、monorepo で開発しています。monorepo については過去色んな記事で言及されているのでぜひご覧ください。

blog.studysapuri.jp

blog.studysapuri.jp

最近、monorepo で Cursor Project Rules を管理する仕組みを整えたので、その話をします。

Cursor Project Rules の課題

さて、読者の皆さんの周囲でも利用している人は多いかと思いますが、AI エージェント系の開発ツールを利用しておりまして、私たちの組織では Cursor が多く使われています。

とっても便利な Cursor の Project Rules、チームで利活用できるよう育てていきたいですよね。しかし、これを monorepo で使う場合、困ったことがありました。それはCursor で monorepo を root で開くかサービスディレクトリで開くかによって Project Rules の適用が変わってしまう問題です。

この問題は、@kumackey さんから指摘していただきました:

Cursor Project Rules の課題

どういうことかというと、ルートディレクトリで開く人にとっては、Cursor が想定しているように .cursor 以下のプロジェクトルールが読み込まれます。一方、サービスディレクトリで開く人にとっては、ルートディレクトリのプロジェクトルールは読み込まれません。これは Cursor が読み込んだディレクトリに存在しないので、当然そうなります。

そのため、サービスディレクトリで開く人は、自身のディレクトリ内の .cursor 以下にルールを置いて適用することになります。しかし、この方法だと、ルートディレクトリで開く人がサービスディレクトリのルールを適用できないという問題が発生します。このように、開発スタイルの違いによってルールが共有できないという課題が生まれていました。

monorepo を共有して使う以上、どちらの開発スタイルを強制したくもないわけなので、こういった課題を解決する方法を考えてみました。

解決策:自動同期の仕組み

最初に、@n-makoto さんから symlink での解決策を提案いただきました。

symlink による解決策の提案

試してみたのですが、適用 globs がうまくワークしないことがわかりました。

symlink の課題

というわけで、この問題を踏まえてシンプルに同期と path の編集をしちゃえば良いのではないかと考えました。サービスディレクトリを Single Source of Truth とし、ルートディレクトリにそれを定期コピーする仕組みを実装しました。

シンプルな bash スクリプトGitHub Actions で実現しています。ポイントとしては、ファイル名や globs の path の書き換えを入れているところぐらいですかね。

#!/bin/bash

set -euo pipefail

# ルートディレクトリの作成
mkdir -p .cursor/rules

# GitHub Actionsのワークフローファイルのパス
WORKFLOW_URL="https://github.com/xxxxxxx/xxxxxx/blob/main/.github/workflows/sync-cursor-rules.yml"

# サービスディレクトリを検索
for service_dir in */; do
  # .cursor/rulesディレクトリが存在する場合のみ処理
  if [ -d "${service_dir}.cursor/rules" ]; then
    service_name="${service_dir%/}"
    echo "Processing rules from ${service_name}"
    
    # ルールファイルをコピー(プレフィックスを付けて)
    for rule_file in "${service_dir}.cursor/rules/"*.mdc; do
      if [ -f "$rule_file" ]; then
        base_name="$(basename "$rule_file")"
        target_file=".cursor/rules/${service_name}--${base_name}"
        
        # ファイルをコピー
        cp "$rule_file" "$target_file"
        
        # Front Matterを保持しつつ、その後に注意書きを追加
        temp_file="$(mktemp)"
        awk -v service="$service_name" -v base="$base_name" -v url="$WORKFLOW_URL" '
          BEGIN { in_front_matter=0; has_globs=0 }
          /^---$/ {
            if (!in_front_matter) {
              in_front_matter=1
              print "---"
            } else {
              in_front_matter=0
              if (!has_globs) {
                print "globs: " service "/**/*"
              }
              print "---"
              print ""
              print "# 警告: このファイルを直接修正しないでください。"
              print "# このファイルは " url " で自動生成されています。"
              print "# " service "/.cursor/rules/" base " を修正してください。"
              print ""
            }
            next
          }
          in_front_matter {
            if ($0 ~ /^description:/) {
              # descriptionの:を-に置換
              desc = substr($0, 13)
              gsub(/:/, "-", desc)
              print "description: " desc
            } else if ($0 ~ /^globs:/) {
              has_globs=1
              # globsパターンを修正(サービス名が最初に来るように)
              pattern = substr($0, 7)
              # パターンをトリムし、サービス名部分を除去
              gsub(/^[[:space:]]+|[[:space:]]+$/, "", pattern)
              gsub("^" service "/+", "", pattern)
              if (pattern == "") {
                print "globs: " service "/**/*"
              } else {
                print "globs: " service "/" pattern
              }
            } else {
              print
            }
            next
          }
          { print }
        ' "$target_file" > "$temp_file"
        mv "$temp_file" "$target_file"
      fi
    done
  fi
done 

GitHub Actions のワークフローはこんな感じです:

name: Sync Cursor Rules

on:
  push:
    paths:
      - "**/.cursor/rules/**/*.mdc"
    branches:
      - develop
  pull_request:
    paths:
      - "**/.cursor/rules/**/*.mdc"
    branches:
      - develop
  workflow_dispatch:

jobs:
  sync-rules:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v4
      - name: Sync rules from services
        run: |
          .github/scripts/sync-cursor-rules.sh
      - uses: actions/create-github-app-token@v1
        id: monorepo-ci-token
        with:
          app-id: ...
          private-key: ...
      - uses: int128/update-generated-files-action@v2
        with:
          token: ${{ steps.monorepo-ci-token.outputs.token }}

@int128 さんの update-generated-files-action が最高に便利です。

このワークフローは以下のタイミングで実行されます:

  • develop ブランチへのプッシュ時(.cursor/rules/**/*.mdc の変更がある場合)
  • develop ブランチへの PR 作成/更新時(.cursor/rules/**/*.mdc の変更がある場合)
  • 手動実行時

実際の動作イメージはこんな感じです:

Cursor Rules 同期の流れ

実際のPRの変更内容

おわりに

Cursor で monorepo を root 直下で開く派の人も、サービスディレクトリ直下で開く派の人も、両方ハッピーですね。

スタディプリプロダクトチームは学びを、もっと、新しくするために、今後も AI を使った開発効率化に取り組んでいきます。

Cursor の活用について話したいことがある人はぜひお話ししましょう。@chaspy まで連絡お願いします。それでは!