Skip to content

Changelog draft

Changelog draft #62

name: Changelog draft
on:
workflow_dispatch:
inputs:
since:
description: "Override start date YYYY-MM-DD (with 'until')"
required: false
until:
description: "Override end date YYYY-MM-DD (with 'since')"
required: false
schedule:
- cron: "0 13 * * *" # 13:00 UTC daily (9am EDT / 8am EST) — drafts the previous Eastern day
concurrency:
group: changelog-draft
cancel-in-progress: false
permissions:
contents: write
pull-requests: write
id-token: write
env:
BUN_VERSION: "1.3.14"
jobs:
draft:
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: ${{ env.BUN_VERSION }}
- name: Skip if an open draft PR already exists
id: existing
env:
GH_TOKEN: ${{ github.token }}
run: |
count=$(gh pr list --state open --search "head:changelog/draft" --json number --jq 'length')
echo "open=$count" >> "$GITHUB_OUTPUT"
if [ "$count" != "0" ]; then echo "An open changelog draft PR exists; skipping."; fi
- name: Resolve window
id: window
if: steps.existing.outputs.open == '0'
env:
INPUT_SINCE: ${{ github.event.inputs.since }}
INPUT_UNTIL: ${{ github.event.inputs.until }}
run: bun scripts/changelog/compute-window.ts
- name: Draft the changelog
if: steps.existing.outputs.open == '0' && steps.window.outputs.skip == 'false'
uses: anthropics/claude-code-action@62238ddb33772a079b0a6d8665a1ff3043583067 # v1.0.114
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
claude_args: '--allowed-tools "Bash(gh pr:*),Bash(git:*),Read,Edit,Write"'
prompt: |
You are drafting the daily self-changelog for releases.sh.
FIRST read docs/changelog-style.md in full — it defines the audience, the
include/exclude rules, the conventional-commit prior, the format, and the voice. The
top sections of CHANGELOG.md are your voice/density reference.
Changelog days are Eastern (America/New_York) calendar days, NOT UTC days. GitHub
timestamps are UTC; bucket each PR into its Eastern day using these UTC ranges
(a PR belongs to the day where startUtc <= mergedAt < endUtc — ISO timestamps
compare as plain strings):
${{ steps.window.outputs.day_bounds }}
The window to draft is ${{ steps.window.outputs.since }} through
${{ steps.window.outputs.until }} (inclusive). The first entry in the ranges above
is one day BEFORE the window — an overlap guard, handled in step 4.
1. List every PR merged in the window with ONE search:
gh pr list --state merged --base main --json number,title,body,mergedAt,labels \
--search "merged:${{ steps.window.outputs.search_start }}..${{ steps.window.outputs.search_end }}" \
--limit 200
Bucket the results by Eastern day. Ignore any PR that falls outside every
listed range.
2. Keep only user-facing changes per the style guide; inspect a PR's diff with
`gh pr diff <number>` when its user impact is unclear.
3. For each window day with user-facing survivors, PREPEND a `## <Month D, YYYY>`
section to CHANGELOG.md (newest at the top, directly under the `# Changelog`
preamble), with **Added**/**Changed**/**Fixed** sections. A quiet/all-internal
day → write nothing.
4. Overlap-guard day (the day before the window): if CHANGELOG.md already has a
section for it, treat it as already curated — do NOT rewrite or reorder it;
only APPEND a clearly user-facing PR that is missing from it (the publish step
updates the published entry in place). If it has no section, treat it like a
window day.
CHANGELOG.md is the ONLY file you may modify. If a pre-commit hook, lint rule, or
any other tooling blocks your commit, that is a bug to report in the PR body — do
not edit config, workflows, or `package.json` to work around it, and do not bypass
the hook. If you cannot commit without touching another file, stop and open no PR.
If you wrote nothing, stop and open no PR.
If CHANGELOG.md changed, commit it on a new branch named
`changelog/draft-${{ steps.window.outputs.until }}` and open a PR titled
`changelog: ${{ steps.window.outputs.since }}..${{ steps.window.outputs.until }}`
with a one-line body listing the dates you drafted. Do not merge it.
- name: Guard — the draft may only touch CHANGELOG.md
if: steps.existing.outputs.open == '0' && steps.window.outputs.skip == 'false'
env:
BRANCH: changelog/draft-${{ steps.window.outputs.until }}
run: |
git fetch --quiet origin main "$BRANCH" || git fetch --quiet origin main
if ! git rev-parse --verify --quiet "origin/$BRANCH" >/dev/null; then
echo "No draft branch pushed; nothing to guard."
exit 0
fi
changed=$(git diff --name-only "origin/main...origin/$BRANCH")
echo "Files changed on $BRANCH:"
echo "$changed"
unexpected=$(echo "$changed" | grep -v '^CHANGELOG\.md$' || true)
if [ -n "$unexpected" ]; then
echo "::error::The changelog draft modified files other than CHANGELOG.md:"
echo "$unexpected"
exit 1
fi