mirror of
https://github.com/renovatebot/renovate.git
synced 2026-10-25 06:51:11 +00:00
ac0407bdee
* feat(lint): add renovate/zod-schema-location and renovate/prefer-json-pipe rules Backlog item #23: reviewers repeatedly ask for consistent Zod schema structure. Two new oxlint JS-plugin rules, both at "warn": - renovate/zod-schema-location flags `export const X = <zod schema>` at module scope in lib/modules/**/ files that aren't schema.ts (or a colocated schema/ directory), reusing the leftmost-identifier schema detection from zod-schema-naming.js / prefer-nullish-util.js. Only exported top-level schemas are flagged — non-exported ones are treated as intentionally local/trivial (e.g. tightly coupled to file-local helpers), which keeps the rule's signal high. - renovate/prefer-json-pipe flags `Schema.parse(JSON.parse(x))` / `Schema.safeParse(JSON.parse(x))` and suggests `Json.pipe(Schema)` from lib/util/schema-utils instead. A third mined slice (require a matching `z.infer` type export for every exported schema) was evaluated and skipped: ~275+ exported schema-like consts in schema.ts files vs. 231 existing z.infer exports repo-wide is too high a mismatch to flag mechanically without heavy per-site judgment, so it would be mostly false positives. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor: fix a clean subset of renovate/zod-schema-location and renovate/prefer-json-pipe violations renovate/prefer-json-pipe: fixed all 5 safe `.parse(JSON.parse(...))` call sites (bazel datasource, generate-mise-registry script, a hook spec) by switching to `Json.pipe(Schema).parse(...)`. Left the 2 `.safeParse(JSON.parse(...))` sites (pretooluse-check.ts, github-query-items.ts) unfixed: JSON.parse throwing before safeParse is reached today crashes the script, whereas Json.pipe folds the bad-JSON case into a graceful `{ success: false }`, which is an observable behavior change, not just a mechanical rewrite. renovate/zod-schema-location: fixed the one clean, self-contained violation (platform/github's GithubIssue schema, moved into the already-existing platform/github/schema.ts) as a representative example. Left the other 20: they require moving exported schemas out of files where they're built from local helper functions/config tables (manager/github-actions/community.ts) or restructuring an entire directory into a new schema.ts with import-graph updates across many files (manager/bazel/rules/*.ts, manager/bazel-module/parser/*.ts) — real refactors needing per-site judgment, not mechanical fixes. Both rules stay at "warn": renovate/prefer-json-pipe has 2 remaining violations that shouldn't be auto-fixed, and renovate/zod-schema-location has 20. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor: resolve remaining prefer-json-pipe and zod-schema-location findings - github-actions: move the derived `CommunityActions` union into the existing colocated `schema.ts`; the action config table and the `actionSchema` builder stay in `community.ts` and are now exported. - tools/docs: fold `JSON.parse` + `safeParse`/rethrow of the `gh` CLI output into `Json.pipe(...)`. Invalid JSON now surfaces as a ZodError instead of a SyntaxError; the only consumer catches generically, so behavior is preserved. - tools/agents/hooks: keep `JSON.parse` + `safeParse` with a reasoned disable comment — hook scripts must stay dependency-light, and the `Json` helper lives in lib/util/schema-utils, which drags in the full lib import graph (logger, yaml, toml). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(lint): enforce zod-schema-location and prefer-json-pipe as errors Narrow `renovate/zod-schema-location` to files whose directory already contains a colocated `schema.ts`. The mined reviewer feedback ("move this to schema.ts") presumes the module follows that convention and the schema leaked into a logic file. Modules that deliberately export schemas from differently-named schema-only files — bazel `rules/*.ts` Target schemas, bazel-module `parser/fragments.ts` — are a different, accepted layout, not "schema mixed into logic", and are no longer flagged. With that narrowing and the preceding migrations, both rules are clean repo-wide, so promote them from "warn" to "error". Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor: drop GithubIssue re-export from issue.ts Consumers now import the schema directly from schema.ts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(lint): register zod-schema-location rule The rule registration was lost while merging main, so oxlint failed to parse .oxlintrc.json with "Rule 'zod-schema-location' not found in plugin 'renovate'". Re-add the import and rules entry, and restore alphabetical ordering for prefer-json-pipe. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(lint): allow sync fs in zod-schema-location rule ESLint rules execute synchronously, so the sibling `schema.ts` lookup cannot use async fs. Silence `no-sync` with a justification. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
131 lines
3.7 KiB
TypeScript
131 lines
3.7 KiB
TypeScript
import { DateTime } from 'luxon';
|
|
import { logger } from '../../../logger/index.ts';
|
|
import * as memCache from '../../../util/cache/memory/index.ts';
|
|
import { getCache } from '../../../util/cache/repository/index.ts';
|
|
import type { GithubIssue } from './schema.ts';
|
|
|
|
type CacheData = Record<number, GithubIssue>;
|
|
|
|
export class GithubIssueCache {
|
|
private static reset(cacheData: CacheData | null): void {
|
|
memCache.set('github-issues-reconcile-queue', null);
|
|
const repoCache = getCache();
|
|
repoCache.platform ??= {};
|
|
repoCache.platform.github ??= {};
|
|
if (cacheData) {
|
|
repoCache.platform.github.issuesCache = cacheData;
|
|
} else {
|
|
delete repoCache.platform.github.issuesCache;
|
|
}
|
|
}
|
|
|
|
private static get data(): CacheData | null {
|
|
let cacheData: CacheData | undefined | null = getCache().platform?.github
|
|
?.issuesCache as CacheData | undefined;
|
|
if (!cacheData) {
|
|
return null;
|
|
}
|
|
|
|
cacheData = this.reconcile(cacheData);
|
|
return cacheData;
|
|
}
|
|
|
|
static getIssues(): GithubIssue[] | null {
|
|
const cacheData = this.data;
|
|
if (!cacheData) {
|
|
return null;
|
|
}
|
|
|
|
const sortedResult = Object.values(cacheData).sort(
|
|
({ lastModified: a }, { lastModified: b }) =>
|
|
DateTime.fromISO(b).toMillis() - DateTime.fromISO(a).toMillis(),
|
|
);
|
|
|
|
return sortedResult;
|
|
}
|
|
|
|
static setIssues(issues: GithubIssue[]): void {
|
|
const cacheData: CacheData = {};
|
|
for (const issue of issues) {
|
|
cacheData[issue.number] = issue;
|
|
}
|
|
|
|
logger.debug(`Issues cache: Setting ${issues.length} issues in cache`);
|
|
this.reset(cacheData);
|
|
}
|
|
|
|
static updateIssue(issue: GithubIssue): void {
|
|
const cacheData = this.data;
|
|
if (cacheData) {
|
|
logger.debug(`Issues cache: Updating issue ${issue.number} in cache`);
|
|
cacheData[issue.number] = issue;
|
|
}
|
|
}
|
|
|
|
static deleteIssue(number: number): void {
|
|
const cacheData = this.data;
|
|
if (cacheData) {
|
|
logger.debug(`Issues cache: Deleting issue ${number} from cache`);
|
|
delete cacheData[number];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* At the moment of repo initialization, repository cache is not available.
|
|
* What we can do is to store issues for later reconciliation.
|
|
*/
|
|
static addIssuesToReconcile(issues: GithubIssue[] | undefined): void {
|
|
logger.debug(
|
|
`Issues cache: Adding ${issues?.length} issues to reconcile queue`,
|
|
);
|
|
memCache.set('github-issues-reconcile-queue', issues);
|
|
}
|
|
|
|
private static reconcile(cacheData: CacheData): CacheData | null {
|
|
const issuesToReconcile = memCache.get<GithubIssue[]>(
|
|
'github-issues-reconcile-queue',
|
|
);
|
|
if (!issuesToReconcile) {
|
|
return cacheData;
|
|
}
|
|
|
|
let isReconciled = false;
|
|
|
|
for (const issue of issuesToReconcile) {
|
|
const cachedIssue = cacheData[issue.number];
|
|
|
|
// If we reached the item which is already in the cache,
|
|
// it means sync is done.
|
|
if (
|
|
cachedIssue?.number === issue.number &&
|
|
cachedIssue.lastModified === issue.lastModified
|
|
) {
|
|
isReconciled = true;
|
|
logger.debug(`Issues cache: Done reconciling at issue ${issue.number}`);
|
|
break;
|
|
}
|
|
|
|
cacheData[issue.number] = issue;
|
|
}
|
|
|
|
// If we've just iterated over all the items in the cache,
|
|
// it means sync is also done.
|
|
if (issuesToReconcile.length >= Object.keys(cacheData).length) {
|
|
logger.debug(
|
|
`Issues cache: Done reconciling by iterating over all items`,
|
|
);
|
|
isReconciled = true;
|
|
}
|
|
|
|
if (!isReconciled) {
|
|
logger.debug('Issues cache: reset');
|
|
this.reset(null);
|
|
return null;
|
|
}
|
|
|
|
logger.debug('Issues cache: synced');
|
|
this.reset(cacheData);
|
|
return cacheData;
|
|
}
|
|
}
|