Files
renovate/lib/modules/platform/github/issue.spec.ts
Sebastian Poxhofer ac0407bdee refactor(linters): add linters for Json.pipe and schema location (#44684)
* 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>
2026-07-23 13:43:41 +00:00

275 lines
6.6 KiB
TypeScript

import * as memCache from '../../../util/cache/memory/index.ts';
import { getCache, resetCache } from '../../../util/cache/repository/index.ts';
import { GithubIssueCache } from './issue.ts';
import type { GithubIssue } from './schema.ts';
describe('modules/platform/github/issue', () => {
describe('GithubIssueCache', () => {
let cache = getCache();
beforeEach(() => {
resetCache();
cache = getCache();
memCache.init();
});
it('returns null for empty cache', () => {
expect(GithubIssueCache.getIssues()).toBeNull();
});
it('stores issues to the cache', () => {
const issues: GithubIssue[] = [
{
number: 1,
body: 'body-1',
state: 'open',
title: 'title-1',
lastModified: '2020-01-01T00:00:00.000Z',
},
{
number: 2,
body: 'body-2',
state: 'closed',
title: 'title-2',
lastModified: '2020-01-02T00:00:00.000Z',
},
];
GithubIssueCache.setIssues(issues);
expect(cache).toEqual({
platform: {
github: {
issuesCache: {
'1': {
number: 1,
body: 'body-1',
state: 'open',
title: 'title-1',
lastModified: '2020-01-01T00:00:00.000Z',
},
'2': {
number: 2,
body: 'body-2',
state: 'closed',
title: 'title-2',
lastModified: '2020-01-02T00:00:00.000Z',
},
},
},
},
});
});
it('returns issues from the cache in the correct order', () => {
cache.platform = {
github: {
issuesCache: {
'2': {
number: 2,
body: 'body-2',
state: 'closed',
title: 'title-2',
lastModified: '2020-01-02T00:00:00.000Z',
},
'1': {
number: 1,
body: 'body-1',
state: 'open',
title: 'title-1',
lastModified: '2020-01-01T00:00:00.000Z',
},
'3': {
number: 3,
body: 'body-3',
state: 'closed',
title: 'title-3',
lastModified: '2020-01-03T00:00:00.000Z',
},
},
},
};
const res = GithubIssueCache.getIssues();
expect(res).toEqual([
{
number: 3,
body: 'body-3',
state: 'closed',
title: 'title-3',
lastModified: '2020-01-03T00:00:00.000Z',
},
{
number: 2,
body: 'body-2',
state: 'closed',
title: 'title-2',
lastModified: '2020-01-02T00:00:00.000Z',
},
{
number: 1,
body: 'body-1',
state: 'open',
title: 'title-1',
lastModified: '2020-01-01T00:00:00.000Z',
},
]);
});
it('updates particular issue in the cache', () => {
cache.platform = {
github: {
issuesCache: {
'1': {
number: 1,
body: 'body-1',
state: 'open',
title: 'title-1',
lastModified: '2020-01-01T00:00:00.000Z',
},
},
},
};
const issue: GithubIssue = {
number: 1,
body: 'new-body-1',
state: 'closed',
title: 'new-title-1',
lastModified: '2020-01-02T00:00:00.000Z',
};
GithubIssueCache.updateIssue(issue);
expect(cache).toEqual({
platform: {
github: {
issuesCache: {
'1': {
number: 1,
body: 'new-body-1',
state: 'closed',
title: 'new-title-1',
lastModified: '2020-01-02T00:00:00.000Z',
},
},
},
},
});
});
it('removes particular issue from the cache', () => {
cache.platform = {
github: {
issuesCache: {
'1': {
number: 1,
body: 'body-1',
state: 'open',
title: 'title-1',
lastModified: '2020-01-01T00:00:00.000Z',
},
},
},
};
GithubIssueCache.deleteIssue(1);
expect(cache).toEqual({
platform: {
github: {
issuesCache: {},
},
},
});
});
it('reconciles cache', () => {
cache.platform = {
github: {
issuesCache: {
'1': {
number: 1,
body: 'body-1',
state: 'open',
title: 'title-1',
lastModified: '2020-01-01T00:00:00.000Z',
},
'2': {
number: 2,
body: 'body-2',
state: 'closed',
title: 'title-2',
lastModified: '2020-01-02T00:00:00.000Z',
},
},
},
};
GithubIssueCache.addIssuesToReconcile([
{
number: 1,
body: 'new-body-1',
state: 'open',
title: 'new-title-1',
lastModified: '2020-01-04T00:00:00.000Z',
},
{
number: 2,
body: 'body-2',
state: 'closed',
title: 'title-2',
lastModified: '2020-01-02T00:00:00.000Z',
},
]);
const res = GithubIssueCache.getIssues();
expect(res).toEqual([
{
number: 1,
body: 'new-body-1',
state: 'open',
title: 'new-title-1',
lastModified: '2020-01-04T00:00:00.000Z',
},
{
number: 2,
body: 'body-2',
state: 'closed',
title: 'title-2',
lastModified: '2020-01-02T00:00:00.000Z',
},
]);
});
it('resets cache during failed reconciliation', () => {
cache.platform = {
github: {
issuesCache: {
'1': {
number: 1,
body: 'body-1',
state: 'open',
title: 'title-1',
lastModified: '2020-01-01T00:00:00.000Z',
},
'2': {
number: 2,
body: 'body-2',
state: 'closed',
title: 'title-2',
lastModified: '2020-01-02T00:00:00.000Z',
},
},
},
};
GithubIssueCache.addIssuesToReconcile([]);
const res = GithubIssueCache.getIssues();
expect(res).toBeNull();
});
});
});