CI that keeps up

How do I enforce Prettier formatting in CI?

To enforce Prettier formatting in CI, install one exact Prettier version in the project. Run prettier --check in CI, so that unformatted files fail the job. Make sure that every run ignores the same files. Format with prettier --write before you push, with the same version and config. Teams usually meet these requirements with a CI job and a pre-commit hook. Dagger’s official Prettier module meets the same requirements in one place, and dagger check runs it the same way on a laptop and in Cloud Checks.

What an enforced Prettier setup needs

Prettier’s own docs give four requirements for formatting that CI enforces and every developer can match:

  1. Use one exact Prettier version and the project’s config. Prettier’s install page installs an exact version in the project: “This makes sure that everyone in the project gets the exact same version of Prettier.” It warns that “even a patch release of Prettier can result in slightly different formatting”. Prettier finds the config for each file “starting from the location of the file being formatted”, and then looks in the directories above it.
  2. Fail CI on unformatted files. prettier --check lists the files that are not formatted. It exits with code 1 when a file is not formatted, which the docs call “helpful inside the CI pipelines”.
  3. Ignore the same files everywhere. “By default, Prettier looks for ./.gitignore and ./.prettierignore” (--ignore-path). It reads these files only in the directory where it runs.
  4. Write fixes with the same version and config. prettier --write “rewrites all processed files in place”. The install page suggests a pre-commit hook too: “This makes sure all your commits are formatted, without having to wait for your CI build to finish.”

Meet them with prettier –check, a CI job, and a hook

By hand, pin Prettier with an exact version in devDependencies, as "prettier": "3.9.10" with no ^. Then run the project’s Prettier with npx. This guide’s project has a root .prettierrc and a second config in packages/ui. Check the formatting from the project root:

npx prettier --check .
Checking formatting...
[warn] package.json
[warn] src/config.js
[warn] Code style issues found in 2 files. Run Prettier with --write to fix.

The command exited with code 1. Write the fixes:

npx prettier --write .
.prettierrc 7ms (unchanged)
package-lock.json 2ms (unchanged)
package.json 0ms
packages/ui/.prettierrc 2ms (unchanged)
packages/ui/src/button.js 3ms (unchanged)
src/config.js 1ms

The ignore files depend on the directory where Prettier runs. The root .gitignore of this project ignores packages/ui/dist, so the root run did not check it. Run the check from packages/ui:

cd packages/ui
npx prettier --check .
Checking formatting...
[warn] dist/app.js
[warn] Code style issues found in the above file. Run Prettier with --write to fix.

This run read no .gitignore, so it checked the build output in dist. To skip a directory in every run, add it to a .prettierignore in each directory where Prettier runs.

In CI, a job installs Node.js, runs npm ci to install the exact versions in the lockfile, and runs npx prettier --check .. The exit code of Prettier fails the job. On GitHub Actions, actions/setup-node installs Node.js. GitLab CI and other systems run the same commands in a Node.js image (GitLab CI/CD YAML reference). For the pre-commit hook, Prettier’s install page sets up husky and lint-staged, which format the staged files.

Where the hand-built setup strains

The hand-built setup meets each requirement in several places, and each place must stay in step:

  • Exact version. A range such as ^3.9.0 lets two machines install different patch releases. The pin must stay exact in every package.json that installs Prettier.
  • Ignored files. A run from the root and a run from a package can check different files, as the run above shows. CI checks only what its own command and directory select.
  • Fixes. A pre-commit hook is optional. git commit --no-verify skips it, so CI is the first place that some unformatted files show.
  • CI syntax. Each CI system has its own job syntax. A move to another CI system means a rewrite of the Prettier job.

Meet them with the Dagger Prettier module

The Dagger Prettier module meets the four requirements with one definition that runs on a laptop and in CI. A Dagger module is a package of functions that build, test, or check a project. If you have not used Dagger before, do the Quickstart first. It sets up dagger.toml, the workspace configuration that holds your installed modules and their settings. Install the official Prettier module from the project root:

dagger module install dagger.io/js/prettier
dagger.io/js/prettier ➡️ https://github.com/dagger/prettier#v0.2.0
Created workspace config in /work
Installed module "prettier" in /work/dagger.toml

The command creates dagger.toml if the file does not exist, and adds the module to it. The command also writes dagger.lock, which pins the exact module version. Commit both files. The next four sections show how the module meets each requirement.

Use one exact Prettier version and the project’s config

The check runs the Prettier that your project installs. The module does not download a different Prettier version, so the check uses the same version as your editor and npx prettier. If the project has a package.json without Prettier in it, the check fails with this message:

prettier is not installed: add it to the devDependencies of package.json (or of a package.json between it and the project)

The module finds projects by the name of the config file. These directories are projects:

  • A directory that holds .prettierrc, or .prettierrc with one of these extensions: .json, .json5, .yaml, .yml, .toml, .js, .mjs, .cjs, .ts, .mts, .cts.
  • A directory that holds prettier.config with one of these extensions: .js, .mjs, .cjs, .ts, .mts, .cts.

A config in the prettier field of package.json does not make a directory a project (Prettier module reference). If your config is in that field, move it to a .prettierrc file. A project covers its directory and every directory below it, except nested projects. Each nested project runs from its own directory, so its own config and .prettierignore apply to it (module source).

Install scripts see only the dependency files, not the full source. So a postinstall script that needs source files fails (module source). If you have a script like that, stop the install scripts:

dagger module settings prettier installFlags '["--ignore-scripts"]'

