* docs(agents): document code conventions and pnpm check as the verification Co-Authored-By: Claude Opus 5.5 <michael.kriese+claude-code@mend.io> * docs: move the code conventions into the best practices guide Co-Authored-By: Claude Opus 5.5 <michael.kriese+claude-code@mend.io> Co-Authored-By: Claude Sonnet 5 <michael.kriese+claude-code@mend.io> --------- Co-authored-by: Claude Opus 5.5 <michael.kriese+claude-code@mend.io>
15 KiB
Best practices
This document explains our best practices. Follow these best practices when you're working on our code.
Git branch names
Branch names should start with a Conventional Commits scope like feat/ or fix/.
If you're closing an issue with your PR then put the issue number after the scope.
Finally, describe the changes in the branch in a few words.
Some good branch names:
feat/13732-cacache-cleanupfix/15431-gitea-automerge-strategyrefactor/vitest-reset-mocksdocs/rewrite-packageRules-section
Avoid branch names like patch-1.
If you don't know the correct Conventional Commit scope: give your branch a descriptive name like issue-1-feature-foo.
If you forgot to pick a good branch name when you started work, then rename the branch before creating the pull request. Read the GitHub Docs, renaming a branch to learn how to rename your branch on GitHub.
General
- Prefer full function declarations for readability and better stack traces, so avoid
const func = ():void => {} - Prefer
interfaceovertypefor TypeScript type declarations - Avoid Enums, use unions or immutable objects instead
- Always add unit tests for full code coverage
- Only use
v8comments for unreachable code coverage that is needed forcodecovcompletion - Use descriptive
v8comments - Do not add a line count after
next, for examplenext 3, because V8 does not honor the count and always exempts only the next code block - Do not remove existing
v8comments; document why the branch is unreachable instead - Before adding a
v8comment, try to construct an input that reaches the branch, and prefer a real test
- Only use
- Avoid cast or prefer
x as Tinstead of<T>xcast - Prefer
satisfiesoperator overas, read the TypeScript release notes forsatisfiesoperator to learn more - Avoid
Booleaninstead useisfunctions from@sindresorhus/ispackage, for example:is.string - Keep TSDoc to a factual statement of what a function does, without explanatory prose paragraphs
- Build strings with template literals rather than
+, at least in code you add or rewrite
/* v8 ignore next -- can never happen */
Functions
- Use
function foo(){...}to declare named functions - Use function declaration instead of assigning function expression into local variables (
const f = function(){...}) (TypeScript already prevents rebinding functions)- Exception: if the function accesses the outer scope's
thisthen use arrow functions assigned to variables instead of function declarations
- Exception: if the function accesses the outer scope's
- Regular functions should not access
this, but arrow functions and methods may accessthis - Only use nested functions when the lexical scope is used
Use arrow functions in expressions
Avoid:
bar(function(){...})
Use:
bar(() => {
this.doSomething();
});
Generally the this pointer should not be rebound.
Only use function expressions if you need to dynamically rebind this.
Source: Google TypeScript Style Guide, function declarations.
Code style
Write understandable code
Write code that is easier to read, review and maintain. Avoid "clever" code that's hard to understand.
Prefer verbose code which is easier for others to read and maintain than concise code which may be hard or slower for others to understand.
For example, Array reduce() functions are often hard to understand first time, and can be replaced with simpler for loops.
A for loop is just as fast but is simpler to understand and maintain.
Write single purpose functions
Single purpose functions are easier to understand, test and debug.
function caller() {
// ..code..
calculateUpdateAndPrint(data)
// ..code..
}
function calculateUpdateAndPrint(...) { /* code */ }
Simplified code:
function caller() {
// code..
const res = calculate(data);
update(res);
print(res);
// code..
}
function calculate(...) { /* code */ }
function update(...) { /* code */ }
function print(...) { /* code */ }
Keep indentation level low
Fail quickly. Nested code logic is hard to read and prone to logic mistakes.
function foo(str: string): boolean {
let result = false;
if (condition(str)) {
const x = extractData(str);
if (x) {
// do something
result = true;
}
}
return result;
}
Simplified code:
function foo(str: string): boolean {
if (!condition(str)) {
return false;
}
const x = extractData(str);
if (!x) {
return false;
}
// do something
return true;
}
Refactor one thing at a time
Refactor the code or refactor the tests.
Avoid refactoring the code and tests at the same time, this can mask regression errors.
Logging
For WARN, ERROR and FATAL log messages use logger metadata.
Also use logger metadata the result is a complex metadata object needing a multiple-line pretty stringification.
For INFO and DEBUG log messages inline the metadata into the log message where feasible.
It is OK to not inline metadata if it's complex, but in that case first think whether that much information really needs to be logged.
WARN, ERROR and FATAL messages are often used in metrics or error catching services.
These log messages should have a static msg component, so they can be automatically grouped or associated.
When logging an error object, always put it under the err metadata key, at any log level.
Bunyan only applies the error serializer (stack handling, redaction) to the err key, and a single key keeps error logs searchable.
Good:
logger.debug({ config }, 'Full config');
logger.debug(`Generated branchName: ${branchName}`);
logger.warn({ presetName }, 'Failed to look up preset');
logger.debug({ packageFile, err: parsed.error }, 'Failed to parse file');
Avoid:
logger.debug({ branchName }, 'Generated branchName');
logger.warn(`Failed to look up preset ${presetName}`);
logger.debug({ packageFile, error: parsed.error }, 'Failed to parse file');
Array constructor
Avoid the Array() constructor, with or without new, in your TypeScript code.
It has confusing and contradictory usage.
So you should avoid:
const a = new Array(2); // [undefined, undefined]
const b = new Array(2, 3); // [2, 3];
Instead, always use bracket notation to initialize arrays, or from to initialize an Array with a certain size.
For example:
// [0, 0, 0, 0, 0]
Array.from<number>({ length: 5 }).fill(0);
Iterating objects & containers
Use for ( ... of ...) loops instead of [Array|Set|Map].prototype.forEach and for ( ... in ...).
- Using
for ( ... in ...)for objects is error-prone. It will include enumerable properties from the prototype chain - Using
for ( ... in ...)to iterate over arrays, will counterintuitively return the array's indices - Avoid
[Array|Set|Map].prototype.forEach. It makes code harder to debug and defeats some useful compiler checks like reachability - Prefer branch-free forms such as
.filter(isTruthy)orflatMapoverif (!x) continueinside loops; each explicit branch needs its own test for full coverage
Only use Array.prototype.map() when the return value is used, otherwise use for ( ... of ...).
Source: Google TypeScript Style Guide, iterating objects
Exports
Use named exports in all code.
Avoid default exports.
This way all imports follow the same pattern.
Source: Google TypeScript Style Guide, exports
Imports
Use ES6 module syntax. For example:
import { square, diag } from 'lib';
// You may also use:
import * as lib from 'lib';
And avoid require:
import x = require('...');
HTTP & RESTful API request handling
Prefer using Http from util/http to simplify HTTP request handling and to enable authentication and caching, as our Http class will transparently handle host rules.
For example:
import { Http } from '../../../util/http';
const http = new Http('some-host-type');
try {
const body = (await http.getJson<Response>(url)).body;
} catch (err) {
...
}
Async functions
Never use Promise.resolve in async functions.
Never use Promise.reject in async functions, instead throw an Error class type.
Never put await inside a ternary or other conditional expression; use if/else, as V8 coverage misreports such constructs.
Dates and times
Use the Luxon package to handle dates and times.
Use UTC to be time zone independent.
if (end) {
const now = DateTime.now().toUTC();
const eol = DateTime.fromISO(end, { zone: 'utc' });
return eol < now;
}
Unit testing
- Spec files have exactly one root
describe, named by the file path (e.g.describe('workers/repository/process/extract-update', ...)); nest any grouping inside it - Separate the Arrange, Act and Assert phases with newlines
- Use
it.eachrather thantest.each - Prefer Tagged Template Literal style for
it.each, Prettier will help with formatting- See Example
- Mock Date/Time when testing a Date/Time dependent module
- For
Luxonmocking see Example
- For
- Prefer
vi.spyOnfor mocking single functions, or mock entire modules- Avoid overwriting functions, for example: (
func = vi.fn();)
- Avoid overwriting functions, for example: (
- Use plain
vi.mock('./module.ts', ()=>({ defaut: vi.fn(), some: vi.fn() }))when only a few exports are touched, and stub them explicitly - Use
vi.mock('./module.ts', () => mockDeep())only when the module is pulled in transitively and hand-stubbing every export is impracticalmockDeepreturns a mock for any property access, so typos in mocked names won't fail the test
- Prefer
toEqual - Use
toMatchObjectfor huge objects when only parts need to be tested - Do not use snapshot matchers (
toMatchSnapshot,toMatchInlineSnapshot,toThrowErrorMatchingSnapshot), write explicit assertions instead- For huge strings like the Renovate PR body text, assert on the sections the test is about with
toContain,toStartWithortoEndWith; compare the whole string withtoBeonly when producing exactly that text is the point of the test - For huge complex objects where you only need to test parts, use
toMatchObject
- For huge strings like the Renovate PR body text, assert on the sections the test is about with
- Avoid exporting functions purely for the purpose of testing unless you really need to
- Avoid cast or prefer
x as Tinstead of<T>xcast- Use
partial<T>()fromtest/utilif only a partial object is required - Use
fakeSha(seed)fromtest/utilto generate deterministic, validLongCommitShavalues in tests
- Use
Fixtures
Where possible, reduce the test fixture to a size where an inline codeBlock is possible to use instead of a separate fixture file.
Inline codeBlocks improve performance plus are more readable.
Write them inline with real indentation, not as joined arrays, and do not add a new file under __fixtures__ for content that fits inline.
import { codeBlock } from 'common-tags';
const input = codeBlock`
line one
line two
`;
// → 'line one\nline two'
Use the Fixture class if loading fixtures from files.
For example:
Fixture.get('./file.json'); // for loading string data
Fixture.getJson('./file.json'); // for loading and parsing objects
Fixture.getBinary('./file.json'); // for retrieving a buffer
Working with vanilla JS files (renovate/tools only)
Declare types and function prototypes with JSDoc.
Classes
- Use Typescript getter setters (Accessors) when needed.
The getter must be a
pure functioni.e.- The function return values are identical for identical arguments
- The function has no side effects
- Omit constructors when defining Static classes
- No
#privatefields, use TypeScript's visibility annotations instead - Avoid underscore suffixes or prefixes like
_prop, instead use whole words as the suffix or prefix likeinternalProp
regex
Use Named Capturing Groups when capturing groups.
Prefer named groups even for a single capture — positional references (match[1]) break silently when the pattern changes.
Non-capturing groups ((?:...)) are always fine and encouraged when the captured value is not needed.
Always use the regEx() utility from lib/util/regex.ts instead of the RegExp constructor or regex literals.
It transparently uses the RE2 engine when available for safer, denial-of-service-resistant matching.
import { regEx } from '~/lib/util/regex';
const pattern = regEx(/^(?<major>\d+)\.(?<minor>\d+)(?:\.(?<patch>\d+))?$/);
const match = pattern.exec(version);
if (match?.groups) {
const { major, minor, patch } = match.groups;
}
Windows
We recommend you set core.autocrlf = input in your Git config.
You can do this by running this Git command:
git config --global core.autocrlf input
This prevents the carriage return \r\n which may confuse Renovate.
You can also set the line endings in your repository by adding * text=auto eol=lf to your .gitattributes file.