← Blog  ·  September 2026

Why your git history is useless to stakeholders — and how to auto-generate changelogs that aren't

Sprint ends. PM asks "what shipped this week?" You stare at 47 commits: "fix typo", "WIP", "more fixes", "finally", "plz work".

Every developer knows this moment. The sprint is over, a release just went out, and someone asks what changed. You open git log --oneline and see something like this:

a3f9c12 fix
9b1e4d8 WIP — do not merge
c7a2e11 finally
d8b3f00 more fixes
e1d5a99 hotfix for prod
f2c6b88 add stuff
04a79c1 cleanup
b5e8d77 tests

This is completely normal. Commit messages are written under time pressure, for the author's short-term memory, not for a PM, a user, or a future developer nine months from now. They're internal shorthand — useful during the sprint, useless outside it.

The result: writing a release changelog is manual, slow, and usually gets skipped. Which means stakeholders don't know what changed, users don't know what's new, and the release just quietly ships into a void.


Why good commit messages alone don't solve this

Conventional Commits is a popular spec that adds structure to commit messages: feat: add dark mode, fix: resolve login redirect loop, chore: update deps. When your team follows it consistently, you can use tools like auto-changelog or semantic-release to mechanically generate a changelog.

The problem is "when your team follows it consistently." Most teams don't. Even teams that start with good intentions drift. And even when they do follow it, mechanical grouping of raw commit messages isn't the same as a human-readable changelog. feat: add dark mode toggle to user settings screen (closes #412) is better than "stuff" — but it's still developer-centric, not user-centric.

A user-facing changelog reads like: "You can now switch between light and dark mode in Settings → Appearance." That takes rewriting, not just grouping.


The gap between commit history and stakeholder communication

Commit message What it means User-facing changelog entry
fix: resolve race cond in auth flow Login was occasionally failing for users on slow connections Fixed intermittent login failures on slow connections
feat: add dark mode toggle New UI setting New: Dark mode — toggle in Settings → Appearance
refactor: migrate from axios to fetch Internal cleanup (omit — no user impact)
fix: CORS issue on /api/export CSV export was broken from certain browsers Fixed CSV export from Safari and Firefox

The transformation from commit message to changelog entry requires context and judgment. The developer knows what the race condition actually affected. A mechanical tool doesn't. But an LLM can make a reasonable inference, and in practice, the result is better than nothing — which is what most teams produce today.


The approach: pipe git log into an LLM

The simplest useful implementation is a script that:

  1. Runs git log for a configurable window (last 7 days, or since last tag)
  2. Formats the commits into a prompt
  3. Sends the prompt to an LLM API (OpenAI, Anthropic, or local model)
  4. Outputs formatted markdown grouped by: New Features, Bug Fixes, Improvements, Internal (or omit internal entirely)

Here's a working implementation in Node.js (~40 lines):

#!/usr/bin/env node
// generate-changelog.js
// Usage: node generate-changelog.js [--since "7 days ago"] [--output CHANGELOG.md]

const { execSync } = require('child_process');
const https = require('https');

const since = process.argv.includes('--since')
  ? process.argv[process.argv.indexOf('--since') + 1]
  : '7 days ago';

const outputFile = process.argv.includes('--output')
  ? process.argv[process.argv.indexOf('--output') + 1]
  : null;

// 1. Get commits
const commits = execSync(
  `git log --since="${since}" --pretty=format:"- %s (%an)" --no-merges`
).toString().trim();

if (!commits) {
  console.log('No commits found in the given window.');
  process.exit(0);
}

// 2. Build prompt
const prompt = `You are a technical writer. Convert the following git commits into a concise, user-facing changelog.

Group entries under these headings (omit empty sections):
## New Features
## Bug Fixes  
## Improvements
## Internal (for changes with no user impact)

Rules:
- Rewrite each entry in plain English for non-developers
- Omit trivial chores (dep updates, typo fixes) unless impactful
- Do not invent details — only describe what the commit message implies
- Keep entries to one sentence each

Commits:
${commits}

Output only the markdown changelog, no preamble.`;

// 3. Call OpenAI
const body = JSON.stringify({
  model: 'gpt-4o-mini',
  messages: [{ role: 'user', content: prompt }],
  max_tokens: 1000,
  temperature: 0.3,
});

const req = https.request({
  hostname: 'api.openai.com',
  path: '/v1/chat/completions',
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`,
    'Content-Type': 'application/json',
    'Content-Length': Buffer.byteLength(body),
  },
}, (res) => {
  let data = '';
  res.on('data', chunk => data += chunk);
  res.on('end', () => {
    const result = JSON.parse(data);
    const changelog = result.choices?.[0]?.message?.content ?? 'Error: no output';
    if (outputFile) {
      require('fs').writeFileSync(outputFile, changelog + '\n');
      console.log(`Changelog written to ${outputFile}`);
    } else {
      console.log(changelog);
    }
  });
});

req.on('error', err => { console.error('Request failed:', err.message); process.exit(1); });
req.write(body);
req.end();

Run it with:

OPENAI_API_KEY=sk-... node generate-changelog.js --since "7 days ago"
# or since last git tag:
OPENAI_API_KEY=sk-... node generate-changelog.js --since "$(git describe --tags --abbrev=0)"

Typical output for a real sprint:

## New Features
- Users can now switch between light and dark mode in Settings → Appearance

## Bug Fixes
- Fixed intermittent login failures on slow network connections
- Resolved CSV export errors in Safari and Firefox

## Improvements
- Reduced initial page load time for dashboards with large datasets

## Internal
- Migrated HTTP client from Axios to native fetch (no user impact)

Automating it with GitHub Actions

Run this on every merge to main and push the changelog to Slack or save it as a release artifact:

# .github/workflows/changelog.yml
name: Generate changelog on merge
on:
  push:
    branches: [main]

jobs:
  changelog:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # need full history for git log
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - name: Generate changelog
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
        run: |
          node generate-changelog.js --since "$(git log --merges -1 --format='%cd' --date=iso HEAD~1)" \
            --output /tmp/CHANGELOG_LATEST.md
          cat /tmp/CHANGELOG_LATEST.md
      - name: Post to Slack (optional)
        if: env.SLACK_WEBHOOK != ''
        env:
          SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
        run: |
          BODY=$(cat /tmp/CHANGELOG_LATEST.md | head -20)
          curl -s -X POST "$SLACK_WEBHOOK" \
            -H 'Content-type: application/json' \
            --data "{\"text\": \"*Release notes:*\n\`\`\`${BODY}\`\`\`\"}"

Total: ~10 lines of workflow YAML. No external services. Runs on every merge. LLM cost per run is roughly $0.001 with gpt-4o-mini.


Where this breaks down

The script above covers 80% of the use case. What it doesn't handle:

These are solvable — but they're engineering work on top of the core generation logic. At some point you're building a changelog pipeline, not a script.

The practical tradeoff The script in this article gets you from "no changelog" to "decent changelog" in under an hour. Getting from "decent changelog" to "automated, versioned, multi-repo, multi-destination changelog delivered on schedule" is a different project.

What to do right now

  1. Copy the script above into your project root as generate-changelog.js
  2. Run it against your last 7 days: OPENAI_API_KEY=sk-... node generate-changelog.js
  3. Review the output — fix anything that seems wrong in the prompt
  4. Add the GitHub Actions workflow if you want automated runs on merge
  5. Decide whether to send output to Slack, save to file, or both

Most teams get a working changelog in an afternoon. The hard part isn't the LLM call — it's deciding where the output should go and who owns keeping it updated.


Try it now — free browser tool

Paste your git log --oneline output and get a formatted changelog instantly. Groups commits into ✨ New Features, 🐛 Bug Fixes, and 🔧 Improvements — handles both conventional commits and messy histories. Runs entirely in the browser; nothing leaves your machine.

Generate your changelog →

Want this automated and pushed to your team automatically?

PushLog is building automated changelog delivery from git history — versioned, multi-repo, with Slack/email delivery on your schedule. No scripting required.

If maintaining changelogs is something you keep meaning to fix, join the waitlist:

Join the PushLog waitlist →

← More articles