mirror of
https://github.com/renovatebot/renovate.git
synced 2026-10-27 07:50:47 +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>
335 lines
8.6 KiB
TypeScript
335 lines
8.6 KiB
TypeScript
import { isNonEmptyString, isObject } from '@sindresorhus/is';
|
|
import { DateTime } from 'luxon';
|
|
import type { NewValueConfig, VersioningApi } from '../types.ts';
|
|
import { parse } from './parse.ts';
|
|
import { sortParsed } from './util.ts';
|
|
|
|
export const id = 'rust-release-channel';
|
|
export const displayName = 'Rust Release Channel';
|
|
export const urls = [
|
|
'[Rustup - Toolchains](https://rust-lang.github.io/rustup/concepts/toolchains.html)',
|
|
];
|
|
export const supportsRanges = true;
|
|
export const supportedRangeStrategies = ['pin', 'replace'];
|
|
|
|
/** Rust 1.0.0 release date for major version calculation */
|
|
const rust1Date = DateTime.utc(2015, 5, 15);
|
|
|
|
class RustReleaseChannelVersioning implements VersioningApi {
|
|
isValid(input: string): boolean {
|
|
return parse(input) !== null;
|
|
}
|
|
|
|
isVersion(input: string | undefined | null): boolean {
|
|
if (!isNonEmptyString(input)) {
|
|
return false;
|
|
}
|
|
|
|
const parsed = parse(input);
|
|
if (!parsed) {
|
|
return false;
|
|
}
|
|
|
|
const { channel, date } = parsed;
|
|
|
|
// Support dated nightly versions like `nightly-2025-11-24`
|
|
if (channel === 'nightly') {
|
|
return date !== undefined;
|
|
}
|
|
|
|
// Support versioned channels like `1.82.0` or `1.83.0-beta.5`
|
|
if (isObject(channel)) {
|
|
// Without patch, it's a range
|
|
if (channel.patch === undefined) {
|
|
return false;
|
|
}
|
|
|
|
// With beta without number, it's a range
|
|
return !channel.prerelease || channel.prerelease.number !== undefined;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
isSingleVersion(input: string): boolean {
|
|
return this.isVersion(input);
|
|
}
|
|
|
|
isStable(version: string): boolean {
|
|
const parsed = parse(version);
|
|
if (!parsed) {
|
|
return false;
|
|
}
|
|
|
|
const { channel } = parsed;
|
|
|
|
// Channel names and ranges are not stable
|
|
if (!isObject(channel)) {
|
|
return false;
|
|
}
|
|
|
|
// Version without patch is a range
|
|
if (channel.patch === undefined) {
|
|
return false;
|
|
}
|
|
|
|
// Version with prerelease is not stable
|
|
return !channel.prerelease;
|
|
}
|
|
|
|
isCompatible(version: string, current?: string): boolean {
|
|
if (!current) {
|
|
return true;
|
|
}
|
|
|
|
const parsedVersion = parse(version);
|
|
const parsedCurrent = parse(current);
|
|
if (!parsedVersion || !parsedCurrent) {
|
|
return false;
|
|
}
|
|
|
|
// Host triples must match (both undefined, or both the same value)
|
|
if (parsedVersion.host !== parsedCurrent.host) {
|
|
return false;
|
|
}
|
|
|
|
const channelVersion = parsedVersion.channel;
|
|
const channelCurrent = parsedCurrent.channel;
|
|
|
|
// Nightly versions are compatible only with other nightlies
|
|
// Stable and beta versions are compatible with each other
|
|
|
|
const isVersionNightly = channelVersion === 'nightly';
|
|
const isCurrentNightly = channelCurrent === 'nightly';
|
|
|
|
return isVersionNightly === isCurrentNightly;
|
|
}
|
|
|
|
getMajor(version: string): number | null {
|
|
const parsed = parse(version);
|
|
if (!parsed) {
|
|
return null;
|
|
}
|
|
|
|
const { channel, date } = parsed;
|
|
|
|
// Nightly with date
|
|
if (channel === 'nightly' && date) {
|
|
const versionDate = DateTime.utc(date.year, date.month, date.day);
|
|
return versionDate >= rust1Date ? 1 : 0;
|
|
}
|
|
|
|
// Version object
|
|
if (isObject(channel)) {
|
|
return channel.major;
|
|
}
|
|
|
|
// Default to major version 1 otherwise
|
|
return 1;
|
|
}
|
|
|
|
getMinor(version: string): number | null {
|
|
const parsed = parse(version);
|
|
if (!parsed) {
|
|
return null;
|
|
}
|
|
|
|
const { channel } = parsed;
|
|
|
|
// Only version objects have minor
|
|
if (isObject(channel)) {
|
|
return channel.minor;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
getPatch(version: string): number | null {
|
|
const parsed = parse(version);
|
|
if (!parsed) {
|
|
return null;
|
|
}
|
|
|
|
const { channel } = parsed;
|
|
|
|
// Only version objects have patch
|
|
if (isObject(channel)) {
|
|
return channel.patch ?? null;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
sortVersions(version: string, other: string): number {
|
|
const parsedA = parse(version);
|
|
const parsedB = parse(other);
|
|
|
|
if (!parsedA || !parsedB) {
|
|
return version.localeCompare(other);
|
|
}
|
|
|
|
return sortParsed(parsedA, parsedB);
|
|
}
|
|
|
|
equals(version: string, other: string): boolean {
|
|
const parsedA = parse(version);
|
|
const parsedB = parse(other);
|
|
|
|
if (!parsedA || !parsedB) {
|
|
return false;
|
|
}
|
|
|
|
return sortParsed(parsedA, parsedB) === 0;
|
|
}
|
|
|
|
isGreaterThan(version: string, other: string): boolean {
|
|
return this.sortVersions(version, other) > 0;
|
|
}
|
|
|
|
getSatisfyingVersion(versions: string[], range: string): string | null {
|
|
const matching = versions.filter((version) => this.matches(version, range));
|
|
if (matching.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
// Sort and return the highest (last in sorted array)
|
|
matching.sort((a, b) => this.sortVersions(a, b));
|
|
return matching.at(-1)!;
|
|
}
|
|
|
|
minSatisfyingVersion(versions: string[], range: string): string | null {
|
|
const matching = versions.filter((version) => this.matches(version, range));
|
|
if (matching.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
// Sort and return the lowest (first in sorted array)
|
|
matching.sort((a, b) => this.sortVersions(a, b));
|
|
return matching[0];
|
|
}
|
|
|
|
getNewValue({
|
|
currentValue,
|
|
rangeStrategy,
|
|
newVersion,
|
|
}: NewValueConfig): string | null {
|
|
const parsedCurrent = parse(currentValue);
|
|
const parsedNew = parse(newVersion);
|
|
|
|
if (!parsedCurrent || !parsedNew) {
|
|
return null;
|
|
}
|
|
|
|
const currentChannel = parsedCurrent.channel;
|
|
const newChannel = parsedNew.channel;
|
|
|
|
// Pin strategy: always return exact newVersion
|
|
if (rangeStrategy === 'pin') {
|
|
return newVersion;
|
|
}
|
|
|
|
// Dated nightlies should be replaced with new dated nightly
|
|
if (currentChannel === 'nightly' && parsedCurrent.date) {
|
|
return newVersion;
|
|
}
|
|
|
|
// Channel names (without dates) stay as channel names
|
|
if (!isObject(currentChannel)) {
|
|
return currentValue;
|
|
}
|
|
|
|
if (isObject(newChannel)) {
|
|
// If current had no patch, new shouldn't either (range style)
|
|
if (currentChannel.patch === undefined) {
|
|
return `${newChannel.major}.${newChannel.minor}`;
|
|
}
|
|
|
|
// If current had prerelease without number (beta range), create beta range
|
|
if (
|
|
currentChannel.prerelease &&
|
|
currentChannel.prerelease.number === undefined
|
|
) {
|
|
return `${newChannel.major}.${newChannel.minor}.${newChannel.patch ?? 0}-beta`;
|
|
}
|
|
}
|
|
|
|
return newVersion;
|
|
}
|
|
|
|
matches(version: string, range: string): boolean {
|
|
const parsedVersion = parse(version);
|
|
const parsedRange = parse(range);
|
|
|
|
if (!parsedVersion || !parsedRange) {
|
|
return false;
|
|
}
|
|
|
|
const versionChannel = parsedVersion.channel;
|
|
const rangeChannel = parsedRange.channel;
|
|
|
|
// For 'nightly' range, version must be a dated nightly
|
|
if (rangeChannel === 'nightly') {
|
|
return versionChannel === 'nightly' && parsedVersion.date !== undefined;
|
|
}
|
|
|
|
// For 'beta' range, version must be a beta version
|
|
if (rangeChannel === 'beta') {
|
|
return (
|
|
isObject(versionChannel) && versionChannel.prerelease?.name === 'beta'
|
|
);
|
|
}
|
|
|
|
// For 'stable' range, version must be a final release
|
|
if (rangeChannel === 'stable') {
|
|
return (
|
|
isObject(versionChannel) &&
|
|
versionChannel.patch !== undefined &&
|
|
!versionChannel.prerelease
|
|
);
|
|
}
|
|
|
|
// Handle version range matching
|
|
if (isObject(rangeChannel) && isObject(versionChannel)) {
|
|
// Major and minor must match
|
|
if (
|
|
versionChannel.major !== rangeChannel.major ||
|
|
versionChannel.minor !== rangeChannel.minor
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
// If range has no patch, it's a range (e.g., "1.82" matches "1.82.0", "1.82.1", etc.)
|
|
if (rangeChannel.patch === undefined) {
|
|
return true;
|
|
}
|
|
|
|
// Patch must match
|
|
if (versionChannel.patch !== rangeChannel.patch) {
|
|
return false;
|
|
}
|
|
|
|
// If range has no prerelease, version must also have no prerelease
|
|
if (!rangeChannel.prerelease) {
|
|
return !versionChannel.prerelease;
|
|
}
|
|
|
|
// If range has prerelease but no number, it's a range (e.g., "1.83.0-beta" matches all beta versions)
|
|
if (rangeChannel.prerelease.number === undefined) {
|
|
return !!versionChannel.prerelease;
|
|
}
|
|
|
|
// Both have prerelease with numbers, they must match exactly
|
|
return (
|
|
!!versionChannel.prerelease &&
|
|
versionChannel.prerelease?.number === rangeChannel.prerelease.number
|
|
);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export const api = new RustReleaseChannelVersioning();
|
|
|
|
export default api;
|