diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b535d78 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,27 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npm test \ No newline at end of file diff --git a/.github/workflows/version.yml b/.github/workflows/version.yml index e4acf6e..0b55970 100644 --- a/.github/workflows/version.yml +++ b/.github/workflows/version.yml @@ -1,14 +1,16 @@ name: Version on: - pull_request: - types: [closed] + workflow_run: + workflows: ["CI"] + types: + - completed branches: [main] workflow_dispatch: jobs: release: - if: github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' + if: github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest permissions: contents: write # Required for pushing commits/tags @@ -20,22 +22,12 @@ jobs: with: fetch-depth: 0 # Required for commit analysis - - name: Setup Node.js - uses: actions/setup-node@v4 + - name: Run Grubble (Version + Tag) + uses: davegarvey/grubble@v4 with: - node-version: '18' - cache: npm - - - name: Configure Git - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - - - name: Install dependencies - run: npm ci - - - name: Run tests - run: npm test - - - name: Run Bumper (Version + Tag) - run: npx github:davegarvey/bumper --tag --push + push: true + tag: true + release-notes: true + preset: node + git-user-name: github-actions[bot] + git-user-email: 41898282+github-actions[bot]@users.noreply.github.com diff --git a/src/git.js b/src/git.js index 175bc4d..ba4a720 100644 --- a/src/git.js +++ b/src/git.js @@ -51,27 +51,30 @@ export async function getCommitsSinceLastTag(currentTag, previousTag = null) { // Get commits with subject, body, author, and hash // Format: hash|author|date|subject|body + // Note: commits with no body will have an extra | before the ||| separator const { stdout } = await execAsync( `git log ${range} --pretty=format:"%H|%an|%ai|%s|%b|||"` ); - if (!stdout.trim()) { + if (!stdout || !stdout.trim()) { return []; } // Parse commits + // Split by ||| and remove the trailing | from entries (from empty body commits) const commits = stdout .split('|||') - .filter(entry => entry.trim()) + .map(entry => entry.replace(/^\|?\n?/, '')) // Remove leading | or newline + .filter(entry => entry && entry.trim()) .map(entry => { const [hash, author, date, subject, ...bodyParts] = entry.split('|'); const body = bodyParts.join('|').trim(); return { - hash: hash.trim(), - author: author.trim(), - date: date.trim(), - subject: subject.trim(), + hash: hash?.trim() || '', + author: author?.trim() || '', + date: date?.trim() || '', + subject: subject?.trim() || '', body: body || '' }; }) diff --git a/test/git.test.js b/test/git.test.js index 6c09106..689472d 100644 --- a/test/git.test.js +++ b/test/git.test.js @@ -1,11 +1,125 @@ -import { describe, it, expect } from 'vitest'; +import { describe, it, expect, vi } from 'vitest'; +import { exec } from 'child_process'; +import { promisify } from 'util'; + +// Mock child_process +vi.mock('child_process', () => ({ + exec: vi.fn() +})); + +const execAsync = promisify(exec); describe('git.js', () => { - // Simplified tests that don't require complex mocking it('should export expected functions', async () => { const git = await import('../src/git.js'); expect(typeof git.getCommitsSinceLastTag).toBe('function'); expect(typeof git.tagExists).toBe('function'); expect(typeof git.getRepoUrl).toBe('function'); }); + + describe('getCommitsSinceLastTag', () => { + it('should parse commits with no body correctly', async () => { + // Mock git output with empty body (creates ||||) + const mockOutput = 'abc123|John Doe|2025-12-15 10:00:00 +0000|feat: add feature||||'; + + vi.mocked(exec).mockImplementation((cmd, callback) => { + callback(null, { stdout: mockOutput, stderr: '' }); + }); + + const { getCommitsSinceLastTag } = await import('../src/git.js'); + const commits = await getCommitsSinceLastTag('v1.0.0', 'v0.9.0'); + + expect(commits).toHaveLength(1); + expect(commits[0]).toEqual({ + hash: 'abc123', + author: 'John Doe', + date: '2025-12-15 10:00:00 +0000', + subject: 'feat: add feature', + body: '' + }); + }); + + it('should parse commits with body correctly', async () => { + // Mock git output with body content + const mockOutput = 'def456|Jane Smith|2025-12-15 11:00:00 +0000|fix: fix bug|This is the body content|||'; + + vi.mocked(exec).mockImplementation((cmd, callback) => { + callback(null, { stdout: mockOutput, stderr: '' }); + }); + + const { getCommitsSinceLastTag } = await import('../src/git.js'); + const commits = await getCommitsSinceLastTag('v1.0.0', 'v0.9.0'); + + expect(commits).toHaveLength(1); + expect(commits[0]).toEqual({ + hash: 'def456', + author: 'Jane Smith', + date: '2025-12-15 11:00:00 +0000', + subject: 'fix: fix bug', + body: 'This is the body content' + }); + }); + + it('should parse multiple commits with mixed body presence', async () => { + // Real-world scenario: mix of commits with and without bodies + const mockOutput = `abc123|John Doe|2025-12-15 10:00:00 +0000|feat: add feature|||| +def456|Jane Smith|2025-12-15 11:00:00 +0000|fix: fix bug|Body with details||| +ghi789|Bob Jones|2025-12-15 12:00:00 +0000|docs: update README|||| +jkl012|Alice Brown|2025-12-15 13:00:00 +0000|chore: bump version||||`; + + vi.mocked(exec).mockImplementation((cmd, callback) => { + callback(null, { stdout: mockOutput, stderr: '' }); + }); + + const { getCommitsSinceLastTag } = await import('../src/git.js'); + const commits = await getCommitsSinceLastTag('v1.0.0', 'v0.9.0'); + + expect(commits).toHaveLength(4); + expect(commits[0].hash).toBe('abc123'); + expect(commits[0].body).toBe(''); + expect(commits[1].hash).toBe('def456'); + expect(commits[1].body).toBe('Body with details'); + expect(commits[2].hash).toBe('ghi789'); + expect(commits[2].body).toBe(''); + expect(commits[3].hash).toBe('jkl012'); + expect(commits[3].body).toBe(''); + }); + + it('should handle empty git output', async () => { + vi.mocked(exec).mockImplementation((cmd, callback) => { + callback(null, { stdout: '', stderr: '' }); + }); + + const { getCommitsSinceLastTag } = await import('../src/git.js'); + const commits = await getCommitsSinceLastTag('v1.0.0', 'v0.9.0'); + + expect(commits).toHaveLength(0); + }); + + it('should handle undefined stdout', async () => { + vi.mocked(exec).mockImplementation((cmd, callback) => { + callback(null, { stdout: undefined, stderr: '' }); + }); + + const { getCommitsSinceLastTag } = await import('../src/git.js'); + const commits = await getCommitsSinceLastTag('v1.0.0', 'v0.9.0'); + + expect(commits).toHaveLength(0); + }); + + it('should handle body with pipe characters', async () => { + // Body content that includes pipe characters + const mockOutput = 'abc123|John Doe|2025-12-15 10:00:00 +0000|feat: add feature|This | has | pipes|||'; + + vi.mocked(exec).mockImplementation((cmd, callback) => { + callback(null, { stdout: mockOutput, stderr: '' }); + }); + + const { getCommitsSinceLastTag } = await import('../src/git.js'); + const commits = await getCommitsSinceLastTag('v1.0.0', 'v0.9.0'); + + expect(commits).toHaveLength(1); + expect(commits[0].body).toBe('This | has | pipes'); + }); + }); }); \ No newline at end of file