appscript.dev
Automation Intermediate Docs

Automate GitHub release notes

Compile Northwind's commits into formatted changelogs — readable, grouped, automatic.

Published Jul 31, 2025

Every Northwind release ships with a changelog, and every time someone has to write it by hand — scrolling the commit history, copying messages, tidying them into something a non-developer can read. It is dull work that gets skipped or rushed, so release notes end up thin or missing entirely.

This script builds the changelog straight from the repository. Give it two tags — the previous release and the new one — and it asks the GitHub API for every commit between them, then drops a formatted list into a fresh Google Doc you can polish and share. The mechanical part is done in seconds; you only edit.

What you’ll need

  • A GitHub repository for the project, with releases tagged (for example v1.4.0). The script compares two tags, so you need at least two.
  • A GitHub personal access token with read access to the repo, saved as GITHUB_TOKEN in Script Properties — see Store API keys and secrets securely.
  • Nothing else — the script creates the Doc itself in your Drive.

The script

// The repository to read commits from, in "owner/name" form.
const REPO = 'northwind/site';

/**
 * Builds a release-notes Doc from every commit between two tags.
 *
 * @param {string} tagFrom  The previous release tag, e.g. "v1.3.0".
 * @param {string} tagTo    The new release tag, e.g. "v1.4.0".
 * @return {string} The URL of the created Doc.
 */
function buildReleaseNotes(tagFrom, tagTo) {
  // 1. Read the GitHub token from Script Properties.
  const token = PropertiesService.getScriptProperties()
    .getProperty('GITHUB_TOKEN');

  // 2. Ask the GitHub compare API for everything between the two tags.
  const url = 'https://api.github.com/repos/' + REPO +
    '/compare/' + tagFrom + '...' + tagTo;
  const res = JSON.parse(
    UrlFetchApp.fetch(url, {
      headers: { Authorization: 'Bearer ' + token },
    }).getContentText()
  );

  // 3. Bail out early if nothing changed between the tags.
  if (!res.commits || !res.commits.length) {
    Logger.log('No commits between ' + tagFrom + ' and ' + tagTo + '.');
    return null;
  }

  // 4. Keep only the first line of each commit message — the summary.
  const commits = res.commits.map((c) => '- ' + c.commit.message.split('\n')[0]);

  // 5. Create a Doc and write the formatted changelog into it.
  const doc = DocumentApp.create('Release ' + tagTo);
  doc.getBody().setText(
    'Northwind release ' + tagTo + '\n\n' +
    'Changes since ' + tagFrom + ':\n' +
    commits.join('\n')
  );
  doc.saveAndClose();

  Logger.log('Built release notes for ' + tagTo + ' — ' + commits.length +
    ' commits.');
  return doc.getUrl();
}

How it works

  1. buildReleaseNotes reads the GITHUB_TOKEN from Script Properties so the token never appears in the code.
  2. It builds a call to GitHub’s compare endpoint, which returns every commit between tagFrom and tagTo, and parses the JSON response.
  3. If there are no commits between the tags, it logs a message and stops — no empty Doc gets created.
  4. It maps each commit to a bullet, keeping only the first line of the message. Commit bodies are often long and internal; the first line is the summary.
  5. It creates a new Google Doc titled Release <tag>, writes a heading and the bulleted list of changes, saves it, and returns the Doc URL.

Example run

Calling buildReleaseNotes('v1.3.0', 'v1.4.0') against a repo with three commits in between produces a Doc titled Release v1.4.0 containing:

Northwind release v1.4.0

Changes since v1.3.0:
- Fix booking form validation on Safari
- Add dark-mode toggle to the dashboard
- Bump dependencies and tidy build config

From there it is a two-minute job to group the bullets under headings and drop the build-config line — far quicker than assembling the list from scratch.

Run it

This runs once per release, so trigger it by hand:

  1. In the Apps Script editor, add a small wrapper that passes the two tags:
function release140() {
  Logger.log(buildReleaseNotes('v1.3.0', 'v1.4.0'));
}
  1. Select the wrapper and click Run, approving the authorisation prompt the first time.
  2. Open the logged URL to read and polish the Doc.

To make it hands-off, call buildReleaseNotes from a GitHub webhook handler that fires on a new tag, passing the previous tag and the new one.

Watch out for

  • Unauthenticated GitHub requests are limited to 60 per hour; with a token the limit is 5,000. Always send the token, even for a public repo.
  • The compare endpoint returns at most 250 commits. For a release with more changes than that, paginate the commit list or compare in smaller ranges.
  • Both tags must already exist on GitHub. If you call the script before the new release is tagged, the API returns a 404 and JSON.parse throws.
  • Commit messages become the changelog verbatim. Sloppy messages make sloppy notes — this automates the assembly, not the wording.
  • The Doc lands in the root of your Drive. Move it into a releases folder, or add a DriveApp call to file it automatically.

Related