Skip to content

Commit 723dbcb

Browse files
authored
Merge pull request #41 from kaifcoder/feature/ci-workflow
feat: add option to generate GitHub Actions CI workflow during projec…
2 parents bf78197 + c44ecc2 commit 723dbcb

File tree

5 files changed

+83
-3
lines changed

5 files changed

+83
-3
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,10 +178,20 @@ create-polyglot dev --docker
178178
| `--package-manager <pm>` | One of `npm|pnpm|yarn|bun` (default: detect or npm) |
179179
| `--frontend-generator` | Use `create-next-app` (falls back to template on failure) |
180180
| `--force` | Overwrite existing target directory if it exists |
181+
| `--with-actions` | Generate a starter GitHub Actions CI workflow (`.github/workflows/ci.yml`) |
181182
| `--yes` | Accept defaults & suppress interactive prompts |
182183

183184
If you omit flags, the wizard will prompt interactively (similar to `create-next-app`).
184185

186+
### Optional GitHub Actions CI
187+
Pass `--with-actions` (or answer "yes" to the prompt) and the scaffold adds a minimal workflow at `.github/workflows/ci.yml` that:
188+
- Triggers on pushes & pull requests targeting `main` / `master`
189+
- Sets up Node.js (20.x) with dependency cache
190+
- Installs dependencies (respects your chosen package manager)
191+
- Runs the test suite (`npm test` / `yarn test` / `pnpm test` / `bun test`)
192+
193+
You can extend it with build, lint, docker publish, or matrix strategies. If you skip it initially you can always add later manually.
194+
185195
## Generated Structure
186196
```
187197
my-org/

bin/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ program
2828
.option('--force', 'Overwrite if directory exists and not empty')
2929
.option('--package-manager <pm>', 'npm | pnpm | yarn | bun (default: npm)')
3030
.option('--frontend-generator', 'Use create-next-app to scaffold the frontend instead of the bundled template')
31+
.option('--with-actions', 'Generate a GitHub Actions CI workflow (ci.yml)')
3132
.option('--yes', 'Skip confirmation (assume yes) for non-interactive use')
3233
.action(async (...args) => {
3334
const projectNameArg = args[0];

bin/lib/scaffold.js

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,16 @@ export async function scaffoldMonorepo(projectNameArg, options) {
6161
initial: true
6262
});
6363
}
64+
if (options.withActions === undefined) {
65+
interactiveQuestions.push({
66+
type: 'toggle',
67+
name: 'withActions',
68+
message: 'Generate GitHub Actions CI workflow?',
69+
active: 'yes',
70+
inactive: 'no',
71+
initial: false
72+
});
73+
}
6474

6575
let answers = {};
6676
const nonInteractive = !!options.yes || process.env.CI === 'true';
@@ -81,6 +91,9 @@ export async function scaffoldMonorepo(projectNameArg, options) {
8191
case 'git':
8292
answers.git = false;
8393
break;
94+
case 'withActions':
95+
answers.withActions = false; // default disabled in non-interactive mode
96+
break;
8497
default:
8598
break;
8699
}
@@ -104,6 +117,12 @@ export async function scaffoldMonorepo(projectNameArg, options) {
104117
options.preset = options.preset || answers.preset || '';
105118
options.packageManager = options.packageManager || answers.packageManager || 'npm';
106119
if (options.git === undefined) options.git = answers.git;
120+
if (options.withActions === undefined) options.withActions = answers.withActions;
121+
// Commander defines '--no-install' as option 'install' defaulting to true, false when flag passed.
122+
if (Object.prototype.hasOwnProperty.call(options, 'install')) {
123+
// Normalize to legacy noInstall boolean used below.
124+
options.noInstall = options.install === false;
125+
}
107126

108127
console.log(chalk.cyanBright(`\n🚀 Creating ${projectName} monorepo...\n`));
109128

@@ -437,7 +456,8 @@ export async function scaffoldMonorepo(projectNameArg, options) {
437456
}
438457

439458
const pm = options.packageManager || 'npm';
440-
if (!options.noInstall) {
459+
// Commander maps --no-install to options.install = false
460+
if (options.install !== false) {
441461
console.log(chalk.cyan(`\n📦 Installing root dependencies using ${pm}...`));
442462
const installCmd = pm === 'yarn' ? ['install'] : pm === 'pnpm' ? ['install'] : pm === 'bun' ? ['install'] : ['install'];
443463
try {
@@ -447,6 +467,25 @@ export async function scaffoldMonorepo(projectNameArg, options) {
447467
}
448468
}
449469

470+
// Optionally generate GitHub Actions workflow
471+
if (options.withActions) {
472+
try {
473+
const wfDir = path.join(projectDir, '.github', 'workflows');
474+
await fs.mkdirp(wfDir);
475+
const wfPath = path.join(wfDir, 'ci.yml');
476+
if (!(await fs.pathExists(wfPath))) {
477+
const nodeVersion = '20.x';
478+
const installStep = pm === 'yarn' ? 'yarn install --frozen-lockfile || yarn install' : pm === 'pnpm' ? 'pnpm install' : pm === 'bun' ? 'bun install' : 'npm ci || npm install';
479+
const testCmd = pm === 'yarn' ? 'yarn test' : pm === 'pnpm' ? 'pnpm test' : pm === 'bun' ? 'bun test' : 'npm test';
480+
const wf = `# Generated by create-polyglot CI scaffold\nname: CI\n\non:\n push:\n branches: [ main, master ]\n pull_request:\n branches: [ main, master ]\n\njobs:\n build-test:\n runs-on: ubuntu-latest\n steps:\n - name: Checkout\n uses: actions/checkout@v4\n - name: Setup Node.js\n uses: actions/setup-node@v4\n with:\n node-version: ${nodeVersion}\n cache: '${pm === 'npm' ? 'npm' : pm}'\n - name: Install dependencies\n run: ${installStep}\n - name: Run tests\n run: ${testCmd}\n`;
481+
await fs.writeFile(wfPath, wf);
482+
console.log(chalk.green('✅ Added GitHub Actions workflow (.github/workflows/ci.yml)'));
483+
}
484+
} catch (e) {
485+
console.log(chalk.yellow('⚠️ Failed to create GitHub Actions workflow:'), e.message);
486+
}
487+
}
488+
450489
// Write polyglot config
451490
const polyglotConfig = {
452491
name: projectName,
@@ -459,10 +498,11 @@ export async function scaffoldMonorepo(projectNameArg, options) {
459498
printBoxMessage([
460499
'🎉 Monorepo setup complete!',
461500
`cd ${projectName}`,
462-
options.noInstall ? `${pm} install` : '',
501+
options.install === false ? `${pm} install` : '',
463502
`${pm} run list:services # quick list (fancy table)`,
464503
`${pm} run dev # run local node/frontend services`,
465504
'docker compose up --build# run all via docker',
505+
options.withActions ? 'GitHub Actions CI ready (see .github/workflows/ci.yml)' : '',
466506
'',
467507
'Happy hacking!'
468508
].filter(Boolean));

tests/actions-workflow.test.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { execa } from 'execa';
2+
import { describe, it, expect, afterAll } from 'vitest';
3+
import fs from 'fs';
4+
import path from 'path';
5+
import os from 'os';
6+
7+
// Test that --with-actions generates a GitHub Actions workflow file.
8+
9+
describe('GitHub Actions workflow generation', () => {
10+
const tmpParent = fs.mkdtempSync(path.join(os.tmpdir(), 'polyglot-actions-'));
11+
let tmpDir = path.join(tmpParent, 'workspace');
12+
fs.mkdirSync(tmpDir);
13+
const projName = 'ci-proj';
14+
15+
afterAll(() => {
16+
try { fs.rmSync(tmpParent, { recursive: true, force: true }); } catch {}
17+
});
18+
19+
it('creates a ci.yml when --with-actions passed', async () => {
20+
const repoRoot = process.cwd();
21+
const cliPath = path.join(repoRoot, 'bin/index.js');
22+
await execa('node', [cliPath, 'init', projName, '--services', 'node', '--no-install', '--with-actions', '--yes'], { cwd: tmpDir });
23+
const wfPath = path.join(tmpDir, projName, '.github', 'workflows', 'ci.yml');
24+
expect(fs.existsSync(wfPath)).toBe(true);
25+
const content = fs.readFileSync(wfPath, 'utf-8');
26+
expect(content).toMatch(/name: CI/);
27+
expect(content).toMatch(/Run tests/);
28+
}, 30000);
29+
});

tests/smoke.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ describe('create-polyglot CLI smoke', () => {
1717
try { fs.rmSync(tmpParent, { recursive: true, force: true }); } catch {}
1818
});
1919

20-
it('scaffolds a project with a node service', async () => {
20+
it('scaffolds a project with a node service (using init subcommand)', async () => {
2121
const repoRoot = process.cwd();
2222
const cliPath = path.join(repoRoot, 'bin/index.js');
2323
await execa('node', [cliPath, 'init', projName, '--services', 'node', '--no-install', '--yes'], { cwd: tmpDir });

0 commit comments

Comments
 (0)