diff --git a/CHANGELOG.md b/CHANGELOG.md index e1873169..5fe14a18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- **BREAKING: CI workflow generation always uses single validate job** (v0.19.0+) + - Removed non-matrix mode — `generate-workflow` now always produces a single `validate` job + - Matches local `pnpm validate` behavior: all steps run sequentially in one CI runner + - Setup steps (e.g., `npx playwright install`) now run before tests in CI, just like locally + - **Migration**: Run `npx vibe-validate generate-workflow` to regenerate your workflow file + - **API**: `useMatrix` option removed from `GenerateWorkflowOptions` + - **BREAKING: TreeHashResult structure change** (v0.19.0+) - **Before**: `{ hash: string, components: Array<{path, treeHash}> }` - **After**: `{ hash: TreeHash, submoduleHashes?: Record }` diff --git a/docs/commands/watch-pr.md b/docs/commands/watch-pr.md index e558fb9f..55c80495 100644 --- a/docs/commands/watch-pr.md +++ b/docs/commands/watch-pr.md @@ -11,7 +11,7 @@ The `watch-pr` command fetches complete PR check status from GitHub, including G 1. **Fetches PR metadata** (title, branch, labels, linked issues, mergeable state) 2. **Retrieves all checks** from GitHub (Actions + external checks like codecov/SonarCloud) 3. **Classifies checks** into GitHub Actions vs external providers -4. **Extracts errors** from failed GitHub Actions logs (matrix + non-matrix modes) +4. **Extracts errors** from failed GitHub Actions logs (YAML extraction with raw output fallback) 5. **Extracts summaries** from external checks (coverage %, quality gates) 6. **Builds history summary** (last 10 runs, success rate, recent pattern) 7. **Generates guidance** with severity-based next steps @@ -616,7 +616,7 @@ cache: # ✅ NEW: Cache info ``` **Key improvements:** -- ✅ **Error extraction** (matrix + non-matrix modes) +- ✅ **Error extraction** (YAML extraction with raw output fallback) - ✅ **Separate check types** (GitHub Actions vs external) - ✅ **History summary** (success rate, patterns) - ✅ **File changes** (insertions/deletions, top files) diff --git a/docs/skill/SKILL.md b/docs/skill/SKILL.md index e05b6770..41756f45 100644 --- a/docs/skill/SKILL.md +++ b/docs/skill/SKILL.md @@ -1,6 +1,6 @@ --- name: vibe-validate -version: 0.19.0-rc.12 # Tracks vibe-validate package version +version: 0.19.0-rc.13 # Tracks vibe-validate package version description: Expert guidance for vibe-validate, an LLM-optimized validation orchestration tool. Use when working with vibe-validate commands, configuration, pre-commit workflows, or validation orchestration in TypeScript projects. model: claude-sonnet-4-5 tools: diff --git a/docs/skill/resources/cli-reference.md b/docs/skill/resources/cli-reference.md index 4a089fce..4cd080a2 100644 --- a/docs/skill/resources/cli-reference.md +++ b/docs/skill/resources/cli-reference.md @@ -314,8 +314,8 @@ Generate GitHub Actions workflow from vibe-validate config **What it does:** Generates .github/workflows/validate.yml from config -Supports matrix mode (multiple Node/OS versions) -Supports non-matrix mode (separate jobs per phase) +Generates single validate job matching local behavior +Supports matrix strategy for multiple Node/OS versions Can check if workflow is in sync with config **Exit codes:** diff --git a/package.json b/package.json index d259bd6f..e1452c08 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vibe-validate", - "version": "0.19.0-rc.12", + "version": "0.19.0-rc.13", "type": "module", "private": true, "description": "Git-aware validation orchestration for vibe coding (LLM-assisted development)", diff --git a/packages/cli/package.json b/packages/cli/package.json index 38e30743..c573505c 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@vibe-validate/cli", - "version": "0.19.0-rc.12", + "version": "0.19.0-rc.13", "description": "Command-line interface for vibe-validate validation framework", "type": "module", "main": "./dist/index.js", diff --git a/packages/cli/src/bin.ts b/packages/cli/src/bin.ts index 3b4b7925..9b719dfd 100644 --- a/packages/cli/src/bin.ts +++ b/packages/cli/src/bin.ts @@ -355,8 +355,8 @@ function showComprehensiveHelp(program: Command): void { 'generate-workflow': { whatItDoes: [ 'Generates .github/workflows/validate.yml from config', - 'Supports matrix mode (multiple Node/OS versions)', - 'Supports non-matrix mode (separate jobs per phase)', + 'Generates single validate job matching local behavior', + 'Supports matrix strategy for multiple Node/OS versions', 'Can check if workflow is in sync with config' ], exitCodes: { diff --git a/packages/cli/src/commands/generate-workflow.ts b/packages/cli/src/commands/generate-workflow.ts index 78238430..b6e821b7 100644 --- a/packages/cli/src/commands/generate-workflow.ts +++ b/packages/cli/src/commands/generate-workflow.ts @@ -18,7 +18,7 @@ import { readFileSync, writeFileSync, existsSync } from 'node:fs'; import { dirname, join } from 'node:path'; -import type { VibeValidateConfig, ValidationPhase } from '@vibe-validate/config'; +import type { VibeValidateConfig } from '@vibe-validate/config'; import { mkdirSyncReal } from '@vibe-validate/utils'; import { type Command } from 'commander'; import { stringify as yamlStringify } from 'yaml'; @@ -91,9 +91,9 @@ interface GitHubWorkflow { * Generate GitHub Actions workflow options */ export interface GenerateWorkflowOptions { - /** Node.js versions to test (default: ['20', '22']) - set to single version to disable matrix */ + /** Node.js versions to test (default: auto-detect from package.json engines) */ nodeVersions?: string[]; - /** Operating systems to test (default: ['ubuntu-latest']) - set to single OS to disable matrix */ + /** Operating systems to test (default: ['ubuntu-latest']) */ os?: string[]; /** Package manager (default: auto-detect from packageManager field or lockfiles) */ packageManager?: PackageManager; @@ -103,8 +103,6 @@ export interface GenerateWorkflowOptions { coverageProvider?: 'codecov' | 'coveralls'; /** Codecov token secret name (default: 'CODECOV_TOKEN') */ codecovTokenSecret?: string; - /** Use matrix strategy for multi-OS/Node testing (default: true if multiple values provided) */ - useMatrix?: boolean; /** Fail fast in matrix (default: false) */ matrixFailFast?: boolean; /** Project root directory for detecting package.json and lockfiles (default: process.cwd()) */ @@ -123,124 +121,6 @@ export function toJobId(name: string): string { .replaceAll(/(^-)|(-$)/g, ''); } -/** - * Get all job IDs from validation phases - * Handles both phase-based (parallel: false) and step-based (parallel: true) jobs - */ -export function getAllJobIds(phases: ValidationPhase[]): string[] { - const jobIds: string[] = []; - - for (const phase of phases) { - if (phase.parallel === false) { - // Phase-based: one job per phase - jobIds.push(toJobId(phase.name)); - } else { - // Step-based: one job per step - for (const step of phase.steps) { - jobIds.push(toJobId(step.name)); - } - } - } - - return jobIds; -} - -/** - * Create common job setup steps (checkout, node, package manager) - */ -function createCommonJobSetupSteps( - nodeVersion: string, - packageManager: PackageManager -): GitHubWorkflowStep[] { - const steps: GitHubWorkflowStep[] = [ - { - uses: ACTIONS_CHECKOUT_V4, - with: { - [WORKFLOW_PROPERTY_FETCH_DEPTH]: 0 // Fetch all history for git-based checks (doctor command) - } - }, - ]; - - // Setup package manager and Node.js - if (packageManager === 'bun') { - // Bun uses its own setup action (includes Node.js) - steps.push( - { - name: 'Setup Bun', - uses: ACTIONS_SETUP_BUN_V2, - }, - { run: getInstallCommand(packageManager) } - ); - } else if (packageManager === 'pnpm') { - steps.push( - { - name: STEP_NAME_SETUP_PNPM, - uses: ACTIONS_SETUP_PNPM_V2, - with: { version: '8' }, - }, - { - uses: ACTIONS_SETUP_NODE_V4, - with: { - [WORKFLOW_PROPERTY_NODE_VERSION]: nodeVersion, - cache: 'pnpm', - }, - }, - { run: getInstallCommand(packageManager) } - ); - } else if (packageManager === 'yarn') { - steps.push( - { - uses: ACTIONS_SETUP_NODE_V4, - with: { - [WORKFLOW_PROPERTY_NODE_VERSION]: nodeVersion, - cache: 'yarn', - }, - }, - { run: getInstallCommand(packageManager) } - ); - } else { - // npm - steps.push( - { - uses: ACTIONS_SETUP_NODE_V4, - with: { - [WORKFLOW_PROPERTY_NODE_VERSION]: nodeVersion, - cache: 'npm', - }, - }, - { run: getInstallCommand(packageManager) } - ); - } - - return steps; -} - -/** - * Add coverage reporting steps if enabled for this step - */ -function addCoverageReportingSteps( - jobSteps: GitHubWorkflowStep[], - stepName: string, - enableCoverage: boolean, - coverageProvider: string -): void { - if (enableCoverage && stepName.toLowerCase().includes('coverage')) { - if (coverageProvider === 'codecov') { - jobSteps.push({ - name: 'Upload coverage to Codecov', - uses: 'codecov/codecov-action@v3', - with: { - 'fail_ci_if_error': true, - }, - }); - } else if (coverageProvider === 'coveralls') { - jobSteps.push({ - name: 'Upload coverage to Coveralls', - uses: 'coverallsapp/github-action@v2', - }); - } - } -} /** * Generate bash script to check all job statuses @@ -291,8 +171,14 @@ function detectNodeVersion(cwd: string = process.cwd()): string { /** * Generate GitHub Actions workflow from validation config + * + * Always generates a single "validate" job that runs all validation steps + * sequentially within one CI runner - matching the local `pnpm validate` experience. + * This ensures setup steps (e.g., playwright install) run before tests, and + * new checkouts for developers and CI just work. + * + * Supports matrix strategy for testing across multiple Node.js versions and OS. */ -// eslint-disable-next-line sonarjs/cognitive-complexity -- Complexity 69 acceptable for workflow generation (converts config phases/steps into GitHub Actions YAML with proper dependency management and caching logic) export function generateWorkflow( config: VibeValidateConfig, options: GenerateWorkflowOptions = {} @@ -308,15 +194,90 @@ export function generateWorkflow( matrixFailFast = false, } = options; - // Determine if we should use matrix strategy - const useMatrix = options.useMatrix ?? (nodeVersions.length > 1 || os.length > 1); - const jobs: Record = {}; const phases = config.validation.phases; - if (useMatrix) { - // Matrix strategy: Create single job that runs validation with matrix - const jobSteps: GitHubWorkflowStep[] = [ + // Single validate job - runs all validation steps sequentially (like local) + const jobSteps: GitHubWorkflowStep[] = [ + { + uses: ACTIONS_CHECKOUT_V4, + with: { + [WORKFLOW_PROPERTY_FETCH_DEPTH]: 0 // Fetch all history for git-based checks (doctor command) + } + }, + ]; + + // Setup package manager + if (packageManager === 'bun') { + jobSteps.push({ + name: 'Setup Bun', + uses: ACTIONS_SETUP_BUN_V2, + }); + } else if (packageManager === 'pnpm') { + jobSteps.push({ + name: STEP_NAME_SETUP_PNPM, + uses: ACTIONS_SETUP_PNPM_V2, + with: { version: '9' }, + }); + } + + // Setup Node.js with matrix variable + // Important: Even for Bun projects, Node.js setup ensures compatibility testing + // across versions that npm package consumers will use + const nodeCacheConfig = packageManager === 'bun' ? {} : { cache: packageManager }; + jobSteps.push({ + name: 'Setup Node.js ${{ matrix.node }}', + uses: ACTIONS_SETUP_NODE_V4, + with: { + [WORKFLOW_PROPERTY_NODE_VERSION]: '${{ matrix.node }}', + ...nodeCacheConfig, + }, + }); + + // Install dependencies + jobSteps.push({ + name: 'Install dependencies', + run: getInstallCommand(packageManager), + }); + + // Add build step if needed (common pattern) + const hasBuildPhase = phases.some(p => + p.steps.some(s => s.name.toLowerCase().includes('build')) + ); + if (hasBuildPhase) { + jobSteps.push({ + name: STEP_NAME_BUILD_PACKAGES, + run: getBuildCommand(packageManager), + }); + } + + // Run validation - use exit code to determine success/failure + // No need for YAML output, grep checks, or platform-specific logic + // GitHub Actions will automatically fail the job if exit code != 0 + jobSteps.push({ + name: 'Run validation', + run: getValidateCommand(packageManager), + env: { + GH_TOKEN: '${{ github.token }}', + }, + }); + + jobs['validate'] = { + name: 'Run vibe-validate validation', + 'runs-on': '${{ matrix.os }}', + steps: jobSteps, + strategy: { + 'fail-fast': matrixFailFast, + matrix: { + os, + node: nodeVersions, + }, + }, + }; + + // Add coverage job if enabled (separate, runs on ubuntu only) + if (enableCoverage) { + const coverageSteps: GitHubWorkflowStep[] = [ { uses: ACTIONS_CHECKOUT_V4, with: { @@ -325,251 +286,68 @@ export function generateWorkflow( }, ]; - // Setup package manager if (packageManager === 'bun') { - jobSteps.push({ + coverageSteps.push({ name: 'Setup Bun', uses: ACTIONS_SETUP_BUN_V2, }); } else if (packageManager === 'pnpm') { - jobSteps.push({ + coverageSteps.push({ name: STEP_NAME_SETUP_PNPM, uses: ACTIONS_SETUP_PNPM_V2, with: { version: '9' }, }); } - // Setup Node.js with matrix variable - // Important: Even for Bun projects, Node.js setup ensures compatibility testing - // across versions that npm package consumers will use - const nodeCacheConfig = packageManager === 'bun' ? {} : { cache: packageManager }; - jobSteps.push({ - name: 'Setup Node.js ${{ matrix.node }}', - uses: ACTIONS_SETUP_NODE_V4, - with: { - [WORKFLOW_PROPERTY_NODE_VERSION]: '${{ matrix.node }}', - ...nodeCacheConfig, - }, - }); + if (packageManager !== 'bun') { + coverageSteps.push({ + name: 'Setup Node.js', + uses: ACTIONS_SETUP_NODE_V4, + with: { + [WORKFLOW_PROPERTY_NODE_VERSION]: nodeVersions[0], + cache: packageManager, + }, + }); + } - // Install dependencies - jobSteps.push({ + coverageSteps.push({ name: 'Install dependencies', run: getInstallCommand(packageManager), }); - // Add build step if needed (common pattern) - const hasBuildPhase = phases.some(p => - p.steps.some(s => s.name.toLowerCase().includes('build')) - ); if (hasBuildPhase) { - jobSteps.push({ + coverageSteps.push({ name: STEP_NAME_BUILD_PACKAGES, run: getBuildCommand(packageManager), }); } - // Run validation - use exit code to determine success/failure - // No need for YAML output, grep checks, or platform-specific logic - // GitHub Actions will automatically fail the job if exit code != 0 - jobSteps.push({ - name: 'Run validation', - run: getValidateCommand(packageManager), - env: { - GH_TOKEN: '${{ github.token }}', - }, + coverageSteps.push({ + name: 'Run tests with coverage', + run: getCoverageCommand(packageManager), }); - jobs['validate'] = { - name: 'Run vibe-validate validation', - 'runs-on': '${{ matrix.os }}', - steps: jobSteps, - strategy: { - 'fail-fast': matrixFailFast, - matrix: { - os, - node: nodeVersions, - }, - }, - }; - - // Add coverage job if enabled (separate, runs on ubuntu only) - if (enableCoverage) { - const coverageSteps: GitHubWorkflowStep[] = [ - { - uses: ACTIONS_CHECKOUT_V4, - with: { - [WORKFLOW_PROPERTY_FETCH_DEPTH]: 0 // Fetch all history for git-based checks (doctor command) - } - }, - ]; - - if (packageManager === 'bun') { - coverageSteps.push({ - name: 'Setup Bun', - uses: ACTIONS_SETUP_BUN_V2, - }); - } else if (packageManager === 'pnpm') { - coverageSteps.push({ - name: STEP_NAME_SETUP_PNPM, - uses: ACTIONS_SETUP_PNPM_V2, - with: { version: '9' }, - }); - } - - if (packageManager !== 'bun') { - coverageSteps.push({ - name: 'Setup Node.js', - uses: ACTIONS_SETUP_NODE_V4, - with: { - [WORKFLOW_PROPERTY_NODE_VERSION]: nodeVersions[0], - cache: packageManager, - }, - }); - } - - coverageSteps.push({ - name: 'Install dependencies', - run: getInstallCommand(packageManager), - }); - - if (hasBuildPhase) { - coverageSteps.push({ - name: STEP_NAME_BUILD_PACKAGES, - run: getBuildCommand(packageManager), - }); - } - + if (coverageProvider === 'codecov') { coverageSteps.push({ - name: 'Run tests with coverage', - run: getCoverageCommand(packageManager), + name: 'Upload coverage to Codecov', + uses: 'codecov/codecov-action@v4', + with: { + token: `\${{ secrets.${codecovTokenSecret} }}`, + files: './coverage/coverage-final.json', + 'fail_ci_if_error': false, + }, }); - - if (coverageProvider === 'codecov') { - coverageSteps.push({ - name: 'Upload coverage to Codecov', - uses: 'codecov/codecov-action@v4', - with: { - token: `\${{ secrets.${codecovTokenSecret} }}`, - files: './coverage/coverage-final.json', - 'fail_ci_if_error': false, - }, - }); - } - - jobs['validate-coverage'] = { - name: 'Run validation with coverage', - 'runs-on': DEFAULT_RUNNER_OS, - steps: coverageSteps, - }; } - } else { - // Non-matrix: Create jobs based on parallel flag - // - parallel: false → One job per phase (phase-based grouping) - // - parallel: true → One job per step (step-based parallelism) - - let previousJobIds: string[] | undefined; - - for (const phase of phases) { - - // Determine needs based on previous phase - const needs: string[] | undefined = previousJobIds; - - if (phase.parallel === false) { - // Phase-based grouping: ONE job with sequential workflow steps - const phaseJobId = toJobId(phase.name); - const jobSteps = createCommonJobSetupSteps(nodeVersions[0], packageManager); - - // Add bootstrap build if this phase has a build step - const hasBuildStep = phase.steps.some(s => s.name.toLowerCase().includes('build')); - if (hasBuildStep) { - jobSteps.push({ - name: STEP_NAME_BUILD_PACKAGES, - run: getBuildCommand(packageManager), - }); - } - - // Add each step as a separate workflow step - for (const step of phase.steps) { - const stepWorkflowStep: GitHubWorkflowStep = { - name: step.name, - run: step.command, - }; - - // Add working directory if specified (relative to git root) - if (step.cwd) { - stepWorkflowStep['working-directory'] = step.cwd; - } - - // Add environment variables from step config - if (step.env) { - stepWorkflowStep.env = { ...step.env }; - } - - jobSteps.push(stepWorkflowStep); - - // Add coverage reporting if enabled - addCoverageReportingSteps(jobSteps, step.name, enableCoverage, coverageProvider); - } - - jobs[phaseJobId] = { - name: phase.name, - 'runs-on': os[0], - ...(needs && { needs }), - steps: jobSteps, - }; - - // Next phase depends on this phase job - previousJobIds = [phaseJobId]; - } else { - // Step-based parallelism: Separate job for each step (existing behavior) - const stepJobIds: string[] = []; - - for (const step of phase.steps) { - const jobId = toJobId(step.name); - stepJobIds.push(jobId); - const jobSteps = createCommonJobSetupSteps(nodeVersions[0], packageManager); - - // Add the actual validation command - const testStep: GitHubWorkflowStep = { run: step.command }; - - // Add working directory if specified (relative to git root) - if (step.cwd) { - testStep['working-directory'] = step.cwd; - } - - // Add environment variables from step config - if (step.env) { - testStep.env = { ...step.env }; - } - - jobSteps.push(testStep); - - // Add coverage reporting if enabled - addCoverageReportingSteps(jobSteps, step.name, enableCoverage, coverageProvider); - - jobs[jobId] = { - name: step.name, - 'runs-on': os[0], - ...(needs && { needs }), - steps: jobSteps, - }; - } - - // Next phase depends on ALL step jobs from this phase - previousJobIds = stepJobIds; - } - } + jobs['validate-coverage'] = { + name: 'Run validation with coverage', + 'runs-on': DEFAULT_RUNNER_OS, + steps: coverageSteps, + }; } // Add gate job - all validation must pass - let allJobs: string[]; - if (useMatrix) { - allJobs = enableCoverage ? ['validate', 'validate-coverage'] : ['validate']; - } else { - allJobs = getAllJobIds(phases); - } + const allJobs = enableCoverage ? ['validate', 'validate-coverage'] : ['validate']; jobs['all-validation-passed'] = { name: 'All Validation Passed', @@ -781,10 +559,9 @@ The \`generate-workflow\` command generates a \`.github/workflows/validate.yml\` ## How It Works 1. Reads vibe-validate.config.yaml configuration -2. Generates GitHub Actions workflow with proper job dependencies -3. Supports matrix mode (multiple Node/OS versions) -4. Supports non-matrix mode (separate jobs per phase) -5. Can check if workflow is in sync with config +2. Generates a single validate job matching local behavior +3. Supports matrix strategy for multiple Node/OS versions +4. Can check if workflow is in sync with config ## Options diff --git a/packages/cli/test/bin/wrapper.test.ts b/packages/cli/test/bin/wrapper.test.ts index da7fb754..3110b74e 100644 --- a/packages/cli/test/bin/wrapper.test.ts +++ b/packages/cli/test/bin/wrapper.test.ts @@ -15,7 +15,7 @@ import { executeWrapperSync, type WrapperResultSync } from '../helpers/test-comm */ // Test constants -const EXPECTED_VERSION = '0.19.0-rc.12'; // BUMP_VERSION_UPDATE +const EXPECTED_VERSION = '0.19.0-rc.13'; // BUMP_VERSION_UPDATE const REPO_ROOT = join(__dirname, '../../../..'); const PACKAGES_CORE = join(__dirname, '../../../core'); diff --git a/packages/cli/test/commands/generate-workflow.test.ts b/packages/cli/test/commands/generate-workflow.test.ts index 6d2f1acf..ad51522a 100644 --- a/packages/cli/test/commands/generate-workflow.test.ts +++ b/packages/cli/test/commands/generate-workflow.test.ts @@ -8,7 +8,7 @@ import type { VibeValidateConfig } from '@vibe-validate/config'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { parse as parseYaml } from 'yaml'; -import { generateWorkflow, checkSync, toJobId, getAllJobIds, type GenerateWorkflowOptions } from '../../src/commands/generate-workflow.js'; +import { generateWorkflow, checkSync, toJobId, type GenerateWorkflowOptions } from '../../src/commands/generate-workflow.js'; import { mockConfig as baseMockConfig } from '../helpers/generate-workflow-fixtures.js'; @@ -120,7 +120,7 @@ function generateAndParseWorkflow( } /** - * Test package manager detection from packageManager field + * Test package manager detection from packageManager field (uses validate job) */ function testPackageManagerDetection( pm: string, @@ -131,8 +131,8 @@ function testPackageManagerDetection( mockPackageJson(`${pm}@${version}`); vi.mocked(existsSync).mockReturnValue(true); - const workflow = generateAndParseWorkflow(baseMockConfig, { useMatrix: false }); - const job = workflow.jobs['typescript-type-check']; + const workflow = generateAndParseWorkflow(baseMockConfig); + const job = workflow.jobs['validate']; if (expectedAction) { expectStepWithUses(job, expectedAction); @@ -143,7 +143,7 @@ function testPackageManagerDetection( } /** - * Test package manager detection from lockfile + * Test package manager detection from lockfile (uses validate job) */ function testLockfileDetection( lockfileConfig: { @@ -158,8 +158,8 @@ function testLockfileDetection( mockLockfiles({ ...lockfileConfig, packageJsonExists: true }); mockPackageJson(undefined, { node: '>=22.0.0' }); - const workflow = generateAndParseWorkflow(baseMockConfig, { useMatrix: false }); - const job = workflow.jobs['typescript-type-check']; + const workflow = generateAndParseWorkflow(baseMockConfig); + const job = workflow.jobs['validate']; if (expectedAction) { expectStepWithUses(job, expectedAction); @@ -168,7 +168,7 @@ function testLockfileDetection( } /** - * Test package manager commands in build workflow + * Test package manager commands in build workflow (uses validate job) */ function testBuildCommands(pm: string, version: string, installCmd: string, buildCmd: string) { mockPackageJson(`${pm}@${version}`); @@ -187,10 +187,9 @@ function testBuildCommands(pm: string, version: string, installCmd: string, buil }, }; - const workflow = generateAndParseWorkflow(configWithBuild, { useMatrix: false }); - const job = workflow.jobs['build']; + const workflow = generateAndParseWorkflow(configWithBuild); + const job = workflow.jobs['validate']; expectStepWithRun(job, installCmd); - expectStepWithRun(job, buildCmd); } /** @@ -205,8 +204,7 @@ function generateBunMatrixWorkflow(options: { vi.mocked(existsSync).mockReturnValue(true); const workflow = generateAndParseWorkflow(baseMockConfig, { - useMatrix: true, - nodeVersions: options.nodeVersions ?? ['20', '22'], + nodeVersions: options.nodeVersions ?? ['20', '22'], os: options.os ?? ['ubuntu-latest', 'windows-latest'] }); @@ -244,22 +242,6 @@ describe('generate-workflow command', () => { }); }); - describe('getAllJobIds', () => { - it('should extract all job IDs from phases', () => { - const jobIds = getAllJobIds(mockConfig.validation.phases); - expect(jobIds).toEqual([ - 'typescript-type-check', - 'eslint-code-quality', - 'testing', // Phase 2 has parallel: false, so job is named after phase - ]); - }); - - it('should handle empty phases', () => { - const jobIds = getAllJobIds([]); - expect(jobIds).toEqual([]); - }); - }); - describe('generateWorkflow', () => { it('should generate valid GitHub Actions workflow YAML', () => { const workflow = generateAndParseWorkflow(); @@ -269,196 +251,50 @@ describe('generate-workflow command', () => { expect(workflow.on.pull_request.branches).toContain('main'); }); - it('should generate jobs for each validation step in non-matrix mode', () => { - const workflow = generateAndParseWorkflow(mockConfig, { useMatrix: false }); + it('should always generate single validate job', () => { + const workflow = generateAndParseWorkflow(mockConfig); - expect(workflow.jobs).toHaveProperty('typescript-type-check'); - expect(workflow.jobs).toHaveProperty('eslint-code-quality'); - expect(workflow.jobs).toHaveProperty('testing'); // Phase 2 has parallel: false - }); - - it('should auto-depend on previous phase in non-matrix mode', () => { - const workflow = generateAndParseWorkflow(mockConfig, { useMatrix: false }); - - // Testing phase (phase 2) auto-depends on Pre-Qualification phase (phase 1) - expect(workflow.jobs['testing'].needs).toEqual([ - 'typescript-type-check', - 'eslint-code-quality', - ]); + // Always generates a single validate job + expect(workflow.jobs).toHaveProperty('validate'); + expect(workflow.jobs).not.toHaveProperty('typescript-type-check'); + expect(workflow.jobs).not.toHaveProperty('eslint-code-quality'); + expect(workflow.jobs).not.toHaveProperty('testing'); }); - it('should include checkout and setup-node steps in non-matrix mode', () => { - const workflow = generateAndParseWorkflow(mockConfig, { useMatrix: false }); + it('should include checkout and setup-node steps', () => { + const workflow = generateAndParseWorkflow(mockConfig); - const job = workflow.jobs['typescript-type-check']; + const job = workflow.jobs['validate']; expect(job.steps[0].uses).toBe('actions/checkout@v4'); - expect(job.steps[1].uses).toBe('actions/setup-node@v4'); + expect(job.steps.some((s: any) => s.uses === 'actions/setup-node@v4')).toBe(true); }); + it('should add all-validation-passed gate job', () => { + const workflow = generateAndParseWorkflow(mockConfig); - it('should add all-validation-passed gate job in non-matrix mode', () => { - const workflow = generateAndParseWorkflow(mockConfig, { useMatrix: false }); - - expectGateJob(workflow, [ - 'typescript-type-check', - 'eslint-code-quality', - 'testing', // Phase 2 job named after phase - ]); + expectGateJob(workflow, ['validate']); }); - it('should detect pnpm and add pnpm installation steps in non-matrix mode', () => { + it('should detect pnpm and add pnpm installation steps', () => { const workflow = generateAndParseWorkflow(mockConfig, { packageManager: 'pnpm', - useMatrix: false, }); - const job = workflow.jobs['typescript-type-check']; + const job = workflow.jobs['validate']; const pnpmStep = expectStepWithUses(job, 'pnpm/action-setup'); - expect(pnpmStep.with.version).toBe('8'); + expect(pnpmStep.with.version).toBe('9'); expectStepWithRun(job, 'pnpm install --frozen-lockfile'); }); - it('should use npm ci when packageManager is npm in non-matrix mode', () => { + it('should use npm ci when packageManager is npm', () => { const workflow = generateAndParseWorkflow(mockConfig, { packageManager: 'npm', - useMatrix: false, }); - const job = workflow.jobs['typescript-type-check']; + const job = workflow.jobs['validate']; expectStepWithRun(job, 'npm ci'); }); - it('should add coverage reporting when enabled in non-matrix mode', () => { - const workflowYaml = generateWorkflow(mockConfig, { - enableCoverage: true, - coverageProvider: 'codecov', - useMatrix: false, - }); - const workflow = parseWorkflowYaml(workflowYaml); - - const coverageJob = workflow.jobs['testing']; // Phase 2 job named after phase - const codecovStep = coverageJob.steps.find( - (s: any) => s.uses === 'codecov/codecov-action@v3' - ); - expect(codecovStep).toBeDefined(); - }); - - it('should preserve step environment variables in non-matrix mode', () => { - const configWithEnv: VibeValidateConfig = { - ...mockConfig, - validation: { - ...mockConfig.validation, - phases: [ - { - name: 'Test', - parallel: false, - steps: [ - { - name: 'Test with Env', - command: 'npm test', - env: { - NODE_ENV: 'test', - API_KEY: '${{ secrets.API_KEY }}', - }, - }, - ], - timeout: 300000, - failFast: true, - }, - ], - }, - }; - - const workflowYaml = generateWorkflow(configWithEnv, { useMatrix: false }); - const workflow = parseWorkflowYaml(workflowYaml); - - const job = workflow.jobs['test']; // Phase has parallel: false, job named after phase - const testStep = job.steps.find((s: any) => s.run === 'npm test'); - expect(testStep.env.NODE_ENV).toBe('test'); - expect(testStep.env.API_KEY).toBe('${{ secrets.API_KEY }}'); - }); - - it('should add working-directory when step has cwd field in non-matrix mode (phase-based)', () => { - const configWithCwd: VibeValidateConfig = { - ...mockConfig, - validation: { - ...mockConfig.validation, - phases: [ - { - name: 'Test Backend', - parallel: false, - steps: [ - { - name: 'Run backend tests', - command: 'npm test', - cwd: 'packages/backend', - }, - ], - timeout: 300000, - failFast: true, - }, - ], - }, - }; - - const workflowYaml = generateWorkflow(configWithCwd, { useMatrix: false }); - const workflow = parseWorkflowYaml(workflowYaml); - - const job = workflow.jobs['test-backend']; // Phase has parallel: false, job named after phase - const testStep = job.steps.find((s: any) => s.run === 'npm test'); - expect(testStep['working-directory']).toBe('packages/backend'); - }); - - it('should add working-directory when step has cwd field in non-matrix mode (step-based)', () => { - const configWithCwd: VibeValidateConfig = { - ...mockConfig, - validation: { - ...mockConfig.validation, - phases: [ - { - name: 'Test', - parallel: true, - steps: [ - { - name: 'Test Frontend', - command: 'npm test', - cwd: 'packages/frontend', - }, - { - name: 'Test Backend', - command: 'npm test', - cwd: 'packages/backend', - }, - ], - timeout: 300000, - failFast: false, - }, - ], - }, - }; - - const workflowYaml = generateWorkflow(configWithCwd, { useMatrix: false }); - const workflow = parseWorkflowYaml(workflowYaml); - - // In step-based parallelism, each step becomes a separate job - const frontendJob = workflow.jobs['test-frontend']; - const frontendStep = frontendJob.steps.find((s: any) => s.run === 'npm test'); - expect(frontendStep['working-directory']).toBe('packages/frontend'); - - const backendJob = workflow.jobs['test-backend']; - const backendStep = backendJob.steps.find((s: any) => s.run === 'npm test'); - expect(backendStep['working-directory']).toBe('packages/backend'); - }); - - it('should not add working-directory when step has no cwd field', () => { - const workflowYaml = generateWorkflow(mockConfig, { useMatrix: false }); - const workflow = parseWorkflowYaml(workflowYaml); - - const job = workflow.jobs['typescript-type-check']; - const testStep = job.steps.find((s: any) => s.run === 'pnpm -r typecheck'); - expect(testStep['working-directory']).toBeUndefined(); - }); - it('should include header without timestamp', () => { const workflowYaml = generateWorkflow(mockConfig); expect(workflowYaml).not.toContain('# Generated:'); // No timestamp in v0.9.6+ @@ -517,66 +353,24 @@ describe('generate-workflow command', () => { expect(workflow.jobs.validate.strategy['fail-fast']).toBe(true); }); - it('should use non-matrix mode when single node version and single OS', () => { + it('should always use single validate job even with single node version and OS', () => { const workflowYaml = generateWorkflow(mockConfig, { nodeVersions: ['20'], os: ['ubuntu-latest'], - useMatrix: false, - }); - const workflow = parseWorkflowYaml(workflowYaml); - - // In non-matrix mode, jobs are created per step - expect(workflow.jobs).toHaveProperty('typescript-type-check'); - expect(workflow.jobs).toHaveProperty('eslint-code-quality'); - expect(workflow.jobs).not.toHaveProperty('validate'); - }); - - it('should create validate job in matrix mode', () => { - const workflowYaml = generateWorkflow(mockConfig, { - nodeVersions: ['20', '22'], - os: ['ubuntu-latest'], - useMatrix: true, }); const workflow = parseWorkflowYaml(workflowYaml); - // In matrix mode, single validate job with strategy + // Always generates a single validate job with matrix strategy expect(workflow.jobs).toHaveProperty('validate'); expect(workflow.jobs.validate).toHaveProperty('strategy'); - expect(workflow.jobs).not.toHaveProperty('typescript-type-check'); + expect(workflow.jobs.validate.strategy.matrix.node).toEqual(['20']); + expect(workflow.jobs.validate.strategy.matrix.os).toEqual(['ubuntu-latest']); }); - it('should include checkout and setup steps in matrix mode', () => { - const workflowYaml = generateWorkflow(mockConfig, { - nodeVersions: ['20', '22'], - useMatrix: true, - }); - const workflow = parseWorkflowYaml(workflowYaml); - - const job = workflow.jobs['validate']; - expect(job.steps[0].uses).toBe('actions/checkout@v4'); - expect(job.steps.some((s: any) => s.uses === 'actions/setup-node@v4')).toBe(true); - }); - - it('should add pnpm setup in matrix mode', () => { - const workflowYaml = generateWorkflow(mockConfig, { - packageManager: 'pnpm', - nodeVersions: ['20', '22'], - useMatrix: true, - }); - const workflow = parseWorkflowYaml(workflowYaml); - - const job = workflow.jobs['validate']; - const pnpmStep = job.steps.find((s: any) => s.uses === 'pnpm/action-setup@v2'); - expect(pnpmStep).toBeDefined(); - expect(pnpmStep.with.version).toBe('9'); - }); - - it('should NOT add validation state upload (deprecated in v0.12.0)', () => { const workflowYaml = generateWorkflow(mockConfig, { nodeVersions: ['20', '22'], os: ['ubuntu-latest', 'macos-latest'], - useMatrix: true, }); const workflow = parseWorkflowYaml(workflowYaml); @@ -593,8 +387,7 @@ describe('generate-workflow command', () => { enableCoverage: true, nodeVersions: ['20', '22', '24'], os: ['ubuntu-latest', 'macos-latest'], - useMatrix: true, - }); + }); const workflow = parseWorkflowYaml(workflowYaml); // Should have validate job with matrix @@ -614,8 +407,7 @@ describe('generate-workflow command', () => { it('should add all-validation-passed gate job in matrix mode', () => { const workflowYaml = generateWorkflow(mockConfig, { nodeVersions: ['20', '22'], - useMatrix: true, - }); + }); const workflow = parseWorkflowYaml(workflowYaml); expect(workflow.jobs).toHaveProperty('all-validation-passed'); @@ -627,8 +419,7 @@ describe('generate-workflow command', () => { const workflowYaml = generateWorkflow(mockConfig, { enableCoverage: true, nodeVersions: ['20', '22'], - useMatrix: true, - }); + }); const workflow = parseWorkflowYaml(workflowYaml); expect(workflow.jobs['all-validation-passed'].needs).toEqual([ @@ -739,8 +530,8 @@ describe('generate-workflow command', () => { mockPackageJson('npm@10.0.0'); vi.mocked(existsSync).mockReturnValue(true); - const workflow = generateAndParseWorkflow(mockConfig, { useMatrix: false }); - const job = workflow.jobs['typescript-type-check']; + const workflow = generateAndParseWorkflow(mockConfig); + const job = workflow.jobs['validate']; expectStepWithRun(job, 'npm ci'); expectNoStepWithUses(job, 'pnpm/action-setup'); }); @@ -749,10 +540,10 @@ describe('generate-workflow command', () => { mockLockfiles({ hasPackageLock: true, hasPnpmLock: true }); mockPackageJson(undefined, { node: '>=22.0.0' }); - const workflow = generateAndParseWorkflow(mockConfig, { useMatrix: false }); + const workflow = generateAndParseWorkflow(mockConfig); // Should default to npm when both exist (more conservative) - const job = workflow.jobs['typescript-type-check']; + const job = workflow.jobs['validate']; expectStepWithRun(job, 'npm ci'); expectNoStepWithUses(job, 'pnpm/action-setup'); }); @@ -761,10 +552,10 @@ describe('generate-workflow command', () => { mockLockfiles({ hasPackageLock: false, hasPnpmLock: true, packageJsonExists: true }); mockPackageJson(undefined, { node: '>=22.0.0' }); - const workflow = generateAndParseWorkflow(mockConfig, { useMatrix: false }); + const workflow = generateAndParseWorkflow(mockConfig); // Should use pnpm when only pnpm-lock exists - const job = workflow.jobs['typescript-type-check']; + const job = workflow.jobs['validate']; expectStepWithUses(job, 'pnpm/action-setup'); }); @@ -772,10 +563,10 @@ describe('generate-workflow command', () => { mockLockfiles({ hasPackageLock: true, hasPnpmLock: false, packageJsonExists: true }); mockPackageJson(undefined, { node: '>=22.0.0' }); - const workflow = generateAndParseWorkflow(mockConfig, { useMatrix: false }); + const workflow = generateAndParseWorkflow(mockConfig); // Should use npm when only package-lock exists - const job = workflow.jobs['typescript-type-check']; + const job = workflow.jobs['validate']; expectStepWithRun(job, 'npm ci'); }); @@ -783,10 +574,10 @@ describe('generate-workflow command', () => { vi.mocked(existsSync).mockReturnValue(true); mockPackageJson('pnpm@9.0.0', { node: '>=22.0.0' }); - const workflow = generateAndParseWorkflow(mockConfig, { useMatrix: false }); + const workflow = generateAndParseWorkflow(mockConfig); // Should use pnpm from packageManager field (not default to npm) - const job = workflow.jobs['typescript-type-check']; + const job = workflow.jobs['validate']; expectStepWithUses(job, 'pnpm/action-setup'); }); @@ -838,7 +629,7 @@ describe('generate-workflow command', () => { testBuildCommands('yarn', '4.0.0', 'yarn install --frozen-lockfile', 'yarn run build'); }); - it('should use bun in matrix mode', () => { + it('should use bun with validate job', () => { const job = generateBunMatrixWorkflow(); expect(job.strategy.matrix.node).toEqual(['20', '22']); @@ -848,7 +639,7 @@ describe('generate-workflow command', () => { expectStepWithRun(job, 'bun run validate'); }); - it('should include Node.js setup for Bun projects with matrix for compatibility testing', () => { + it('should include Node.js setup for Bun projects for compatibility testing', () => { const job = generateBunMatrixWorkflow(); // Should have BOTH Bun and Node.js setup @@ -872,8 +663,7 @@ describe('generate-workflow command', () => { mockPackageJson(undefined, { node: '>=22.0.0' }); const workflow = generateAndParseWorkflow(mockConfig, { - useMatrix: true, - nodeVersions: ['22', '24'], + nodeVersions: ['22', '24'], }); const job = workflow.jobs['validate']; @@ -887,13 +677,12 @@ describe('generate-workflow command', () => { expect(nodeStep.with.cache).toBeUndefined(); }); - it('should use yarn in matrix mode', () => { + it('should use yarn with validate job', () => { mockPackageJson('yarn@4.0.0'); vi.mocked(existsSync).mockReturnValue(true); const workflow = generateAndParseWorkflow(mockConfig, { - useMatrix: true, - nodeVersions: ['20', '22'], + nodeVersions: ['20', '22'], os: ['ubuntu-latest'] }); diff --git a/packages/config/package.json b/packages/config/package.json index 520e7adc..8978365f 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -1,6 +1,6 @@ { "name": "@vibe-validate/config", - "version": "0.19.0-rc.12", + "version": "0.19.0-rc.13", "description": "Configuration system for vibe-validate with TypeScript-first design and config templates", "type": "module", "main": "./dist/index.js", diff --git a/packages/core/package.json b/packages/core/package.json index e6d6e7cd..3ca36b4f 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@vibe-validate/core", - "version": "0.19.0-rc.12", + "version": "0.19.0-rc.13", "description": "Core validation orchestration engine for vibe-validate", "type": "module", "main": "./dist/index.js", diff --git a/packages/extractors/package.json b/packages/extractors/package.json index 54834e83..149a5ac3 100644 --- a/packages/extractors/package.json +++ b/packages/extractors/package.json @@ -1,6 +1,6 @@ { "name": "@vibe-validate/extractors", - "version": "0.19.0-rc.12", + "version": "0.19.0-rc.13", "description": "LLM-optimized error extractors for validation output", "type": "module", "main": "./dist/index.js", diff --git a/packages/git/package.json b/packages/git/package.json index ce2e880d..ee855d70 100644 --- a/packages/git/package.json +++ b/packages/git/package.json @@ -1,6 +1,6 @@ { "name": "@vibe-validate/git", - "version": "0.19.0-rc.12", + "version": "0.19.0-rc.13", "description": "Git utilities for vibe-validate - tree hash calculation, branch sync, and post-merge cleanup", "type": "module", "main": "./dist/index.js", diff --git a/packages/history/package.json b/packages/history/package.json index 0dae32c5..20bfa98b 100644 --- a/packages/history/package.json +++ b/packages/history/package.json @@ -1,6 +1,6 @@ { "name": "@vibe-validate/history", - "version": "0.19.0-rc.12", + "version": "0.19.0-rc.13", "description": "Validation history tracking via git notes for vibe-validate", "type": "module", "main": "./dist/index.js", diff --git a/packages/utils/package.json b/packages/utils/package.json index 68a8bd17..37ed0cb8 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -1,6 +1,6 @@ { "name": "@vibe-validate/utils", - "version": "0.19.0-rc.12", + "version": "0.19.0-rc.13", "description": "Common utilities for vibe-validate packages (command execution, path normalization)", "type": "module", "main": "./dist/index.js", diff --git a/packages/vibe-validate/package.json b/packages/vibe-validate/package.json index 04e817cf..2d1fb91b 100644 --- a/packages/vibe-validate/package.json +++ b/packages/vibe-validate/package.json @@ -1,6 +1,6 @@ { "name": "vibe-validate", - "version": "0.19.0-rc.12", + "version": "0.19.0-rc.13", "description": "Git-aware validation orchestration for vibe coding (LLM-assisted development) - umbrella package", "type": "module", "bin": {