Skip to content

raian-pollock/release-commit-digest

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

release-commit-digest

A daily "what shipped" email, written from your git log, in each reader's own language.

Every day your repository records exactly what changed, in the git log, and almost nobody reads it. The engineer who wrote the code does not need to, and the non-technical people who most want to know what shipped cannot, because a commit log is written for machines and authors. So the honest daily answer to "what did we ship yesterday" lives in a place only one person can read, and everyone else finds out late, in a meeting, or not at all. This is a scheduled job that reads yesterday's commits, has a model translate them into one short HTML email per audience, and delivers each one once. Extracted from a production agent fleet that ran a real company. The prompt is the crown jewel; the code is a thin, distrustful harness around it.

The failure mode this prevents

A team ships every day and communicates it badly, in a specific and repeating shape:

  1. Real work lands as commits. The record is complete and accurate, and unreadable to anyone who did not write it.
  2. The people who care most about what shipped, a non-technical co-founder, a stakeholder, an operator, cannot parse a git log and should not have to.
  3. So the update either does not happen, or the author hand-writes it, which competes with shipping and loses, so it happens late and thins out over time.
  4. Knowledge of what the product actually became drifts apart between the person building it and the people counting on it, one quiet day at a time.

A changelog does not fix this, because a changelog is one voice for everyone and still reads like release notes. A standup does not fix it, because it is verbal, undated, and gone by lunch. What fixes it is a job that pays the translation cost for you, every morning, and writes a different email for each reader out of the same commits.

The second failure mode, which is worse

Once a model is writing the email, it will embellish, and an embellished digest is more dangerous than no digest, because people act on it as fact. Two embellishments in particular destroy trust, and the prompt fights both by name:

  • Inventing a human source. A terse commit becomes "a customer flagged this", when no customer did; the phrase was internal QA. Now a real-sounding signal is in circulation that never existed. The prompt forbids attributing a change to any person unless the commit, issue, or PR names them explicitly, and tells the model to fall back to team or passive voice otherwise.
  • Framing an improvement as a new feature. Commit messages say "add X" when they mean "improve the X that already shipped months ago". A digest that announces existing capabilities as new makes the whole email untrustworthy. The prompt defaults to "improved" and "upgraded" unless the capability genuinely did not exist before.

These rules are the reason to use a written prompt at all rather than a one-line "summarize the commits". A summary optimizes for sounding good. A digest has to optimize for being true, because someone downstream will repeat it.

The third failure mode, which is embarrassing

The first version of this tool had no delivery lock. When the scheduler missed a few days and a catch-up run backfilled them, the reader got three "what shipped" emails inside twenty-five minutes, one per missed date, and every re-invocation would have sent them again. The fix, and a load-bearing part of this design, is a per-audience per-date delivery lock: once an audience has been sent a given date, that pair is marked, and no re-run, backfill, or overlapping job sends it twice. Delivery is idempotent by construction, not by the operator remembering not to run it twice.

Architecture

Four stages, once a day:

gather   ->  compile   ->  write   ->  deliver
git log      one LLM       one HTML    per audience,
for the      pass over     file per    once, behind a
date         the commits   audience    per-date lock
  • gather reads the day's commits from a repo: subjects, author, stats, and diffs capped to a byte budget so a heavy day cannot blow up the prompt. Zero commits short-circuits here.
  • compile is the one model call. The system prompt (prompt.md), the audience list, and the commit data go to whatever you point DIGEST_LLM_CMD at. The model returns a JSON object with one subject and one HTML email per audience. All the judgment lives in the prompt, which is why the prompt is the artifact that matters.
  • write is deterministic and distrustful. Every configured audience must come back with a non-empty subject and HTML, or the whole run fails loudly rather than deliver a blank email. It writes one <audience>.html per audience and the raw commits.txt alongside.
  • deliver hands each audience's HTML to a pluggable delivery command, once, guarded by the per-audience per-date lock. A delivery that fails leaves the lock unset so the next run retries; a delivery that succeeds sets the lock so no run repeats it.

Both the model and the delivery channel are commands you supply. The harness never talks to a network itself, which is what lets the whole thing run end-to-end from a mock with no API key.

The two pluggable commands

Nothing about a specific model or a specific mailer is baked in. Two environment variables carry the whole integration, and each follows the same stdin-to-stdout contract as the mocks in examples/.

DIGEST_LLM_CMD reads the full prompt on stdin and prints the JSON digest on stdout:

