Skip to content

Commit c9f2bbb

Browse files
committed
Add local Moon-based validation via scripts/check (#256599)
Introduce a new `scripts/check` entrypoint that uses Moon changed-file resolution to run eslint, jest, and type_check against local, staged, or branch-scoped changes. Keep the existing `eslint`, `jest`, and `type_check` commands intact for their default direct usage, and only route them through the new validation contract when scoped validation flags are present. This also adds dedicated contract runners for jest, eslint, and type_check, plus clearer help and shallow-repo warnings for the new workflow. Updates #255391 --------- Signed-off-by: Tyler Smalley <tyler.smalley@elastic.co> Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> (cherry picked from commit 3a3c8c5) # Conflicts: # packages/kbn-ts-type-check-cli/run_type_check_cli.ts # src/dev/eslint/lint_files.ts # src/dev/eslint/pick_files_to_lint.ts
1 parent 3591488 commit c9f2bbb

38 files changed

Lines changed: 4137 additions & 271 deletions

.moon/tasks/tag-jest-unit-tests.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ tasks:
99
command: node
1010
args:
1111
- scripts/jest.js
12-
- '--runInBand'
1312
- '--passWithNoTests'
1413
- '--config'
1514
- '@files(jest-config)'

packages/kbn-moon/jest.config.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
module.exports = {
11+
preset: '@kbn/test/jest_node',
12+
rootDir: '../..',
13+
roots: ['<rootDir>/packages/kbn-moon'],
14+
};

packages/kbn-moon/moon.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,12 @@ tags:
2727
- package
2828
- dev
2929
- group-undefined
30+
- jest-unit-tests
3031
fileGroups:
3132
src:
3233
- '**/*.ts'
3334
- '**/*.tsx'
3435
- '!target/**/*'
36+
jest-config:
37+
- jest.config.js
3538
tasks: {}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
const mockExeca = jest.fn();
11+
12+
jest.mock('fs', () => ({
13+
...jest.requireActual('fs'),
14+
existsSync: jest.fn().mockReturnValue(true),
15+
}));
16+
17+
jest.mock('@kbn/repo-info', () => ({
18+
REPO_ROOT: '/repo',
19+
}));
20+
21+
jest.mock('@kbn/dev-utils', () => ({
22+
getRemoteDefaultBranchRefs: jest.fn(),
23+
resolveNearestMergeBase: jest.fn(),
24+
}));
25+
26+
jest.mock('execa', () => ({
27+
__esModule: true,
28+
default: mockExeca,
29+
}));
30+
31+
const createMoonProjectsOutput = (projects: Array<{ id: string; sourceRoot: string }>) =>
32+
JSON.stringify({
33+
projects: projects.map((project) => ({
34+
id: project.id,
35+
source: project.sourceRoot,
36+
config: {
37+
project: {
38+
metadata: {
39+
sourceRoot: project.sourceRoot,
40+
},
41+
},
42+
},
43+
})),
44+
});
45+
46+
describe('getAffectedMoonProjectsFromChangedFiles', () => {
47+
beforeEach(() => {
48+
jest.resetModules();
49+
mockExeca.mockReset();
50+
});
51+
52+
it('adds the root project for repo-root TypeScript inputs', async () => {
53+
mockExeca.mockResolvedValueOnce({
54+
stdout: createMoonProjectsOutput([]),
55+
});
56+
57+
const { getAffectedMoonProjectsFromChangedFiles } = await import('./query_projects');
58+
59+
await expect(
60+
getAffectedMoonProjectsFromChangedFiles({
61+
changedFilesJson: JSON.stringify({ files: ['tsconfig.base.json'] }),
62+
})
63+
).resolves.toEqual([{ id: 'kibana', sourceRoot: '.' }]);
64+
});
65+
66+
it('adds the root project alongside affected package projects for typings changes', async () => {
67+
mockExeca.mockResolvedValueOnce({
68+
stdout: createMoonProjectsOutput([{ id: 'foo', sourceRoot: 'packages/foo' }]),
69+
});
70+
71+
const { getAffectedMoonProjectsFromChangedFiles } = await import('./query_projects');
72+
73+
await expect(
74+
getAffectedMoonProjectsFromChangedFiles({
75+
changedFilesJson: JSON.stringify({
76+
files: ['packages/foo/src/index.ts', 'typings/something.d.ts'],
77+
}),
78+
})
79+
).resolves.toEqual([
80+
{ id: 'foo', sourceRoot: 'packages/foo' },
81+
{ id: 'kibana', sourceRoot: '.' },
82+
]);
83+
});
84+
85+
it('does not add the root project for package-owned files', async () => {
86+
mockExeca.mockResolvedValueOnce({
87+
stdout: createMoonProjectsOutput([{ id: 'foo', sourceRoot: 'packages/foo' }]),
88+
});
89+
90+
const { getAffectedMoonProjectsFromChangedFiles } = await import('./query_projects');
91+
92+
await expect(
93+
getAffectedMoonProjectsFromChangedFiles({
94+
changedFilesJson: JSON.stringify({ files: ['packages/foo/src/index.ts'] }),
95+
})
96+
).resolves.toEqual([{ id: 'foo', sourceRoot: 'packages/foo' }]);
97+
});
98+
99+
it('does not add the root project for unrelated repo-root files', async () => {
100+
mockExeca.mockResolvedValueOnce({
101+
stdout: createMoonProjectsOutput([]),
102+
});
103+
104+
const { getAffectedMoonProjectsFromChangedFiles } = await import('./query_projects');
105+
106+
await expect(
107+
getAffectedMoonProjectsFromChangedFiles({
108+
changedFilesJson: JSON.stringify({ files: ['.github/CODEOWNERS'] }),
109+
})
110+
).resolves.toEqual([]);
111+
});
112+
113+
it('keeps Moon-reported root project results without querying all projects again', async () => {
114+
mockExeca.mockResolvedValueOnce({
115+
stdout: createMoonProjectsOutput([{ id: 'kibana', sourceRoot: '.' }]),
116+
});
117+
118+
const { getAffectedMoonProjectsFromChangedFiles } = await import('./query_projects');
119+
120+
await expect(
121+
getAffectedMoonProjectsFromChangedFiles({
122+
changedFilesJson: JSON.stringify({ files: ['tsconfig.base.json'] }),
123+
})
124+
).resolves.toEqual([{ id: 'kibana', sourceRoot: '.' }]);
125+
126+
expect(mockExeca).toHaveBeenCalledTimes(1);
127+
});
128+
});

packages/kbn-moon/src/query_projects.ts

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,15 +51,29 @@ interface MoonQueryProjectsResponse {
5151
}>;
5252
}
5353

