Skip to content

Commit ec975f0

Browse files
committed
feat: add option to generate GitHub Actions CI workflow during project scaffold
1 parent 842bfe7 commit ec975f0

File tree

4 files changed

+75
-0
lines changed

4 files changed

+75
-0
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: 2 additions & 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 (projectNameArg, options) => {
3334
await scaffoldMonorepo(projectNameArg, options);
@@ -43,6 +44,7 @@ program
4344
.option('--force', '(Deprecated) Overwrite directory')
4445
.option('--package-manager <pm>', '(Deprecated) Package manager')
4546
.option('--frontend-generator', '(Deprecated) Use create-next-app for frontend')
47+
.option('--with-actions', '(Deprecated) Generate GitHub Actions workflow')
4648
.option('--yes', '(Deprecated) Assume yes for prompts')
4749
.action(async (projectNameArg, options) => {
4850
if (!options._deprecatedNoticeShown) {

bin/lib/scaffold.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,16 @@ export async function scaffoldMonorepo(projectNameArg, options) {
7676
initial: true
7777
});
7878
}
79+
if (options.withActions === undefined) {
80+
interactiveQuestions.push({
81+
type: 'toggle',
82+
name: 'withActions',
83+
message: 'Generate GitHub Actions CI workflow?',
84+
active: 'yes',
85+
inactive: 'no',
86+
initial: false
87+
});
88+
}
7989

8090
let answers = {};
8191
const nonInteractive = !!options.yes || process.env.CI === 'true';
@@ -99,6 +109,9 @@ export async function scaffoldMonorepo(projectNameArg, options) {
99109
case 'git':
100110
answers.git = false;
101111
break;
112+
case 'withActions':
113+
answers.withActions = false; // default disabled in non-interactive mode
114+
break;
102115
default:
103116
break;
104117
}
@@ -126,6 +139,7 @@ export async function scaffoldMonorepo(projectNameArg, options) {
126139
options.preset = options.preset || answers.preset || '';
127140
options.packageManager = options.packageManager || answers.packageManager || 'npm';
128141
if (options.git === undefined) options.git = answers.git;
142+
if (options.withActions === undefined) options.withActions = answers.withActions;
129143

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

@@ -500,6 +514,25 @@ export async function scaffoldMonorepo(projectNameArg, options) {
500514
}
501515
}
502516

517+
// Optionally generate GitHub Actions workflow
518+
if (options.withActions) {
519+
try {
520+
const wfDir = path.join(projectDir, '.github', 'workflows');
521+
await fs.mkdirp(wfDir);
522+
const wfPath = path.join(wfDir, 'ci.yml');
523+
if (!(await fs.pathExists(wfPath))) {
524+
const nodeVersion = '20.x';
525+
const installStep = pm === 'yarn' ? 'yarn install --frozen-lockfile || yarn install' : pm === 'pnpm' ? 'pnpm install' : pm === 'bun' ? 'bun install' : 'npm ci || npm install';
526+
const testCmd = pm === 'yarn' ? 'yarn test' : pm === 'pnpm' ? 'pnpm test' : pm === 'bun' ? 'bun test' : 'npm test';
527+
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`;
528+
await fs.writeFile(wfPath, wf);
529+
console.log(chalk.green('✅ Added GitHub Actions workflow (.github/workflows/ci.yml)'));
530+
}
531+
} catch (e) {
532+
console.log(chalk.yellow('⚠️ Failed to create GitHub Actions workflow:'), e.message);
533+
}
534+
}
535+
503536
// Write polyglot config
504537
const polyglotConfig = {
505538
name: projectName,
@@ -516,6 +549,7 @@ export async function scaffoldMonorepo(projectNameArg, options) {
516549
`${pm} run list:services # quick list (fancy table)`,
517550
`${pm} run dev # run local node/frontend services`,
518551
'docker compose up --build# run all via docker',
552+
options.withActions ? 'GitHub Actions CI ready (see .github/workflows/ci.yml)' : '',
519553
'',
520554
'Happy hacking!'
521555
].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, 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+
});

0 commit comments

Comments
 (0)