export DIGEST_LLM_CMD='claude -p --max-turns 1'
# or: export DIGEST_LLM_CMD='llm -m gpt-5 --no-stream'
# or point it at a five-line script that calls any API

The JSON it must print, one key per configured audience:

{
  "audiences": {
    "team": { "subject": "Shipped Tue 14 Apr", "html": "<full self-contained HTML email>" },
    "eng":  { "subject": "Shipped Tue 14 Apr", "html": "<full self-contained HTML email>" }
  },
  "summary": "one sentence describing the day"
}

DIGEST_DELIVER_CMD reads one audience's HTML on stdin and sends it, using DIGEST_TO, DIGEST_SUBJECT, DIGEST_AUDIENCE, and DIGEST_DATE from its environment. Exit 0 means delivered; a non-zero exit means "retry next run, do not lock". Omit it entirely to write the HTML files and send nothing.

export DIGEST_DELIVER_CMD='python3 send.py --to "$DIGEST_TO" --subject "$DIGEST_SUBJECT" --html-stdin'

Audiences

An audience is one reader who gets one tailored email. They are data, in audiences.json, so adding a reader is editing a file, not the code:

{
  "audiences": [
    { "id": "team", "label": "Non-technical stakeholder", "deliver_to": "stakeholder@example.com",
      "brief": "Does not read code. Cares about what customers will see, content, growth, and the business. Plain language, one line of why-it-matters per item, skip internal work." },
    { "id": "eng", "label": "Technical author", "deliver_to": "engineering@example.com",
      "brief": "Wrote the code. Wants a tight technical log, grouped by category, with follow-ups noted." }
  ]
}

id is the file stem and the key the model returns HTML under. brief is the whole of what the model knows about this reader, so it is where you tune voice and scope. deliver_to is handed to your delivery command; omit it for a file-only audience that is written but never sent. The same commit can appear in two emails phrased differently, or in only one.

Try it with no API key

The bundled mocks run the full pipeline against a throwaway git repo, with no network:

# End-to-end, including the idempotent re-run and the zero-commit path:
bash examples/selftest.sh

# Or drive it by hand against your own repo, writing files but sending nothing:
DIGEST_LLM_CMD='bash examples/mock-llm.sh' \
  node commit-digest.mjs --repo . --audiences audiences.json --date 2026-04-14 --out /tmp/digest

examples/mock-llm.sh ignores the prompt and prints a fixture digest; examples/mock-deliver.sh records what would have been sent to a log instead of sending it. Swap each for the real command when you are ready.

Options and environment

  • --repo DIR (default .), --audiences FILE (default audiences.json beside the script), --date YYYY-MM-DD (default yesterday, local time), --out DIR (default <repo>/digests/<date>), --all-branches (digest every branch, not just HEAD), --dry-run (gather, compile, write files, deliver nothing).
  • DIGEST_LLM_CMD, DIGEST_DELIVER_CMD, DIGEST_PROMPT (override the prompt path), DIGEST_LOCK_BYPASS=1 (ignore the delivery lock and send anyway).

examples/cron.md has a crontab line and a launchd plist for a nightly run, plus how to backfill missed days safely.

Anti-patterns

The ways a digest like this goes wrong, most of which the design fights but none of which it can fully prevent for you:

  • The digest nobody trusts. One invented attribution or one "new" feature that was not new, and readers quietly stop believing the email. The accuracy rules in the prompt are the guard; keep them, and tune the briefs rather than loosening them.
  • The digest nobody reads. One voice for every reader, so the non-technical person skims a wall of jargon and the engineer skims marketing copy, and both stop opening it. The whole point of per-audience emails is that each one is short and entirely relevant.
  • Duplicate sends. The delivery lock is off, or the operator runs the job twice by hand thinking the first did not take. Leave the lock on; it is the difference between "safe to re-run" and "call the reader to apologize".
  • Silent blank emails. A model returns HTML for one audience and forgets another. The write stage fails the whole run when any audience is missing, on purpose, so a blank email is never what goes out. Do not route around it.
  • Prompt drift into a summary. "Just summarize the commits" creeps back in and the accuracy rules erode. A summary sounds good; a digest has to be true. Keep the prompt pointed at true.

Attribution

Extracted from a production agent fleet that ran a real company. MIT licensed. Use the prompt; edit the audience briefs; point the two commands at your own model and mailer.

About

No description, website, or topics provided.

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors