こんにちは、@chaspy です。開発部長をしています。本日は小ネタで失礼します。
私たちは普段、monorepo で開発しています。monorepo については過去色んな記事で言及されているのでぜひご覧ください。
最近、monorepo で Cursor Project Rules を管理する仕組みを整えたので、その話をします。
Cursor Project Rules の課題
さて、読者の皆さんの周囲でも利用している人は多いかと思いますが、AI エージェント系の開発ツールを利用しておりまして、私たちの組織では Cursor が多く使われています。
とっても便利な Cursor の Project Rules、チームで利活用できるよう育てていきたいですよね。しかし、これを monorepo で使う場合、困ったことがありました。それはCursor で monorepo を root で開くかサービスディレクトリで開くかによって Project Rules の適用が変わってしまう問題です。
この問題は、@kumackey さんから指摘していただきました:

どういうことかというと、ルートディレクトリで開く人にとっては、Cursor が想定しているように .cursor 以下のプロジェクトルールが読み込まれます。一方、サービスディレクトリで開く人にとっては、ルートディレクトリのプロジェクトルールは読み込まれません。これは Cursor が読み込んだディレクトリに存在しないので、当然そうなります。
そのため、サービスディレクトリで開く人は、自身のディレクトリ内の .cursor 以下にルールを置いて適用することになります。しかし、この方法だと、ルートディレクトリで開く人がサービスディレクトリのルールを適用できないという問題が発生します。このように、開発スタイルの違いによってルールが共有できないという課題が生まれていました。
monorepo を共有して使う以上、どちらの開発スタイルを強制したくもないわけなので、こういった課題を解決する方法を考えてみました。
解決策:自動同期の仕組み
最初に、@n-makoto さんから symlink での解決策を提案いただきました。

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

というわけで、この問題を踏まえてシンプルに同期と 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 で monorepo を root 直下で開く派の人も、サービスディレクトリ直下で開く派の人も、両方ハッピーですね。
スタディサプリプロダクトチームは学びを、もっと、新しくするために、今後も AI を使った開発効率化に取り組んでいきます。
Cursor の活用について話したいことがある人はぜひお話ししましょう。@chaspy まで連絡お願いします。それでは!