54+
interface MoonChangedFilesInput {
55+
files?: string[];
56+
}
57+
5458
/** Options for resolving the affected base revision from git state. */
5559
export interface ResolveMoonAffectedBaseOptions {
5660
headRef?: string;
5761
}
5862

5963
export const ROOT_MOON_PROJECT_ID = 'kibana';
6064

65+
// Module-level cache — acceptable for short-lived CLI processes, tests mock getMoonExecutablePath.
6166
let moonExecutablePath: string | undefined;
6267

68+
const ROOT_MOON_PROJECT_TRIGGER_FILES = new Set([
69+
'tsconfig.json',
70+
'tsconfig.base.json',
71+
'tsconfig.base.type_check.json',
72+
'tsconfig.browser.json',
73+
'tsconfig.refs.json',
74+
'tsconfig.type_check.json',
75+
]);
76+
6377
/** Normalizes repository-relative paths to POSIX separators for stable matching. */
6478
export const normalizeRepoRelativePath = (pathValue: string) =>
6579
Path.normalize(pathValue).split(Path.sep).join('/');
@@ -134,6 +148,39 @@ const parseMoonProjectsResponse = (stdout: string): MoonProject[] => {
134148
});
135149
};
136150

151+
const parseChangedFilesInput = (changedFilesJson: string): string[] => {
152+
const payload = JSON.parse(changedFilesJson) as MoonChangedFilesInput;
153+
154+
return (payload.files ?? []).map(normalizeRepoRelativePath);
155+
};
156+
157+
const isRootMoonProjectTriggerFile = (repoRelPath: string) => {
158+
if (repoRelPath.startsWith('typings/')) {
159+
return true;
160+
}
161+
162+
return !repoRelPath.includes('/') && ROOT_MOON_PROJECT_TRIGGER_FILES.has(repoRelPath);
163+
};
164+
165+
const shouldIncludeRootMoonProject = ({
166+
changedFilesJson,
167+
projects,
168+
}: {
169+
changedFilesJson: string;
170+
projects: MoonProject[];
171+
}): boolean => {
172+
if (projects.some((project) => project.id === ROOT_MOON_PROJECT_ID)) {
173+
return false;
174+
}
175+
176+
const changedFiles = parseChangedFilesInput(changedFilesJson);
177+
if (changedFiles.length === 0) {
178+
return false;
179+
}
180+
181+
return changedFiles.some(isRootMoonProjectTriggerFile);
182+
};
183+
137184
/**
138185
* Queries Moon for affected projects by piping pre-resolved changed files JSON
139186
* into `moon query projects --affected`.
@@ -164,7 +211,15 @@ export const getAffectedMoonProjectsFromChangedFiles = async ({
164211
},
165212
});
166213

167-
return parseMoonProjectsResponse(stdout);
214+
const projects = parseMoonProjectsResponse(stdout);
215+
216+
// Moon currently omits the root `kibana` project for global TypeScript inputs owned by
217+
// sourceRoot `.`, for example repo-root tsconfig files and `typings/**`.
218+
if (shouldIncludeRootMoonProject({ changedFilesJson, projects })) {
219+
return [...projects, { id: ROOT_MOON_PROJECT_ID, sourceRoot: '.' }];
220+
}
221+
222+
return projects;
168223
};
169224

170225
/** Summarizes affected Moon projects into non-root source roots and root-project flag. */

0 commit comments

Comments
 (0)