From a31f4b2accea35b7407ed8a8c1323209fe487b2a Mon Sep 17 00:00:00 2001 From: Doug Fennell Date: Tue, 11 Nov 2025 17:38:47 -0600 Subject: [PATCH] feat(init): add --ci to scaffold ci-ratchet workflow; add reusable workflow for ratchet (#216) --- .github/workflows/ratchet-reusable.yml | 53 +++++++++++++++++ lib/commands/init/index.js | 11 ++++ lib/generators/ci-workflow.js | 82 ++++++++++++++++++++++++++ 3 files changed, 146 insertions(+) create mode 100644 .github/workflows/ratchet-reusable.yml create mode 100644 lib/generators/ci-workflow.js diff --git a/.github/workflows/ratchet-reusable.yml b/.github/workflows/ratchet-reusable.yml new file mode 100644 index 0000000..193a5e8 --- /dev/null +++ b/.github/workflows/ratchet-reusable.yml @@ -0,0 +1,53 @@ +name: ratchet-reusable + +on: + workflow_call: + inputs: + node-version: + required: false + type: string + default: '20' + +jobs: + ratchet-and-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-node@v4 + with: + node-version: ${{ inputs.node-version }} + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: ESLint JSON + run: npm run lint:json + + - name: Analyze current + run: npm run analyze:current + + - name: Ratchet (fail on new debt) + run: npm run ratchet + + - name: Health gate (context-aware ratchet) + run: | + if npm run -s | grep -q "ratchet:context"; then + npm run ratchet:context + else + node scripts/ratchet.js --mode=context --baseline=analysis-baseline.json --current=analysis-current.json + fi + + - name: Upload artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: analysis-and-lint-${{ github.sha }} + path: | + lint-results.json + analysis-current.json + if-no-files-found: error + retention-days: 7 \ No newline at end of file diff --git a/lib/commands/init/index.js b/lib/commands/init/index.js index 5fe122c..aa00c09 100644 --- a/lib/commands/init/index.js +++ b/lib/commands/init/index.js @@ -23,6 +23,7 @@ const { writeAgentsMd } = require(path.join(__dirname, '..', '..', 'generators', const { writeCursorRules } = require(path.join(__dirname, '..', '..', 'generators', 'cursorrules')); const { writeEslintConfig } = require(path.join(__dirname, '..', '..', 'generators', 'eslint-config')); const { ensurePackageScripts } = require(path.join(__dirname, '..', '..', 'generators', 'package-scripts')); +const { ensureCIWorkflow } = require(path.join(__dirname, '..', '..', 'generators', 'ci-workflow')); // Utilities const { applyFingerprintToConfig } = require(path.join(__dirname, '..', '..', 'utils', 'fingerprint')); @@ -110,6 +111,16 @@ if (shouldWriteEslint) writeEslintConfig(cwd, cfg); // Add CI-safe scripts (non-destructive; only adds missing) try { ensurePackageScripts(cwd); } catch { /* ignore */ } + // Optional: scaffold CI workflow file + if (args && (args.ci || String(args.ci).toLowerCase() === 'true')) { + try { + ensureCIWorkflow(cwd); + console.log('\nCI workflow generated. Recommended required checks for branch protection:'); + console.log(' - ratchet-and-tests'); + console.log(' - (optional) your primary test job'); + } catch { /* ignore */ } + } + // Post-init guidance console.log('\nProject initialized.'); printRatchetNextSteps(); diff --git a/lib/generators/ci-workflow.js b/lib/generators/ci-workflow.js new file mode 100644 index 0000000..6a01657 --- /dev/null +++ b/lib/generators/ci-workflow.js @@ -0,0 +1,82 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +function ensureDir(p) { + if (!fs.existsSync(p)) fs.mkdirSync(p, { recursive: true }); +} + +function ensureCIWorkflow(cwd) { + try { + const dir = path.join(cwd, '.github', 'workflows'); + const file = path.join(dir, 'ci-ratchet.yml'); + if (fs.existsSync(file)) { + console.log('Found existing .github/workflows/ci-ratchet.yml — leaving it unchanged.'); + return false; + } + ensureDir(dir); + const yaml = [ + 'name: ci-ratchet', + '', + 'on:', + ' pull_request:', + ' push:', + ' branches: [main]', + ' workflow_dispatch:', + '', + 'jobs:', + ' ratchet-and-tests:', + ' runs-on: ubuntu-latest', + ' steps:', + ' - uses: actions/checkout@v4', + ' with:', + ' fetch-depth: 0', + '', + ' - uses: actions/setup-node@v4', + ' with:', + " node-version: '20'", + " cache: 'npm'", + '', + ' - name: Install dependencies', + ' run: npm ci', + '', + ' - name: ESLint JSON', + ' run: npm run lint:json', + '', + ' - name: Analyze current', + ' run: npm run analyze:current', + '', + ' - name: Ratchet (fail on new debt)', + ' run: npm run ratchet', + '', + ' - name: Health gate (context-aware ratchet)', + ' run: |', + ' if npm run -s | grep -q "ratchet:context"; then', + ' npm run ratchet:context', + ' else', + ' node scripts/ratchet.js --mode=context --baseline=analysis-baseline.json --current=analysis-current.json', + ' fi', + '', + ' - name: Upload artifacts', + ' if: always()', + ' uses: actions/upload-artifact@v4', + ' with:', + ' name: analysis-and-lint-${{ github.sha }}', + ' path: |', + ' lint-results.json', + ' analysis-current.json', + ' if-no-files-found: error', + ' retention-days: 7', + '' + ].join('\n'); + fs.writeFileSync(file, yaml + '\n'); + console.log('Created .github/workflows/ci-ratchet.yml'); + return true; + } catch (e) { + console.warn(`Warning: could not create CI workflow: ${e && e.message}`); + return false; + } +} + +module.exports = { ensureCIWorkflow }; \ No newline at end of file