The setting goes into dagger.toml, so every developer and every CI run uses it. The module reference covers the Node.js image, the package manager, and environment variables.

Fail CI on unformatted files

A check is a function that passes or fails. dagger check runs the checks (Checking your code). List them:

dagger check -l
MODULE    PRETTIER-PROJECT  CHECK         DESCRIPTION
prettier  *                 format-check  Check formatting of this project.
# Use --all to expand collections and list each item.

The * stands for every Prettier project that the module found. To see each project, run dagger list prettier-projects -a. Each project’s key is its directory, relative to the workspace root:

MODULE    PRETTIER-PROJECT  DESCRIPTION
prettier  .                 A Prettier project, keyed by its root.
prettier  packages/ui       A Prettier project, keyed by its root.

format-check installs dependencies, then runs the project’s Prettier with --check from the project directory:

dagger check

In a project with two unformatted files, the check fails and names each file:

== CHECKS ==  ✘ 1 failed
✘ dag://?check=prettier/projects/format-check 16.5s ERROR
  ┃ [npm] 
  ┃ [npm] added 1 package, and audited 2 packages in 721ms
  ┃ [npm] 
  ┃ [npm] 1 package is looking for funding
  ┃ [npm]   run `npm fund` for details
  ┃ [npm] 
  ┃ [npm] found 0 vulnerabilities
  ┃ [dagger-prettier] [.] Checking formatting...
  ┃ [dagger-prettier] [.] [warn] package.json
  ┃ [dagger-prettier] [.] [warn] src/config.js
  ┃ [dagger-prettier] [.] [warn] Code style issues found in 2 files. Run Prettier with --write to fix.
  ┃ [dagger-prettier] [packages/ui] Checking formatting...
  ┃ [dagger-prettier] [packages/ui] All matched files use Prettier code style!
  ! Prettier failed in 1 project(s):
    - .: prettier --check failed (exit 1):
    [.] [warn] package.json
    [.] [warn] src/config.js
    [.] [warn] Code style issues found in 2 files. Run Prettier with --write to fix.
  ✘ exec.processRun 0.1s ERROR
  ! /usr/local/bin/crun did not terminate successfully: exit status 1

== RUN LOCALLY ==
dagger check "dag://?check=prettier/projects/format-check"

The [warn] lines are the same as the output of npx prettier --check .. Prettier’s exit code 1 makes the check fail. In a monorepo, the projects run at the same time, and the failure message names every project that failed. To check one project, select it by its key:

dagger check --prettier-project=packages/ui

The module installs dependencies again only when a dependency file changes, such as a package.json or a lockfile. In this guide’s project, the first dagger check took 17.0 seconds, and a rerun with no changes took 0.6 seconds. After the guide added a new source file, the next run took 0.8 seconds and did not run npm install.

Ignore the same files everywhere

Before Prettier runs, the module removes the files that any .gitignore in the tree ignores (module source). Prettier also applies the project’s .prettierignore. Thus, the check skips packages/ui/dist for the root project and for the packages/ui project, because the root .gitignore ignores it. A local prettier --check . in packages/ui still checks it, as the earlier run shows. That local run can fail when dagger check passes. To skip a directory in both, add it to .prettierignore.

Write fixes with the same version and config

format runs Prettier with --write, in the same container and with the same Prettier as the check. It returns the reformatted files as a changeset, a set of file changes that you can apply or reject. format is a function, not a check, so dagger check does not run it. It is not a generator either, so dagger generate does not run it. Call it on the project that contains a path:

dagger api call -y prettier project --path=. format

The -y flag applies the changeset without a prompt. Without -y, the command asks before it writes files. The changes are then in your working tree. This part of the git diff output shows the change to src/config.js:

-const   x = {a:1,
-b:2}
-export default x
+const x = { a: 1, b: 2 };
+export default x;

To format a different project, give a path inside it, such as --path=packages/ui. After you apply the changes, run dagger check again:

== CHECKS ==  ✔ 1 passed
✔ dag://?check=prettier/projects/format-check 1.4s OK

Run the check locally and with Cloud Checks

dagger check runs the same check on a laptop and in CI. Cloud Checks run your checks on Dagger’s Cloud Engines after each push and pull request, with no CI workflow file. The repository must be on GitHub, and its Git origin remote must point to that repository. From the project root, run this command:

dagger cloud checks on

In an interactive terminal, the command offers to complete the prerequisite steps first. These steps are to sign in to Dagger Cloud and to connect your GitHub account. When the command is done, commit the workspace files and push them:

git add dagger.toml dagger.lock
git commit -m "Check formatting with Prettier"
git push

The push starts format-check on Dagger Cloud. To see the results, run dagger workspace activity. If the check fails, run dagger api call -y prettier project --path=. format locally. Then commit and push the fixes.

If the project also lints with ESLint, Prettier’s docs recommend eslint-config-prettier, which “turns off all ESLint rules that are unnecessary or might conflict with Prettier”. How do I run ESLint the same way locally and in CI? covers the ESLint check. Biome is one tool that formats and lints. How do I run Biome lint and format checks in CI? says when it fits.

Bottom line: Pin one exact Prettier version, and fail CI with prettier --check. Ignore the same files in every run, and write fixes with the same version and config. By hand, that means an exact pin, a CI job, a pre-commit hook, and ignore files in each directory where Prettier runs. The Dagger Prettier module meets these requirements in one place, and dagger check runs it the same way locally and in Cloud Checks.