| |

Dart 88 🎯 Continuous Integration pipelines for Dart Codebase Quality Gates

⏱️ Estimated read: 14 min | |

A Dart CI pipeline is not a single dart test command. It is a sequence of independent gates—formatting, static analysis, testing, coverage, and publishability—each of which fails the build for a different class of defect. The Dart team’s own repositories enforce these gates in a fixed order: format check, then analyze with --fatal-infos, then test . The order matters because a formatting failure is cheap to fix and a test failure is expensive to debug; failing fast on the cheap checks keeps the feedback loop short.

The gates themselves are mechanically simple. dart format --output=none --set-exit-if-changed . exits non-zero if any file would be reformatted, which catches style drift before it accumulates . dart analyze --fatal-infos treats informational lints as errors, which prevents the “I’ll fix it later” accumulation of warnings that eventually drowns the analyzer output . dart test runs the test suite. flutter test --coverage generates an lcov.info file that a coverage service can consume . dart pub publish --dry-run validates that the package archive is publishable before a release tag exists .

The orchestration layer depends on the repository shape. A single package uses a GitHub Actions workflow with dart-lang/setup-dart and sequential steps. A Melos workspace uses a root melos configuration with scripts that run melos exec across packages, and the workflow invokes melos run analyze, melos run format, and melos run test instead of per-package commands . The Melos approach centralizes the script definitions so that local development and CI execute the same commands, which is the difference between a pipeline that catches problems and one that catches problems the developer never sees locally.

This chapter covers the individual gates and their flags, the GitHub Actions workflow structure for a single package, the Melos workspace orchestration that scales the same gates across multiple packages, the coverage reporting path, and the publishability preflight that catches release failures before they reach the release branch. It assumes familiarity with GitHub Actions and the Dart toolchain.

Key point: A quality gate is a command that exits non-zero on a defect. dart format --set-exit-if-changed exits non-zero on formatting drift; dart analyze --fatal-infos exits non-zero on lint violations; dart test exits non-zero on test failure. The pipeline runs these commands in order, and the first failure stops the build. For a workspace, melos exec runs the same commands across every package, and melos run ensures the local and CI invocations are identical.


Why CI quality gates exist as separate steps

A passing test suite does not mean the code is mergeable. A test suite can pass while the code has unused imports, missing return types, and formatting that differs from the project standard. The analyzer and formatter catch these independently of the tests. Running them as separate steps means a formatting failure reports as a formatting failure, not as a mysterious test timeout caused by a developer trying to debug the wrong thing .

The analyzer’s exit code is the gate, not the output. dart analyze prints warnings and errors to stdout, but its exit code depends on the flags. Without --fatal-infos, informational lints do not fail the build. With --fatal-infos, they do. The flutter analyze --fatal-infos variant does the same for Flutter packages . The distinction matters because a project that ignores infos accumulates them until the analyzer output is too noisy to read, and the real errors disappear in the noise.

Coverage is a gate only if the threshold is enforced. flutter test --coverage generates the report, but a report without a threshold check is just a number. The threshold can be enforced by a custom script (dart run tool/check_coverage.dart) or by a coverage action that fails below a specified percentage . The choice depends on whether the project wants a hard floor or a trend line. A hard floor prevents coverage from decreasing; a trend line requires a service that tracks history.

Publishability is a release-time gate that should run on every pull request. dart pub publish --dry-run validates the package archive, the pubspec.yaml, the README.md, the CHANGELOG.md, and the license. A package that fails the dry run cannot be published, and the failure is discovered at release time—after the version tag exists and the release branch is merged. Running the dry run on the pull request that introduces the change catches the failure before it reaches the release path .

The trade-off. Each gate adds a step and a few seconds to the pipeline. The full sequence—format, analyze, test, coverage, publish dry run—takes minutes, not seconds, on a cold cache. The alternative is a pipeline that runs only the tests, which catches logic errors but not style drift, lint accumulation, or release blockers. The gates are cheap relative to the cost of discovering the problem in production or at release time.


