mirror of
https://github.com/renovatebot/renovate.git
synced 2026-10-26 07:20:39 +00:00
716baf630b
* feat(lint): add renovate/prefer-fs-util and renovate/prefer-luxon rules Two shared-util substitution rules, both at warn level: - renovate/prefer-fs-util flags value imports of fs, node:fs, fs/promises, node:fs/promises, fs-extra and fs-extra/esm in lib/ source files outside lib/util/fs/, pointing to the scoped helpers from lib/util/fs. Spec files and type-only imports are exempt. A config-only change via no-restricted-imports was considered but rejected: that rule is at error severity globally and per-path warn severity is not expressible, so the 13 existing call sites would break the baseline. - renovate/prefer-luxon flags new Date(...) and Date.now() in lib/ source files, pointing to Luxon per best-practices. Date/time utility modules (lib/util/date, lib/util/pretty-time, lib/util/timestamp) and spec files (vi.setSystemTime etc.) are exempt. Baseline: 13 prefer-fs-util and 45 prefer-luxon warnings, zero errors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor: use Luxon instead of new Date() at remaining call sites Fixes all renovate/prefer-luxon violations for `new Date(...)`: - `new Date().toISOString()` -> `DateTime.utc().toISO()` (byte-identical output) in the http cache provider, schedule logging and PR cache. - `new Date()` -> `DateTime.now().toJSDate()` for the git commit-date fallback and the rubygems `syncedAt` markers (same `Date` values). - rust-release-channel now computes the Rust 1.0 release date and nightly version dates via `DateTime.utc(...)`; previously `new Date(y, m, d)` used the local timezone while the epoch used UTC, so the comparison was timezone-dependent at the boundary date. Tests run with TZ=UTC, where behavior is unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor: use lib/util/fs helpers in repository worker Fixes renovate/prefer-fs-util violations outside bootstrap code: - `fs.ensureDir()` -> `ensureDir()` from lib/util/fs (same fs-extra call underneath; it no-ops on an empty dir name, so the repository worker spec now simulates the init failure via a rejected `initRepo` instead of relying on `fs.ensureDir('')` throwing). - `fs.remove(privateCacheDir())` -> `cachePathExists()` + `rmCache()`, keeping the remove-if-present semantics while asserting cacheDir containment. Two call sites keep raw imports with reasoned disable comments: - lib/util/modules.ts synchronously enumerates Renovate's own installed module directories at import time. - lib/util/cache/package/impl/sqlite.ts receives cacheDir as an explicit argument during global initialization, before GlobalConfig (which the scoped cache helpers resolve against) contains cacheDir. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(lint): promote prefer-fs-util and prefer-luxon to error Both rules are narrowed to what is actually accepted practice before being promoted: prefer-luxon no longer flags `Date.now()`: it returns UTC-based epoch milliseconds used for elapsed-time measurement and TTL arithmetic, which is timezone-independent by construction. The mined reviewer feedback behind this rule ("use Luxon for date arithmetic outside util") targets timezone-dependent date construction/formatting, which cannot occur with a raw millis number; the rule keeps flagging `new Date(...)`. prefer-fs-util exempts code that legitimately needs raw fs access because the scoped lib/util/fs helpers resolve against GlobalConfig localDir/cacheDir, which does not exist yet or does not apply there: - lib/logger/: log-file streams at user-configured system paths during bootstrap; lib/util/fs also depends on the logger (import cycle). - lib/workers/global/: creates baseDir/cacheDir themselves and reads/writes user-supplied paths before GlobalConfig is seeded. - lib/config-validator.ts: standalone CLI without GlobalConfig. - lib/util/git/: owns the localDir lifecycle (clone, emptyDir, symlinks) and private-key files with sync cleanup on process exit. Also fixes a pre-existing tsc error in prefer-fs-util.js: `importKind` is a TypeScript-ESTree extension missing from eslint's base AST types. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(fs): make rmCache ignore missing paths All rmCache callers use it for cleanup, where a missing path is a no-op, so bake force: true in instead of guarding call sites with an existence check. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(linters): exempt lib/instrumentation from prefer-fs-util `lib/instrumentation/file-exporter.ts` landed on main while this branch was open and imports `node:fs/promises` directly. The exporter writes to a user-configured system path (`RENOVATE_TRACING_FILE_EXPORTER_PATH`) outside `localDir`/`cacheDir`, so the scoped helpers do not apply. `lib/instrumentation` is also deliberately kept out of the module graph of instrumented libraries (see the `⚠️` note in `lib/renovate.ts`), so it must not pull in `lib/util/fs` and its config/logger dependencies. Exempt the directory, as is already done for `lib/logger`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: shorten doc --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
155 lines
4.7 KiB
TypeScript
155 lines
4.7 KiB
TypeScript
import type { DatabaseSync, StatementSync } from 'node:sqlite';
|
|
// oxlint-disable-next-line renovate/prefer-fs-util -- create() runs during global initialization with cacheDir passed as an explicit argument, before GlobalConfig (which the scoped cache helpers resolve against) contains cacheDir
|
|
import fs from 'fs-extra';
|
|
import upath from 'upath';
|
|
import { logger } from '../../../../logger/index.ts';
|
|
import { getEnv } from '../../../env.ts';
|
|
import { ensureDir } from '../../../fs/index.ts';
|
|
import { parseInteger } from '../../../number.ts';
|
|
import type { PackageCacheNamespace } from '../types.ts';
|
|
import { PackageCacheBase } from './base.ts';
|
|
|
|
const { exists } = fs;
|
|
|
|
export class PackageCacheSqlite extends PackageCacheBase {
|
|
static async create(cacheDir: string): Promise<PackageCacheSqlite> {
|
|
const { DatabaseSync: Sqlite } = await import('node:sqlite');
|
|
const sqliteDir = upath.join(cacheDir, 'renovate/renovate-cache-sqlite');
|
|
await ensureDir(sqliteDir);
|
|
const sqliteFile = upath.join(sqliteDir, 'db.sqlite');
|
|
|
|
if (await exists(sqliteFile)) {
|
|
logger.debug(`Using SQLite package cache: ${sqliteFile}`);
|
|
} else {
|
|
logger.debug(`Creating SQLite package cache: ${sqliteFile}`);
|
|
}
|
|
|
|
const { RENOVATE_X_SQLITE_BUSY_TIMEOUT } = getEnv();
|
|
const timeout = parseInteger(RENOVATE_X_SQLITE_BUSY_TIMEOUT, 5000);
|
|
|
|
const client = new Sqlite(sqliteFile, { timeout });
|
|
return new PackageCacheSqlite(client);
|
|
}
|
|
|
|
private readonly upsertStatement: StatementSync;
|
|
private readonly getStatement: StatementSync;
|
|
private readonly deleteStatement: StatementSync;
|
|
private readonly deleteExpiredRows: StatementSync;
|
|
private readonly countStatement: StatementSync;
|
|
|
|
readonly client: DatabaseSync;
|
|
|
|
private constructor(client: DatabaseSync) {
|
|
super();
|
|
this.client = client;
|
|
client.exec('PRAGMA journal_mode = WAL');
|
|
client.exec("PRAGMA encoding = 'UTF-8'");
|
|
|
|
client.exec(
|
|
`
|
|
CREATE TABLE IF NOT EXISTS package_cache (
|
|
namespace TEXT NOT NULL,
|
|
key TEXT NOT NULL,
|
|
expiry INTEGER NOT NULL,
|
|
data BLOB NOT NULL,
|
|
PRIMARY KEY (namespace, key)
|
|
)
|
|
`,
|
|
);
|
|
client.exec('CREATE INDEX IF NOT EXISTS expiry ON package_cache (expiry)');
|
|
|
|
this.upsertStatement = client.prepare(`
|
|
INSERT INTO package_cache (namespace, key, data, expiry)
|
|
VALUES (@namespace, @key, @data, unixepoch() + @ttlSeconds)
|
|
ON CONFLICT (namespace, key) DO UPDATE SET
|
|
data = @data,
|
|
expiry = unixepoch() + @ttlSeconds
|
|
`);
|
|
|
|
this.getStatement = client.prepare(
|
|
`
|
|
SELECT data FROM package_cache
|
|
WHERE
|
|
namespace = @namespace AND key = @key AND expiry > unixepoch()
|
|
`,
|
|
);
|
|
|
|
this.deleteStatement = client.prepare(`
|
|
DELETE FROM package_cache
|
|
WHERE namespace = @namespace AND key = @key
|
|
`);
|
|
|
|
this.deleteExpiredRows = client.prepare(`
|
|
DELETE FROM package_cache
|
|
WHERE expiry <= unixepoch()
|
|
`);
|
|
|
|
this.countStatement = client.prepare(
|
|
'SELECT COUNT(*) as total FROM package_cache',
|
|
);
|
|
}
|
|
|
|
protected override writeRaw(
|
|
namespace: PackageCacheNamespace,
|
|
key: string,
|
|
data: Buffer,
|
|
ttlSeconds: number,
|
|
): void {
|
|
this.upsertStatement.run({
|
|
namespace,
|
|
key,
|
|
data,
|
|
ttlSeconds,
|
|
});
|
|
}
|
|
|
|
protected override readRaw(
|
|
namespace: PackageCacheNamespace,
|
|
key: string,
|
|
): Buffer | undefined {
|
|
const row = this.getStatement.get({ namespace, key }) as
|
|
| { data: Uint8Array }
|
|
| undefined;
|
|
|
|
if (!row) {
|
|
return undefined;
|
|
}
|
|
|
|
// `Buffer.from(row.data)` copies the whole payload.
|
|
// This call does zero copy.
|
|
return Buffer.from(
|
|
row.data.buffer,
|
|
row.data.byteOffset,
|
|
row.data.byteLength,
|
|
);
|
|
}
|
|
|
|
protected override rm(namespace: PackageCacheNamespace, key: string): void {
|
|
logger.trace({ namespace, key }, 'Removing cache entry');
|
|
this.deleteStatement.run({ namespace, key });
|
|
}
|
|
|
|
override destroy(): Promise<void> {
|
|
try {
|
|
const startTime = Date.now();
|
|
// `COUNT(*)` is always returning a row
|
|
const totalCount = this.countStatement.get()!.total as number;
|
|
const { changes: deletedCount } = this.deleteExpiredRows.run();
|
|
const durationMs = Date.now() - startTime;
|
|
logger.debug(
|
|
`SQLite package cache: deleted ${deletedCount} of ${totalCount} entries in ${durationMs}ms`,
|
|
);
|
|
} catch (err) {
|
|
logger.warn({ err }, 'SQLite package cache cleanup failed');
|
|
}
|
|
|
|
try {
|
|
this.client.close();
|
|
} catch (err) {
|
|
logger.warn({ err }, 'SQLite package cache close failed');
|
|
}
|
|
|
|
return Promise.resolve();
|
|
}
|
|
}
|