mirror of
https://github.com/renovatebot/renovate.git
synced 2026-10-26 07:20:39 +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>
186 lines
4.7 KiB
TypeScript
186 lines
4.7 KiB
TypeScript
import { z } from 'zod/v4';
|
|
import { logger } from '../../../logger/index.ts';
|
|
import {
|
|
DeepNullish,
|
|
LooseArray,
|
|
Nullish,
|
|
} from '../../../util/schema-utils/index.ts';
|
|
|
|
const Ecosystem = z.enum([
|
|
'actions',
|
|
'composer',
|
|
'go',
|
|
'maven',
|
|
'npm',
|
|
'nuget',
|
|
'pip',
|
|
'rubygems',
|
|
'rust',
|
|
]);
|
|
export type Ecosystem = z.infer<typeof Ecosystem>;
|
|
|
|
const Package = z.object({
|
|
ecosystem: Ecosystem.catch((ctx) => {
|
|
logger.debug(
|
|
{ ecosystem: ctx.input },
|
|
'Skipping vulnerability alert with unsupported ecosystem',
|
|
);
|
|
return undefined as any;
|
|
}),
|
|
name: z.string(),
|
|
});
|
|
|
|
const Severity = z.enum(['low', 'medium', 'high', 'critical']);
|
|
|
|
const SecurityVulnerability = z
|
|
.object({
|
|
first_patched_version: Nullish(z.object({ identifier: z.string() })),
|
|
package: Package,
|
|
severity: Severity,
|
|
vulnerable_version_range: z.string(),
|
|
})
|
|
.nullable();
|
|
|
|
const CvssSeverity = z.object({
|
|
vector_string: z.string().nullable(),
|
|
score: z.number().nullable(),
|
|
});
|
|
|
|
const SecurityAdvisory = z.object({
|
|
ghsa_id: z.string(),
|
|
summary: z.string(),
|
|
description: z.string(),
|
|
identifiers: z.array(
|
|
z.object({
|
|
type: z.string(),
|
|
value: z.string(),
|
|
}),
|
|
),
|
|
references: z.array(z.object({ url: z.string() })).optional(),
|
|
severity: Severity,
|
|
cvss_severities: Nullish(
|
|
DeepNullish(
|
|
z.object({
|
|
cvss_v3: CvssSeverity.optional(),
|
|
cvss_v4: CvssSeverity.optional(),
|
|
}),
|
|
),
|
|
),
|
|
});
|
|
export type SecurityAdvisory = z.infer<typeof SecurityAdvisory>;
|
|
|
|
export const GithubVulnerabilityAlerts = LooseArray(
|
|
z.object({
|
|
dismissed_reason: Nullish(z.string()),
|
|
security_advisory: SecurityAdvisory,
|
|
security_vulnerability: SecurityVulnerability,
|
|
dependency: z.object({
|
|
manifest_path: z.string(),
|
|
}),
|
|
}),
|
|
{
|
|
onError: ({ error }) => {
|
|
logger.debug(
|
|
{ error },
|
|
'Vulnerability Alert: Failed to parse some alerts',
|
|
);
|
|
},
|
|
},
|
|
).transform((alerts) =>
|
|
alerts.filter((alert) => alert.security_vulnerability?.package?.ecosystem),
|
|
);
|
|
export type GithubVulnerabilityAlerts = z.infer<
|
|
typeof GithubVulnerabilityAlerts
|
|
>;
|
|
export type GithubVulnerabilityAlert = GithubVulnerabilityAlerts[number];
|
|
|
|
// https://docs.github.com/en/rest/repos/contents?apiVersion=2022-11-28#get-repository-content
|
|
const GithubResponseMetadata = z.object({
|
|
name: z.string(),
|
|
path: z.string(),
|
|
});
|
|
|
|
export const GithubFileMeta = GithubResponseMetadata.extend({
|
|
type: z.literal('file'),
|
|
});
|
|
export type GithubFileMeta = z.infer<typeof GithubFileMeta>;
|
|
|
|
export const GithubFile = GithubFileMeta.extend({
|
|
content: z.string(),
|
|
encoding: z.string(),
|
|
});
|
|
export type GithubFile = z.infer<typeof GithubFile>;
|
|
|
|
export const GithubDirectory = GithubResponseMetadata.extend({
|
|
type: z.literal('dir'),
|
|
});
|
|
|
|
export type GithubDirectory = z.infer<typeof GithubDirectory>;
|
|
|
|
export const GithubOtherContent = GithubResponseMetadata.extend({
|
|
type: z.literal('symlink').or(z.literal('submodule')),
|
|
});
|
|
|
|
export type GithubOtherContent = z.infer<typeof GithubOtherContent>;
|
|
|
|
export const GithubElement = GithubFile.or(GithubFileMeta)
|
|
.or(GithubDirectory)
|
|
.or(GithubOtherContent);
|
|
export type GithubElement = z.infer<typeof GithubElement>;
|
|
|
|
export const GithubContentResponse = z.array(GithubElement).or(GithubElement);
|
|
|
|
export const GithubBranchProtection = z.object({
|
|
required_status_checks: Nullish(
|
|
z.object({
|
|
strict: z.boolean(),
|
|
}),
|
|
),
|
|
});
|
|
export type GithubBranchProtection = z.infer<typeof GithubBranchProtection>;
|
|
|
|
const GithubRulesetRule = z.discriminatedUnion('type', [
|
|
z.object({
|
|
type: z.literal('non_fast_forward'),
|
|
}),
|
|
z.object({
|
|
type: z.literal('required_status_checks'),
|
|
parameters: z.object({
|
|
strict_required_status_checks_policy: z.boolean().optional(),
|
|
}),
|
|
}),
|
|
// prevents deletion
|
|
z.object({
|
|
type: z.literal('deletion'),
|
|
}),
|
|
]);
|
|
|
|
export const GithubBranchRulesets = LooseArray(GithubRulesetRule);
|
|
export type GithubBranchRulesets = z.infer<typeof GithubBranchRulesets>;
|
|
|
|
const GithubIssueBase = z.object({
|
|
number: z.number(),
|
|
state: z.string().transform((val) => val.toLowerCase()),
|
|
title: z.string(),
|
|
body: z.string(),
|
|
});
|
|
|
|
const GithubGraphqlIssue = GithubIssueBase.extend({
|
|
updatedAt: z.string(),
|
|
}).transform((issue) => {
|
|
const lastModified = issue.updatedAt;
|
|
const { number, state, title, body } = issue;
|
|
return { number, state, title, body, lastModified };
|
|
});
|
|
|
|
const GithubRestIssue = GithubIssueBase.extend({
|
|
updated_at: z.string(),
|
|
}).transform((issue) => {
|
|
const lastModified = issue.updated_at;
|
|
const { number, state, title, body } = issue;
|
|
return { number, state, title, body, lastModified };
|
|
});
|
|
|
|
export const GithubIssue = z.union([GithubGraphqlIssue, GithubRestIssue]);
|
|
export type GithubIssue = z.infer<typeof GithubIssue>;
|