a. The individual gates and their commands

The formatting gate uses dart format with two flags. --output=none suppresses the diff output, and --set-exit-if-changed makes the command exit non-zero if any file would be reformatted. The combination is a check that does not modify files .

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

The analysis gate uses dart analyze with --fatal-infos. The --fatal-warnings flag is also available for projects that want warnings to fail the build, but the Dart team’s own workflows use --fatal-infos as the baseline . Flutter packages use flutter analyze --fatal-infos instead .

- name: Analyze code
  run: dart analyze --fatal-infos

The test gate is dart test for pure Dart and flutter test for Flutter packages. The --coverage flag generates coverage/lcov.info . A test ordering seed can be added for reproducibility checking: dart test --test-randomize-ordering-seed=random .

- name: Run tests
  run: dart test

- name: Run tests with coverage
  run: flutter test --coverage

The publishability gate is dart pub publish --dry-run or flutter pub publish --dry-run. It validates the package without uploading. The output lists warnings and errors; a clean dry run with zero warnings is the target .

- name: Validate package publishability
  run: dart pub publish --dry-run

b. The GitHub Actions workflow for a single package

The workflow uses actions/checkout and dart-lang/setup-dart. The setup-dart action installs the SDK and adds it to the PATH. The actions/cache step caches ~/.pub-cache keyed on pubspec.lock to avoid re-downloading dependencies on every run .

name: CI

on:
  push:
    branches: [main]
  pull_request:

jobs:
  quality:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: dart-lang/setup-dart@v1
        with:
          sdk: stable

      - name: Cache pub packages
        uses: actions/cache@v4
        with:
          path: ~/.pub-cache
          key: ${{ runner.os }}-pub-${{ hashFiles('pubspec.lock') }}
          restore-keys: ${{ runner.os }}-pub-

      - name: Install dependencies
        run: dart pub get

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

      - name: Analyze code
        run: dart analyze --fatal-infos

      - name: Run tests
        run: dart test

      - name: Validate publishability
        run: dart pub publish --dry-run

The if: always() && steps.install.outcome == 'success' guard is a detail from the Dart team’s workflows: the formatting and analysis steps run only if dependency installation succeeded, but they run even if a previous step failed, so the developer sees all the failures in one run rather than one at a time .


c. Melos workspace orchestration

A Melos workspace runs the same gates across multiple packages. The root pubspec.yaml defines the scripts, and the workflow invokes melos run instead of per-package commands .

# Root pubspec.yaml melos section
melos:
  scripts:
    analyze:
      run: melos exec -- dart analyze . --fatal-infos
      description: Analyze all packages
    format:
      run: melos exec -- dart format . --set-exit-if-changed
      description: Check formatting in all packages
    test:
      run: melos exec --dir-exists=test -- dart test
      description: Run tests in packages with a test directory
    publish:dry-run:
      run: melos exec --no-private -- dart pub publish --dry-run
      description: Validate publishability of non-private packages

The workflow bootstraps the workspace, then runs the scripts:

- name: Bootstrap workspace
  run: dart run melos bootstrap

- name: Analyze packages
  run: dart run melos run analyze

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

- name: Run tests
  run: dart run melos run test

- name: Validate publish surfaces
  run: dart run melos run publish:dry-run

The --no-private filter on the publish dry run excludes packages with publish_to: none, which cannot be published and would fail the dry run for the wrong reason . The --dir-exists=test filter on the test script skips packages that have no tests, which is common in a workspace with shared utility packages that are tested through their consumers .

The concurrency and failFast options in the exec configuration control how many packages run in parallel and whether a failure in one package stops the others. The Dart team’s own Melos configuration sets concurrency to 1 for analysis because the analysis server can crash under parallel load on low-resource CI runners .


Complete Example Session

# ============================================
# PART 1: SINGLE-PACKAGE WORKFLOW
# ============================================
name: CI

on:
  push:
    branches: [main]
  pull_request:

