appscript.dev
Automation Intermediate Sheets

Write release notes from commit messages

Summarise Northwind's commits into user-facing release notes — group by type, write for humans.

Published Jan 14, 2026

Commit messages are written for the next developer, not the customer. “Fix null check in invoice parser” means something to the engineer who wrote it and nothing to the Northwind account manager who has to explain the release. So the release notes either get skipped, or someone spends an hour every sprint translating shorthand into plain English.

This script hands that translation to Claude. You paste the sprint’s commit messages into a Commits tab, run the script, and it groups them into Features, Fixes, and Behind the scenes — rewritten for non-technical readers — on a Release notes tab you can copy straight into an email or changelog.

What you’ll need

  • A Google Sheet with two tabs. A Commits tab holding one commit message per row in column A (paste them straight from git log --oneline), and a Release notes tab the script writes to — it creates that tab if it is missing.
  • An Anthropic API key saved as ANTHROPIC_API_KEY in Script Properties — see Store API keys and secrets securely.
  • Nothing else — release notes land on their own tab.

The script

// The spreadsheet that holds the Commits tab and the Release notes tab.
const RELEASE_SHEET_ID = '1abcReleaseId';

// Tab names. The Commits tab is read; the Release notes tab is written.
const COMMITS_TAB = 'Commits';
const NOTES_TAB = 'Release notes';

/**
 * Reads the Commits tab, asks Claude to rewrite the messages as
 * user-facing release notes, and writes the result to the Release
 * notes tab.
 */
function publishReleaseNotes() {
  const ss = SpreadsheetApp.openById(RELEASE_SHEET_ID);

  // 1. Read column A of the Commits tab and drop blank rows.
  const commits = ss.getSheetByName(COMMITS_TAB)
    .getRange('A2:A')
    .getValues()
    .flat()
    .filter(Boolean);

  if (!commits.length) {
    Logger.log('No commits to summarise — nothing to do.');
    return;
  }

  // 2. Turn the raw commits into grouped, plain-English notes.
  const notes = writeReleaseNotes(commits);

  // 3. Rebuild the Release notes tab so it always shows the latest run.
  const sheet = ss.getSheetByName(NOTES_TAB) || ss.insertSheet(NOTES_TAB);
  sheet.clear();
  sheet.getRange(1, 1).setValue(notes);
  Logger.log('Wrote release notes for ' + commits.length + ' commits.');
}

/**
 * Asks Claude to group commit messages into Features, Fixes, and
 * Behind the scenes, rewritten for non-technical readers.
 */
function writeReleaseNotes(commits) {
  // A fixed set of headings keeps every release looking the same.
  const prompt =
    'Turn these git commits into user-facing Northwind release notes. ' +
    'Group as: Features, Fixes, Behind the scenes. ' +
    'Write for non-technical readers — no jargon, no commit hashes. ' +
    'Skip purely internal commits if they would not interest a customer.\n\n' +
    commits.join('\n');
  return callClaude(prompt, 'claude-sonnet-4-6', 1500);
}

/**
 * Minimal Anthropic API call. The key lives in Script Properties — it
 * is never pasted into the code.
 */
function callClaude(prompt, model = 'claude-haiku-4-5-20251001', maxTokens = 400) {
  const key = PropertiesService.getScriptProperties()
    .getProperty('ANTHROPIC_API_KEY');
  const res = UrlFetchApp.fetch('https://api.anthropic.com/v1/messages', {
    method: 'post',
    contentType: 'application/json',
    headers: { 'x-api-key': key, 'anthropic-version': '2023-06-01' },
    payload: JSON.stringify({
      model,
      max_tokens: maxTokens,
      messages: [{ role: 'user', content: prompt }],
    }),
    muteHttpExceptions: true,
  });
  return JSON.parse(res.getContentText()).content[0].text.trim();
}

How it works

  1. publishReleaseNotes opens the release spreadsheet and reads column A of the Commits tab, flattening the range into a plain list and dropping blanks.
  2. If there are no commits, it logs a message and stops — no wasted API call.
  3. It passes the list to writeReleaseNotes, which builds a prompt pinning the output to three fixed headings: Features, Fixes, and Behind the scenes.
  4. It calls Claude Sonnet, which is worth the extra cost here because rewriting terse commit shorthand into clear prose needs real reasoning.
  5. It rebuilds the Release notes tab from scratch and writes the result into cell A1, so the tab always reflects the latest run.

Example run

Say the Commits tab holds the raw output of git log --oneline for a sprint:

A (commit message)
feat: add CSV export to invoice list
fix: null check in invoice parser
chore: bump dependency versions
feat: remember last-used currency per client
fix: timezone offset wrong on overdue badge

After a run, cell A1 of Release notes holds something like:

Features

  • You can now export your invoice list to a CSV file.
  • Northwind remembers the last currency you used for each client.

Fixes

  • Fixed a crash that could happen when opening certain invoices.
  • The “overdue” badge now shows the correct date in every timezone.

Behind the scenes

  • Updated internal libraries to keep things fast and secure.

That is text you can paste straight into a changelog or a customer email — no manual translation.

Run it

This is a once-per-sprint job, so run it by hand when the release is cut:

  1. Paste the sprint’s commit messages into column A of the Commits tab.
  2. In the Apps Script editor, select publishReleaseNotes and click Run.
  3. Approve the authorisation prompt the first time.
  4. Open the Release notes tab to read and copy the result.

To let non-editors trigger it, add a custom menu so it appears in the spreadsheet itself:

function onOpen() {
  SpreadsheetApp.getUi()
    .createMenu('Release tools')
    .addItem('Publish release notes', 'publishReleaseNotes')
    .addToUi();
}

Watch out for

  • Garbage in, garbage out. If your commit messages are vague (“fix stuff”, “wip”), Claude has nothing to translate. Tidy commit messages — or a conventional-commits prefix like feat: or fix: — give far better notes.
  • The result is a draft, not a publish-ready post. Always read it before sending: Claude can mislabel an internal change as a feature, or vice versa.
  • The whole commit list goes into one prompt. A very large sprint may exceed the token budget — raise max_tokens, or split the commits across two runs and merge the headings by hand.
  • Sensitive commit messages get sent to the API. If a message names an unannounced client or an embargoed feature, strip it from the Commits tab first.

Related