Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .moon/tasks/tag-jest-unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ tasks:
command: node
args:
- scripts/jest.js
- '--runInBand'
- '--passWithNoTests'
- '--config'
- '@files(jest-config)'
Expand Down
14 changes: 14 additions & 0 deletions packages/kbn-moon/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

module.exports = {
preset: '@kbn/test/jest_node',
rootDir: '../..',
roots: ['<rootDir>/packages/kbn-moon'],
};
3 changes: 3 additions & 0 deletions packages/kbn-moon/moon.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,12 @@ tags:
- package
- dev
- group-undefined
- jest-unit-tests
fileGroups:
src:
- '**/*.ts'
- '**/*.tsx'
- '!target/**/*'
jest-config:
- jest.config.js
tasks: {}
128 changes: 128 additions & 0 deletions packages/kbn-moon/src/query_projects.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

const mockExeca = jest.fn();

jest.mock('fs', () => ({
...jest.requireActual('fs'),
existsSync: jest.fn().mockReturnValue(true),
}));

jest.mock('@kbn/repo-info', () => ({
REPO_ROOT: '/repo',
}));

jest.mock('@kbn/dev-utils', () => ({
getRemoteDefaultBranchRefs: jest.fn(),
resolveNearestMergeBase: jest.fn(),
}));

jest.mock('execa', () => ({
__esModule: true,
default: mockExeca,
}));

const createMoonProjectsOutput = (projects: Array<{ id: string; sourceRoot: string }>) =>
JSON.stringify({
projects: projects.map((project) => ({
id: project.id,
source: project.sourceRoot,
config: {
project: {
metadata: {
sourceRoot: project.sourceRoot,
},
},
},
})),
});

describe('getAffectedMoonProjectsFromChangedFiles', () => {
beforeEach(() => {
jest.resetModules();
mockExeca.mockReset();
});

it('adds the root project for repo-root TypeScript inputs', async () => {
mockExeca.mockResolvedValueOnce({
stdout: createMoonProjectsOutput([]),
});

const { getAffectedMoonProjectsFromChangedFiles } = await import('./query_projects');

await expect(
getAffectedMoonProjectsFromChangedFiles({
changedFilesJson: JSON.stringify({ files: ['tsconfig.base.json'] }),
})
).resolves.toEqual([{ id: 'kibana', sourceRoot: '.' }]);
});

it('adds the root project alongside affected package projects for typings changes', async () => {
mockExeca.mockResolvedValueOnce({
stdout: createMoonProjectsOutput([{ id: 'foo', sourceRoot: 'packages/foo' }]),
});

const { getAffectedMoonProjectsFromChangedFiles } = await import('./query_projects');

await expect(
getAffectedMoonProjectsFromChangedFiles({
changedFilesJson: JSON.stringify({
files: ['packages/foo/src/index.ts', 'typings/something.d.ts'],
}),
})
).resolves.toEqual([
{ id: 'foo', sourceRoot: 'packages/foo' },
{ id: 'kibana', sourceRoot: '.' },
]);
});

it('does not add the root project for package-owned files', async () => {
mockExeca.mockResolvedValueOnce({
stdout: createMoonProjectsOutput([{ id: 'foo', sourceRoot: 'packages/foo' }]),
});

const { getAffectedMoonProjectsFromChangedFiles } = await import('./query_projects');

await expect(
getAffectedMoonProjectsFromChangedFiles({
changedFilesJson: JSON.stringify({ files: ['packages/foo/src/index.ts'] }),
})
).resolves.toEqual([{ id: 'foo', sourceRoot: 'packages/foo' }]);
});

it('does not add the root project for unrelated repo-root files', async () => {
mockExeca.mockResolvedValueOnce({
stdout: createMoonProjectsOutput([]),
});

const { getAffectedMoonProjectsFromChangedFiles } = await import('./query_projects');

await expect(
getAffectedMoonProjectsFromChangedFiles({
changedFilesJson: JSON.stringify({ files: ['.github/CODEOWNERS'] }),
})
).resolves.toEqual([]);
});

it('keeps Moon-reported root project results without querying all projects again', async () => {
mockExeca.mockResolvedValueOnce({
stdout: createMoonProjectsOutput([{ id: 'kibana', sourceRoot: '.' }]),
});

const { getAffectedMoonProjectsFromChangedFiles } = await import('./query_projects');

await expect(
getAffectedMoonProjectsFromChangedFiles({
changedFilesJson: JSON.stringify({ files: ['tsconfig.base.json'] }),
})
).resolves.toEqual([{ id: 'kibana', sourceRoot: '.' }]);

expect(mockExeca).toHaveBeenCalledTimes(1);
});
});
57 changes: 56 additions & 1 deletion packages/kbn-moon/src/query_projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,29 @@ interface MoonQueryProjectsResponse {
}>;
}

interface MoonChangedFilesInput {
files?: string[];
}

/** Options for resolving the affected base revision from git state. */
export interface ResolveMoonAffectedBaseOptions {
headRef?: string;
}

export const ROOT_MOON_PROJECT_ID = 'kibana';

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

const ROOT_MOON_PROJECT_TRIGGER_FILES = new Set([
'tsconfig.json',
'tsconfig.base.json',
'tsconfig.base.type_check.json',
'tsconfig.browser.json',
'tsconfig.refs.json',
'tsconfig.type_check.json',
]);

/** Normalizes repository-relative paths to POSIX separators for stable matching. */
export const normalizeRepoRelativePath = (pathValue: string) =>
Path.normalize(pathValue).split(Path.sep).join('/');
Expand Down Expand Up @@ -134,6 +148,39 @@ const parseMoonProjectsResponse = (stdout: string): MoonProject[] => {
});
};

const parseChangedFilesInput = (changedFilesJson: string): string[] => {
const payload = JSON.parse(changedFilesJson) as MoonChangedFilesInput;

return (payload.files ?? []).map(normalizeRepoRelativePath);
};

const isRootMoonProjectTriggerFile = (repoRelPath: string) => {
if (repoRelPath.startsWith('typings/')) {
return true;
}

return !repoRelPath.includes('/') && ROOT_MOON_PROJECT_TRIGGER_FILES.has(repoRelPath);
};

const shouldIncludeRootMoonProject = ({
changedFilesJson,
projects,
}: {
changedFilesJson: string;
projects: MoonProject[];
}): boolean => {
if (projects.some((project) => project.id === ROOT_MOON_PROJECT_ID)) {
return false;
}

const changedFiles = parseChangedFilesInput(changedFilesJson);
if (changedFiles.length === 0) {
return false;
}

return changedFiles.some(isRootMoonProjectTriggerFile);
};

/**
* Queries Moon for affected projects by piping pre-resolved changed files JSON
* into `moon query projects --affected`.
Expand Down Expand Up @@ -164,7 +211,15 @@ export const getAffectedMoonProjectsFromChangedFiles = async ({
},
});

return parseMoonProjectsResponse(stdout);
const projects = parseMoonProjectsResponse(stdout);

// Moon currently omits the root `kibana` project for global TypeScript inputs owned by
// sourceRoot `.`, for example repo-root tsconfig files and `typings/**`.
if (shouldIncludeRootMoonProject({ changedFilesJson, projects })) {
return [...projects, { id: ROOT_MOON_PROJECT_ID, sourceRoot: '.' }];
}

return projects;
};

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