Skip to content

Commit 16e99f8

Browse files
committed
Merge remote-tracking branch 'origin/main' into feat/plugin-eslint/add-artifact-loading-logic
# Conflicts: # packages/models/src/index.ts
2 parents 1825a65 + b08a9a7 commit 16e99f8

File tree

12 files changed

+338
-39
lines changed

12 files changed

+338
-39
lines changed

e2e/ci-e2e/tests/nx-monorepo.e2e.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,6 @@ describe('CI - monorepo mode (Nx)', () => {
215215
).toMatchFileSnapshot(
216216
path.join(TEST_SNAPSHOTS_DIR, 'nx-monorepo-report-diff.md'),
217217
);
218-
});
218+
}, 120_000);
219219
});
220220
});

nx.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,11 @@
9595
"!{projectRoot}/code-pushup.config.?(m)[jt]s",
9696
"!{projectRoot}/zod2md.config.ts"
9797
],
98-
"sharedGlobals": []
98+
"sharedGlobals": [
99+
{ "runtime": "node -e \"console.log(require('os').platform())\"" },
100+
{ "runtime": "node -v" },
101+
{ "runtime": "npm -v" }
102+
]
99103
},
100104
"workspaceLayout": {
101105
"appsDir": "examples",

packages/ci/src/index.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
export type { SourceFileIssue } from './lib/issues.js';
22
export type * from './lib/models.js';
33
export {
4-
MONOREPO_TOOLS,
54
isMonorepoTool,
5+
MONOREPO_TOOLS,
66
type MonorepoTool,
77
} from './lib/monorepo/index.js';
88
export { runInCI } from './lib/run.js';
9+
export { configPatternsSchema } from './lib/schemas.js';
10+
export {
11+
DEFAULT_SETTINGS,
12+
MAX_SEARCH_COMMITS,
13+
MIN_SEARCH_COMMITS,
14+
parseConfigPatternsFromString,
15+
} from './lib/settings.js';

packages/ci/src/lib/constants.ts

Lines changed: 0 additions & 22 deletions
This file was deleted.

packages/ci/src/lib/monorepo/list-projects.unit.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import path from 'node:path';
33
import type { PackageJson } from 'type-fest';
44
import { MEMFS_VOLUME } from '@code-pushup/test-utils';
55
import * as utils from '@code-pushup/utils';
6-
import { DEFAULT_SETTINGS } from '../constants.js';
76
import type { Settings } from '../models.js';
7+
import { DEFAULT_SETTINGS } from '../settings.js';
88
import {
99
type MonorepoProjects,
1010
listMonorepoProjects,

packages/ci/src/lib/run-utils.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,6 @@ import {
2828
runCompare,
2929
runPrintConfig,
3030
} from './cli/index.js';
31-
import {
32-
DEFAULT_SETTINGS,
33-
MAX_SEARCH_COMMITS,
34-
MIN_SEARCH_COMMITS,
35-
} from './constants.js';
3631
import { listChangedFiles, normalizeGitRef } from './git.js';
3732
import { type SourceFileIssue, filterRelevantIssues } from './issues.js';
3833
import type {
@@ -49,6 +44,11 @@ import type {
4944
import type { ProjectConfig } from './monorepo/index.js';
5045
import { saveOutputFiles } from './output-files.js';
5146
import { downloadFromPortal } from './portal/download.js';
47+
import {
48+
DEFAULT_SETTINGS,
49+
MAX_SEARCH_COMMITS,
50+
MIN_SEARCH_COMMITS,
51+
} from './settings.js';
5252

5353
export type RunEnv = {
5454
refs: NormalizedGitRefs;

packages/ci/src/lib/schemas.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { ZodError, z } from 'zod';
2+
import {
3+
DEFAULT_PERSIST_CONFIG,
4+
persistConfigSchema,
5+
slugSchema,
6+
uploadConfigSchema,
7+
} from '@code-pushup/models';
8+
import { interpolate } from '@code-pushup/utils';
9+
10+
// eslint-disable-next-line unicorn/prefer-top-level-await, unicorn/catch-error-name
11+
export const interpolatedSlugSchema = slugSchema.catch(ctx => {
12+
// allow {projectName} interpolation (invalid slug)
13+
if (
14+
typeof ctx.value === 'string' &&
15+
ctx.issues.length === 1 &&
16+
ctx.issues[0]?.code === 'invalid_format'
17+
) {
18+
// if only regex failed, try if it would pass once we insert known variables
19+
const { success } = slugSchema.safeParse(
20+
interpolate(ctx.value, { projectName: 'example' }),
21+
);
22+
if (success) {
23+
return ctx.value;
24+
}
25+
}
26+
throw new ZodError(ctx.error.issues);
27+
});
28+
29+
export const configPatternsSchema = z.object({
30+
persist: persistConfigSchema.transform(persist => ({
31+
...DEFAULT_PERSIST_CONFIG,
32+
...persist,
33+
})),
34+
upload: uploadConfigSchema
35+
.omit({ organization: true, project: true })
36+
.extend({
37+
organization: interpolatedSlugSchema,
38+
project: interpolatedSlugSchema,
39+
})
40+
.optional(),
41+
});
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { ZodError } from 'zod';
3+
import type { ConfigPatterns } from './models.js';
4+
import { configPatternsSchema, interpolatedSlugSchema } from './schemas.js';
5+
6+
describe('interpolatedSlugSchema', () => {
7+
it('should accept a valid slug', () => {
8+
expect(interpolatedSlugSchema.parse('valid-slug')).toBe('valid-slug');
9+
});
10+
11+
it('should accept a slug with {projectName} interpolation', () => {
12+
expect(interpolatedSlugSchema.parse('{projectName}-slug')).toBe(
13+
'{projectName}-slug',
14+
);
15+
});
16+
17+
it('should reject an invalid slug that cannot be fixed by interpolation', () => {
18+
expect(() => interpolatedSlugSchema.parse('Invalid Slug!')).toThrow(
19+
ZodError,
20+
);
21+
});
22+
23+
it('should reject a non-string value', () => {
24+
expect(() => interpolatedSlugSchema.parse(123)).toThrow(ZodError);
25+
});
26+
});
27+
28+
describe('configPatternsSchema', () => {
29+
it('should accept valid persist and upload configs', () => {
30+
const configPatterns: Required<ConfigPatterns> = {
31+
persist: {
32+
outputDir: '.code-pushup/{projectName}',
33+
filename: 'report',
34+
format: ['json', 'md'],
35+
skipReports: false,
36+
},
37+
upload: {
38+
server: 'https://api.code-pushup.example.com/graphql',
39+
apiKey: 'cp_...',
40+
organization: 'example',
41+
project: '{projectName}',
42+
},
43+
};
44+
expect(configPatternsSchema.parse(configPatterns)).toEqual(configPatterns);
45+
});
46+
47+
it('should accept persist config without upload', () => {
48+
const configPatterns: ConfigPatterns = {
49+
persist: {
50+
outputDir: '.code-pushup/{projectName}',
51+
filename: 'report',
52+
format: ['json', 'md'],
53+
skipReports: false,
54+
},
55+
};
56+
expect(configPatternsSchema.parse(configPatterns)).toEqual(configPatterns);
57+
});
58+
59+
it('fills in default persist values if missing', () => {
60+
expect(
61+
configPatternsSchema.parse({
62+
persist: {
63+
filename: '{projectName}-report',
64+
},
65+
}),
66+
).toEqual<ConfigPatterns>({
67+
persist: {
68+
outputDir: '.code-pushup',
69+
filename: '{projectName}-report',
70+
format: ['json', 'md'],
71+
skipReports: false,
72+
},
73+
});
74+
});
75+
76+
it('should reject if persist is missing', () => {
77+
expect(() => configPatternsSchema.parse({})).toThrow(ZodError);
78+
});
79+
80+
it('should reject if persist has invalid values', () => {
81+
expect(() =>
82+
configPatternsSchema.parse({
83+
persist: {
84+
format: 'json', // should be array
85+
},
86+
}),
87+
).toThrow(ZodError);
88+
});
89+
90+
it('should reject if upload is missing required fields', () => {
91+
expect(() =>
92+
configPatternsSchema.parse({
93+
persist: {
94+
outputDir: '.code-pushup/{projectName}',
95+
filename: 'report',
96+
format: ['json', 'md'],
97+
skipReports: false,
98+
},
99+
upload: {
100+
server: 'https://api.code-pushup.example.com/graphql',
101+
organization: 'example',
102+
project: '{projectName}',
103+
// missing apiKey
104+
},
105+
}),
106+
).toThrow(ZodError);
107+
});
108+
});

packages/ci/src/lib/settings.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { ZodError, z } from 'zod';
2+
import type { ConfigPatterns, Settings } from './models.js';
3+
import { configPatternsSchema } from './schemas.js';
4+
5+
export const DEFAULT_SETTINGS: Settings = {
6+
monorepo: false,
7+
parallel: false,
8+
projects: null,
9+
task: 'code-pushup',
10+
bin: 'npx --no-install code-pushup',
11+
config: null,
12+
directory: process.cwd(),
13+
silent: false,
14+
debug: false,
15+
detectNewIssues: true,
16+
logger: console,
17+
nxProjectsFilter: '--with-target={task}',
18+
skipComment: false,
19+
configPatterns: null,
20+
searchCommits: false,
21+
};
22+
23+
export const MIN_SEARCH_COMMITS = 1;
24+
export const MAX_SEARCH_COMMITS = 100;
25+
26+
export function parseConfigPatternsFromString(
27+
value: string,
28+
): ConfigPatterns | null {
29+
if (!value) {
30+
return null;
31+
}
32+
33+
try {
34+
const json = JSON.parse(value);
35+
return configPatternsSchema.parse(json);
36+
} catch (error) {
37+
if (error instanceof SyntaxError) {
38+
throw new TypeError(
39+
`Invalid JSON value for configPatterns input - ${error.message}`,
40+
);
41+
}
42+
if (error instanceof ZodError) {
43+
throw new TypeError(
44+
`Invalid shape of configPatterns input:\n${z.prettifyError(error)}`,
45+
);
46+
}
47+
throw error;
48+
}
49+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import type { CoreConfig } from '@code-pushup/models';
2+
import type { ConfigPatterns } from './models.js';
3+
import { parseConfigPatternsFromString } from './settings.js';
4+
5+
describe('parseConfigPatternsFromString', () => {
6+
it('should return for empty string', () => {
7+
expect(parseConfigPatternsFromString('')).toBeNull();
8+
});
9+
10+
it('should parse full persist and upload configs', () => {
11+
const configPatterns: Required<ConfigPatterns> = {
12+
persist: {
13+
outputDir: '.code-pushup/{projectName}',
14+
filename: 'report',
15+
format: ['json', 'md'],
16+
skipReports: false,
17+
},
18+
upload: {
19+
server: 'https://api.code-pushup.example.com/graphql',
20+
apiKey: 'cp_...',
21+
organization: 'example',
22+
project: '{projectName}',
23+
},
24+
};
25+
expect(
26+
parseConfigPatternsFromString(JSON.stringify(configPatterns)),
27+
).toEqual(configPatterns);
28+
});
29+
30+
it('should parse full persist config without upload config', () => {
31+
const configPatterns: ConfigPatterns = {
32+
persist: {
33+
outputDir: '.code-pushup/{projectName}',
34+
filename: 'report',
35+
format: ['json', 'md'],
36+
skipReports: false,
37+
},
38+
};
39+
expect(
40+
parseConfigPatternsFromString(JSON.stringify(configPatterns)),
41+
).toEqual(configPatterns);
42+
});
43+
44+
it('should fill in default persist values where missing', () => {
45+
expect(
46+
parseConfigPatternsFromString(
47+
JSON.stringify({
48+
persist: {
49+
filename: '{projectName}-report',
50+
},
51+
} satisfies Pick<CoreConfig, 'persist'>),
52+
),
53+
).toEqual<ConfigPatterns>({
54+
persist: {
55+
outputDir: '.code-pushup',
56+
filename: '{projectName}-report',
57+
format: ['json', 'md'],
58+
skipReports: false,
59+
},
60+
});
61+
});
62+
63+
it('should throw if input string is not valid JSON', () => {
64+
expect(() =>
65+
parseConfigPatternsFromString('outputDir: .code-pushup/{projectName}'),
66+
).toThrow('Invalid JSON value for configPatterns input - Unexpected token');
67+
});
68+
69+
it('should throw if persist config is missing', () => {
70+
expect(() => parseConfigPatternsFromString('{}')).toThrow(
71+
/Invalid shape of configPatterns input.*expected object, received undefined.*at persist/s,
72+
);
73+
});
74+
75+
it('should throw if persist config has invalid values', () => {
76+
expect(() =>
77+
parseConfigPatternsFromString(
78+
JSON.stringify({ persist: { format: 'json' } }),
79+
),
80+
).toThrow(
81+
/Invalid shape of configPatterns input.*expected array, received string.*at persist\.format/s,
82+
);
83+
});
84+
85+
it('should throw if upload config has missing values', () => {
86+
expect(() =>
87+
parseConfigPatternsFromString(
88+
JSON.stringify({
89+
persist: {},
90+
upload: {
91+
server: 'https://api.code-pushup.example.com/graphql',
92+
organization: 'example',
93+
project: '{projectName}',
94+
},
95+
}),
96+
),
97+
).toThrow(
98+
/Invalid shape of configPatterns input.*expected string, received undefined.*at upload\.apiKey/s,
99+
);
100+
});
101+
});

0 commit comments

Comments
 (0)