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>
209 lines
4.6 KiB
TypeScript
209 lines
4.6 KiB
TypeScript
import { DateTime } from 'luxon';
|
|
import { z } from 'zod/v4';
|
|
import { logger } from '../../lib/logger/index.ts';
|
|
import { Json } from '../../lib/util/schema-utils/index.ts';
|
|
import { exec } from '../utils/exec.ts';
|
|
|
|
export interface ItemsEntity {
|
|
url: string;
|
|
number: number;
|
|
title: string;
|
|
labels: LabelsEntity[];
|
|
issueType: 'Bug' | 'Feature';
|
|
}
|
|
|
|
export interface LabelsEntity {
|
|
name: string;
|
|
}
|
|
|
|
export interface RenovateOpenItems {
|
|
managers: OpenItems;
|
|
platforms: OpenItems;
|
|
datasources: OpenItems;
|
|
versionings: OpenItems;
|
|
}
|
|
|
|
export type OpenItems = Record<string, Items | undefined>;
|
|
|
|
export interface Items {
|
|
bugs: ItemsEntity[];
|
|
features: ItemsEntity[];
|
|
}
|
|
|
|
const GhOutput = Json.pipe(
|
|
z.array(
|
|
z.object({
|
|
url: z.string(),
|
|
title: z.string(),
|
|
labels: z.array(
|
|
z.object({
|
|
name: z.string(),
|
|
}),
|
|
),
|
|
number: z.number(),
|
|
}),
|
|
),
|
|
);
|
|
|
|
async function getIssuesByIssueType(
|
|
issueType: 'Bug' | 'Feature',
|
|
): Promise<ItemsEntity[]> {
|
|
const execRes = await exec(
|
|
'gh',
|
|
[
|
|
'issue',
|
|
'list',
|
|
'--json',
|
|
'title,number,url,labels',
|
|
'--search',
|
|
`type:${issueType}`,
|
|
'--limit',
|
|
'1000',
|
|
],
|
|
{
|
|
env: {
|
|
GITHUB_TOKEN: process.env.GITHUB_TOKEN,
|
|
},
|
|
},
|
|
);
|
|
const res = GhOutput.parse(execRes.stdout);
|
|
|
|
return res.map((issue) => {
|
|
return { ...issue, issueType };
|
|
});
|
|
}
|
|
|
|
export async function getOpenGitHubItems(): Promise<RenovateOpenItems> {
|
|
const result: RenovateOpenItems = {
|
|
managers: {},
|
|
platforms: {},
|
|
datasources: {},
|
|
versionings: {},
|
|
};
|
|
|
|
if (process.env.SKIP_GITHUB_ISSUES) {
|
|
logger.warn('Skipping GitHub issues');
|
|
return result;
|
|
}
|
|
|
|
if (!process.env.GITHUB_TOKEN) {
|
|
logger.warn(
|
|
'No GITHUB_TOKEN found in env, cannot fetch Github issues. Skipping...',
|
|
);
|
|
return result;
|
|
}
|
|
|
|
if (
|
|
process.env.CI &&
|
|
process.env.GITHUB_REF === 'main' &&
|
|
process.env.GITHUB_REPOSITORY !== 'renovatebot/renovatebot.github.io' &&
|
|
process.env.GITHUB_REPOSITORY !== 'renovatebot/renovate'
|
|
) {
|
|
logger.warn(
|
|
{
|
|
repository: process.env.GITHUB_REPOSITORY,
|
|
ref: process.env.GITHUB_REF,
|
|
},
|
|
"Skipping collection of open GitHub Issues, as we're running CI on a non-HEAD branch of Renovate or its docs site",
|
|
);
|
|
return result;
|
|
}
|
|
|
|
try {
|
|
const rawItems = (await getIssuesByIssueType('Bug')).concat(
|
|
await getIssuesByIssueType('Feature'),
|
|
);
|
|
|
|
result.managers = extractIssues(rawItems, 'manager:');
|
|
result.platforms = extractIssues(rawItems, 'platform:');
|
|
result.datasources = extractIssues(rawItems, 'datasource:');
|
|
result.versionings = extractIssues(rawItems, 'versioning:');
|
|
|
|
return result;
|
|
} catch (err) {
|
|
logger.error({ err }, 'Error getting query results');
|
|
if (process.env.CI) {
|
|
throw err;
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
|
|
function extractIssues(items: ItemsEntity[], labelPrefix: string): OpenItems {
|
|
const issuesMap: OpenItems = {};
|
|
|
|
for (const item of items) {
|
|
const type = item.issueType;
|
|
|
|
if (!type) {
|
|
continue;
|
|
}
|
|
const label = item.labels
|
|
.find((l) => l.name.startsWith(labelPrefix))
|
|
?.name.split(':')[1];
|
|
if (!label) {
|
|
continue;
|
|
}
|
|
issuesMap[label] ??= { bugs: [], features: [] };
|
|
switch (type) {
|
|
case 'Bug':
|
|
issuesMap[label]?.bugs.push(item);
|
|
break;
|
|
case 'Feature':
|
|
issuesMap[label]?.features.push(item);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
return issuesMap;
|
|
}
|
|
|
|
function stringifyIssues(items: ItemsEntity[] | undefined): string {
|
|
if (!items) {
|
|
return '';
|
|
}
|
|
let list = '';
|
|
for (const item of items) {
|
|
list += ` - ${item.title} [#${item.number}](${item.url})\n`;
|
|
}
|
|
return list;
|
|
}
|
|
|
|
export function generateFeatureAndBugMarkdown(
|
|
issuesMap: OpenItems,
|
|
key: string,
|
|
): string {
|
|
let md = '\n\n';
|
|
|
|
const featureList = stringifyIssues(issuesMap[key]?.features);
|
|
const bugList = stringifyIssues(issuesMap[key]?.bugs);
|
|
|
|
if (featureList || bugList) {
|
|
md += '## Open items\n\n';
|
|
}
|
|
|
|
if (featureList || bugList) {
|
|
const now = DateTime.utc().toFormat('MMMM dd, yyyy');
|
|
const lists = `list of ${featureList ? 'features' : ''}${
|
|
featureList && bugList ? ' and ' : ''
|
|
}${bugList ? 'bugs' : ''}`;
|
|
md += `The below ${lists} were current when this page was generated on ${now}.\n\n`;
|
|
}
|
|
|
|
if (featureList) {
|
|
md += '### Feature requests\n\n';
|
|
md += featureList;
|
|
md += '\n';
|
|
}
|
|
|
|
if (bugList) {
|
|
md += '### Bug reports\n\n';
|
|
md += bugList;
|
|
md += '\n';
|
|
}
|
|
|
|
return md;
|
|
}
|