|
| 1 | +--- |
| 2 | +title: Monorepo Optimization |
| 3 | +description: Optimize merge queue batching for monorepos with scopes. |
| 4 | +--- |
| 5 | + |
| 6 | +In monorepo environments, not every pull request affects the entire codebase. Running all tests for |
| 7 | +every change wastes time and CI resources. Mergify's **scopes** feature allows you to intelligently |
| 8 | +batch pull requests based on which parts of your codebase they modify, dramatically improving merge |
| 9 | +queue efficiency. |
| 10 | + |
| 11 | +## Understanding Scopes |
| 12 | + |
| 13 | +Scopes define discrete areas of your monorepo (like packages, services, or components) and map them |
| 14 | +to file patterns. When a pull request modifies files, Mergify automatically determines which scopes |
| 15 | +are affected and uses this information to optimize batching. |
| 16 | + |
| 17 | +### The Batching Challenge in Monorepos |
| 18 | + |
| 19 | +Without scopes, Mergify batches pull requests together regardless of what they change. This means: |
| 20 | + |
| 21 | +- A Python service change and a JavaScript frontend change might be batched together |
| 22 | +- Both sets of tests run even though they're completely independent |
| 23 | +- If one fails, both PRs are affected by the batch split process |
| 24 | + |
| 25 | +With scopes, Mergify can: |
| 26 | + |
| 27 | +- Batch together PRs that affect the same scopes (e.g., multiple Python changes) |
| 28 | +- Keep independent changes in separate batches |
| 29 | +- Reduce unnecessary CI runs for unrelated parts of your codebase |
| 30 | + |
| 31 | +## Configuring Scopes |
| 32 | + |
| 33 | +Define scopes in your `.mergify.yml` configuration file using file patterns: |
| 34 | + |
| 35 | +```yaml |
| 36 | +scopes: |
| 37 | + source: |
| 38 | + files: |
| 39 | + python-api: |
| 40 | + includes: |
| 41 | + - api/**/*.py |
| 42 | + - libs/shared/**/*.py |
| 43 | + frontend: |
| 44 | + includes: |
| 45 | + - web/**/*.js |
| 46 | + - web/**/*.jsx |
| 47 | + - web/**/*.ts |
| 48 | + - web/**/*.tsx |
| 49 | + docs: |
| 50 | + includes: |
| 51 | + - docs/**/*.md |
| 52 | + - docs/**/*.mdx |
| 53 | + |
| 54 | +queue_rules: |
| 55 | + - name: default |
| 56 | + batch_size: 5 |
| 57 | + queue_conditions: |
| 58 | + - check-success=alls-green |
| 59 | + merge_conditions: |
| 60 | + - check-success=alls-green |
| 61 | +``` |
| 62 | +
|
| 63 | +In this example: |
| 64 | +- Changes to Python files in `api/` or `libs/shared/` get the `python-api` scope |
| 65 | +- Changes to frontend files in `web/` get the `frontend` scope |
| 66 | +- Documentation changes get the `docs` scope |
| 67 | + |
| 68 | +:::tip |
| 69 | +Mergify will intelligently batch PRs with overlapping scopes together. For example, if PR1 affects |
| 70 | +`python-api` and PR2 affects both `python-api` and `frontend`, they'll be batched together since |
| 71 | +they share a common scope. |
| 72 | +::: |
| 73 | + |
| 74 | +## Setting Up CI with Scopes |
| 75 | + |
| 76 | +To leverage scopes in your CI workflow, use the |
| 77 | +[gha-mergify-ci](https://github.com/Mergifyio/gha-mergify-ci) GitHub Action. This action detects |
| 78 | +which scopes are affected by a pull request and allows you to run only the relevant tests. |
| 79 | + |
| 80 | +### GitHub Actions Integration |
| 81 | + |
| 82 | +Here's a complete example showing how to set up scope-aware CI: |
| 83 | + |
| 84 | +```yaml |
| 85 | +name: Continuous Integration |
| 86 | +on: |
| 87 | + pull_request: |
| 88 | +
|
| 89 | +jobs: |
| 90 | + scopes: |
| 91 | + runs-on: ubuntu-24.04 |
| 92 | + outputs: |
| 93 | + python-api: ${{ fromJSON(steps.scopes.outputs.scopes).python-api }} |
| 94 | + frontend: ${{ fromJSON(steps.scopes.outputs.scopes).frontend }} |
| 95 | + docs: ${{ fromJSON(steps.scopes.outputs.scopes).docs }} |
| 96 | + merge-queue: ${{ fromJSON(steps.scopes.outputs.scopes).merge-queue }} |
| 97 | + steps: |
| 98 | + - uses: actions/checkout@v5 |
| 99 | + - name: Get PR scopes |
| 100 | + id: scopes |
| 101 | + uses: Mergifyio/gha-mergify-ci@v9 |
| 102 | + with: |
| 103 | + action: scopes |
| 104 | + token: ${{ secrets.MERGIFY_TOKEN }} |
| 105 | +
|
| 106 | + python-tests: |
| 107 | + if: ${{ needs.scopes.outputs.python-api == 'true' }} |
| 108 | + needs: scopes |
| 109 | + uses: ./.github/workflows/python-tests.yaml |
| 110 | + secrets: inherit |
| 111 | +
|
| 112 | + frontend-tests: |
| 113 | + if: ${{ needs.scopes.outputs.frontend == 'true' }} |
| 114 | + needs: scopes |
| 115 | + uses: ./.github/workflows/frontend-tests.yaml |
| 116 | + secrets: inherit |
| 117 | +
|
| 118 | + docs-tests: |
| 119 | + if: ${{ needs.scopes.outputs.docs == 'true' }} |
| 120 | + needs: scopes |
| 121 | + uses: ./.github/workflows/docs-tests.yaml |
| 122 | + secrets: inherit |
| 123 | +
|
| 124 | + integration-tests: |
| 125 | + if: ${{ needs.scopes.outputs.merge-queue == 'true' }} |
| 126 | + needs: scopes |
| 127 | + uses: ./.github/workflows/integration-tests.yaml |
| 128 | + secrets: inherit |
| 129 | +
|
| 130 | + alls-green: |
| 131 | + if: ${{ !cancelled() }} |
| 132 | + needs: |
| 133 | + - python-tests |
| 134 | + - frontend-tests |
| 135 | + - docs-tests |
| 136 | + - integration-tests |
| 137 | + runs-on: ubuntu-latest |
| 138 | + steps: |
| 139 | + - name: Verify all jobs succeeded |
| 140 | + uses: re-actors/alls-green@release/v1 |
| 141 | + with: |
| 142 | + allowed-skips: ${{ toJSON(needs) }} |
| 143 | + jobs: ${{ toJSON(needs) }} |
| 144 | +``` |
| 145 | + |
| 146 | +### Key Components |
| 147 | + |
| 148 | +1. **Scopes Job**: Detects which scopes are affected and outputs boolean values |
| 149 | + |
| 150 | +2. **Conditional Jobs**: Each test suite runs only if its scope is affected |
| 151 | + |
| 152 | +3. **Integration Tests**: The special `merge-queue` scope is automatically set to `true` when |
| 153 | + running in the merge queue context |
| 154 | + |
| 155 | +4. **Alls-Green**: Aggregates all job results, handling skipped jobs correctly |
| 156 | + |
| 157 | +## The Merge Queue Scope |
| 158 | + |
| 159 | +The `gha-mergify-ci` action automatically provides a special `merge-queue` scope that returns `true` |
| 160 | +only when running in a merge queue context (on temporary merge queue branches). |
| 161 | + |
| 162 | +This is useful for: |
| 163 | + |
| 164 | +- **Integration tests** that only need to run before merging |
| 165 | +- **End-to-end tests** that are expensive and should only run on final batches |
| 166 | +- **Deployment validation** that needs to happen before code reaches the main branch |
| 167 | + |
| 168 | +```yaml |
| 169 | +integration-tests: |
| 170 | + if: ${{ needs.scopes.outputs.merge-queue == 'true' }} |
| 171 | + needs: scopes |
| 172 | + runs-on: ubuntu-22.04 |
| 173 | + steps: |
| 174 | + - uses: actions/checkout@v5 |
| 175 | + - name: Run expensive integration tests |
| 176 | + run: npm run test:integration |
| 177 | +``` |
| 178 | + |
| 179 | +## Important Behaviors |
| 180 | + |
| 181 | +### Scope Detection is PR-Specific |
| 182 | + |
| 183 | +The `gha-mergify-ci` action only analyzes files changed by the specific pull request, **not** files |
| 184 | +from other PRs in the merge queue batch. This ensures: |
| 185 | + |
| 186 | +- Each PR's scopes reflect only its own changes |
| 187 | +- Batching decisions remain consistent even as the queue changes |
| 188 | +- Tests run for the correct scopes regardless of what else is in the batch |
| 189 | + |
| 190 | +### Path Filtering vs Scopes |
| 191 | + |
| 192 | +GitHub Actions offers path filtering (`on.pull_request.paths`), but it has critical limitations in |
| 193 | +merge queue scenarios: |
| 194 | + |
| 195 | +```yaml |
| 196 | +# ❌ Don't use path filtering for merge queues |
| 197 | +on: |
| 198 | + pull_request: |
| 199 | + paths: |
| 200 | + - 'api/**' |
| 201 | +``` |
| 202 | + |
| 203 | +**Problems with path filtering:** |
| 204 | + |
| 205 | +- When a job doesn't run, you can't distinguish between "filtered out" and "CI failed to start" |
| 206 | + |
| 207 | +- Required status checks fail if jobs are skipped due to filtering |
| 208 | + |
| 209 | +- In merge queues, you don't want to skip tests on PR2 just because PR1 in the batch modified |
| 210 | + different files |
| 211 | + |
| 212 | +**✅ Use scopes instead:** |
| 213 | + |
| 214 | +- Jobs always run but can conditionally skip work based on scope detection |
| 215 | +- Status checks always report (success or skipped) |
| 216 | +- Merge queue batching respects scope boundaries |
| 217 | + |
| 218 | +## Example: Multi-Language Monorepo |
| 219 | + |
| 220 | +Here's a real-world example for a monorepo with Python, JavaScript, and Go services: |
| 221 | + |
| 222 | +```yaml |
| 223 | +scopes: |
| 224 | + source: |
| 225 | + files: |
| 226 | + python-api: |
| 227 | + includes: |
| 228 | + - services/api/**/*.py |
| 229 | + - libs/python/**/*.py |
| 230 | + user-service: |
| 231 | + includes: |
| 232 | + - services/users/**/*.go |
| 233 | + frontend: |
| 234 | + includes: |
| 235 | + - apps/web/**/*.{js,jsx,ts,tsx} |
| 236 | + shared-config: |
| 237 | + includes: |
| 238 | + - config/**/* |
| 239 | + - docker/**/* |
| 240 | +
|
| 241 | +queue_rules: |
| 242 | + - name: default |
| 243 | + batch_size: 8 |
| 244 | + batch_max_wait_time: 5 min |
| 245 | + queue_conditions: |
| 246 | + - check-success=alls-green |
| 247 | + merge_conditions: |
| 248 | + - check-success=alls-green |
| 249 | +``` |
| 250 | + |
| 251 | +With this configuration: |
| 252 | +- PRs affecting only `frontend` will batch together |
| 253 | + |
| 254 | +- PRs affecting `python-api` will batch together |
| 255 | + |
| 256 | +- PRs affecting `shared-config` will batch with everything (since config affects all |
| 257 | + services) |
| 258 | + |
| 259 | +- Independent service changes can be tested in parallel batches |
| 260 | + |
| 261 | +## Best Practices |
| 262 | + |
| 263 | +### 1. Define Clear Scope Boundaries |
| 264 | + |
| 265 | +```yaml |
| 266 | +# ✅ Good: Clear, non-overlapping scopes |
| 267 | +scopes: |
| 268 | + source: |
| 269 | + files: |
| 270 | + backend: |
| 271 | + includes: |
| 272 | + - backend/**/* |
| 273 | + frontend: |
| 274 | + includes: |
| 275 | + - frontend/**/* |
| 276 | +``` |
| 277 | + |
| 278 | +### 2. Handle Shared Dependencies |
| 279 | + |
| 280 | +If you have shared libraries, consider whether changes should affect all dependent |
| 281 | +scopes: |
| 282 | + |
| 283 | +```yaml |
| 284 | +# Shared lib changes affect everything |
| 285 | +shared-libs: |
| 286 | + includes: |
| 287 | + - libs/common/**/* |
| 288 | +``` |
| 289 | + |
| 290 | +### 3. Use Integration Tests Wisely |
| 291 | + |
| 292 | +Reserve expensive tests for the merge queue context: |
| 293 | + |
| 294 | +```yaml |
| 295 | +unit-tests: |
| 296 | + if: ${{ needs.scopes.outputs.backend == 'true' }} |
| 297 | + # Runs on every PR update |
| 298 | +
|
| 299 | +integration-tests: |
| 300 | + if: ${{ needs.scopes.outputs.merge-queue == 'true' }} |
| 301 | + # Only runs in merge queue |
| 302 | +``` |
| 303 | + |
| 304 | +## Monitoring Scope-Based Batching |
| 305 | + |
| 306 | +You can monitor how scopes affect your merge queue performance in the |
| 307 | +[Mergify dashboard](https://dashboard.mergify.com): |
| 308 | + |
| 309 | +- View which scopes are active in each batch |
| 310 | +- See how scopes influence batch composition |
| 311 | +- Track CI time savings from scope-aware batching |
| 312 | + |
| 313 | +## Conclusion |
| 314 | + |
| 315 | +Scopes transform merge queue performance in monorepos by: |
| 316 | + |
| 317 | +- Batching related changes together while keeping unrelated changes separate |
| 318 | +- Reducing CI costs by running only relevant tests |
| 319 | +- Maintaining merge velocity by creating more efficient batches |
| 320 | +- Preserving reliability by ensuring appropriate tests run for each change |
| 321 | + |
| 322 | +By combining scopes with [batch merging](/merge-queue/batches) and |
| 323 | +[parallel checks](/merge-queue/parallel-checks), you can achieve optimal merge queue performance |
| 324 | +tailored to your monorepo's structure. |
0 commit comments