Skip to content

Commit 9ae9a2c

Browse files
authored
feat(create-cli): add CI/CD setup step (#1266)
1 parent 9468272 commit 9ae9a2c

8 files changed

Lines changed: 410 additions & 10 deletions

File tree

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.117.0",
3030
"@code-pushup/utils": "0.117.0",
3131
"@inquirer/prompts": "^8.0.0",
32+
"yaml": "^2.5.1",
3233
"yargs": "^17.7.2"
3334
},
3435
"files": [

packages/create-cli/src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import yargs from 'yargs';
33
import { hideBin } from 'yargs/helpers';
44
import { parsePluginSlugs, validatePluginSlugs } from './lib/setup/plugins.js';
55
import {
6+
CI_PROVIDERS,
67
CONFIG_FILE_FORMATS,
78
type PluginSetupBinding,
89
SETUP_MODES,
@@ -39,6 +40,11 @@ const argv = await yargs(hideBin(process.argv))
3940
choices: SETUP_MODES,
4041
describe: 'Setup mode (default: auto-detected from project)',
4142
})
43+
.option('ci', {
44+
type: 'string',
45+
choices: CI_PROVIDERS,
46+
describe: 'CI/CD integration (github, gitlab, or none)',
47+
})
4248
.check(parsed => {
4349
validatePluginSlugs(bindings, parsed.plugins);
4450
return true;
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import { select } from '@inquirer/prompts';
2+
import * as YAML from 'yaml';
3+
import { getGitDefaultBranch, logger } from '@code-pushup/utils';
4+
import {
5+
CI_PROVIDERS,
6+
type CiProvider,
7+
type CliArgs,
8+
type ConfigContext,
9+
type Tree,
10+
} from './types.js';
11+
12+
const GITHUB_WORKFLOW_PATH = '.github/workflows/code-pushup.yml';
13+
const GITLAB_CONFIG_PATH = '.gitlab-ci.yml';
14+
const GITLAB_CONFIG_SEPARATE_PATH = '.gitlab/ci/code-pushup.gitlab-ci.yml';
15+
16+
export async function promptCiProvider(cliArgs: CliArgs): Promise<CiProvider> {
17+
if (isCiProvider(cliArgs.ci)) {
18+
return cliArgs.ci;
19+
}
20+
if (cliArgs.yes) {
21+
return 'none';
22+
}
23+
return select<CiProvider>({
24+
message: 'CI/CD integration:',
25+
choices: [
26+
{ name: 'GitHub Actions', value: 'github' },
27+
{ name: 'GitLab CI/CD', value: 'gitlab' },
28+
{ name: 'none', value: 'none' },
29+
],
30+
default: 'none',
31+
});
32+
}
33+
34+
export async function resolveCi(
35+
tree: Tree,
36+
provider: CiProvider,
37+
context: ConfigContext,
38+
): Promise<void> {
39+
switch (provider) {
40+
case 'github':
41+
await writeGitHubWorkflow(tree, context);
42+
break;
43+
case 'gitlab':
44+
await writeGitLabConfig(tree);
45+
break;
46+
case 'none':
47+
break;
48+
}
49+
}
50+
51+
async function writeGitHubWorkflow(
52+
tree: Tree,
53+
context: ConfigContext,
54+
): Promise<void> {
55+
await tree.write(GITHUB_WORKFLOW_PATH, await generateGitHubYaml(context));
56+
}
57+
58+
async function generateGitHubYaml({
59+
mode,
60+
tool,
61+
}: ConfigContext): Promise<string> {
62+
const branch = await getGitDefaultBranch();
63+
const lines = [
64+
'name: Code PushUp',
65+
'',
66+
'on:',
67+
' push:',
68+
` branches: [${branch}]`,
69+
' pull_request:',
70+
` branches: [${branch}]`,
71+
'',
72+
'permissions:',
73+
' contents: read',
74+
' actions: read',
75+
' pull-requests: write',
76+
'',
77+
'jobs:',
78+
' code-pushup:',
79+
' runs-on: ubuntu-latest',
80+
' name: Code PushUp',
81+
' steps:',
82+
' - name: Clone repository',
83+
' uses: actions/checkout@v5',
84+
' - name: Set up Node.js',
85+
' uses: actions/setup-node@v6',
86+
' - name: Install dependencies',
87+
' run: npm ci',
88+
' - name: Code PushUp',
89+
' uses: code-pushup/github-action@v0',
90+
...(mode === 'monorepo' && tool != null
91+
? [' with:', ` monorepo: ${tool}`]
92+
: []),
93+
];
94+
return `${lines.join('\n')}\n`;
95+
}
96+
97+
async function writeGitLabConfig(tree: Tree): Promise<void> {
98+
const filePath = await resolveGitLabFilePath(tree);
99+
await tree.write(filePath, generateGitLabYaml());
100+
101+
if (filePath === GITLAB_CONFIG_SEPARATE_PATH) {
102+
await patchRootGitLabConfig(tree);
103+
}
104+
}
105+
106+
function generateGitLabYaml(): string {
107+
const lines = [
108+
'workflow:',
109+
' rules:',
110+
' - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH',
111+
" - if: $CI_PIPELINE_SOURCE == 'merge_request_event'",
112+
'',
113+
'include:',
114+
' - https://gitlab.com/code-pushup/gitlab-pipelines-template/-/raw/latest/code-pushup.yml',
115+
];
116+
return `${lines.join('\n')}\n`;
117+
}
118+
119+
async function patchRootGitLabConfig(tree: Tree): Promise<void> {
120+
const content = await tree.read(GITLAB_CONFIG_PATH);
121+
if (content == null) {
122+
return;
123+
}
124+
const doc = YAML.parseDocument(content);
125+
if (!YAML.isMap(doc.contents)) {
126+
logger.warn(
127+
`Could not update ${GITLAB_CONFIG_PATH}. Add an include entry for ${GITLAB_CONFIG_SEPARATE_PATH} to your config.`,
128+
);
129+
return;
130+
}
131+
const entry = { local: GITLAB_CONFIG_SEPARATE_PATH };
132+
const include = doc.get('include', true);
133+
if (include == null) {
134+
doc.set('include', doc.createNode([entry]));
135+
} else if (YAML.isSeq(include)) {
136+
include.add(doc.createNode(entry));
137+
} else {
138+
const existing = doc.get('include');
139+
doc.set('include', doc.createNode([existing, entry]));
140+
}
141+
await tree.write(GITLAB_CONFIG_PATH, doc.toString());
142+
}
143+
144+
async function resolveGitLabFilePath(tree: Tree): Promise<string> {
145+
if (await tree.exists(GITLAB_CONFIG_PATH)) {
146+
return GITLAB_CONFIG_SEPARATE_PATH;
147+
}
148+
return GITLAB_CONFIG_PATH;
149+
}
150+
151+
function isCiProvider(value: string | undefined): value is CiProvider {
152+
const validValues: readonly string[] = CI_PROVIDERS;
153+
return value != null && validValues.includes(value);
154+
}

0 commit comments

Comments
 (0)