Changelog #16
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Changelog | |
| # claude-code-action does not support the `release` event, so the release | |
| # trigger only re-dispatches this workflow as a workflow_dispatch run. | |
| on: | |
| release: | |
| types: [published] | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: Release tag to write the changelog for | |
| required: true | |
| jobs: | |
| dispatch: | |
| if: github.event_name == 'release' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| actions: write | |
| steps: | |
| - name: Re-dispatch as workflow_dispatch | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| TAG: ${{ github.event.release.tag_name }} | |
| run: gh workflow run changelog.yml --repo "$GITHUB_REPOSITORY" --ref main -f "tag=$TAG" | |
| changelog: | |
| if: github.event_name == 'workflow_dispatch' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| id-token: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: main | |
| fetch-depth: 0 | |
| - name: Collect changes since previous release | |
| id: commits | |
| env: | |
| TAG: ${{ inputs.tag }} | |
| run: | | |
| prev=$(git describe --tags --abbrev=0 "${TAG}^" 2>/dev/null || true) | |
| if [ -n "$prev" ]; then | |
| range="${prev}..${TAG}" | |
| diffbase="$prev" | |
| else | |
| # First release: diff against the empty tree so every file counts as added. | |
| range="$TAG" | |
| diffbase=$(git hash-object -t tree /dev/null) | |
| fi | |
| echo "Previous tag: ${prev:-none (first release)}" | |
| echo "Range: $range" | |
| # Exclude generated, vendored, and lock files so the diff reflects | |
| # real source changes, not machine output. These are the source of | |
| # truth for the notes — commit subjects are only a secondary hint. | |
| exclude=( | |
| ':(exclude)CHANGELOG.md' | |
| ':(exclude)*.lock' | |
| ':(exclude)package-lock.json' | |
| ':(exclude)composer.lock' | |
| ':(exclude)resources/js/routes/**' | |
| ':(exclude)resources/js/actions/**' | |
| ) | |
| { | |
| echo 'log<<COMMITS_EOF' | |
| git log --no-merges --pretty=format:'%h %s' "$range" | |
| echo | |
| echo 'COMMITS_EOF' | |
| } >> "$GITHUB_OUTPUT" | |
| { | |
| echo 'diffstat<<DIFFSTAT_EOF' | |
| git diff --stat "$diffbase" "$TAG" -- . "${exclude[@]}" | |
| echo 'DIFFSTAT_EOF' | |
| } >> "$GITHUB_OUTPUT" | |
| # Cap the diff so a large release can't blow past the env-var size | |
| # limit that feeds the prompt. Signal truncation if we hit the cap. | |
| full=$(git diff "$diffbase" "$TAG" -- . "${exclude[@]}") | |
| capped=$(printf '%s\n' "$full" | head -c 90000) | |
| { | |
| echo 'diff<<DIFF_EOF' | |
| printf '%s\n' "$capped" | |
| if [ "${#full}" -gt "${#capped}" ]; then | |
| echo | |
| echo '[diff truncated — see the file list above for the full scope of changes]' | |
| fi | |
| echo 'DIFF_EOF' | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Write changelog with Claude | |
| id: changelog | |
| uses: anthropics/claude-code-action@v1 | |
| with: | |
| anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} | |
| # This workflow is intentionally re-dispatched by the release job, so | |
| # the workflow_dispatch run is initiated by github-actions[bot]. | |
| allowed_bots: github-actions | |
| prompt: | | |
| Write a technical changelog for release ${{ inputs.tag }} by reading the actual code diff below and describing what changed. | |
| Rules: | |
| - The DIFF is the source of truth. Read the added/removed lines and describe what the code now does that it didn't before. Do not rely on the commit subjects to tell you what changed — they are only a rough hint for naming and grouping, and are often vague or incomplete. If the diff adds a capability the commit subject never mentions, the changelog must still cover it. | |
| - Do not invent or infer anything the diff does not show. Never describe a removal as an addition: if lines/files were deleted, that capability is gone. | |
| - Group entries under exactly these headings, in this order: `### Features`, `### Fixes`, `### Chores`. Omit any heading that has no entries. | |
| - Features are user-facing additions or changes. Fixes are bug fixes. Everything else (refactors, CI, dependencies, tooling, docs, removals of internal scaffolding) is a Chore. | |
| - One bullet per meaningful change. Split distinct capabilities into separate bullets even when they came from a single commit. Skip pure merge/version-bump noise. | |
| - Write each bullet as a plain past-tense sentence. No commit hashes, no PR numbers, no marketing language. | |
| Return ONLY the markdown for these sections as the `body` field of the structured output — no release heading, no preamble. If the diff shows no meaningful changes, set `body` to "_No changes._". | |
| Commit subjects (rough hint only — one per line, `<hash> <subject>`): | |
| ${{ steps.commits.outputs.log }} | |
| Files changed: | |
| ${{ steps.commits.outputs.diffstat }} | |
| Code diff (the source of truth): | |
| ```diff | |
| ${{ steps.commits.outputs.diff }} | |
| ``` | |
| claude_args: | | |
| --json-schema '{"type":"object","properties":{"body":{"type":"string"}},"required":["body"]}' | |
| --allowedTools "" | |
| - name: Update CHANGELOG.md | |
| env: | |
| STRUCTURED: ${{ steps.changelog.outputs.structured_output }} | |
| TAG: ${{ inputs.tag }} | |
| run: | | |
| body=$(echo "$STRUCTURED" | jq -r '.body') | |
| if [ -z "$body" ] || [ "$body" = "null" ]; then | |
| echo "Claude did not produce a changelog body." >&2 | |
| exit 1 | |
| fi | |
| if [ -f CHANGELOG.md ] && grep -q "^## ${TAG} " CHANGELOG.md; then | |
| echo "CHANGELOG.md already has a section for ${TAG}, skipping." | |
| exit 0 | |
| fi | |
| tmp=$(mktemp) | |
| { | |
| echo "# Changelog" | |
| echo | |
| echo "## ${TAG} - $(date -u +%Y-%m-%d)" | |
| echo | |
| echo "$body" | |
| echo | |
| if [ -f CHANGELOG.md ]; then | |
| sed '1{/^# Changelog$/d;}' CHANGELOG.md | sed '/./,$!d' | |
| fi | |
| } > "$tmp" | |
| mv "$tmp" CHANGELOG.md | |
| - name: Commit CHANGELOG.md | |
| env: | |
| TAG: ${{ inputs.tag }} | |
| GITHUB_TOKEN: ${{ github.token }} | |
| run: | | |
| git add CHANGELOG.md | |
| if git diff --cached --quiet -- CHANGELOG.md; then | |
| echo "No changelog changes to commit." | |
| exit 0 | |
| fi | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git commit -m "Update CHANGELOG.md for ${TAG}" | |
| # claude-code-action replaces the checkout credentials, so push with | |
| # the workflow token explicitly. | |
| git push "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" HEAD:main | |
| - name: Write release notes with Claude | |
| id: release_notes | |
| uses: anthropics/claude-code-action@v1 | |
| with: | |
| anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} | |
| allowed_bots: github-actions | |
| prompt: | | |
| Write user-facing release notes for release ${{ inputs.tag }} by reading the actual code diff below and describing what changed for the user. | |
| Audience: the people who use the app, not developers. Write for them, and make them excited to try what's new. | |
| Tone: | |
| - Fun, playful, and full of personality — like a small team that loves what they're building and can't wait for people to try it. Have a sense of humour. Make the reader smile. | |
| - Warm and energetic. Lead with the delight: what's more fun, faster, or easier now. A well-placed emoji is welcome (roughly one per bullet, tops) when it adds warmth rather than noise. | |
| - Keep it real. Playful never means fake: no empty hype words that say nothing ("game-changing", "revolutionary", "supercharged"), no wall of exclamation marks, no forced enthusiasm about something boring. | |
| - Every ounce of fun must be backed by something actually in the commits. The personality lives in HOW you say it — never in inventing a capability that isn't there. | |
| Rules: | |
| - The DIFF is the source of truth. Read the added/removed lines and work out what a user can now do that they couldn't before. Do NOT rely on the commit subjects — they are only a rough hint and are often vague (a single "Modify feedback" commit might add BOTH editing and deleting; the diff will show both, so cover both). Anything the diff clearly adds must appear, even if no commit subject mentions it. | |
| - Read routes, controllers, buttons, form fields, and UI text in the diff to discover user-facing capabilities. A new route + a new button labelled "Delete" means users can now delete — say so. | |
| - Do not invent or infer anything the diff does not show. Never describe a removal as an addition: if lines/files were deleted, that capability is gone. | |
| - Plain English. No jargon, no code identifiers, no framework or library names, no commit hashes, no PR numbers. | |
| - Frame every entry around what changed for the user — what they can now do, what now works better, what looks or behaves differently. | |
| - Skip anything purely internal that a user would never notice: refactors, CI, dependency bumps, tooling, tests, migrations, docs, and internal scaffolding. If a change has no user-visible effect, leave it out entirely. | |
| - Be granular. Give each distinct user-facing capability its own bullet — if the diff adds editing AND deleting, that is TWO bullets, not one. Only fold together changes that are genuinely the same thing. | |
| - Structure each bullet as **A short bold headline** — then a sentence or two of plain-English detail: exactly what was added or changed, and where the user finds or uses it. Enough specificity that a reader knows precisely what's new without seeing the app. | |
| - Group related bullets under short `###` headings when the release is large enough to warrant it (e.g. `### New`, `### Improved`, `### Fixed`). For a small release, a single flat bullet list is fine — don't force headings. | |
| Return ONLY the markdown as the `body` field of the structured output — no top-level release heading, no preamble. If nothing in the diff is user-facing, set `body` to "_No user-facing changes in this release._". | |
| Commit subjects (rough hint only — one per line, `<hash> <subject>`): | |
| ${{ steps.commits.outputs.log }} | |
| Files changed: | |
| ${{ steps.commits.outputs.diffstat }} | |
| Code diff (the source of truth): | |
| ```diff | |
| ${{ steps.commits.outputs.diff }} | |
| ``` | |
| claude_args: | | |
| --json-schema '{"type":"object","properties":{"body":{"type":"string"}},"required":["body"]}' | |
| --allowedTools "" | |
| - name: Update release description | |
| env: | |
| STRUCTURED: ${{ steps.release_notes.outputs.structured_output }} | |
| TAG: ${{ inputs.tag }} | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| body=$(echo "$STRUCTURED" | jq -r '.body') | |
| if [ -z "$body" ] || [ "$body" = "null" ]; then | |
| echo "Claude did not produce release notes." >&2 | |
| exit 1 | |
| fi | |
| gh release edit "$TAG" --repo "$GITHUB_REPOSITORY" --notes "$body" |