Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,10 +178,20 @@ create-polyglot dev --docker
| `--package-manager <pm>` | One of `npm|pnpm|yarn|bun` (default: detect or npm) |
| `--frontend-generator` | Use `create-next-app` (falls back to template on failure) |
| `--force` | Overwrite existing target directory if it exists |
| `--with-actions` | Generate a starter GitHub Actions CI workflow (`.github/workflows/ci.yml`) |
| `--yes` | Accept defaults & suppress interactive prompts |

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

### Optional GitHub Actions CI
Pass `--with-actions` (or answer "yes" to the prompt) and the scaffold adds a minimal workflow at `.github/workflows/ci.yml` that:
- Triggers on pushes & pull requests targeting `main` / `master`
- Sets up Node.js (20.x) with dependency cache
- Installs dependencies (respects your chosen package manager)
- Runs the test suite (`npm test` / `yarn test` / `pnpm test` / `bun test`)

You can extend it with build, lint, docker publish, or matrix strategies. If you skip it initially you can always add later manually.

## Generated Structure
```
my-org/
Expand Down
1 change: 1 addition & 0 deletions bin/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ program
.option('--force', 'Overwrite if directory exists and not empty')
.option('--package-manager <pm>', 'npm | pnpm | yarn | bun (default: npm)')
.option('--frontend-generator', 'Use create-next-app to scaffold the frontend instead of the bundled template')
.option('--with-actions', 'Generate a GitHub Actions CI workflow (ci.yml)')
.option('--yes', 'Skip confirmation (assume yes) for non-interactive use')
.action(async (...args) => {
const projectNameArg = args[0];
Expand Down
44 changes: 42 additions & 2 deletions bin/lib/scaffold.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,16 @@ export async function scaffoldMonorepo(projectNameArg, options) {
initial: true
});
}
if (options.withActions === undefined) {
interactiveQuestions.push({
type: 'toggle',
name: 'withActions',
message: 'Generate GitHub Actions CI workflow?',
active: 'yes',
inactive: 'no',
initial: false
});
}

let answers = {};
const nonInteractive = !!options.yes || process.env.CI === 'true';
Expand All @@ -81,6 +91,9 @@ export async function scaffoldMonorepo(projectNameArg, options) {
case 'git':
answers.git = false;
break;
case 'withActions':
answers.withActions = false; // default disabled in non-interactive mode
break;
default:
break;
}
Expand All @@ -104,6 +117,12 @@ export async function scaffoldMonorepo(projectNameArg, options) {
options.preset = options.preset || answers.preset || '';
options.packageManager = options.packageManager || answers.packageManager || 'npm';
if (options.git === undefined) options.git = answers.git;
if (options.withActions === undefined) options.withActions = answers.withActions;
// Commander defines '--no-install' as option 'install' defaulting to true, false when flag passed.
if (Object.prototype.hasOwnProperty.call(options, 'install')) {
// Normalize to legacy noInstall boolean used below.
options.noInstall = options.install === false;
}

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

Expand Down Expand Up @@ -437,7 +456,8 @@ export async function scaffoldMonorepo(projectNameArg, options) {
}

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

// Optionally generate GitHub Actions workflow
if (options.withActions) {
try {
const wfDir = path.join(projectDir, '.github', 'workflows');
await fs.mkdirp(wfDir);
const wfPath = path.join(wfDir, 'ci.yml');
if (!(await fs.pathExists(wfPath))) {
const nodeVersion = '20.x';
const installStep = pm === 'yarn' ? 'yarn install --frozen-lockfile || yarn install' : pm === 'pnpm' ? 'pnpm install' : pm === 'bun' ? 'bun install' : 'npm ci || npm install';
const testCmd = pm === 'yarn' ? 'yarn test' : pm === 'pnpm' ? 'pnpm test' : pm === 'bun' ? 'bun test' : 'npm test';
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`;
await fs.writeFile(wfPath, wf);
console.log(chalk.green('✅ Added GitHub Actions workflow (.github/workflows/ci.yml)'));
}
} catch (e) {
console.log(chalk.yellow('⚠️ Failed to create GitHub Actions workflow:'), e.message);
}
}

// Write polyglot config
const polyglotConfig = {
name: projectName,
Expand All @@ -459,10 +498,11 @@ export async function scaffoldMonorepo(projectNameArg, options) {
printBoxMessage([
'🎉 Monorepo setup complete!',
`cd ${projectName}`,
options.noInstall ? `${pm} install` : '',
options.install === false ? `${pm} install` : '',
`${pm} run list:services # quick list (fancy table)`,
`${pm} run dev # run local node/frontend services`,
'docker compose up --build# run all via docker',
options.withActions ? 'GitHub Actions CI ready (see .github/workflows/ci.yml)' : '',
'',
'Happy hacking!'
].filter(Boolean));
Expand Down
29 changes: 29 additions & 0 deletions tests/actions-workflow.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { execa } from 'execa';
import { describe, it, expect, afterAll } from 'vitest';
import fs from 'fs';
import path from 'path';
import os from 'os';

// Test that --with-actions generates a GitHub Actions workflow file.

describe('GitHub Actions workflow generation', () => {
const tmpParent = fs.mkdtempSync(path.join(os.tmpdir(), 'polyglot-actions-'));
let tmpDir = path.join(tmpParent, 'workspace');
fs.mkdirSync(tmpDir);
const projName = 'ci-proj';

afterAll(() => {
try { fs.rmSync(tmpParent, { recursive: true, force: true }); } catch {}
});

it('creates a ci.yml when --with-actions passed', async () => {
const repoRoot = process.cwd();
const cliPath = path.join(repoRoot, 'bin/index.js');
await execa('node', [cliPath, 'init', projName, '--services', 'node', '--no-install', '--with-actions', '--yes'], { cwd: tmpDir });
const wfPath = path.join(tmpDir, projName, '.github', 'workflows', 'ci.yml');
expect(fs.existsSync(wfPath)).toBe(true);
const content = fs.readFileSync(wfPath, 'utf-8');
expect(content).toMatch(/name: CI/);
expect(content).toMatch(/Run tests/);
}, 30000);
});
2 changes: 1 addition & 1 deletion tests/smoke.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ describe('create-polyglot CLI smoke', () => {
try { fs.rmSync(tmpParent, { recursive: true, force: true }); } catch {}
});

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