jobs:
  quality:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: dart-lang/setup-dart@v1
        with:
          sdk: stable

      - uses: actions/cache@v4
        with:
          path: ~/.pub-cache
          key: ${{ runner.os }}-pub-${{ hashFiles('pubspec.lock') }}

      - run: dart pub get

      - name: Format
        run: dart format --output=none --set-exit-if-changed .

      - name: Analyze
        run: dart analyze --fatal-infos

      - name: Test
        run: dart test

      - name: Publish dry run
        run: dart pub publish --dry-run

# ============================================
# PART 2: MELOS WORKSPACE SCRIPTS
# ============================================
# Root pubspec.yaml
melos:
  scripts:
    analyze:
      run: melos exec -c 1 -- dart analyze . --fatal-infos
      description: Analyze all packages sequentially
    format:
      run: melos exec -- dart format . --set-exit-if-changed
      description: Check formatting in all packages
    test:
      run: melos exec --dir-exists=test -- dart test
      description: Test packages that have tests
    publish:dry-run:
      run: melos exec --no-private --fail-fast -- dart pub publish --dry-run
      description: Validate publishability of public packages

# ============================================
# PART 3: MELOS WORKFLOW
# ============================================
# jobs:
#   quality:
#     runs-on: ubuntu-latest
#     steps:
#       - uses: actions/checkout@v4
#       - uses: dart-lang/setup-dart@v1
#       - run: dart run melos bootstrap
#       - run: dart run melos run analyze
#       - run: dart run melos run format -- --set-exit-if-changed
#       - run: dart run melos run test
#       - run: dart run melos run publish:dry-run

# ============================================
# PART 4: COVERAGE REPORTING
# ============================================
# - run: flutter test --coverage
# - uses: codecov/codecov-action@v4
#   with:
#     file: coverage/lcov.info
#     token: ${{ secrets.CODECOV_TOKEN }}
#     fail_ci_if_error: false

# ============================================
# PART 5: COVERAGE THRESHOLD
# ============================================
# - run: flutter test --coverage
# - run: dart run tool/check_coverage.dart
# tool/check_coverage.dart reads lcov.info
# and exits non-zero if below threshold.

# ============================================
# PART 6: PUBLISH DRY RUN ON PR
# ============================================
# - name: Validate publishability
#   run: dart pub publish --dry-run
#   if: github.event_name == 'pull_request'

# ============================================
# PART 7: CACHING
# ============================================
# actions/cache keyed on pubspec.lock hash.
# path: ~/.pub-cache
# restore-keys for partial hits.

# ============================================
# PART 8: FAILURE BEHAVIOR
# ============================================
# Each step exits non-zero on failure.
# GitHub Actions stops the job at the first failure
# unless if: always() is used.
# Format and analyze use if: always() after install
# to surface all defects in one run.

# ============================================
# PART 9: SDK VERSION MATRIX
# ============================================
# strategy:
#   matrix:
#     sdk: [stable, beta, dev]
# Tests against multiple SDK channels.
# Dart team's own workflows use sdk: [dev].

# ============================================
# PART 10: THE COMPLETE FLOW
# ============================================
# 1. Checkout.
# 2. Setup SDK.
# 3. Cache pub.
# 4. Install dependencies.
# 5. Format check (--set-exit-if-changed).
# 6. Analyze (--fatal-infos).
# 7. Test (with coverage).
# 8. Publish dry run.
# 9. First non-zero exit fails the build.

Quick Reference

Quality Gate Commands

GateCommandFails On
Formatdart format --output=none --set-exit-if-changed .Any unformatted file
Analyzedart analyze --fatal-infosAny error, warning, or info
Testdart testAny test failure
Coverageflutter test --coverageGenerates report
Publishdart pub publish --dry-runAny archive/pubspec warning

Melos Script Filters

FilterEffect
--dir-exists=testOnly packages with a test/ directory
--no-privateExclude publish_to: none
--fail-fastStop on first package failure
-c 1Sequential execution (analysis server safety)

GitHub Actions Steps

