Skip to content

Commit be6698e

Browse files
committed
refactor(create-cli): improve CI yaml generation
1 parent e5f9719 commit be6698e

File tree

5 files changed

+123
-25
lines changed

5 files changed

+123
-25
lines changed

packages/create-cli/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"@code-pushup/models": "0.116.0",
3030
"@code-pushup/utils": "0.116.0",
3131
"@inquirer/prompts": "^8.0.0",
32+
"yaml": "^2.5.1",
3233
"yargs": "^17.7.2"
3334
},
3435
"files": [

packages/create-cli/src/lib/setup/ci.ts

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { select } from '@inquirer/prompts';
2-
import { logger } from '@code-pushup/utils';
2+
import path from 'node:path';
3+
import * as YAML from 'yaml';
4+
import { getGitDefaultBranch, logger } from '@code-pushup/utils';
35
import {
46
CI_PROVIDERS,
57
type CiProvider,
@@ -10,7 +12,11 @@ import {
1012

1113
const GITHUB_WORKFLOW_PATH = '.github/workflows/code-pushup.yml';
1214
const GITLAB_CONFIG_PATH = '.gitlab-ci.yml';
13-
const GITLAB_CONFIG_SEPARATE_PATH = 'code-pushup.gitlab-ci.yml';
15+
const GITLAB_CONFIG_SEPARATE_PATH = path.join(
16+
'.gitlab',
17+
'ci',
18+
'code-pushup.gitlab-ci.yml',
19+
);
1420

1521
export async function promptCiProvider(cliArgs: CliArgs): Promise<CiProvider> {
1622
if (isCiProvider(cliArgs.ci)) {
@@ -51,18 +57,22 @@ async function writeGitHubWorkflow(
5157
tree: Tree,
5258
context: ConfigContext,
5359
): Promise<void> {
54-
await tree.write(GITHUB_WORKFLOW_PATH, generateGitHubYaml(context));
60+
await tree.write(GITHUB_WORKFLOW_PATH, await generateGitHubYaml(context));
5561
}
5662

57-
function generateGitHubYaml({ mode, tool }: ConfigContext): string {
63+
async function generateGitHubYaml({
64+
mode,
65+
tool,
66+
}: ConfigContext): Promise<string> {
67+
const branch = await getGitDefaultBranch();
5868
const lines = [
5969
'name: Code PushUp',
6070
'',
6171
'on:',
6272
' push:',
63-
' branches: [main]',
73+
` branches: [${branch}]`,
6474
' pull_request:',
65-
' branches: [main]',
75+
` branches: [${branch}]`,
6676
'',
6777
'permissions:',
6878
' contents: read',
@@ -72,6 +82,7 @@ function generateGitHubYaml({ mode, tool }: ConfigContext): string {
7282
'jobs:',
7383
' code-pushup:',
7484
' runs-on: ubuntu-latest',
85+
' name: Code PushUp',
7586
' steps:',
7687
' - name: Clone repository',
7788
' uses: actions/checkout@v5',
@@ -93,13 +104,7 @@ async function writeGitLabConfig(tree: Tree): Promise<void> {
93104
await tree.write(filePath, generateGitLabYaml());
94105

95106
if (filePath === GITLAB_CONFIG_SEPARATE_PATH) {
96-
logger.warn(
97-
[
98-
`Add the following to your ${GITLAB_CONFIG_PATH}:`,
99-
' include:',
100-
` - local: ${GITLAB_CONFIG_SEPARATE_PATH}`,
101-
].join('\n'),
102-
);
107+
await patchRootGitLabConfig(tree);
103108
}
104109
}
105110

@@ -116,6 +121,31 @@ function generateGitLabYaml(): string {
116121
return `${lines.join('\n')}\n`;
117122
}
118123

124+
async function patchRootGitLabConfig(tree: Tree): Promise<void> {
125+
const content = await tree.read(GITLAB_CONFIG_PATH);
126+
if (content == null) {
127+
return;
128+
}
129+
const doc = YAML.parseDocument(content);
130+
if (!YAML.isMap(doc.contents)) {
131+
logger.warn(
132+
`Could not update ${GITLAB_CONFIG_PATH}. Add an include entry for ${GITLAB_CONFIG_SEPARATE_PATH} to your config.`,
133+
);
134+
return;
135+
}
136+
const entry = { local: GITLAB_CONFIG_SEPARATE_PATH };
137+
const include = doc.get('include', true);
138+
if (include == null) {
139+
doc.set('include', doc.createNode([entry]));
140+
} else if (YAML.isSeq(include)) {
141+
include.add(doc.createNode(entry));
142+
} else {
143+
const existing = doc.get('include');
144+
doc.set('include', doc.createNode([existing, entry]));
145+
}
146+
await tree.write(GITLAB_CONFIG_PATH, doc.toString());
147+
}
148+
119149
async function resolveGitLabFilePath(tree: Tree): Promise<string> {
120150
if (await tree.exists(GITLAB_CONFIG_PATH)) {
121151
return GITLAB_CONFIG_SEPARATE_PATH;

packages/create-cli/src/lib/setup/ci.unit.test.ts

Lines changed: 65 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { select } from '@inquirer/prompts';
22
import { vol } from 'memfs';
33
import { MEMFS_VOLUME } from '@code-pushup/test-utils';
4-
import { logger } from '@code-pushup/utils';
54
import { promptCiProvider, resolveCi } from './ci.js';
65
import type { ConfigContext } from './types.js';
76
import { createTree } from './virtual-fs.js';
@@ -10,6 +9,11 @@ vi.mock('@inquirer/prompts', () => ({
109
select: vi.fn(),
1110
}));
1211

12+
vi.mock('@code-pushup/utils', async importOriginal => ({
13+
...(await importOriginal<typeof import('@code-pushup/utils')>()),
14+
getGitDefaultBranch: vi.fn().mockResolvedValue('main'),
15+
}));
16+
1317
describe('promptCiProvider', () => {
1418
it.each(['github', 'gitlab', 'none'] as const)(
1519
'should return %j when --ci %s is provided',
@@ -64,6 +68,7 @@ describe('resolveCi', () => {
6468
jobs:
6569
code-pushup:
6670
runs-on: ubuntu-latest
71+
name: Code PushUp
6772
steps:
6873
- name: Clone repository
6974
uses: actions/checkout@v5
@@ -100,6 +105,7 @@ describe('resolveCi', () => {
100105
jobs:
101106
code-pushup:
102107
runs-on: ubuntu-latest
108+
name: Code PushUp
103109
steps:
104110
- name: Clone repository
105111
uses: actions/checkout@v5
@@ -127,30 +133,77 @@ describe('resolveCi', () => {
127133
path: '.gitlab-ci.yml',
128134
type: 'CREATE',
129135
});
136+
await expect(tree.read('.gitlab-ci.yml')).resolves.toMatchInlineSnapshot(`
137+
"workflow:
138+
rules:
139+
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
140+
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
141+
142+
include:
143+
- https://gitlab.com/code-pushup/gitlab-pipelines-template/-/raw/latest/code-pushup.yml
144+
"
145+
`);
130146
});
131147

132-
it('should create separate file and log include instruction when .gitlab-ci.yml already exists', async () => {
148+
it('should append local include when .gitlab-ci.yml has include array', async () => {
133149
vol.fromJSON(
134150
{
135151
'package.json': '{}',
136-
'.gitlab-ci.yml': 'stages:\n - test\n',
152+
'.gitlab-ci.yml': 'include:\n - local: .gitlab/ci/version.yml\n',
137153
},
138154
MEMFS_VOLUME,
139155
);
140156
const tree = createTree(MEMFS_VOLUME);
141157

142158
await resolveCi(tree, 'gitlab', STANDALONE_CONTEXT);
143159

144-
expect(tree.listChanges()).toPartiallyContain({
145-
path: 'code-pushup.gitlab-ci.yml',
146-
type: 'CREATE',
147-
});
148-
expect(tree.listChanges()).not.toPartiallyContain({
149-
path: '.gitlab-ci.yml',
150-
});
151-
expect(logger.warn).toHaveBeenCalledWith(
152-
expect.stringContaining('code-pushup.gitlab-ci.yml'),
160+
await expect(tree.read('.gitlab-ci.yml')).resolves.toMatchInlineSnapshot(`
161+
"include:
162+
- local: .gitlab/ci/version.yml
163+
- local: .gitlab/ci/code-pushup.gitlab-ci.yml
164+
"
165+
`);
166+
});
167+
168+
it('should wrap single include object into array and append', async () => {
169+
vol.fromJSON(
170+
{
171+
'package.json': '{}',
172+
'.gitlab-ci.yml': 'include:\n local: .gitlab/ci/version.yml\n',
173+
},
174+
MEMFS_VOLUME,
175+
);
176+
const tree = createTree(MEMFS_VOLUME);
177+
178+
await resolveCi(tree, 'gitlab', STANDALONE_CONTEXT);
179+
180+
await expect(tree.read('.gitlab-ci.yml')).resolves.toMatchInlineSnapshot(`
181+
"include:
182+
- local: .gitlab/ci/version.yml
183+
- local: .gitlab/ci/code-pushup.gitlab-ci.yml
184+
"
185+
`);
186+
});
187+
188+
it('should create include array when .gitlab-ci.yml has no include key', async () => {
189+
vol.fromJSON(
190+
{
191+
'package.json': '{}',
192+
'.gitlab-ci.yml': 'stages:\n - test\n',
193+
},
194+
MEMFS_VOLUME,
153195
);
196+
const tree = createTree(MEMFS_VOLUME);
197+
198+
await resolveCi(tree, 'gitlab', STANDALONE_CONTEXT);
199+
200+
await expect(tree.read('.gitlab-ci.yml')).resolves.toMatchInlineSnapshot(`
201+
"stages:
202+
- test
203+
include:
204+
- local: .gitlab/ci/code-pushup.gitlab-ci.yml
205+
"
206+
`);
154207
});
155208
});
156209

packages/utils/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ export {
7979
} from './lib/git/git.commits-and-tags.js';
8080
export {
8181
formatGitPath,
82+
getGitDefaultBranch,
8283
getGitRoot,
8384
guardAgainstLocalChanges,
8485
safeCheckout,

packages/utils/src/lib/git/git.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,25 @@
11
import path from 'node:path';
22
import { type StatusResult, simpleGit } from 'simple-git';
3+
import { stringifyError } from '../errors.js';
34
import { logger } from '../logger.js';
45
import { toUnixPath } from '../transform.js';
56

67
export function getGitRoot(git = simpleGit()): Promise<string> {
78
return git.revparse('--show-toplevel');
89
}
910

11+
export async function getGitDefaultBranch(git = simpleGit()): Promise<string> {
12+
try {
13+
const head = await git.revparse('--abbrev-ref origin/HEAD');
14+
return head.replace(/^origin\//, '');
15+
} catch (error) {
16+
logger.warn(
17+
`Failed to get the default Git branch, falling back to main - ${stringifyError(error)}`,
18+
);
19+
return 'main';
20+
}
21+
}
22+
1023
export function formatGitPath(filePath: string, gitRoot: string): string {
1124
const absolutePath = path.isAbsolute(filePath)
1225
? filePath

0 commit comments

Comments
 (0)