Stop Copilot Merge Conflicts: Two-Pipeline Strategy for TypeScript & Flutter

Braided rivers — parallel work merging cleanly Photo by Karsten Winegeart on Unsplash

Ship multiple Copilot-assigned issues in parallel without drowning in formatting-only conflicts.

The Pain Most Teams Hit with Copilot

Assigning GitHub issues to Copilot (or running many agent tasks at once) is powerful—but it surfaced a repeat problem in my repos:

The fix wasn’t “argue about style.” It was to automate formatting and enforce quality gates in CI so conflicts collapse to near-zero and reviews stay focused on behavior.

My Setup (Works Great with Copilot Agents)

The Two-Pipeline Strategy

1) Auto-Format on Commit (per feature branch)

2) Validate, Build, and Test

Key: Good test coverage is the trust layer. With solid tests, you can accept agent-generated changes quickly, knowing functionality didn’t regress.


What This Guide Gives You

Issue Planning to Limit Conflicts

Implementation: TypeScript

Follow these steps in the root of your TypeScript project (or in functions/ if you’re formatting only your Firebase Functions code):

1) Install formatters (one-time)

# run this in the project folder that contains package.json
npm install --save-dev prettier eslint

Why: Prettier enforces a single, consistent style; ESLint catches code issues.

2) Create a Prettier config (one-time) Create a file named .prettierrc.json in the same folder as your package.json and paste:

{
  "printWidth": 100,
  "tabWidth": 2,
  "semi": true,
  "singleQuote": true,
  "trailingComma": "es5"
}

Why: This defines the exact rules your team (and Copilot) will follow to avoid “tiny formatting” diffs.

3) Add scripts to package.json (one-time) Open package.json and add these under the top-level scripts key:

{
  "scripts": {
    "format": "prettier --write .",
    "format:check": "prettier --check .",
    "lint": "eslint . --max-warnings=0"
  }
}

Why: format rewrites files to your standard. format:check fails CI if someone forgot to format. lint enforces code quality.

4) Normalize your repo (before enabling CI)

npm run format

Why: This cleans up existing style drift so future diffs stay focused on behavior, not whitespace.

5) Enforce in CI Use prettier --check . (or npm run format:check) in your validate workflow to fail on unformatted code.

Implementation: Flutter

Run these in the Flutter project root (the folder with pubspec.yaml).

1) Format, analyze, and test locally

dart format .            # formats files in-place
flutter analyze          # static analysis (can be continue-on-error in CI)
flutter test             # run your tests

Why: Establishes a consistent baseline and confirms nothing broke.

2) Optional CI-safe format check

dart format --output=none --set-exit-if-changed .

Why: This command exits with a non‑zero status if files need formatting—perfect to gate PRs. Pair it with a separate auto‑format workflow that commits formatting changes on branches, so PRs are clean.


Video: GitHub Actions Overview

GitHub Actions Pipelines

These mirror my production setups so auto‑format runs on main and dev, and CI validates before merge.

Flutter CI (build, analyze, test, verify formatting)

# .github/workflows/flutter-ci.yml
name: Flutter CI

on:
  push:
    branches: [ "main" , "dev" ]
  pull_request:
    branches: [ "main" , "dev" ]
  workflow_dispatch:
    inputs:
      commit_sha:
        description: 'Commit SHA to test and build (leave empty for latest)'
        required: false
        type: string

jobs:
  build:
    runs-on: ubuntu-latest
 
    steps:
      - uses: actions/checkout@v3
        with:
          ref: $

      - name: Flutter action
        uses: subosito/flutter-action@v2.13.0

      - name: Install dependencies
        run: flutter pub get

      - name: Setup Firebase Config
        run: ./setup.sh

      - name: Verify formatting
        run: dart format --set-exit-if-changed .

      - name: Analyze project source
        run: flutter analyze
        continue-on-error: true

      - name: Run tests
        run: flutter test

Dart Auto-Format (commit formatted code back)

# .github/workflows/dart-format.yml
name: Dart Format

permissions:
  contents: write
  pull-requests: write

on:
  push:
    branches: [ "main" , "dev" ]
  pull_request:
    branches: [ "main" , "dev" ]
  workflow_dispatch:
    inputs:
      commit_sha:
        description: 'Commit SHA to format (leave empty for latest)'
        required: false
        type: string

jobs:
  format:
    runs-on: ubuntu-latest
    
    steps:
    - name: Checkout code
      uses: actions/checkout@v4
      with:
        token: $
        fetch-depth: 0
        ref: $
        
    - name: Set up Flutter
      uses: subosito/flutter-action@v2
      with:
        channel: 'stable'
        
    - name: Get dependencies
      run: flutter pub get
      
    - name: Run dart format
      run: dart format .
      
    - name: Check for changes
      id: verify-changed-files
      run: |
        if [ -n "$(git status --porcelain)" ]; then
          echo "changed=true" >> $GITHUB_OUTPUT
        else
          echo "changed=false" >> $GITHUB_OUTPUT
        fi
        
    - name: Commit and push formatted code
      if: steps.verify-changed-files.outputs.changed == 'true'
      run: |
        git config --local user.email "action@github.com"
        git config --local user.name "GitHub Action"
        git add .
        git commit -m "Auto-format Dart code with dart format"
        git push origin HEAD:$

