DEV Community

Taylor Zhu
Taylor Zhu

Posted on

Case Study: Turning Commit Noise into a Release Digest on a Free Stack

A 300-line pipeline can turn a messy commit history into a release digest people actually read. This case study walks one small project end to end: the background, the goal, the implementation, the results you can measure, and the limits. The stack is deliberately cheap — a free model for classification and a free server for hosting.

Background: release notes were a copy-paste ritual

The project is a small web app with a two-week release cadence. Every release, someone opened the GitHub compare view, copy-pasted merge commit titles into a changelog, and called it done.

The result looked like this:

Merge pull request #412 from team/feature-x
Merge pull request #408 from team/fix-null-author
Merge pull request #401 from team/dark-mode
Enter fullscreen mode Exit fullscreen mode

Nobody read it. The titles described branches, not user impact. The "fix null author" entry didn't say what broke or where. The ritual cost about 40 minutes per release, but the real cost was invisible: every reader had to decode the history themselves.

Goal: three measurable criteria

Before writing code, define what "better" means. For this project:

  1. Time: from git tag to a reviewable digest in under 10 minutes.
  2. Coverage: every commit in the release window classified into one of five categories.
  3. Readability: a non-maintainer can explain what changed after one read.

Notice what's missing: "perfect classification." The goal is a draft a human can review, not an unedited document.

Implementation: three small pieces

The pipeline has three parts: a collector, a classifier, and a publisher.

1. Collector: git log since the last tag

import subprocess

def commits_since(tag: str) -> list[str]:
    out = subprocess.run(
        ["git", "log", f"{tag}..HEAD", "--oneline", "--no-merges"],
        capture_output=True, text=True, check=True
    )
    return [line.split(" ", 1)[1] for line in out.stdout.strip().splitlines()]
Enter fullscreen mode Exit fullscreen mode

--no-merges removes the noise immediately. Merge commits rarely belong in release notes.

2. Classifier: a free model with a constrained prompt

This is where MonkeyCode's free model access fits. For a two-week release, the volume is a few dozen commits — well within a free tier, and the task is narrow enough that a leaderboard-topping model is overkill.

Disclosure: This article was prepared as part of MonkeyCode's product outreach.

The prompt matters more than the model. The classifier asks for JSON, nothing else:

import os
import requests

CATEGORIES = ["fix", "feature", "improvement", "docs", "chore"]

def classify(commits: list[str]) -> str:
    prompt = f"""
You are a release note editor. Classify each commit into exactly one category.
Categories: {', '.join(CATEGORIES)}.
Return a JSON array: [{{"commit": "...", "category": "...", "note": "..."}}]
The note must describe user impact, not the implementation.

Commits:
{chr(10).join(f'- {c}' for c in commits)}
"""
    resp = requests.post(
        os.environ["MODEL_URL"],
        headers={"Authorization": f"Bearer {os.environ['MODEL_API_KEY']}"},
        json={"messages": [{"role": "user", "content": prompt}]},
        timeout=60,
    )
    resp.raise_for_status()
    return resp.json()["choices"][0]["message"]["content"]
Enter fullscreen mode Exit fullscreen mode

The key constraint is the output contract. If the model returns anything that isn't valid JSON, the pipeline fails loudly instead of publishing garbage.

3. Publisher: static digest on a free server

The digest is a single Markdown file. The free server option in MonkeyCode is enough to host a static page for a small team's release notes — no database, no build step on the server.

name: publish-release-digest
on:
  push:
    tags: ["v*"]
jobs:
  digest:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }
      - uses: actions/setup-python@v5
      - run: pip install requests
      - run: python digest.py > digest.md
        env:
          MODEL_URL: ${{ secrets.MODEL_URL }}
          MODEL_API_KEY: ${{ secrets.MODEL_API_KEY }}
      - run: deploy-digest digest.md
Enter fullscreen mode Exit fullscreen mode

The decision table behind the prompt

The categories map to release note actions:

Category Example commit Release note action
fix fix: handle null author in post view "Fixed a crash when a post has no author."
feature feat: add dark mode toggle "Added dark mode."
improvement refactor: cache tag queries "Faster tag pages."
docs docs: update setup guide "Updated setup documentation."
chore chore: bump lodash Omitted from the digest.

The "chore" row saves the most time. Most release note pain is noise, and the model's job is to filter it before a human ever looks.

Results: what to measure, not what to trust

Here is a measurement plan you can run on your own repo in one release cycle:

  1. Record the time from tag to reviewed digest for three releases before, then three after.
  2. Spot-check 20 classified commits per release against your own judgment.
  3. Ask one non-maintainer to summarize the digest in one sentence.

The pattern that typically emerges: agreement is high on fix and chore, and lowest on the improvement vs feature boundary. That boundary is where the human review earns its keep.

A generated digest looks like this:

## v2.4.0

### Features
- Added dark mode.
- Added per-post reading time.

### Fixes
- Fixed a crash when a post has no author.
- Fixed pagination on the archive page.

### Improvements
- Faster tag pages.
Enter fullscreen mode Exit fullscreen mode

Limitations

  • Probabilistic output: the model will misclassify edge cases. The pipeline is a draft generator, not an editor.
  • Commit hygiene is the ceiling: if messages are already "stuff", the digest will be confidently wrong. Fix the commit convention first.
  • Public by default: a free static server means the digest is public. Not suitable for internal-only release notes.
  • Volume limits: a free tier is fine for dozens of commits per release. For hundreds, check the current limits before committing to the workflow.

Who should not use this

Skip this approach if you need legally reviewed release notes, if your releases contain hundreds of commits, or if your team won't do the review pass. A bad digest with no human gate is worse than no digest — it manufactures false confidence.

Lessons learned

  • Scope the task tightly. "Classify a commit into five categories" is a small problem. A small model with a strict output contract is enough.
  • The bottleneck is input, not intelligence. The model's accuracy tracks the quality of your commit messages.
  • Fail loudly. A parser that accepts any shape of model output will publish nonsense. Validate the JSON schema before deploying.

If you want to test this pipeline on your own small repo, MonkeyCode's free model access and free server option are enough to run the whole thing without spending anything. The artifact is small on purpose: the value is in the workflow, not the code.

Top comments (0)