mirror of
https://github.com/Jermolene/TiddlyWiki5.git
synced 2025-12-06 02:30:46 -08:00
Bot to remind of change note (#9407)
* let claude write this * Update validate-changenotes.yml * Update validate-changenotes.yml * Update validate-changenotes.yml * Update validate-changenotes.yml * refactor: move logic to sh file
This commit is contained in:
parent
234667cc31
commit
845f988ab0
2 changed files with 494 additions and 0 deletions
187
.github/workflows/validate-changenotes.yml
vendored
Normal file
187
.github/workflows/validate-changenotes.yml
vendored
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
name: Validate Change Notes
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, reopened, synchronize]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
validate-changenotes:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Checkout base branch
|
||||
run: |
|
||||
git fetch origin ${{ github.base_ref }}
|
||||
|
||||
- name: Get all changed files in PR
|
||||
id: all-files
|
||||
uses: tj-actions/changed-files@v47
|
||||
with:
|
||||
base_sha: ${{ github.event.pull_request.base.sha }}
|
||||
|
||||
- name: Get changed note files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v47
|
||||
with:
|
||||
files: |
|
||||
editions/*/tiddlers/releasenotes/**/*.tid
|
||||
base_sha: ${{ github.event.pull_request.base.sha }}
|
||||
|
||||
- name: Check if PR needs change notes
|
||||
id: check-needs-notes
|
||||
run: |
|
||||
chmod +x bin/changenote.sh
|
||||
|
||||
if bin/changenote.sh check-needs ${{ steps.all-files.outputs.all_changed_files }}; then
|
||||
echo "needs_note=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "needs_note=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
echo "has_notes=${{ steps.changed-files.outputs.any_changed }}" >> $GITHUB_OUTPUT
|
||||
shell: bash
|
||||
|
||||
- name: Validate change note format
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
id: validate
|
||||
continue-on-error: true
|
||||
run: |
|
||||
chmod +x bin/changenote.sh
|
||||
|
||||
if bin/changenote.sh validate ${{ steps.changed-files.outputs.all_changed_files }}; then
|
||||
echo "result=success" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "result=failure" >> $GITHUB_OUTPUT
|
||||
# Save error details if they exist
|
||||
if [ -f /tmp/validation_errors.md ]; then
|
||||
{
|
||||
echo 'errors<<EOF'
|
||||
cat /tmp/validation_errors.md
|
||||
echo EOF
|
||||
} >> $GITHUB_OUTPUT
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- name: Parse change note summaries
|
||||
if: steps.changed-files.outputs.any_changed == 'true' && steps.validate.outcome == 'success'
|
||||
id: parse-notes
|
||||
run: |
|
||||
chmod +x bin/changenote.sh
|
||||
|
||||
summaries=$(bin/changenote.sh parse ${{ steps.changed-files.outputs.all_changed_files }})
|
||||
|
||||
{
|
||||
echo 'summaries<<EOF'
|
||||
echo "$summaries"
|
||||
echo EOF
|
||||
} >> $GITHUB_OUTPUT
|
||||
shell: bash
|
||||
|
||||
- name: Find existing bot comment
|
||||
if: always()
|
||||
uses: peter-evans/find-comment@v3
|
||||
id: find-comment
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
comment-author: 'github-actions[bot]'
|
||||
body-includes: 'Change Note Status'
|
||||
|
||||
- name: Create or update comment (validation passed)
|
||||
if: steps.changed-files.outputs.any_changed == 'true' && steps.validate.outcome == 'success'
|
||||
uses: peter-evans/create-or-update-comment@v5
|
||||
with:
|
||||
comment-id: ${{ steps.find-comment.outputs.comment-id }}
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
edit-mode: replace
|
||||
body: |
|
||||
## ✅ Change Note Status
|
||||
|
||||
All change notes are properly formatted and validated!
|
||||
|
||||
${{ steps.parse-notes.outputs.summaries }}
|
||||
|
||||
<details>
|
||||
<summary>📖 Change Note Guidelines</summary>
|
||||
|
||||
Change notes help track and communicate changes effectively. See the [full documentation](https://tiddlywiki.com/prerelease/#Release%20Notes%20and%20Changes) for details.
|
||||
|
||||
</details>
|
||||
|
||||
- name: Create or update comment (validation failed)
|
||||
if: steps.changed-files.outputs.any_changed == 'true' && steps.validate.outcome == 'failure'
|
||||
uses: peter-evans/create-or-update-comment@v5
|
||||
with:
|
||||
comment-id: ${{ steps.find-comment.outputs.comment-id }}
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
edit-mode: replace
|
||||
body: |
|
||||
## ❌ Change Note Status
|
||||
|
||||
Change note validation failed. Please fix the following issues:
|
||||
|
||||
${{ steps.validate.outputs.errors }}
|
||||
|
||||
---
|
||||
|
||||
[Release Notes and Changes](https://tiddlywiki.com/prerelease/#Release%20Notes%20and%20Changes)
|
||||
|
||||
- name: Create or update comment (missing change note)
|
||||
if: steps.check-needs-notes.outputs.needs_note == 'true' && steps.changed-files.outputs.any_changed != 'true'
|
||||
uses: peter-evans/create-or-update-comment@v5
|
||||
with:
|
||||
comment-id: ${{ steps.find-comment.outputs.comment-id }}
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
edit-mode: replace
|
||||
body: |
|
||||
## ⚠️ Change Note Status
|
||||
|
||||
This PR appears to contain code changes but doesn't include a change note.
|
||||
|
||||
Please add a change note by creating a `.tid` file in `editions/tw5.com/tiddlers/releasenotes/<version>/` with the following format:
|
||||
|
||||
[Release Notes and Changes](https://tiddlywiki.com/prerelease/#Release%20Notes%20and%20Changes)
|
||||
|
||||
Note: If this is a documentation-only change or doesn't require a change note, you can ignore this message.
|
||||
|
||||
- name: Create or update comment (doc only - has no notes)
|
||||
if: steps.check-needs-notes.outputs.needs_note == 'false' && steps.changed-files.outputs.any_changed != 'true'
|
||||
uses: peter-evans/create-or-update-comment@v5
|
||||
with:
|
||||
comment-id: ${{ steps.find-comment.outputs.comment-id }}
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
edit-mode: replace
|
||||
body: |
|
||||
## ✅ Change Note Status
|
||||
|
||||
This PR contains documentation or configuration changes that typically don't require a change note.
|
||||
|
||||
- name: Remove comment if not needed
|
||||
if: steps.check-needs-notes.outputs.needs_note == 'false' && steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: peter-evans/create-or-update-comment@v5
|
||||
with:
|
||||
comment-id: ${{ steps.find-comment.outputs.comment-id }}
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
edit-mode: replace
|
||||
body: |
|
||||
## ✅ Change Note Status
|
||||
|
||||
This PR contains documentation or configuration changes that typically don't require a change note.
|
||||
|
||||
- name: Fail workflow if validation failed
|
||||
if: steps.validate.outcome == 'failure'
|
||||
run: |
|
||||
echo "::error::Change note validation failed. Please check the PR comment for details."
|
||||
exit 1
|
||||
307
bin/changenote.sh
Normal file
307
bin/changenote.sh
Normal file
|
|
@ -0,0 +1,307 @@
|
|||
#!/bin/bash
|
||||
|
||||
# TiddlyWiki Change Note Management Script
|
||||
# Usage:
|
||||
# changenote.sh check-needs <files...> - Check if files require change notes
|
||||
# changenote.sh validate <files...> - Validate change note format
|
||||
# changenote.sh parse <files...> - Parse and generate summary
|
||||
|
||||
# Define valid values according to "Release Notes and Changes.tid"
|
||||
VALID_TYPES=("bugfix" "feature" "enhancement" "deprecation" "security" "pluginisation")
|
||||
VALID_CATEGORIES=("internal" "translation" "plugin" "widget" "filters" "usability" "palette" "hackability" "nodejs" "performance" "developer")
|
||||
|
||||
# Type emoji mapping
|
||||
declare -A TYPE_EMOJI=(
|
||||
["bugfix"]="🐛"
|
||||
["feature"]="✨"
|
||||
["enhancement"]="⚡"
|
||||
["deprecation"]="⚠️"
|
||||
["security"]="🔒"
|
||||
["pluginisation"]="🔌"
|
||||
)
|
||||
|
||||
# Category emoji mapping
|
||||
declare -A CATEGORY_EMOJI=(
|
||||
["internal"]="🔧"
|
||||
["translation"]="🌐"
|
||||
["plugin"]="🔌"
|
||||
["widget"]="📦"
|
||||
["filters"]="🔍"
|
||||
["usability"]="👥"
|
||||
["palette"]="🎨"
|
||||
["hackability"]="🛠️"
|
||||
["nodejs"]="💻"
|
||||
["performance"]="⚡"
|
||||
["developer"]="👨💻"
|
||||
)
|
||||
|
||||
# Function: Check if files need change notes
|
||||
# Returns 0 if needs change note, 1 if not needed
|
||||
check_needs_changenote() {
|
||||
local all_files="$@"
|
||||
|
||||
if [ -z "$all_files" ]; then
|
||||
echo "No files provided"
|
||||
return 1
|
||||
fi
|
||||
|
||||
for file in $all_files; do
|
||||
# Skip GitHub workflows/configs
|
||||
[[ "$file" =~ ^\.github/ ]] && continue
|
||||
[[ "$file" =~ ^\.vscode/ ]] && continue
|
||||
|
||||
# Skip config files
|
||||
[[ "$file" =~ ^\.editorconfig$ ]] && continue
|
||||
[[ "$file" =~ ^\.gitignore$ ]] && continue
|
||||
[[ "$file" =~ ^LICENSE$ ]] && continue
|
||||
|
||||
# Skip markdown files (except readme.md)
|
||||
[[ "$file" =~ \.md$ ]] && [[ ! "$file" =~ /readme\.md$ ]] && continue
|
||||
|
||||
# Skip documentation in bin folder
|
||||
[[ "$file" =~ ^bin/.*\.md$ ]] && continue
|
||||
|
||||
# Skip test results and reports
|
||||
[[ "$file" =~ ^playwright-report/ ]] && continue
|
||||
[[ "$file" =~ ^test-results/ ]] && continue
|
||||
|
||||
# Skip documentation editions
|
||||
[[ "$file" =~ ^editions/.*-docs?/ ]] && continue
|
||||
|
||||
# Check if it's a tiddler file
|
||||
if [[ "$file" =~ ^editions/.*/tiddlers/.*\.tid$ ]]; then
|
||||
# Core modules, plugins should require change notes
|
||||
[[ "$file" =~ /(core|plugin\.info|modules)/ ]] && echo "✓ Code file: $file" && return 0
|
||||
|
||||
# Release notes themselves don't require additional notes
|
||||
[[ "$file" =~ /releasenotes/ ]] && continue
|
||||
|
||||
# Other tiddlers are documentation
|
||||
continue
|
||||
fi
|
||||
|
||||
# If we reach here, it's a code file that needs a change note
|
||||
echo "✓ Code file requires change note: $file"
|
||||
return 0
|
||||
done
|
||||
|
||||
# All files are documentation/config
|
||||
echo "✓ Only documentation/configuration changes"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Function: Validate change note format
|
||||
validate_changenotes() {
|
||||
local files="$@"
|
||||
local has_errors=false
|
||||
local error_details=""
|
||||
|
||||
if [ -z "$files" ]; then
|
||||
echo "No change note files to validate."
|
||||
return 0
|
||||
fi
|
||||
|
||||
for file in $files; do
|
||||
echo "Validating: $file"
|
||||
|
||||
# Check if file exists
|
||||
if [ ! -f "$file" ]; then
|
||||
echo "::error file=$file::File not found"
|
||||
has_errors=true
|
||||
continue
|
||||
fi
|
||||
|
||||
# Check if it's a changenote file
|
||||
if [[ ! "$file" =~ editions/.*/tiddlers/releasenotes/.* ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Extract metadata from the .tid file
|
||||
title=$(grep -m 1 "^title: " "$file" | sed 's/^title: //')
|
||||
tags=$(grep -m 1 "^tags: " "$file" | sed 's/^tags: //')
|
||||
change_type=$(grep -m 1 "^change-type: " "$file" | sed 's/^change-type: //')
|
||||
change_category=$(grep -m 1 "^change-category: " "$file" | sed 's/^change-category: //')
|
||||
description=$(grep -m 1 "^description: " "$file" | sed 's/^description: //')
|
||||
|
||||
# Track errors for this file
|
||||
file_has_errors=false
|
||||
file_errors=""
|
||||
|
||||
# Validate title format
|
||||
if [[ ! "$title" =~ ^\$:/changenotes/[0-9]+\.[0-9]+\.[0-9]+/(#[0-9]+|[a-f0-9]{40})$ ]]; then
|
||||
echo "::error file=$file::Invalid title format"
|
||||
file_errors+="- Title format: Expected \`\$:/changenotes/<version>/<#issue or commit-hash>\`, found: \`$title\`\n"
|
||||
has_errors=true
|
||||
file_has_errors=true
|
||||
fi
|
||||
|
||||
# Validate tags
|
||||
if [[ -z "$tags" ]]; then
|
||||
echo "::error file=$file::Missing 'tags' field"
|
||||
file_errors+="- Missing field: \`tags\` field is required\n"
|
||||
has_errors=true
|
||||
file_has_errors=true
|
||||
elif [[ ! "$tags" =~ \$:/tags/ChangeNote ]]; then
|
||||
echo "::error file=$file::Tags must include '\$:/tags/ChangeNote'"
|
||||
file_errors+="- Tags: Must include \`\$:/tags/ChangeNote\`, found: \`$tags\`\n"
|
||||
has_errors=true
|
||||
file_has_errors=true
|
||||
fi
|
||||
|
||||
# Validate change-type
|
||||
if [[ -z "$change_type" ]]; then
|
||||
echo "::error file=$file::Missing 'change-type' field"
|
||||
file_errors+="- Missing field: \`change-type\` is required. Valid values: \`${VALID_TYPES[*]}\`\n"
|
||||
has_errors=true
|
||||
file_has_errors=true
|
||||
else
|
||||
valid=false
|
||||
for type in "${VALID_TYPES[@]}"; do
|
||||
[[ "$change_type" == "$type" ]] && valid=true && break
|
||||
done
|
||||
if [[ "$valid" == "false" ]]; then
|
||||
echo "::error file=$file::Invalid change-type '$change_type'"
|
||||
file_errors+="- Invalid change-type: \`$change_type\` is not valid. Must be one of: \`${VALID_TYPES[*]}\`\n"
|
||||
has_errors=true
|
||||
file_has_errors=true
|
||||
fi
|
||||
fi
|
||||
|
||||
# Validate change-category
|
||||
if [[ -z "$change_category" ]]; then
|
||||
echo "::error file=$file::Missing 'change-category' field"
|
||||
file_errors+="- Missing field: \`change-category\` is required. Valid values: \`${VALID_CATEGORIES[*]}\`\n"
|
||||
has_errors=true
|
||||
file_has_errors=true
|
||||
else
|
||||
valid=false
|
||||
for category in "${VALID_CATEGORIES[@]}"; do
|
||||
[[ "$change_category" == "$category" ]] && valid=true && break
|
||||
done
|
||||
if [[ "$valid" == "false" ]]; then
|
||||
echo "::error file=$file::Invalid change-category '$change_category'"
|
||||
file_errors+="- Invalid change-category: \`$change_category\` is not valid. Must be one of: \`${VALID_CATEGORIES[*]}\`\n"
|
||||
has_errors=true
|
||||
file_has_errors=true
|
||||
fi
|
||||
fi
|
||||
|
||||
# Validate description
|
||||
if [[ -z "$description" ]]; then
|
||||
echo "::error file=$file::Missing 'description' field"
|
||||
file_errors+="- Missing field: \`description\` is required\n"
|
||||
has_errors=true
|
||||
file_has_errors=true
|
||||
fi
|
||||
|
||||
# Collect errors
|
||||
if [[ "$file_has_errors" == "true" ]]; then
|
||||
error_details+="### 📄 \`$file\`\n\n$file_errors\n"
|
||||
else
|
||||
echo "✓ $file is valid"
|
||||
fi
|
||||
done
|
||||
|
||||
# Output error details to file for GitHub Actions
|
||||
if [[ "$has_errors" == "true" ]]; then
|
||||
echo -e "$error_details" > /tmp/validation_errors.md
|
||||
echo ""
|
||||
echo "================================"
|
||||
echo "Change note validation failed!"
|
||||
echo "================================"
|
||||
echo ""
|
||||
echo "Please ensure your change notes follow the format specified in:"
|
||||
echo "https://tiddlywiki.com/prerelease/#Release%20Notes%20and%20Changes"
|
||||
return 1
|
||||
else
|
||||
echo ""
|
||||
echo "✓ All change notes are valid!"
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
# Function: Parse change notes and generate summary
|
||||
parse_changenotes() {
|
||||
local files="$@"
|
||||
|
||||
if [ -z "$files" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
for file in $files; do
|
||||
[ ! -f "$file" ] && continue
|
||||
|
||||
# Parse metadata
|
||||
title=""
|
||||
description=""
|
||||
change_type=""
|
||||
change_category=""
|
||||
links=""
|
||||
in_body=false
|
||||
body_first_line=""
|
||||
|
||||
while IFS= read -r line; do
|
||||
# Empty line marks start of body
|
||||
if [ -z "$line" ] && [ "$in_body" = false ]; then
|
||||
in_body=true
|
||||
continue
|
||||
fi
|
||||
|
||||
if [ "$in_body" = false ]; then
|
||||
# Parse metadata
|
||||
[[ "$line" =~ ^title:\ (.*)$ ]] && title="${BASH_REMATCH[1]}"
|
||||
[[ "$line" =~ ^description:\ (.*)$ ]] && description="${BASH_REMATCH[1]}"
|
||||
[[ "$line" =~ ^change-type:\ (.*)$ ]] && change_type="${BASH_REMATCH[1]}"
|
||||
[[ "$line" =~ ^change-category:\ (.*)$ ]] && change_category="${BASH_REMATCH[1]}"
|
||||
[[ "$line" =~ ^links:\ (.*)$ ]] && links="${BASH_REMATCH[1]}"
|
||||
elif [ -z "$description" ] && [ -n "$line" ] && [ -z "$body_first_line" ]; then
|
||||
# Use first non-empty body line if no description in metadata
|
||||
body_first_line="$line"
|
||||
fi
|
||||
done < "$file"
|
||||
|
||||
# Use body first line as description if needed
|
||||
[ -z "$description" ] && [ -n "$body_first_line" ] && description="$body_first_line"
|
||||
|
||||
# Get emojis
|
||||
type_icon="${TYPE_EMOJI[$change_type]:-📝}"
|
||||
cat_icon="${CATEGORY_EMOJI[$change_category]:-📋}"
|
||||
|
||||
# Output markdown
|
||||
echo "### ${type_icon} ${title:-$file}"
|
||||
echo ""
|
||||
echo "**Type:** ${change_type} | **Category:** ${change_category} ${cat_icon}"
|
||||
echo ""
|
||||
|
||||
[ -n "$description" ] && echo "> ${description}" && echo ""
|
||||
[ -n "$links" ] && echo "🔗 [${links}](${links})" && echo ""
|
||||
|
||||
echo "---"
|
||||
echo ""
|
||||
done
|
||||
}
|
||||
|
||||
# Main command dispatcher
|
||||
case "${1:-}" in
|
||||
check-needs)
|
||||
shift
|
||||
check_needs_changenote "$@"
|
||||
;;
|
||||
validate)
|
||||
shift
|
||||
validate_changenotes "$@"
|
||||
;;
|
||||
parse)
|
||||
shift
|
||||
parse_changenotes "$@"
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 {check-needs|validate|parse} <files...>"
|
||||
echo ""
|
||||
echo "Commands:"
|
||||
echo " check-needs Check if files require change notes"
|
||||
echo " validate Validate change note format"
|
||||
echo " parse Parse and generate summary"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
Loading…
Add table
Add a link
Reference in a new issue