From 6c094da58a2b9603706fd1325fc155a9bc5e45da Mon Sep 17 00:00:00 2001 From: David Garvey Date: Fri, 12 Dec 2025 13:55:54 +0100 Subject: [PATCH 1/2] ci: separate CI workflow and update version workflow to use rust bumper --- .github/workflows/ci.yml | 27 +++++++++++++++++++++++++++ .github/workflows/version.yml | 24 +++++++++--------------- 2 files changed, 36 insertions(+), 15 deletions(-) create mode 100644 .github/workflows/ci.yml 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..2377bb9 100644 --- a/.github/workflows/version.yml +++ b/.github/workflows/version.yml @@ -1,8 +1,10 @@ name: Version on: - pull_request: - types: [closed] + workflow_run: + workflows: ["CI"] + types: + - completed branches: [main] workflow_dispatch: @@ -20,22 +22,14 @@ jobs: with: fetch-depth: 0 # Required for commit analysis - - name: Setup Node.js - uses: actions/setup-node@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 + uses: davegarvey/bumper@v1 + with: + push: true + tag: true + preset: node From 3dbb06f1091f036ebf22ad732cdaf60f71aad859 Mon Sep 17 00:00:00 2001 From: David Garvey Date: Mon, 15 Dec 2025 23:03:57 +0100 Subject: [PATCH 2/2] fix(git): handle empty commit bodies in git parsing Fix parsing bug where commits with empty bodies caused undefined errors when used with grubble. Added defensive programming to handle malformed git log output. - Add .replace() to strip leading | or newline from empty body entries - Add null checks for hash, author, date, subject fields - Update CI workflow to use grubble@v4 instead of bumper@v1 - Add comprehensive tests covering edge cases for git parsing --- .github/workflows/version.yml | 14 ++-- src/git.js | 15 +++-- test/git.test.js | 118 +++++++++++++++++++++++++++++++++- 3 files changed, 131 insertions(+), 16 deletions(-) diff --git a/.github/workflows/version.yml b/.github/workflows/version.yml index 2377bb9..0b55970 100644 --- a/.github/workflows/version.yml +++ b/.github/workflows/version.yml @@ -10,7 +10,7 @@ on: 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 @@ -22,14 +22,12 @@ jobs: with: fetch-depth: 0 # Required for commit analysis - - name: Configure Git - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - - - name: Run Bumper (Version + Tag) - uses: davegarvey/bumper@v1 + - name: Run Grubble (Version + Tag) + uses: davegarvey/grubble@v4 with: 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