Dart 88 🎯 Continuous Integration pipelines for Dart Codebase Quality Gates
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
| Gate | Command | Fails On |
|---|---|---|
| Format | dart format --output=none --set-exit-if-changed . | Any unformatted file |
| Analyze | dart analyze --fatal-infos | Any error, warning, or info |
| Test | dart test | Any test failure |
| Coverage | flutter test --coverage | Generates report |
| Publish | dart pub publish --dry-run | Any archive/pubspec warning |
Melos Script Filters
| Filter | Effect |
|---|---|
--dir-exists=test | Only packages with a test/ directory |
--no-private | Exclude publish_to: none |
--fail-fast | Stop on first package failure |
-c 1 | Sequential execution (analysis server safety) |
GitHub Actions Steps
| Action | Purpose |
|---|---|
actions/checkout@v4 | Clone repository |
dart-lang/setup-dart@v1 | Install Dart SDK |
actions/cache@v4 | Cache ~/.pub-cache |
codecov/codecov-action@v4 | Upload lcov.info |
Coverage Tools
| Tool | Purpose |
|---|---|
flutter test --coverage | Generate coverage/lcov.info |
VeryGoodOpenSource/very_good_coverage | Enforce threshold |
codecov/codecov-action | Upload to Codecov |
Custom check_coverage.dart | Project-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
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Analyzer passes locally, fails in CI | Different SDK version | Pin SDK in setup-dart |
| Format check fails on generated files | Generated code not excluded | Add to .gitignore or format separately |
| Publish dry run fails on private package | --no-private missing | Add filter to Melos script |
| Analysis server crashes in CI | High concurrency | -c 1 for analyze |
| Coverage report not uploaded | Missing token | secrets.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
| Item | Value |
|---|---|
| Format gate | dart format --output=none --set-exit-if-changed . |
| Analyze gate | dart analyze --fatal-infos |
| Flutter analyze | flutter analyze --fatal-infos |
| Test gate | dart test / flutter test |
| Coverage | flutter test --coverage → lcov.info |
| Publish gate | dart pub publish --dry-run |
| CI action | dart-lang/setup-dart@v1 |
| Cache path | ~/.pub-cache |
| Melos bootstrap | dart run melos bootstrap |
| Melos scripts | melos 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-changedand--fatal-infosare 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 analyzeexecutes 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.inforeport is data; the threshold check is the gate. Use a coverage action or a custom script to fail below the floor . - Caching
~/.pub-cacheis the difference between a fast pipeline and a slow one. Key the cache onpubspec.lockso 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.
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!