Code Formatting
Overview
Project Tick uses treefmt orchestrated through
treefmt-nix to enforce consistent code formatting
across the entire monorepo. The formatting configuration lives in ci/default.nix and
covers JavaScript, Nix, YAML, GitHub Actions workflows, and sorted-list enforcement.
Configured Formatters
Summary Table
| Formatter | Language/Files | Key Settings |
|---|---|---|
actionlint |
GitHub Actions YAML | Default (syntax + best practices) |
biome |
JavaScript / TypeScript | Single quotes, optional semicolons |
keep-sorted |
Any (marked sections) | Default |
nixfmt |
Nix expressions | nixfmt-rfc-style |
yamlfmt |
YAML files | Retain line breaks |
zizmor |
GitHub Actions YAML | Security scanning |
actionlint
Purpose: Validates GitHub Actions workflow files for syntax errors, type mismatches, and best practices.
Scope: .github/workflows/*.yml
Configuration: Default — no custom settings.
programs.actionlint.enable = true;
What it catches:
- Invalid workflow syntax
- Missing or incorrect
runs-onvalues - Type mismatches in expressions
- Unknown action references
biome
Purpose: Formats JavaScript and TypeScript source files with consistent style.
Scope: All .js and .ts files except *.min.js
Configuration:
programs.biome = {
enable = true;
validate.enable = false;
settings.formatter = {
useEditorconfig = true;
};
settings.javascript.formatter = {
quoteStyle = "single";
semicolons = "asNeeded";
};
settings.json.formatter.enabled = false;
};
settings.formatter.biome.excludes = [
"*.min.js"
];
Style rules:
| Setting | Value | Effect |
|---|---|---|
useEditorconfig |
true |
Respects .editorconfig (indent, etc.) |
quoteStyle |
"single" |
Uses 'string' instead of "string" |
semicolons |
"asNeeded" |
Only inserts ; where ASI requires it |
validate.enable |
false |
No lint-level validation, only formatting |
json.formatter |
disabled |
JSON files are not formatted by biome |
Exclusions: *.min.js — Minified JavaScript files are never reformatted.
keep-sorted
Purpose: Enforces alphabetical ordering in marked sections of any file type.
Scope: Files containing keep-sorted markers.
programs.keep-sorted.enable = true;
Usage: Add markers around sections that should stay sorted:
# keep-sorted start
apple
banana
cherry
# keep-sorted end
nixfmt
Purpose: Formats Nix expressions according to the RFC-style convention.
Scope: All .nix files.
programs.nixfmt = {
enable = true;
package = pkgs.nixfmt;
};
The pkgs.nixfmt package from the pinned Nixpkgs provides the formatter. This
is nixfmt-rfc-style, the official Nix formatting standard.
yamlfmt
Purpose: Formats YAML files with consistent indentation and structure.
Scope: All .yml and .yaml files.
programs.yamlfmt = {
enable = true;
settings.formatter = {
retain_line_breaks = true;
};
};
Key setting: retain_line_breaks = true — Preserves intentional blank lines between
YAML sections, preventing the formatter from collapsing the file into a dense block.
zizmor
Purpose: Security scanner for GitHub Actions workflows. Detects injection vulnerabilities, insecure defaults, and untrusted input handling.
Scope: .github/workflows/*.yml
programs.zizmor.enable = true;
What it detects:
- Script injection via
${{ github.event.* }}inrun:steps - Insecure use of
pull_request_target - Unquoted expressions that could be exploited
- Dangerous permission configurations
treefmt Global Settings
projectRootFile = ".git/config";
settings.verbose = 1;
settings.on-unmatched = "debug";
| Setting | Value | Purpose |
|---|---|---|
projectRootFile |
.git/config |
Identifies repository root for treefmt |
settings.verbose |
1 |
Logs which files each formatter processes |
settings.on-unmatched |
"debug" |
Files with no matching formatter are logged at debug level |
Running Formatters
In CI
The formatting check runs as a Nix derivation:
nix-build ci/ -A fmt.check
This:
- Copies the full source tree (excluding
.git) into the Nix store - Runs all configured formatters
- Fails with a diff if any file would be reformatted
Locally (Nix Shell)
cd ci/
nix-shell # enter CI dev shell
treefmt # format all files
treefmt --check # check without modifying (dry run)
Locally (Nix Build)
# Just check (no modification):
nix-build ci/ -A fmt.check
# Get the formatter binary:
nix-build ci/ -A fmt.pkg
./result/bin/treefmt
Source Tree Construction
The treefmt check operates on a clean copy of the source tree:
fs = pkgs.lib.fileset;
src = fs.toSource {
root = ../.;
fileset = fs.difference ../. (fs.maybeMissing ../.git);
};
This:
- Takes the entire repository directory (
../.fromci/) - Excludes the
.gitdirectory (which is large and irrelevant for formatting) fs.maybeMissinghandles the case where.gitdoesn't exist (e.g., in tarballs)
The resulting source is passed tofmt.check:
check = treefmtEval.config.build.check src;
Formatter Outputs
The formatting system exposes three Nix attributes:
{
shell = treefmtEval.config.build.devShell; # Interactive shell
pkg = treefmtEval.config.build.wrapper; # treefmt binary
check = treefmtEval.config.build.check src; # CI check derivation
}
| Attribute | Use Case |
|---|---|
fmt.shell |
nix develop .#fmt.shell — interactive formatting |
fmt.pkg |
The treefmt wrapper with all formatters bundled |
fmt.check |
nix build .#fmt.check — CI formatting check |
Troubleshooting
"File would be reformatted"
If CI fails with formatting issues:
# Enter the CI shell to get the exact same formatter versions:
cd ci/
nix-shell
# Format all files:
treefmt
# Stage and commit the changes:
git add -u
git commit -m "style(repo): apply treefmt formatting"
Editor Integration
For real-time formatting in VS Code:
- Use the biome extension for JavaScript/TypeScript
- Configure single quotes and optional semicolons to match CI settings
- Use nixpkgs-fmt or nixfmt for Nix files
Formatter Conflicts
Each file type has exactly one formatter assigned by treefmt. If a file matches multiple formatters, treefmt reports a conflict. The current configuration avoids this by:
- Disabling biome's JSON formatter
- Having non-overlapping file type coverage