diff --git a/.github/workflows/renovate.yml b/.github/workflows/renovate.yml index 79cbba3..ed60202 100644 --- a/.github/workflows/renovate.yml +++ b/.github/workflows/renovate.yml @@ -19,11 +19,24 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Validate Renovate token + run: | + TOKEN="${{ secrets.RENOVATE_TOKEN }}" + if [ -z "${TOKEN}" ]; then + echo "RENOVATE_TOKEN secret is required to let Renovate-created PRs trigger CI workflows." + exit 1 + fi + if [[ "${TOKEN}" == ghs_* ]]; then + echo "RENOVATE_TOKEN must be a PAT (ghp_* or github_pat_*), not GITHUB_TOKEN." + exit 1 + fi + - name: Run Renovate uses: renovatebot/github-action@v44.0.3 with: configurationFile: renovate.json - token: ${{ secrets.RENOVATE_TOKEN || github.token }} + token: ${{ secrets.RENOVATE_TOKEN }} env: LOG_LEVEL: debug RENOVATE_REPOSITORIES: ${{ github.repository }} + RENOVATE_PLATFORM_AUTOMERGE: 'false' diff --git a/README.md b/README.md index f935e05..4c7ef1e 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,6 @@ [![Dependency Review](https://github.com/chiragmak10/create-react-forge/actions/workflows/dependency-review.yml/badge.svg)](https://github.com/chiragmak10/create-react-forge/actions/workflows/dependency-review.yml) [![CI](https://github.com/chiragmak10/create-react-forge/actions/workflows/ci.yml/badge.svg)](https://github.com/chiragmak10/create-react-forge/actions/workflows/ci.yml) - # create-react-forge Production-ready React scaffolding CLI with first-class testing, flexible runtimes (Vite/Next.js), and a composable template system inspired by [bulletproof-react](https://github.com/alan2207/bulletproof-react). @@ -117,11 +116,11 @@ The CLI uses pinned, tested versions for all dependencies: ## Automated dependency updates -This repo now uses Renovate to auto-update dependencies (including template manifests under `src/templates/overlays/**/manifest.json`, `src/dependencies/resolver.ts`, and the dependency versions table in this README). The workflow runs weekly and can also be run manually. +This repo now uses Renovate to auto-update dependencies (including template manifests under `src/templates/overlays/**/manifest.json`, `src/dependencies/resolver.ts`, annotated test fixture constants, and the dependency versions table in this README). The workflow runs weekly and can also be run manually. ### One-time setup -1. No additional token is required. Renovate uses the default `secrets.GITHUB_TOKEN`. +1. Add a repository secret named `RENOVATE_TOKEN` (fine-grained PAT with `Contents: Read and write` and `Pull requests: Read and write`) so Renovate PRs can trigger CI workflows. 2. Enable repository auto-merge in GitHub settings. 3. Protect `master` and require CI checks before merge. @@ -131,7 +130,7 @@ Config file: `renovate.json` Behavior: - All dependency updates (major, minor, patch) auto-merge after checks pass. -- Custom regex managers keep template manifests, the resolver registry, and README dependency rows in sync. +- Custom regex managers keep template manifests, the resolver registry, annotated test fixtures, and README dependency rows in sync. ## Screenshot diff --git a/renovate.json b/renovate.json index 377e97f..faedb92 100644 --- a/renovate.json +++ b/renovate.json @@ -9,7 +9,7 @@ "prHourlyLimit": 0, "prConcurrentLimit": 0, "branchConcurrentLimit": 0, - "recreateWhen": "always", + "recreateWhen": "auto", "lockFileMaintenance": { "enabled": true, "automerge": true, @@ -37,6 +37,16 @@ "datasourceTemplate": "npm", "versioningTemplate": "npm" }, + { + "customType": "regex", + "description": "Keep test fixture dependency versions in sync", + "managerFilePatterns": ["/^src\\/__tests__\\/assembler\\.test\\.ts$/"], + "matchStrings": [ + "const\\s+[A-Z0-9_]+_VERSION\\s*=\\s*'(?[~^]?\\d+\\.\\d+\\.\\d+(?:[-+][0-9A-Za-z.-]+)?)';\\s*\\/\\/\\s*renovate:\\s*depName=(?[^\\s]+)" + ], + "datasourceTemplate": "npm", + "versioningTemplate": "npm" + }, { "customType": "regex", "description": "Keep README React version row in sync", diff --git a/src/__tests__/assembler.test.ts b/src/__tests__/assembler.test.ts index 42390c4..dca36af 100644 --- a/src/__tests__/assembler.test.ts +++ b/src/__tests__/assembler.test.ts @@ -5,6 +5,10 @@ import { describe, expect, it } from 'vitest'; import type { ProjectConfig } from '../config/schema'; import { ProjectAssembler } from '../assembler'; +const TSX_VERSION = '^4.19.2'; // renovate: depName=tsx +const REACT_VERSION = '^19.0.0'; // renovate: depName=react +const TYPESCRIPT_VERSION = '^5.7.2'; // renovate: depName=typescript + function createConfig(overrides: Partial = {}): ProjectConfig { return { name: 'test-app', @@ -40,7 +44,9 @@ describe('ProjectAssembler', () => { ); const files = assembler.getFiles(); - expect(files.get('README.md')).toBe('awesome-app | A production-ready React application | | MIT'); + expect(files.get('README.md')).toBe( + 'awesome-app | A production-ready React application | | MIT' + ); files.set('another.md', 'changed'); expect(assembler.getFiles().has('another.md')).toBe(false); @@ -64,11 +70,11 @@ describe('ProjectAssembler', () => { ); assembler.addDependencies({ zeta: '^1.0.0', alpha: '^1.0.0' }); - assembler.addDevDependencies({ tsx: '^4.0.0' }); + assembler.addDevDependencies({ tsx: TSX_VERSION }); assembler.addScripts({ dev: 'vite', build: 'vite build' }); assembler.mergeTemplateDeps({ - dependencies: { react: '^19.0.0' }, - devDependencies: { typescript: '^5.0.0' }, + dependencies: { react: REACT_VERSION }, + devDependencies: { typescript: TYPESCRIPT_VERSION }, scripts: { test: 'vitest' }, }); @@ -91,8 +97,8 @@ describe('ProjectAssembler', () => { expect(Object.keys(pkg.dependencies)).toEqual(['alpha', 'react', 'zeta']); expect(pkg.devDependencies).toMatchObject({ - tsx: '^4.0.0', - typescript: '^5.0.0', + tsx: TSX_VERSION, + typescript: TYPESCRIPT_VERSION, }); expect(pkg.scripts).toMatchObject({ dev: 'vite', diff --git a/src/__tests__/dependency-version-consistency.test.ts b/src/__tests__/dependency-version-consistency.test.ts new file mode 100644 index 0000000..e8c8525 --- /dev/null +++ b/src/__tests__/dependency-version-consistency.test.ts @@ -0,0 +1,82 @@ +import { readdirSync, readFileSync } from 'fs'; +import { join } from 'path'; +import { describe, expect, it } from 'vitest'; +import { VERSION_REGISTRY } from '../dependencies/resolver'; + +const README_VERSION_ROWS: Array<{ label: string; depName: string }> = [ + { label: 'React', depName: 'react' }, + { label: 'Vite', depName: 'vite' }, + { label: 'Next.js', depName: 'next' }, + { label: 'Tailwind CSS', depName: 'tailwindcss' }, + { label: 'TanStack Query', depName: '@tanstack/react-query' }, + { label: 'Vitest', depName: 'vitest' }, + { label: 'Playwright', depName: '@playwright/test' }, + { label: 'TypeScript', depName: 'typescript' }, +]; + +function collectManifestPaths(dirPath: string): string[] { + const manifestPaths: string[] = []; + for (const entry of readdirSync(dirPath, { withFileTypes: true })) { + const fullPath = join(dirPath, entry.name); + if (entry.isDirectory()) { + manifestPaths.push(...collectManifestPaths(fullPath)); + continue; + } + if (entry.name === 'manifest.json') { + manifestPaths.push(fullPath); + } + } + return manifestPaths; +} + +function readReadmeRowVersion(content: string, label: string): string | null { + const escapedLabel = label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const rowRegex = new RegExp(`^\\|\\s*${escapedLabel}\\s*\\|\\s*([^|\\s]+)\\s*\\|\\s*$`, 'm'); + const match = content.match(rowRegex); + return match?.[1] ?? null; +} + +describe('Dependency Version Consistency', () => { + it('should keep tracked README dependency rows in sync with VERSION_REGISTRY', () => { + const readmeContent = readFileSync('README.md', 'utf-8'); + + for (const { label, depName } of README_VERSION_ROWS) { + const actual = readReadmeRowVersion(readmeContent, label); + expect(actual, `README row missing or malformed for "${label}"`).not.toBeNull(); + expect(actual).toBe(VERSION_REGISTRY[depName]); + } + }); + + it('should keep all template manifest dependency versions in sync with VERSION_REGISTRY', () => { + const manifestPaths = collectManifestPaths('src/templates/overlays'); + const missingInRegistry: string[] = []; + const mismatches: string[] = []; + + for (const manifestPath of manifestPaths) { + const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8')) as { + dependencies?: Record; + devDependencies?: Record; + }; + const depEntries = Object.entries({ + ...(manifest.dependencies || {}), + ...(manifest.devDependencies || {}), + }); + + for (const [depName, currentValue] of depEntries) { + const expectedValue = VERSION_REGISTRY[depName]; + if (!expectedValue) { + missingInRegistry.push(`${depName} (${manifestPath})`); + continue; + } + if (currentValue !== expectedValue) { + mismatches.push( + `${depName} (${manifestPath}) expected ${expectedValue} but found ${currentValue}` + ); + } + } + } + + expect(missingInRegistry).toEqual([]); + expect(mismatches).toEqual([]); + }); +}); diff --git a/src/dependencies/resolver.ts b/src/dependencies/resolver.ts index bb4648f..308df78 100644 --- a/src/dependencies/resolver.ts +++ b/src/dependencies/resolver.ts @@ -3,52 +3,60 @@ */ export const VERSION_REGISTRY: Record = { // Runtime - 'vite': '^6.0.7', + vite: '^6.0.7', '@vitejs/plugin-react': '^5.0.0', - 'next': '^16.1.6', - 'react': '^19.0.0', + next: '^16.1.6', + react: '^19.0.0', 'react-dom': '^19.0.0', // Language - 'typescript': '^5.7.2', + typescript: '^5.7.2', // Styling - 'tailwindcss': '^4.0.0', + tailwindcss: '^4.0.0', '@tailwindcss/postcss': '^4.0.0', - 'postcss': '^8.4.49', - 'autoprefixer': '^10.4.20', + postcss: '^8.4.49', + autoprefixer: '^10.4.20', 'styled-components': '^6.1.14', + '@types/styled-components': '^5.1.34', + 'babel-plugin-styled-components': '^2.1.4', // State Management '@reduxjs/toolkit': '^2.5.0', 'react-redux': '^9.2.0', - 'zustand': '^5.0.3', + zustand: '^5.0.3', + jotai: '^2.10.0', // Data Fetching '@tanstack/react-query': '^5.62.10', '@tanstack/react-query-devtools': '^5.62.10', // Testing - Unit - 'vitest': '^2.1.8', + vitest: '^2.1.8', '@vitest/ui': '^2.1.8', - 'jest': '^29.7.0', + '@vitest/coverage-v8': '^2.1.8', + jest: '^29.7.0', + 'jest-environment-jsdom': '^29.7.0', + '@types/jest': '^29.5.14', + 'ts-jest': '^29.2.6', // Testing - Component '@testing-library/react': '^16.1.0', '@testing-library/jest-dom': '^6.6.3', '@testing-library/user-event': '^14.5.2', - 'jsdom': '^25.0.1', + jsdom: '^25.0.1', + msw: '^2.7.0', // Testing - E2E - 'playwright': '^1.49.1', + playwright: '^1.49.1', '@playwright/test': '^1.49.1', - 'cypress': '^15.0.0', + cypress: '^15.0.0', // Formatting - 'prettier': '^3.4.2', + prettier: '^3.4.2', // Build & Dev - 'tsx': '^4.19.2', + tsx: '^4.19.2', // Type definitions '@types/react': '^19.0.6', @@ -125,9 +133,7 @@ export class DependencyResolver { */ private pinVersion(version: string): string { // Check if version is in registry - const pkg = Object.keys(VERSION_REGISTRY).find( - (key) => VERSION_REGISTRY[key] === version - ); + const pkg = Object.keys(VERSION_REGISTRY).find((key) => VERSION_REGISTRY[key] === version); if (pkg) { return VERSION_REGISTRY[pkg]; }