Skip to content

Commit 60d9aeb

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

File tree

5 files changed

+121
-25
lines changed

5 files changed

+121
-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: 41 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, toUnixPath } from '@code-pushup/utils';
35
import {
46
CI_PROVIDERS,
57
type CiProvider,
@@ -10,7 +12,9 @@ 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 = toUnixPath(
16+
path.join('.gitlab', 'ci', 'code-pushup.gitlab-ci.yml'),
17+
);
1418

1519
export async function promptCiProvider(cliArgs: CliArgs): Promise<CiProvider> {
1620
if (isCiProvider(cliArgs.ci)) {
@@ -51,18 +55,22 @@ async function writeGitHubWorkflow(
5155
tree: Tree,
5256
context: ConfigContext,
5357
): Promise<void> {
54-
await tree.write(GITHUB_WORKFLOW_PATH, generateGitHubYaml(context));
58+
await tree.write(GITHUB_WORKFLOW_PATH, await generateGitHubYaml(context));
5559
}
5660

57-
function generateGitHubYaml({ mode, tool }: ConfigContext): string {
61+
async function generateGitHubYaml({
62+
mode,
63+
tool,
64+
}: ConfigContext): Promise<string> {
65+
const branch = await getGitDefaultBranch();
5866
const lines = [
5967
'name: Code PushUp',
6068
'',
6169
'on:',
6270
' push:',
63-
' branches: [main]',
71+
` branches: [${branch}]`,
6472
' pull_request:',
65-
' branches: [main]',
73+
` branches: [${branch}]`,
6674
'',
6775
'permissions:',
6876
' contents: read',
@@ -72,6 +80,7 @@ function generateGitHubYaml({ mode, tool }: ConfigContext): string {
7280
'jobs:',
7381
' code-pushup:',
7482
' runs-on: ubuntu-latest',
83+
' name: Code PushUp',
7584
' steps:',
7685
' - name: Clone repository',
7786
' uses: actions/checkout@v5',
@@ -93,13 +102,7 @@ async function writeGitLabConfig(tree: Tree): Promise<void> {
93102
await tree.write(filePath, generateGitLabYaml());
94103

95104
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-
);
105+
await patchRootGitLabConfig(tree);
103106
}
104107
}
105108

@@ -116,6 +119,31 @@ function generateGitLabYaml(): string {
116119
return `${lines.join('\n')}\n`;
117120
}
118121

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