Incremental Build Rules
An incremental build system guide that saves time by only building changed packages in local development environments.
An incremental build system guide that saves time by only building changed packages in local development environments.
Overview#
| Scenario | Full Build | Incremental Build | Savings |
|---|---|---|---|
| Single feature change | 60min+ | 5-10min | ~85% |
| shared package change | 60min+ | 20-30min | ~50% |
| No changes | 10min+ | Instant | ~99% |
Level-based Parallel Build#
bundle 프로젝트는 .cob/feature-layout.json과 bundle.json.requires·실제 패키지 의존 DAG를 읽어
feature/<d>/{app,console,server,client}를 발견한다. Serverpod는 의존 모듈 → 해당 모듈 → 호스트 순서다. 폴더별 고정 레벨로 모듈을 놓치지 않는다. marker가 없으면 cob의 기존 legacy 트리 판정 → 나머지는 bundle을 따른다.
조건부 legacy — 기존 고정 레벨 (legacy 판정에서만)
Incremental build optimizes performance by building packages at the same priority level in parallel.
레벨 [1]: shared/* (4개) ─────────┬─ 병렬 실행
↓ 레벨 1 완료
레벨 [3]: package/* (8개) ────────┬─ 병렬 실행
↓ 레벨 3 완료
레벨 [4]: feature/common/* (6개) ─┬─ 병렬 실행
↓ 레벨 4 완료
레벨 [5]: feature/application/* ─┬─ 병렬 실행
↓ 레벨 5 완료
레벨 [7]: app/* (1개) ────────────┴─ 빌드
Parallel concurrency control:
# Set concurrent build count (default: 4)
BUILD_CONCURRENCY=6 melos run build:incremental
# Limit to 1 in low-resource environments
BUILD_CONCURRENCY=1 melos run build:incremental:low-resources
Melos Commands#
Basic Commands#
# Incremental build of changed packages + reverse dependencies only
melos run build:incremental
# Check changed packages without building
melos run build:incremental:dry-run
# Check build status
melos run build:incremental:status
Advanced Commands#
| Command | Description | Usage Scenario |
|---|---|---|
build:incremental:force |
Build after cache reset | When cache issues occur |
build:incremental:low-resources |
Memory saving mode | CI environments, low-spec PCs |
build:incremental:clean |
Build after build_runner clean | When generated file conflicts |
build:incremental:verbose | Detailed log output | When debugging |
build:incremental:reset | Delete cache only | When starting fresh |
build:sync-from-ci | Sync CI cache to local | After squash merge |
build:sync-from-ci:dry-run |
CI cache sync preview | Before downloading |
CI Cache Synchronization#
Syncs CI build cache to local, enabling accurate incremental builds even after squash merges.
Usage Scenarios#
PR 브랜치: A → B → C (last-commit.sha = C)
↓ squash merge
main: X → D (D는 새로운 SHA, C는 orphan)
After squash merge, when pulling main locally, the stored commit SHA (C) no longer exists so Git diff may fail. In this case, hash-based fallback automatically activates.
Commands#
# Sync CI cache to local
melos run build:sync-from-ci
# Preview sync (without downloading)
melos run build:sync-from-ci:dry-run
Prerequisites#
- GitHub CLI (
gh) installation required:brew install gh - GitHub authentication required:
gh auth login
How It Works#
- Download
build-metadatafrom GitHub Actions artifacts - Copy hash files to
.build-state/directory - Enables accurate change detection in next incremental build
Change Detection Modes#
Auto Mode (Default)#
# Automatically select optimal mode
melos run build:incremental
Automatically selects Git-based inside Git repo, hash-based outside.
Squash merge handling: Automatically falls back to hash-based detection when stored commits are orphaned.
Git Mode#
# Git diff-based change detection
scripts/local_incremental_build.sh --mode=git
- Compare last build commit with current HEAD
- Include working directory changes
- Detect both staged/unstaged files
Time/Hash Mode#
# File hash-based change detection
scripts/local_incremental_build.sh --mode=time
- Compare source file SHA-256 hashes
- Auto-exclude generated files (
.g.dart,.freezed.dart, etc.) - Works without Git
Environment Variables#
| Environment Variable | Default | Description |
|---|---|---|
BUILD_RUNNER_OPTS |
-d |
build_runner additional options |
BUILD_CONCURRENCY |
4 |
Parallel build concurrency per level |
Usage Examples#
# Build without delete option
BUILD_RUNNER_OPTS="" melos run build:incremental
# Build in low-resources mode
BUILD_RUNNER_OPTS="--low-resources-mode -d" melos run build:incremental
조건부 legacy — Build Priority (legacy 판정에서만)#
Incremental build builds in dependency order:
| Priority | Path Pattern | Example |
|---|---|---|
| 1 | shared/* | config, dependencies, flavor |
| 2 | package/life*, package/auth* | Auth-related |
| 3 | package/* | core, resources, coui |
| 4 | feature/common/* | auth, settings, splash |
| 5 | feature/application/*, feature/console/* |
Feature modules |
| 6 | backend/* | (N/A) |
| 7 | app/* | good_teacher, widgetbook |
Cache Structure#
Storage Location#
.build-state/
├── last-commit.sha # 마지막 빌드 커밋 SHA
├── last-build.timestamp # 마지막 빌드 시간
├── package-feature_console_console_sales_analysis.hash
├── package-package_core.hash
└── ...
Cache File Format#
- File명:
package-{Path_슬래when를_언더스코어로}.hash - 내용: 소스 File들of the SHA-256 해when
Cache Invalidation Conditions#
- Source file modification (
.dartfiles) pubspec.yamlmodification- Using
--forceoption - Deleting
.build-state/directory
Troubleshooting#
Cache Issues#
# Full cache reset
melos run build:incremental:reset
# Force rebuild
melos run build:incremental:force
Change Detection Failure#
# Check with detailed logs
melos run build:incremental:verbose
# Direct script execution
scripts/detect_local_changes.sh --verbose --mode=time
Insufficient Memory#
# Use low-resources mode
melos run build:incremental:low-resources
# Or set via environment variables
BUILD_RUNNER_OPTS="--low-resources-mode" melos run build:incremental
Build Specific Package Only#
# Direct specification instead of incremental build
melos exec --scope=console_sales_analysis -- dart run build_runner build -d
Script Structure#
Local Scripts#
| File | Role |
|---|---|
scripts/detect_local_changes.sh | Detect changed packages |
scripts/local_incremental_build.sh | Local build orchestrator |
scripts/sync_cache_from_ci.sh | CI cache local sync |
CI Scripts#
| File | Role |
|---|---|
.github/scripts/detect_changes.sh | CI change detection |
.github/scripts/smart_incremental_build.sh |
CI incremental build (shared) |
Script Relationships#
┌─────────────────────────────────────────────────┐
│ melos run build:incremental │
└───────────────────┬─────────────────────────────┘
▼
┌─────────────────────────────────────────────────┐
│ scripts/local_incremental_build.sh │
│ - 옵션 파싱 (--dry-run, --force, --low-resources)│
│ - 변경 감지 호출 │
│ - CI 스크립트 재사용 │
└───────────────────┬─────────────────────────────┘
┌──────────┴──────────┐
▼ ▼
┌─────────────────┐ ┌─────────────────────────────┐
│ detect_local_ │ │ .github/scripts/ │
│ changes.sh │ │ smart_incremental_build.sh │
│ (변경 감지) │ │ (실제 빌드 실행) │
└─────────────────┘ └─────────────────────────────┘
Squash Merge Handling#
Problem#
When squash merging a PR, original commits are replaced with a new single commit.
The commit stored in .build-state/last-commit.sha becomes orphaned, causing Git diff to fail.
Solution#
- Auto-fallback: Automatically switches to content hash-based detection on Git diff failure
-
CI sync: Download CI cache with
build:sync-from-cifor improved accuracy
Recommended Workflow#
# After squash merge
git checkout main
git pull
# Option 1: Use auto-fallback (hash-based)
melos run build:incremental
# Option 2: Build after CI cache sync (more accurate)
melos run build:sync-from-ci
melos run build:incremental
Notes#
.build-state/directory is included in.gitignore- CI uses a separate cache strategy (4-layer cache)
- Reverse dependency analysis enabled when
jqis installed - CI build metadata is stored as artifacts for 7 days