diff --git a/.github/workflows/validate-changenotes.yml b/.github/workflows/validate-changenotes.yml new file mode 100644 index 000000000..b750bf578 --- /dev/null +++ b/.github/workflows/validate-changenotes.yml @@ -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<> $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<> $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 }} + +
+ 📖 Change Note Guidelines + + Change notes help track and communicate changes effectively. See the [full documentation](https://tiddlywiki.com/prerelease/#Release%20Notes%20and%20Changes) for 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//` 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 diff --git a/bin/changenote.sh b/bin/changenote.sh new file mode 100644 index 000000000..51c8c3755 --- /dev/null +++ b/bin/changenote.sh @@ -0,0 +1,307 @@ +#!/bin/bash + +# TiddlyWiki Change Note Management Script +# Usage: +# changenote.sh check-needs - Check if files require change notes +# changenote.sh validate - Validate change note format +# changenote.sh parse - 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//<#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} " + 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