ActionPurpose
actions/checkout@v4Clone repository
dart-lang/setup-dart@v1Install Dart SDK
actions/cache@v4Cache ~/.pub-cache
codecov/codecov-action@v4Upload lcov.info

Coverage Tools

ToolPurpose
flutter test --coverageGenerate coverage/lcov.info
VeryGoodOpenSource/very_good_coverageEnforce threshold
codecov/codecov-actionUpload to Codecov
Custom check_coverage.dartProject-specific threshold logic

Best Practices

✅ Do This:

# Run format check before analyze.                          // ✅
# Fail fast on cheap fixes.

# Use --fatal-infos on analyze.                             // ✅
dart analyze --fatal-infos

# Cache ~/.pub-cache keyed on pubspec.lock.                 // ✅
key: ${{ runner.os }}-pub-${{ hashFiles('pubspec.lock') }}

# Run publish dry run on pull requests.                     // ✅
dart pub publish --dry-run

# Use melos run for workspace scripts.                      // ✅
dart run melos run analyze

❌ Don’t Do This:

# Run dart analyze without --fatal-infos.                   // ❌
# Infos accumulate; errors get buried.

# Skip the publish dry run until release.                   // ❌
# Release failures discovered after the tag exists.

# Use per-package commands in a Melos workspace.            // ❌
# melos run ensures local and CI parity.

# Run analysis with high concurrency.                       // ❌
# Analysis server can crash; use -c 1.

Common Pitfalls

PitfallWhy It HappensFix
Analyzer passes locally, fails in CIDifferent SDK versionPin SDK in setup-dart
Format check fails on generated filesGenerated code not excludedAdd to .gitignore or format separately
Publish dry run fails on private package--no-private missingAdd filter to Melos script
Analysis server crashes in CIHigh concurrency-c 1 for analyze
Coverage report not uploadedMissing tokensecrets.CODECOV_TOKEN

Real-World Examples

1. Format Gate

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

2. Analyze Gate

- run: dart analyze --fatal-infos

3. Test Gate

- run: dart test

4. Coverage Gate

- run: flutter test --coverage

5. Publish Dry Run

- run: dart pub publish --dry-run

6. Cache Pub

- uses: actions/cache@v4
  with:
    path: ~/.pub-cache
    key: ${{ runner.os }}-pub-${{ hashFiles('pubspec.lock') }}

7. Melos Bootstrap

- run: dart run melos bootstrap

8. Melos Script

- run: dart run melos run analyze

9. Coverage Upload

- uses: codecov/codecov-action@v4
  with:
    file: coverage/lcov.info
    token: ${{ secrets.CODECOV_TOKEN }}

10. SDK Matrix

strategy:
  matrix:
    sdk: [stable, beta]

Visual: Pipeline Sequence

┌──────────────────────────────────────────────┐
│  CI PIPELINE                                 │
│                                              │
│  Checkout                                    │
│       │                                      │
│       ▼                                      │
│  Setup Dart SDK                              │
│       │                                      │
│       ▼                                      │
│  Cache ~/.pub-cache                          │
│       │                                      │
│       ▼                                      │
│  dart pub get                                │
│       │                                      │
│       ▼                                      │
│  Format check (--set-exit-if-changed)        │
│       │                                      │
│       ├── FAIL → stop, report                │
│       │                                      │
│       ▼                                      │
│  Analyze (--fatal-infos)                     │
│       │                                      │
│       ├── FAIL → stop, report                │
│       │                                      │
│       ▼                                      │
│  Test                                        │
│       │                                      │
│       ├── FAIL → stop, report                │
│       │                                      │
│       ▼                                      │
│  Publish dry run                             │
│       │                                      │
│       ├── FAIL → stop, report                │
│       │                                      │
│       ▼                                      │
│  PASS                                        │
│                                              │
└──────────────────────────────────────────────┘

Melos Workspace Flow