Auto-Format Firebase Functions (TypeScript/JavaScript)

# .github/workflows/auto-format.yml
name: Auto-Format Firebase Functions

permissions:
  contents: write
  pull-requests: write

on:
  push:
    branches: [ "main", "master", "dev" ]
  pull_request:
    branches: [ "main", "master", "dev" ]
  workflow_dispatch:
    inputs:
      branch:
        description: 'Branch to format (leave empty for current branch)'
        required: false
        type: string
      reason:
        description: 'Reason for manual formatting'
        required: false
        default: 'Manual code formatting'
        type: string

jobs:
  format:
    runs-on: ubuntu-latest
    
    steps:
    - name: Checkout code
      uses: actions/checkout@v4
      with:
        token: $
        fetch-depth: 0
        ref: $
        
    - name: Setup Node.js 18.x
      uses: actions/setup-node@v4
      with:
        node-version: '18.x'
        cache: 'npm'
        cache-dependency-path: functions/package-lock.json
        
    - name: Install dependencies
      run: |
        cd functions
        npm ci
      
    - name: Run Prettier formatter
      run: |
        cd functions
        npm run format
      
    - name: Check for changes
      id: verify-changed-files
      run: |
        if [ -n "$(git status --porcelain)" ]; then
          echo "changed=true" >> $GITHUB_OUTPUT
          echo "Files were changed by formatting"
        else
          echo "changed=false" >> $GITHUB_OUTPUT
          echo "No files were changed by formatting"
        fi
        
    - name: Commit and push formatted code
      if: steps.verify-changed-files.outputs.changed == 'true'
      run: |
        git config --local user.email "action@github.com"
        git config --local user.name "GitHub Action"
        git add .
        if [ "$" = "workflow_dispatch" ]; then
          git commit -m "🎨 Manual auto-format: $

        - Formatted TypeScript/JavaScript files in functions directory
        - Applied consistent code style using Prettier
        - Triggered manually on branch: $
        - No functional changes, formatting only"
        else
          git commit -m "Auto-format Firebase Functions code with Prettier

        - Formatted TypeScript/JavaScript files in functions directory
        - Applied consistent code style using Prettier
        - No functional changes, formatting only"
        fi
        git push origin HEAD:$

Firebase Functions CI (lint, build, test, formatting check)

# .github/workflows/firebase-functions-ci.yml
name: Firebase Functions CI

on:
  push:
    branches: [ main, dev ]
  pull_request:
    branches: [ main, dev ]
  workflow_dispatch:
    inputs:
      reason:
        description: 'Reason for manual run'
        required: false
        default: 'Manual trigger'
        type: string

jobs:
  build-and-validate:
    runs-on: ubuntu-latest
    
    strategy:
      matrix:
        node-version: [18.x]
    
    steps:
    - name: Checkout code
      uses: actions/checkout@v4
      
    - name: Setup Node.js $
      uses: actions/setup-node@v4
      with:
        node-version: $
        cache: 'npm'
        cache-dependency-path: functions/package-lock.json
        
    - name: Install dependencies
      run: |
        cd functions
        npm ci
        
    - name: Run linter
      run: |
        cd functions
        npm run lint
        
    - name: Verify code formatting
      run: |
        cd functions
        npx prettier --check .
        
    - name: Build project
      run: |
        cd functions
        npm run build
      env:
        NODE_ENV: test
        CI: true
        
    - name: Run tests
      run: |
        cd functions
        npm run test:ci
        echo "✅ Unit tests completed successfully"
        echo "🔄 Firebase integration tests are run locally with emulators"
      env:
        NODE_ENV: test
        CI: true
        
    - name: Workflow Summary
      if: always()
      run: |
        echo "## Firebase Functions CI Summary" >> $GITHUB_STEP_SUMMARY
        echo "- **Trigger:** $" >> $GITHUB_STEP_SUMMARY
        if [ "$" = "workflow_dispatch" ]; then
          echo "- **Manual Reason:** $" >> $GITHUB_STEP_SUMMARY
        fi
        echo "- **Branch:** $" >> $GITHUB_STEP_SUMMARY
        echo "- **Node Version:** $" >> $GITHUB_STEP_SUMMARY
        echo "- **Status:** $" >> $GITHUB_STEP_SUMMARY

Resources


Move faster with Copilot by automating formatting and letting tests guard correctness. The calm is worth it.

Journal