Code formatting with Spotless
OpenRemote projects use Spotless to apply and verify consistent code formatting and license headers.
Spotless is configured through Gradle and is the authoritative implementation of the repository's formatting rules.
Working directory
Always run Spotless commands from the root directory of the repository, not from an individual Gradle subproject or from the ui directory.
For example:
cd openremote
./gradlew spotlessCheck
This also applies to custom projects and other OpenRemote repositories.
Checking formatting
Before submitting a pull request, check all supported files with:
./gradlew spotlessCheck
This command does not modify files. It fails when one or more files do not comply with the configured formatting rules.
Integration with Gradle verification
Spotless is included in Gradle's standard check lifecycle. Running:
./gradlew check
also runs spotlessCheck together with the other configured verification tasks.
Use spotlessCheck directly when you only want to check formatting without running the complete verification lifecycle.
Applying formatting
To automatically format all supported files, run:
./gradlew spotlessApply
Review the resulting changes before committing them. Depending on the file type, Spotless can update:
- code layout;
- imports;
- indentation and whitespace;
- line endings and final newlines;
- license headers.
Backend and UI tasks
Repositories that separate backend and UI sources, such as openremote/openremote and OpenRemote custom projects, provide additional task groups.
To check only backend or UI sources, run:
./gradlew spotlessBackendCheck
./gradlew spotlessUiCheck
To format only backend or UI sources, run:
./gradlew spotlessBackendApply
./gradlew spotlessUiApply
These grouped tasks are not available in every repository. For example, the openremote/extensions repository does not separate its Spotless configuration into backend and UI groups. Use the general tasks there:
./gradlew spotlessCheck
./gradlew spotlessApply
Run the following command from the repository root to see which Spotless tasks are available:
./gradlew tasks --group verification
Formatting an individual file type
Spotless creates tasks for each configured format. This is useful when you only want to check or apply formatting to one type of source file.
For example:
./gradlew spotlessJavaApply
./gradlew spotlessTypescriptApply
The exact tasks and supported file types differ between repositories.
Java coding convention
Java source code is formatted with google-java-format, which formats Java code according to the Google Java Style Guide.
The formatting convention includes:
- two-space block indentation;
- K&R-style braces;
- consistent line wrapping;
- standardized import ordering;
- removal of unused imports;
- consistent formatting of annotations and Javadoc.
Run Spotless from the repository root to apply or check Java formatting:
./gradlew spotlessJavaApply
./gradlew spotlessJavaCheck
The Google Java Style Guide also contains conventions that cannot be fully enforced by a formatter, including guidance on naming, class structure, programming practices, and documentation. Follow these conventions when writing or reviewing Java code.
IDE integrations such as the IntelliJ IDEA google-java-format plugin can provide immediate feedback, but they may use a different formatter version. The output produced by the repository's Spotless configuration remains authoritative.
Installing the Git pre-push hook
Spotless provides an optional Git pre-push hook that checks formatting before code is pushed.
Install the hook from the repository root:
./gradlew spotlessInstallGitPrePushHook
After installation, pushing changes runs spotlessCheck automatically.
When formatting problems are found, the hook:
- runs
spotlessApplyto fix the affected files; - aborts the push;
- allows you to review and commit the formatting changes;
- lets you push again after the changes have been committed.
The hook is installed in the local Git checkout and is not shared automatically with other contributors. Each contributor who wants to use it must install it separately.
The hook provides a useful local safeguard, but it does not replace the formatting checks run by continuous integration.
UI linting and formatting with Yarn
Projects with a UI and corresponding scripts in their root package.json, such as openremote/openremote and OpenRemote custom projects, can also run ESLint and Prettier directly with Yarn.
Run these commands from the repository root.
Check JavaScript and TypeScript with ESLint:
yarn lint
Apply ESLint fixes where possible:
yarn lint:fix
Apply Prettier formatting:
yarn format
Check Prettier formatting without changing files:
yarn format:check
When applying both ESLint fixes and Prettier formatting, run them in this order:
yarn lint:fix
yarn format
ESLint may change imports or code structure. Running Prettier afterwards ensures that the final result is consistently formatted.
These commands provide quick feedback when working exclusively on the UI. They do not completely replace Spotless because Spotless may also:
- format backend and repository-level files;
- apply or verify license headers;
- run additional formatters;
- enforce repository-specific exclusions and rules.
Before pushing changes, run the appropriate Spotless check from the repository root.
Basic editor settings
The repository-level EditorConfig file, .editorconfig, defines the default text-file settings:
- UTF-8 character encoding;
- Unix-style LF line endings;
- two spaces for indentation;
- spaces instead of tab characters;
- a newline at the end of every file;
- removal of trailing whitespace.
Trailing whitespace is not automatically removed from Markdown files because two trailing spaces can be used to create an explicit Markdown line break.
Most modern IDEs support EditorConfig either directly or through an extension. Enable EditorConfig support and let the repository's .editorconfig control these settings.
Avoid overriding the repository settings with conflicting global IDE preferences.
When EditorConfig support is unavailable, configure the editor manually to use:
Encoding: UTF-8
Line endings: LF
Indentation: 2 spaces
Tabs: disabled
Insert final newline: enabled
Trim trailing whitespace: enabled
Disable trailing-whitespace removal for Markdown files unless the editor understands Markdown's significant trailing spaces.
Prettier and ESLint responsibilities
Prettier and ESLint have different responsibilities:
- Prettier formats supported UI and documentation files.
- ESLint reports JavaScript and TypeScript code-quality problems and applies safe fixes where possible.
- Spotless coordinates these tools with the other repository formatters and performs additional checks such as license-header enforcement.
The repository's ESLint configuration disables conflicting formatting rules through eslint-config-prettier. Do not configure ESLint as a second general-purpose formatter on top of Prettier.
The recommended save sequence is:
- Apply ESLint fixes.
- Format the resulting code with Prettier.
- Let EditorConfig govern basic text-file properties.
Always use the Prettier and ESLint versions installed by the repository. Avoid configuring an IDE to use unrelated global installations or a personal configuration file.
IntelliJ IDEA
Useful plugins include:
Open the root directory of the repository as the IntelliJ IDEA project. This allows the IDE to discover the Gradle project, root package.json, UI configuration files, and .editorconfig.
Configuring Prettier
Open:
Settings
→ Languages & Frameworks
→ JavaScript
→ Prettier
Select Automatic Prettier configuration. The IDE should use:
- the Prettier package installed by the repository;
- the closest
.prettierrc.json; - the applicable
.prettierignore.
Optionally enable:
- Run on save;
- Run on 'Reformat Code' action.
Do not select a globally installed Prettier package when the repository package can be detected.
If automatic detection fails, use manual configuration and select the Prettier package provided by the repository. Do not create a separate IDE-only Prettier configuration.
Configuring ESLint
Open:
Settings
→ Languages & Frameworks
→ JavaScript
→ Code Quality Tools
→ ESLint
Select Automatic ESLint configuration.
The IDE should use the repository's ESLint installation and locate the configuration nearest to the file being edited.
Optionally enable:
Run eslint --fix on save
For repositories with the ESLint configuration in ui, specify ui as the working directory only when automatic detection does not work correctly.
Use ESLint for inspections and fixes and Prettier for formatting. When both run on save, ESLint fixes should be applied before Prettier formats the resulting code.
Avoid enabling both the built-in JavaScript formatter and Prettier for the same save or reformat action because the two formatters may produce different output.
Configuring EditorConfig
Ensure that EditorConfig support is enabled:
Settings
→ Editor
→ Code Style
→ Enable EditorConfig support
The IDE should display an EditorConfig indicator when editing files covered by the repository configuration.
The repository configuration should take precedence over personal code-style settings for indentation, line endings, final newlines, and trailing whitespace.
Visual Studio Code
Install these extensions:
Open the repository root as the Visual Studio Code workspace. This allows the extensions to discover the root package.json, UI configuration files, and .editorconfig.
Configure Prettier as the formatter for JavaScript and TypeScript and run ESLint fixes as a save action:
{
"prettier.requireConfig": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
}
}
Equivalent language-specific settings can be added for CSS, HTML, JSON, JSONC, Markdown, and YAML.
Visual Studio Code requires a separate language block for each language. A combined key such as [javascript][typescript] does not configure both languages correctly.
For repositories whose ESLint configuration is located in the ui directory, automatic detection should normally work. When it does not, add:
{
"eslint.workingDirectories": ["./ui"]
}
Do not set prettier.configPath to a personal or global configuration. Doing so can cause the extension to ignore the repository's ui/.prettierrc.json.
Similarly, do not set eslint.options.overrideConfigFile unless troubleshooting requires it. ESLint should automatically discover the repository configuration.
After configuring the extensions, verify their output against the command-line tools:
yarn lint
yarn format:check
./gradlew spotlessUiCheck
Recommended Visual Studio Code whitespace settings
EditorConfig should normally manage whitespace settings. When explicit Visual Studio Code defaults are needed, use:
{
"editor.insertSpaces": true,
"editor.tabSize": 2,
"files.eol": "\n",
"files.insertFinalNewline": true,
"files.trimTrailingWhitespace": true,
"[markdown]": {
"files.trimTrailingWhitespace": false
}
}
These should be fallback settings rather than replacements for .editorconfig.
IDE formatting remains optional
IDE integration provides faster feedback but does not replace the repository commands.
An IDE extension may:
- use a different formatter version;
- fail to locate the correct configuration;
- ignore part of
.prettierignore; - apply a built-in formatter before or after Prettier;
- format only the currently opened file.
Before pushing changes, always run:
./gradlew spotlessCheck
For repositories that separate backend and UI sources, a narrower check can be used while developing:
./gradlew spotlessBackendCheck
./gradlew spotlessUiCheck
Run the complete spotlessCheck before submitting a substantial or cross-cutting pull request.
Continuous integration
Pull requests are checked for formatting by GitHub Actions. In repositories with separate backend and UI task groups, the workflow can run the relevant Spotless check based on the files changed by the pull request.
When a Spotless check fails:
- Run the corresponding
spotlessApply,spotlessBackendApply, orspotlessUiApplytask locally. - Review the changes.
- Commit and push the formatted files.
Do not manually reproduce the formatter output. The Gradle Spotless tasks are the authoritative formatting implementation.
Temporarily disabling formatting
Some generated, third-party, or unusually formatted code may not be suitable for automatic formatting. Where the configured formatter supports it, formatting can be temporarily disabled using spotless:off and restored using spotless:on in the appropriate comment syntax:
// spotless:off
codeThatMustRetainItsOriginalFormatting();
// spotless:on
Use this only for small, exceptional sections. Repository-wide exclusions should be added to the central Spotless configuration instead.
Configuration
The main Spotless configuration is stored in:
spotless.gradle
Additional formatter configuration and license-header files may be stored under:
tools/spotless/
For projects with a UI, the configuration also uses files such as:
.editorconfig
ui/.eslintrc.json
ui/.prettierignore
ui/.prettierrc.json
package.json
Changes to formatting configuration affect the entire repository and should preferably be reviewed separately from ordinary source-code changes.
Repository-wide formatting workflows
Applying large formatting changes
A repository-wide spotlessApply can modify many files without changing their behavior. Mixing these changes with functional modifications makes pull requests difficult to review and makes later git blame results less useful.
When applying a large formatting change:
- update and review the Spotless configuration first;
- apply formatting across the repository;
- keep the generated formatting changes separate from functional changes;
- commit the formatting changes in a dedicated commit;
- avoid making manual or behavioral changes in that commit.
A dedicated formatting commit makes the change easier to review and allows it to be excluded from blame history.
Ignoring formatting commits in Git blame
For large formatting-only commits, add the final commit hash to a .git-blame-ignore-revs file in the root of the repository.
For example:
# Applied the initial repository-wide Spotless formatting
0123456789abcdef0123456789abcdef01234567
Use the full commit hash and include a comment explaining what the commit changed.
The formatting commit must already exist before its hash can be added. Add the hash to .git-blame-ignore-revs in a separate follow-up commit.
When a pull request is squash-merged, use the final commit hash created on the target branch rather than the hash of an intermediate commit from the pull-request branch.
GitHub automatically uses a root-level .git-blame-ignore-revs file in its blame view.
To use the file explicitly from the command line, run:
git blame --ignore-revs-file .git-blame-ignore-revs <file>
You can also configure the local repository to use it by default:
git config blame.ignoreRevsFile .git-blame-ignore-revs
This configuration applies to the current repository. Add --global only when you intentionally want to use the same ignore-file path for all local repositories.
Only add commits that are overwhelmingly mechanical, such as repository-wide formatting or line-ending changes. Do not ignore commits that contain meaningful functional changes because that would hide useful authorship and history information.
Updating an existing pull request after the initial formatting change
Pull requests created before the repository-wide formatting commit can produce many merge conflicts, even when their functional changes do not overlap.
For openremote/openremote, the relevant commits are:
| Commit | Description |
|---|---|
d6941d97c96ad70e7a6b764a4a4a7682aa906c8a | Last commit before the repository-wide formatting change |
e3a066dcf739efe08d3d0e51e477d2d652dd28f8 | Apply Spotless across the repository |
The master branch can be merged into the pull request in stages so that functional changes are handled separately from the generated formatting changes.
-
Fetch the latest repository history:
git fetch origin -
Merge the commit immediately before the Spotless formatting commit:
git merge d6941d97c96ad70e7a6b764a4a4a7682aa906c8aResolve any functional conflicts and complete the merge normally.
-
Start merging the repository-wide formatting commit without completing the merge:
git merge --no-commit e3a066dcf739efe08d3d0e51e477d2d652dd28f8Git may report many conflicts. Do not resolve them individually.
-
Restore the files to their state immediately before this merge while keeping the merge in progress:
git restore --source=HEAD --staged --worktree -- . -
Apply Spotless and complete the merge:
./gradlew spotlessApplygit add --allgit commit -
Merge the latest
masterbranch as usual:git merge origin/masterResolve any remaining functional conflicts normally. Run
spotlessApplyagain if resolving a conflict required manual source-code changes. -
Verify the final result:
./gradlew spotlessCheck
Do not use the -X ours merge option for the repository-wide formatting commit. It operates on individual conflicting hunks and can combine formatted and unformatted code into invalid source files.
Do not use the -s ours merge strategy either. It would record the formatting commit as merged without applying or regenerating its formatting changes.
Upgrading an existing custom project to Spotless
Custom projects commonly use the reusable CI/CD workflow from the master branch of openremote/openremote:
uses: openremote/openremote/.github/workflows/ci_cd.yml@master
Because this follows master, new workflow behaviour is inherited automatically. This includes the Spotless formatting checks.
Temporarily pinning the workflow
When there is not yet time to upgrade a custom project, temporarily pin the reusable workflow to a commit from before the Spotless checks were added:
uses: openremote/openremote/.github/workflows/ci_cd.yml@51f4c3c0c8edd429a65237268d47d615617d4008
Pinning the workflow also prevents the project from receiving other workflow changes made after that commit. Use this only as a temporary measure.
Pull request 1: Add Spotless and apply formatting
The first pull request adds the Spotless configuration and applies the initial repository-wide formatting.
Synchronizing the Spotless configuration
The Spotless configuration was added to the custom-project template in commit:
3aa39cc8e2134db446d7258d429315fc5615f6e9
Check out the template at this commit:
git clone https://github.com/openremote/custom-project.git custom-project-template
cd custom-project-template
git checkout 3aa39cc8e2134db446d7258d429315fc5615f6e9
Use a directory comparison tool such as Meld to compare the checked-out template with the existing custom project:
custom-project-template
<existing-custom-project>
Synchronize the Spotless-related changes from the template while preserving project-specific configuration. Carefully merge changes to existing files instead of replacing them wholesale.
The exact changes introduced by the Spotless configuration commit can be inspected with:
git show 3aa39cc8e2134db446d7258d429315fc5615f6e9
When other custom-project template updates are also required, compare the existing project with the desired newer template commit and merge those changes in the same way.
Commit 1: Add the configuration
Ensure the reusable workflow reference points to master before creating the first commit:
uses: openremote/openremote/.github/workflows/ci_cd.yml@master
Commit the synchronized configuration and workflow change:
git add --all
git commit -m "Add Spotless configuration"
Commit 2: Apply the formatting
Apply Spotless:
./gradlew spotlessApply
Verify that the formatted project passes the Spotless checks before committing the generated changes:
./gradlew spotlessCheck
Commit the formatting:
git add --all
git commit -m "Apply Spotless"
Keep the mechanical formatting commit separate from configuration, workflow, and functional changes.
Merging pull request 1
Merge the first pull request using rebase and merge, not squash and merge, so the two commits remain separate on the target branch.
At the bottom of the pull request:
- Click the dropdown arrow next to the merge button.
- Select Rebase and merge.
- Click Rebase and merge.
- Confirm the merge when prompted.
GitHub creates new commit hashes when rebasing the commits onto the target branch. The final hash of the Apply Spotless commit must therefore be retrieved after the pull request has been merged.
Pull request 2: Ignore the formatting commit in Git blame
Fetch the latest target branch and find the full hash of the final Apply Spotless commit:
git fetch origin
git log origin/main --format="%H %s" --grep="^Apply Spotless$" -n 1
This prints the complete commit hash followed by its subject. The full hash can also be copied from the commit page on GitHub.
Add the hash to .git-blame-ignore-revs:
# Applied the initial repository-wide Spotless formatting
<final-apply-spotless-commit-hash>
Commit this change on a new branch and create a separate follow-up pull request:
git add .git-blame-ignore-revs
git commit -m "Ignore Spotless formatting commit in Git blame"
Merging pull request 2
Use the normal squash and merge method for the second pull request.
At the bottom of the pull request:
- Click the dropdown arrow next to the merge button.
- Select Squash and merge.
- Click Squash and merge.
- Confirm the merge when prompted.
Selecting Squash and merge for this pull request switches the merge button back to the method normally used for subsequent pull requests.