┌──────────────────────────────────────────────┐
│  MELOS WORKSPACE CI                          │
│                                              │
│  melos bootstrap                             │
│    → pub get across workspace                │
│    → link local packages                     │
│       │                                      │
│       ▼                                      │
│  melos run analyze                           │
│    → melos exec dart analyze                 │
│    → runs in every package                   │
│       │                                      │
│       ▼                                      │
│  melos run format                            │
│    → melos exec dart format --set-exit...    │
│       │                                      │
│       ▼                                      │
│  melos run test                              │
│    → melos exec --dir-exists=test dart test  │
│       │                                      │
│       ▼                                      │
│  melos run publish:dry-run                   │
│    → melos exec --no-private --fail-fast     │
│      dart pub publish --dry-run              │
│                                              │
└──────────────────────────────────────────────┘

Gate Severity

┌──────────────────────────────────────────────┐
│  WHAT EACH GATE CATCHES                      │
│                                              │
│  Format:                                     │
│    Style drift. Cheap to fix.                │
│                                              │
│  Analyze --fatal-infos:                      │
│    Unused imports, missing types,            │
│    deprecated APIs, lint accumulation.       │
│                                              │
│  Test:                                       │
│    Logic errors, regressions.                │
│                                              │
│  Publish dry run:                            │
│    Missing README, CHANGELOG, license,       │
│    invalid pubspec, uncommitted changes.     │
│                                              │
└──────────────────────────────────────────────┘

Coverage Path

┌──────────────────────────────────────────────┐
│  COVERAGE REPORTING                          │
│                                              │
│  flutter test --coverage                     │
│       │                                      │
│       ▼                                      │
│  coverage/lcov.info                          │
│       │                                      │
│       ├── check_coverage.dart                │
│       │     → exit non-zero if below floor   │
│       │                                      │
│       └── codecov-action                     │
│             → upload, post PR comment        │
│                                              │
│  Threshold is a gate only if enforced.       │
│                                              │
└──────────────────────────────────────────────┘

Summary

ItemValue
Format gatedart format --output=none --set-exit-if-changed .
Analyze gatedart analyze --fatal-infos
Flutter analyzeflutter analyze --fatal-infos
Test gatedart test / flutter test
Coverageflutter test --coverage → lcov.info
Publish gatedart pub publish --dry-run
CI actiondart-lang/setup-dart@v1
Cache path~/.pub-cache
Melos bootstrapdart run melos bootstrap
Melos scriptsmelos run analyze, melos run test

Key takeaways:

  • Each gate catches a different defect class. Format catches style drift, analyze catches lint accumulation and type issues, test catches logic errors, and the publish dry run catches release blockers. Running them as separate steps means the failure message matches the defect .
  • --set-exit-if-changed and --fatal-infos are the flags that turn checks into gates. Without them, the commands print output but do not fail the build. The exit code is the gate .
  • The order matters. Format and analyze are cheap; tests are expensive. Running the cheap checks first fails fast on the issues that are easy to fix and keeps the feedback loop short .
  • The publish dry run belongs on pull requests, not just releases. A package that cannot be published should fail the pull request that introduces the problem, not the release branch that has already been tagged .
  • Melos centralizes the script definitions. In a workspace, melos run analyze executes the same command locally and in CI. The alternative—per-package commands in the workflow—drifts from what developers run locally .
  • Coverage is a gate only if a threshold is enforced. The lcov.info report is data; the threshold check is the gate. Use a coverage action or a custom script to fail below the floor .
  • Caching ~/.pub-cache is the difference between a fast pipeline and a slow one. Key the cache on pubspec.lock so it invalidates only when dependencies change .

Remember: A Dart CI pipeline is a sequence of independent gates, each with its own exit code and its own defect class. dart format --set-exit-if-changed catches style drift. dart analyze --fatal-infos catches lint accumulation. dart test catches logic errors. dart pub publish --dry-run catches release blockers. The order is cheap-to-expensive, and the first failure stops the build. For a single package, the workflow is a sequence of shell commands. For a Melos workspace, the workflow invokes melos run scripts that execute the same commands across every package. The publish dry run is the gate that is most often skipped and most expensive to skip, because a package that fails it cannot be released, and discovering that after the tag exists is the wrong time.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!