chore: use AI changelog generation

This commit is contained in:
Jan De Dobbeleer
2025-11-04 18:18:38 +01:00
committed by Jan De Dobbeleer
parent 31e262f32a
commit 026c486270
5 changed files with 2219 additions and 343 deletions
+2
View File
@@ -1 +1,3 @@
* text=auto eol=lf
.github/workflows/*.lock.yml linguist-generated=true merge=ours
File diff suppressed because it is too large Load Diff
+223
View File
@@ -0,0 +1,223 @@
---
name: Enhance release changelog with AI
on:
release:
types: [published]
workflow_dispatch:
inputs:
tag:
description: 'Release tag to test (e.g., v19.0.0)'
required: true
type: string
dry_run:
description: 'Dry run mode - generate changelog but do not update release'
required: false
type: boolean
default: 'true'
permissions:
contents: write
models: read
actions: read
discussions: read
issues: read
pull-requests: read
repository-projects: read
security-events: read
timeout_minutes: 15
tools:
github:
toolsets: [all]
bash: ["git", "cat", "echo", "jq", "curl", "head", "wc", "grep", "sed", "sort"]
engine:
id: copilot
---
# Enhanced Changelog Generator for oh-my-posh
You are a release notes editor for the open-source project "oh-my-posh", a cross-shell prompt theme engine written in Go.
## Your Task
Generate a clear, human-friendly Markdown changelog for this release. Use concise language and organize with headings.
## Context
- Repository: `${{ github.repository }}`
- Release Tag: `${{ github.event.inputs.tag || github.event.release.tag_name }}`
- Release ID: `${{ github.event.release.id }}`
- Dry Run: `${{ github.event.inputs.dry_run }}`
## Step-by-Step Process
### 1. Gather Release Context
First, determine if this is a manual dispatch or release event:
- If `${{ github.event.inputs.tag }}` is provided, this is a manual dispatch - fetch release info for the specified tag using GitHub CLI
- Otherwise, use the release event data directly
Extract:
- Current tag name: `${{ github.event.inputs.tag || github.event.release.tag_name }}`
- Release ID: `${{ github.event.release.id }}`
- Existing release body/notes (fetch via GitHub CLI if needed)
### 2. Determine Diff Range
Find the previous tag to establish the comparison range:
```bash
# Try to find the previous tag
PREV_TAG=$(git describe --tags --abbrev=0 "$CURRENT_TAG^" 2>/dev/null)
# If no previous tag found, use initial commit
if [ -z "$PREV_TAG" ]; then
PREV_TAG=$(git rev-list --max-parents=0 HEAD | tail -n 1)
fi
echo "Comparing $PREV_TAG...$CURRENT_TAG"
```
### 3. Collect Commits and Changes
Gather the following information:
- **Commit subjects**: `git log --no-merges --pretty=format:'%s' "$PREV_TAG...$CURRENT_TAG" | head -n 500`
- **Detailed commits**: `git log --no-merges --pretty=format:'- %s%n%b%n' "$PREV_TAG...$CURRENT_TAG" | head -n 2000`
- **Changed files**: `git diff --name-status "$PREV_TAG...$CURRENT_TAG" | head -n 1000`
- **Contributors**: Use `git shortlog -sne "$PREV_TAG...$CURRENT_TAG"` to extract contributors
- Format as GitHub profile links: `[@username](https://github.com/username)`
- Exclude: Jan De Dobbeleer, dependabot, renovate, github-actions, and other bots
- Limit to 200 contributors
### 4. Collect Issue Context
Extract issue numbers from commit messages (patterns: fixes #123, closes #456, #789):
```bash
# Extract issue numbers from commits
ISSUE_NUMBERS=$(git log --no-merges --pretty=format:'%s %b' "$PREV_TAG...$CURRENT_TAG" | \
grep -oiE '(fix(es|ed)?|close(s|d)?|resolve(s|d)?)?[[:space:]]*#[0-9]+' | \
grep -oE '[0-9]+' | sort -u)
# Fetch issue details for context
for NUM in $ISSUE_NUMBERS; do
gh issue view $NUM --json title,body,labels
done
```
### 5. Read Version Configuration
Read the `.versionrc.json` file to understand which commit types should be included in the changelog.
**CRITICAL**: Respect the .versionrc.json configuration:
- ONLY include these sections with these exact names:
- "Features" (for feat: commits)
- "Bug Fixes" (for fix: commits)
- "Refactor" (for refactor: commits)
- "Reverts" (for revert: commits)
- "Themes" (for theme: commits)
- DO NOT include chore, ci, docs, perf, or test commits (marked as hidden in .versionrc.json)
- Use ONLY the section names specified above, not generic names like "Other"
- ONLY show sections that have actual changes - omit empty sections entirely
### 6. Generate Enhanced Changelog
Create a comprehensive changelog that includes:
**Segment Changes** (public-facing):
- When you see changes to `src/segments/*.go` files (excluding `*_test.go`), these are prompt segments that users
configure
- A segment is a customizable component users add to their shell prompt (e.g., git status, battery level, current
directory)
- Mention segment changes by their user-facing name (infer from the file name), not file paths
- Focus on what users can now do or configure differently with that segment
**Goals**:
- Summarize highlights up front with context and impact
- Group changes ONLY by the section names from .versionrc.json above (skip empty sections)
- Call out breaking changes and required migrations with explicit before/after examples or commands
- Add practical usage notes or snippets to help users adopt new features or changes
- For segment changes, explain the user-facing impact (e.g., "The Git segment now supports...")
- Credit contributors at the end (they are pre-filtered and formatted as GitHub profile links) - ONLY if contributors
list is not empty
- Include a "Full diff" link footer
**Requirements**:
- Output valid Markdown only, no front matter, no HTML, no title heading
- Do not include a title like "Changelog for vX.Y.Z" - start directly with the content
- Keep to ~300-800 words unless there are many breaking changes
- Prefer code blocks for examples with proper language tags (bash, json, yaml, toml, powershell)
- Do not invent features not present in the commits/diff
- Do not list individual file paths unless they are user-facing config/theme files
### 7. Update Release Body
If not in dry run mode (`${{ github.event.inputs.dry_run }}` is false):
```bash
# Update the release body using GitHub API
gh api -X PATCH repos/${{ github.repository }}/releases/$RELEASE_ID \
-f body="$ENHANCED_CHANGELOG"
```
If in dry run mode:
- Display the generated changelog in the workflow summary
- Do not modify the actual release
### 8. Generate Summary
Create a step summary showing:
- If dry run: Preview of the generated changelog with a note that the release was not modified
- If not dry run: Confirmation that the release was updated with the enhanced changelog
- The full enhanced changelog content
## Output Format
The enhanced changelog should be structured as:
```markdown
[Opening paragraph summarizing key highlights and impact]
## Features
[New features from feat: commits]
## Bug Fixes
[Bug fixes from fix: commits]
## Refactor
[Refactoring changes from refactor: commits]
## Reverts
[Reverted changes from revert: commits]
## Themes
[Theme changes from theme: commits]
## Contributors
[List of contributor links - only if list is not empty]
---
**Full Changelog**: [compare link]
```
## Important Notes
- Use the GitHub CLI (`gh`) for all GitHub API operations
- Use git commands for repository analysis
- Store intermediate results in temporary files if needed for reference
- Handle errors gracefully and provide clear feedback
- Respect the conventional commit format used in oh-my-posh
- Focus on user-facing impact, not internal implementation details
-343
View File
@@ -1,343 +0,0 @@
name: Enhance release changelog with AI
on:
release:
types: [published]
workflow_dispatch:
inputs:
tag:
description: 'Release tag to test (e.g., v19.0.0)'
required: true
type: string
dry_run:
description: 'Dry run mode - generate changelog but do not update release'
required: false
type: boolean
default: true
permissions:
contents: write # Update release body
models: read # Access GitHub Models API
jobs:
enhance:
name: Generate enhanced changelog
runs-on: ubuntu-latest
steps:
- name: Checkout repository (with tags)
uses: actions/checkout@v5
with:
ref: ${{ inputs.tag || github.ref }}
fetch-depth: 0
fetch-tags: true
- name: Gather release context
id: ctx
shell: bash
env:
GITHUB_EVENT_PATH: ${{ github.event_path }}
GH_TOKEN: ${{ github.token }}
INPUT_TAG: ${{ inputs.tag }}
DRY_RUN: ${{ inputs.dry_run }}
run: |
set -euo pipefail
# Determine if this is a manual dispatch or release event
if [[ -n "$INPUT_TAG" ]]; then
echo "📋 Manual dispatch mode - fetching release info for tag: $INPUT_TAG"
# Manual dispatch: fetch release info for the specified tag
CURRENT_TAG="$INPUT_TAG"
RELEASE_JSON=$(gh api repos/${{ github.repository }}/releases/tags/$CURRENT_TAG || echo '{}')
RELEASE_ID=$(printf "%s" "$RELEASE_JSON" | jq -r '.id // "0"')
HTML_URL=$(printf "%s" "$RELEASE_JSON" | jq -r '.html_url // ""')
EXISTING_BODY=$(printf "%s" "$RELEASE_JSON" | jq -r '.body // ""')
echo " Release ID: $RELEASE_ID"
echo " Release URL: $HTML_URL"
else
echo "📋 Release event mode - parsing from event payload"
# Release event: parse from event payload
CURRENT_TAG=$(jq -r '.release.tag_name' "$GITHUB_EVENT_PATH")
RELEASE_ID=$(jq -r '.release.id' "$GITHUB_EVENT_PATH")
HTML_URL=$(jq -r '.release.html_url' "$GITHUB_EVENT_PATH")
EXISTING_BODY=$(jq -r '.release.body // ""' "$GITHUB_EVENT_PATH")
echo " Tag: $CURRENT_TAG"
echo " Release ID: $RELEASE_ID"
fi
# Persist to a file for later steps to source
{
echo "CURRENT_TAG=$CURRENT_TAG"
echo "RELEASE_ID=$RELEASE_ID"
echo "HTML_URL=$HTML_URL"
echo "DRY_RUN=${DRY_RUN:-false}"
} > ctx.env
echo "✅ Context saved to ctx.env"
# Save existing body as a file to avoid env escaping issues
printf "%s" "$EXISTING_BODY" > existing_notes.md
echo "✅ Existing notes saved ($(wc -l < existing_notes.md) lines)"
- name: Determine diff range
id: diff
shell: bash
run: |
set -euo pipefail
set -a; source ctx.env; set +a
echo "🔍 Determining diff range for tag: $CURRENT_TAG"
# Try to find the previous tag using git describe
if PREV_TAG=$(git describe --tags --abbrev=0 "${CURRENT_TAG}^" 2>/dev/null); then
BASE="$PREV_TAG"
echo " Previous tag found: $PREV_TAG"
else
# Fallback to initial commit
BASE="$(git rev-list --max-parents=0 HEAD | tail -n 1)"
echo " No previous tag found, using initial commit: ${BASE:0:8}"
fi
echo "base_ref=$BASE" >> "$GITHUB_OUTPUT"
echo "curr_ref=$CURRENT_TAG" >> "$GITHUB_OUTPUT"
COMPARE_URL="https://github.com/${{ github.repository }}/compare/${BASE}...${CURRENT_TAG}"
echo "compare_url=$COMPARE_URL" >> "$GITHUB_OUTPUT"
echo "✅ Diff range: $BASE...$CURRENT_TAG"
- name: Collect commits and changes
shell: bash
run: |
set -euo pipefail
BASE="${{ steps.diff.outputs.base_ref }}"
HEAD="${{ steps.diff.outputs.curr_ref }}"
echo "📝 Collecting commits and changes from $BASE to $HEAD"
git log --no-merges --pretty=format:'%s' "${BASE}..${HEAD}" | head -n 500 > commits_subjects.txt || true
echo " ✅ Commit subjects: $(wc -l < commits_subjects.txt) commits"
git log --no-merges --pretty=format:'- %s%n%b%n' "${BASE}..${HEAD}" | head -n 2000 > commits_detailed.txt || true
echo " ✅ Detailed commits: $(wc -l < commits_detailed.txt) lines"
git diff --name-status "${BASE}..${HEAD}" | head -n 1000 > files_changed.txt || true
echo " ✅ Changed files: $(wc -l < files_changed.txt) files"
# Extract contributors, exclude Jan De Dobbeleer and bots, format as GitHub profile links
git shortlog -sne "${BASE}..${HEAD}" | sed -E 's/^ *[0-9]+\t//g' | while IFS= read -r line; do
name=$(echo "$line" | sed -E 's/ *<.*//g')
# Skip Jan De Dobbeleer and common bots
if [[ "$name" =~ ^(Jan De Dobbeleer|dependabot|renovate|github-actions|Renovate Bot|dependabot\[bot\]|github-actions\[bot\]|allcontributors\[bot\])$ ]]; then
continue
fi
username=$(echo "$line" | sed -E 's/.*<([^@]+)@.*/\1/g')
echo "- [@${username}](https://github.com/${username}) (${name})"
done | head -n 200 > contributors.txt || true
echo " ✅ Contributors: $(wc -l < contributors.txt) people"
- name: Collect issue context
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
BASE="${{ steps.diff.outputs.base_ref }}"
HEAD="${{ steps.diff.outputs.curr_ref }}"
echo "🔍 Collecting issue context for referenced issues"
> issues_context.txt
# Extract issue numbers from commit messages (e.g., fixes #123, closes #456, #789)
ISSUE_NUMBERS=$(git log --no-merges --pretty=format:'%s %b' "${BASE}..${HEAD}" | \
grep -oiE '(fix(es|ed)?|close(s|d)?|resolve(s|d)?)?[[:space:]]*#[0-9]+' | \
grep -oE '[0-9]+' | sort -u || true)
if [ -z "$ISSUE_NUMBERS" ]; then
echo " No issues referenced in commits"
else
COUNT=0
for NUM in $ISSUE_NUMBERS; do
echo " Fetching issue #$NUM..."
if ISSUE_DATA=$(gh api "repos/${{ github.repository }}/issues/$NUM" 2>/dev/null); then
TITLE=$(echo "$ISSUE_DATA" | jq -r '.title')
BODY=$(echo "$ISSUE_DATA" | jq -r '.body // ""' | head -c 1000)
LABELS=$(echo "$ISSUE_DATA" | jq -r '.labels[]?.name' | tr '\n' ', ' | sed 's/,$//')
echo "---" >> issues_context.txt
echo "Issue #$NUM: $TITLE" >> issues_context.txt
[ -n "$LABELS" ] && echo "Labels: $LABELS" >> issues_context.txt
echo "$BODY" >> issues_context.txt
echo "" >> issues_context.txt
COUNT=$((COUNT + 1))
fi
done
echo " ✅ Collected context for $COUNT issues"
fi
- name: Generate enhanced changelog with AI
id: ai
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
set -a; source ctx.env; set +a
echo "🤖 Generating enhanced changelog with AI"
MODEL="openai/gpt-4o-mini"
echo " Model: $MODEL"
SYSTEM_PROMPT=$(cat << 'PROMPT'
You are a release notes editor for the open-source project "oh-my-posh", a cross-shell prompt theme engine written in Go.
Write a clear, human-friendly Markdown changelog for this release. Use concise language and organize with headings.
CRITICAL: Respect the .versionrc.json configuration:
- ONLY include these sections with these exact names:
* "Features" (for feat: commits)
* "Bug Fixes" (for fix: commits)
* "Refactor" (for refactor: commits)
* "Reverts" (for revert: commits)
* "Themes" (for theme: commits)
- DO NOT include chore, ci, docs, perf, or test commits (marked as hidden in .versionrc.json)
- Use ONLY the section names specified above, not generic names like "Other"
- ONLY show sections that have actual changes - omit empty sections entirely
Segment changes (public-facing):
- When you see changes to src/segments/*.go files (excluding *_test.go), these are prompt segments that users configure
- A segment is a customizable component users add to their shell prompt (e.g., git status, battery level, current directory)
- Mention segment changes by their user-facing name (infer from the file name), not file paths
- Focus on what users can now do or configure differently with that segment
Goals:
- Summarize highlights up front with context and impact
- Group changes ONLY by the section names from .versionrc.json above (skip empty sections)
- Call out breaking changes and required migrations with explicit before/after examples or commands
- Add practical usage notes or snippets to help users adopt new features or changes
- For segment changes, explain the user-facing impact (e.g., "The Git segment now supports...")
- Credit contributors at the end (they are pre-filtered and formatted as GitHub profile links) - ONLY if contributors list is not empty
- Include a "Full diff" link footer
Requirements:
- Output valid Markdown only, no front matter, no HTML, no title heading
- Do not include a title like "Changelog for vX.Y.Z" - start directly with the content
- Keep to ~300-800 words unless there are many breaking changes
- Prefer code blocks for examples with proper language tags (bash, json, yaml, toml, powershell)
- Do not invent features not present in the commits/diff
- Do not list individual file paths unless they are user-facing config/theme files
PROMPT
)
# Build the user content
REPO="${{ github.repository }}"
COMPARE_URL="${{ steps.diff.outputs.compare_url }}"
CURR="$CURRENT_TAG"
PREV="${{ steps.diff.outputs.base_ref }}"
EXISTING=$(cat existing_notes.md || true)
SUBJECTS="$(cat commits_subjects.txt || true)"
DETAILS="$(cat commits_detailed.txt || true)"
FILES="$(cat files_changed.txt || true)"
CONTRIBUTORS="$(cat contributors.txt || true)"
ISSUES_CONTEXT="$(cat issues_context.txt || true)"
VERSIONRC="$(cat .versionrc.json || echo '{}')"
USER_CONTENT=$(cat << EOF
Repository: ${REPO}
Release: ${CURR}
Previous: ${PREV:-<none>}
Release URL: ${HTML_URL}
Compare URL: ${COMPARE_URL}
.versionrc.json configuration (sections to show/hide):
---
${VERSIONRC}
---
Existing release notes (from conventional commits):
---
${EXISTING}
---
Conventional commits (subjects):
---
${SUBJECTS}
---
Commits (details):
---
${DETAILS}
---
Changed files (for context on segment changes only, do not list paths in output):
---
${FILES}
---
Referenced issues (for additional context, explain impact in user terms):
---
${ISSUES_CONTEXT}
---
Contributors:
---
${CONTRIBUTORS}
---
EOF
)
echo " Calling GitHub Models API..."
OUTPUT_MD=""
set +e
RESP=$(curl -sS -f -X POST "https://models.github.ai/inference/chat/completions" \
-H "Authorization: Bearer ${GH_TOKEN}" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg model "$MODEL" --arg sys "$SYSTEM_PROMPT" --arg user "$USER_CONTENT" '{model:$model, messages: [{role:"system",content:$sys},{role:"user",content:$user}], temperature: 0.2, max_tokens: 4000}')")
CURL_EXIT=$?
if [ $CURL_EXIT -eq 0 ]; then
OUTPUT_MD=$(printf "%s" "$RESP" | jq -r '.choices[0].message.content // empty')
echo " ✅ API call successful"
else
echo " ❌ API call failed with exit code: $CURL_EXIT"
echo " Response: $RESP"
fi
set -e
if [[ -z "$OUTPUT_MD" ]]; then
echo "❌ AI generation failed or no output produced."
echo "enhanced_body=" >> "$GITHUB_OUTPUT"
exit 0
fi
echo " Generated changelog length: $(printf "%s" "$OUTPUT_MD" | wc -c) characters"
# Save the AI-generated changelog
echo "$OUTPUT_MD" > enhanced_changelog.md
echo "✅ Enhanced changelog saved to enhanced_changelog.md"
echo "enhanced_body<<EOF" >> "$GITHUB_OUTPUT"
cat enhanced_changelog.md >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
echo "✅ Changelog saved to step output"
- name: Update release body
if: ${{ steps.ai.outputs.enhanced_body != '' }}
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
set -a; source ctx.env; set +a
if [[ "$DRY_RUN" == "true" ]]; then
echo "🧪 Dry run mode enabled - skipping release update"
echo " The generated changelog would be applied to release ID: ${RELEASE_ID}"
exit 0
fi
echo "📝 Updating release body for release ID: ${RELEASE_ID}"
# Use the AI-generated changelog as the complete release body
PAYLOAD=$(jq -Rs '{body: .}' < enhanced_changelog.md)
gh api -X PATCH repos/${{ github.repository }}/releases/${RELEASE_ID} -H "Content-Type: application/json" -d "$PAYLOAD"
echo "✅ Release body updated successfully"
- name: Summary
if: ${{ always() && (inputs.dry_run || steps.ai.outputs.enhanced_body != '') }}
shell: bash
run: |
set -a; source ctx.env; set +a
echo "📊 Generating summary..."
if [[ "$DRY_RUN" == "true" ]]; then
echo "## 🧪 Dry Run - Enhanced Changelog Preview" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [[ -f enhanced_changelog.md ]]; then
echo "**Release would not be modified.** Below is the generated changelog:" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
cat enhanced_changelog.md >> $GITHUB_STEP_SUMMARY
else
echo "⚠️ **AI generation failed or no changelog was produced.**" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Check the workflow logs for details." >> $GITHUB_STEP_SUMMARY
fi
else
echo "## ✅ Enhanced Changelog Generated" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Release body has been updated with AI-enhanced changelog:" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
cat enhanced_changelog.md >> $GITHUB_STEP_SUMMARY
fi
echo "✅ Summary generated"
- name: Skipped notice
if: ${{ steps.ai.outputs.enhanced_body == '' }}
run: |
echo "❌ AI changelog generation skipped or failed. Ensure GitHub Models access is enabled for this repo." >> $GITHUB_STEP_SUMMARY
@@ -0,0 +1,43 @@
name: Validate Agentic Workflows
on:
pull_request:
branches: [main]
paths:
- '.github/workflows/*.md'
jobs:
compile:
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ github.token }}
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
- name: Install GitHub CLI
run: |
type -p curl >/dev/null || (sudo apt update && sudo apt install curl -y)
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
sudo apt update
sudo apt install gh -y
- name: Install gh-aw extension
run: gh extension install githubnext/gh-aw
- name: Compile agentic workflows
run: |
for workflow in .github/workflows/*.md; do
echo "Compiling $workflow..."
gh aw compile "$workflow"
done
- name: Check for uncommitted changes
run: |
if ! git diff --exit-code; then
echo "::error::Agentic workflows have uncommitted changes after compilation."
echo "::error::Please run 'gh aw compile' locally and commit the changes."
exit 1
fi