mirror of
https://github.com/wailsapp/wails.git
synced 2026-03-14 06:35:50 +01:00
Add v3 changelog validation workflow to master branch
- Add changelog-v3.yml workflow for GitHub Actions visibility - Add v3/scripts/validate-changelog.go validation script - Monitors PRs to v3-alpha branch for changelog compliance - Automatically fixes misplaced entries by moving to [Unreleased] 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
b987c1b413
commit
d2d9acbdf6
2 changed files with 295 additions and 0 deletions
25
.github/workflows/changelog-v3.yml
vendored
Normal file
25
.github/workflows/changelog-v3.yml
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
name: Changelog V3
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: 'PR number'
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '1.23'
|
||||
- name: Validate
|
||||
run: |
|
||||
echo "PR: ${{ github.event.inputs.pr_number }}"
|
||||
if [ -f "v3/scripts/validate-changelog.go" ]; then
|
||||
echo "Script found"
|
||||
else
|
||||
echo "Script not found"
|
||||
fi
|
||||
270
v3/scripts/validate-changelog.go
Normal file
270
v3/scripts/validate-changelog.go
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 3 {
|
||||
fmt.Println("Usage: go run validate-changelog.go <changelog-file> <added-lines-file>")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
changelogPath := os.Args[1]
|
||||
addedLinesPath := os.Args[2]
|
||||
|
||||
// Read changelog
|
||||
content, err := readFile(changelogPath)
|
||||
if err != nil {
|
||||
fmt.Printf("ERROR: Failed to read changelog: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Read the lines added in this PR
|
||||
addedContent, err := readFile(addedLinesPath)
|
||||
if err != nil {
|
||||
fmt.Printf("ERROR: Failed to read PR added lines: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
addedLines := strings.Split(addedContent, "\n")
|
||||
fmt.Printf("📝 Lines added in this PR: %d\n", len(addedLines))
|
||||
|
||||
// Parse changelog to find where added lines ended up
|
||||
lines := strings.Split(content, "\n")
|
||||
|
||||
// Find problematic entries - only check lines that were ADDED in this PR
|
||||
var issues []Issue
|
||||
currentSection := ""
|
||||
|
||||
for lineNum, line := range lines {
|
||||
// Track current section
|
||||
if strings.HasPrefix(line, "## ") {
|
||||
if strings.Contains(line, "[Unreleased]") {
|
||||
currentSection = "Unreleased"
|
||||
} else if strings.Contains(line, "v3.0.0-alpha") {
|
||||
// Extract version from line like "## v3.0.0-alpha.10 - 2025-07-06"
|
||||
parts := strings.Split(strings.TrimSpace(line[3:]), " - ")
|
||||
if len(parts) >= 1 {
|
||||
currentSection = strings.TrimSpace(parts[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this line was added in this PR AND is in a released version
|
||||
if currentSection != "" && currentSection != "Unreleased" &&
|
||||
strings.HasPrefix(strings.TrimSpace(line), "- ") &&
|
||||
wasAddedInThisPR(line, addedLines) {
|
||||
|
||||
issues = append(issues, Issue{
|
||||
Line: lineNum,
|
||||
Content: strings.TrimSpace(line),
|
||||
Section: currentSection,
|
||||
Category: getCurrentCategory(lines, lineNum),
|
||||
})
|
||||
fmt.Printf("🚨 MISPLACED: Line added to released version %s: %s\n", currentSection, strings.TrimSpace(line))
|
||||
}
|
||||
}
|
||||
|
||||
if len(issues) == 0 {
|
||||
fmt.Println("VALIDATION_RESULT=success")
|
||||
fmt.Println("No misplaced changelog entries found ✅")
|
||||
return
|
||||
}
|
||||
|
||||
// Try to fix the issues
|
||||
fmt.Printf("Found %d potentially misplaced entries:\n", len(issues))
|
||||
for _, issue := range issues {
|
||||
fmt.Printf(" - Line %d in %s: %s\n", issue.Line+1, issue.Section, issue.Content)
|
||||
}
|
||||
|
||||
// Attempt automatic fix
|
||||
fixed, err := attemptFix(content, issues, changelogPath)
|
||||
if err != nil {
|
||||
fmt.Printf("VALIDATION_RESULT=error\n")
|
||||
fmt.Printf("ERROR: Failed to fix changelog: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if fixed {
|
||||
fmt.Println("VALIDATION_RESULT=fixed")
|
||||
fmt.Println("✅ Changelog has been automatically fixed")
|
||||
} else {
|
||||
fmt.Println("VALIDATION_RESULT=cannot_fix")
|
||||
fmt.Println("❌ Cannot automatically fix changelog issues")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
type Issue struct {
|
||||
Line int
|
||||
Content string
|
||||
Section string
|
||||
Category string
|
||||
}
|
||||
|
||||
func wasAddedInThisPR(line string, addedLines []string) bool {
|
||||
trimmedLine := strings.TrimSpace(line)
|
||||
for _, addedLine := range addedLines {
|
||||
trimmedAdded := strings.TrimSpace(addedLine)
|
||||
if trimmedAdded == trimmedLine {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(trimmedAdded, trimmedLine) && len(trimmedAdded) > 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func getCurrentCategory(lines []string, lineNum int) string {
|
||||
for i := lineNum - 1; i >= 0; i-- {
|
||||
line := strings.TrimSpace(lines[i])
|
||||
if strings.HasPrefix(line, "### ") {
|
||||
return strings.TrimSpace(line[4:])
|
||||
}
|
||||
if strings.HasPrefix(line, "## ") &&
|
||||
!strings.Contains(line, "[Unreleased]") &&
|
||||
!strings.Contains(line, "v3.0.0-alpha") {
|
||||
return strings.TrimSpace(line[3:])
|
||||
}
|
||||
if strings.HasPrefix(line, "## ") &&
|
||||
(strings.Contains(line, "[Unreleased]") || strings.Contains(line, "v3.0.0-alpha")) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return "Added"
|
||||
}
|
||||
|
||||
func attemptFix(content string, issues []Issue, outputPath string) (bool, error) {
|
||||
lines := strings.Split(content, "\n")
|
||||
|
||||
// Find unreleased section
|
||||
unreleasedStart := -1
|
||||
unreleasedEnd := -1
|
||||
|
||||
for i, line := range lines {
|
||||
if strings.Contains(line, "[Unreleased]") {
|
||||
unreleasedStart = i
|
||||
for j := i + 1; j < len(lines); j++ {
|
||||
if strings.HasPrefix(lines[j], "## ") && !strings.Contains(lines[j], "[Unreleased]") {
|
||||
unreleasedEnd = j
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if unreleasedStart == -1 {
|
||||
return false, fmt.Errorf("Could not find [Unreleased] section")
|
||||
}
|
||||
|
||||
// Group issues by category
|
||||
issuesByCategory := make(map[string][]Issue)
|
||||
for _, issue := range issues {
|
||||
issuesByCategory[issue.Category] = append(issuesByCategory[issue.Category], issue)
|
||||
}
|
||||
|
||||
// Remove issues from original locations (in reverse order)
|
||||
var linesToRemove []int
|
||||
for _, issue := range issues {
|
||||
linesToRemove = append(linesToRemove, issue.Line)
|
||||
}
|
||||
|
||||
// Sort in reverse order
|
||||
for i := 0; i < len(linesToRemove); i++ {
|
||||
for j := i + 1; j < len(linesToRemove); j++ {
|
||||
if linesToRemove[i] < linesToRemove[j] {
|
||||
linesToRemove[i], linesToRemove[j] = linesToRemove[j], linesToRemove[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove lines
|
||||
for _, lineNum := range linesToRemove {
|
||||
lines = append(lines[:lineNum], lines[lineNum+1:]...)
|
||||
}
|
||||
|
||||
// Add entries to unreleased section
|
||||
for category, categoryIssues := range issuesByCategory {
|
||||
categoryFound := false
|
||||
insertPos := unreleasedStart + 1
|
||||
|
||||
for i := unreleasedStart + 1; i < unreleasedEnd && i < len(lines); i++ {
|
||||
if strings.Contains(lines[i], "### "+category) || strings.Contains(lines[i], "## "+category) {
|
||||
categoryFound = true
|
||||
for j := i + 1; j < unreleasedEnd && j < len(lines); j++ {
|
||||
if strings.HasPrefix(lines[j], "### ") || strings.HasPrefix(lines[j], "## ") {
|
||||
insertPos = j
|
||||
break
|
||||
}
|
||||
if j == len(lines)-1 || j == unreleasedEnd-1 {
|
||||
insertPos = j + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !categoryFound {
|
||||
if unreleasedEnd > 0 {
|
||||
insertPos = unreleasedEnd
|
||||
} else {
|
||||
insertPos = unreleasedStart + 1
|
||||
}
|
||||
|
||||
newLines := []string{
|
||||
"",
|
||||
"### " + category,
|
||||
"",
|
||||
}
|
||||
lines = append(lines[:insertPos], append(newLines, lines[insertPos:]...)...)
|
||||
insertPos += len(newLines)
|
||||
unreleasedEnd += len(newLines)
|
||||
}
|
||||
|
||||
// Add entries to the category
|
||||
for _, issue := range categoryIssues {
|
||||
lines = append(lines[:insertPos], append([]string{issue.Content}, lines[insertPos:]...)...)
|
||||
insertPos++
|
||||
unreleasedEnd++
|
||||
}
|
||||
}
|
||||
|
||||
// Write back to file
|
||||
newContent := strings.Join(lines, "\n")
|
||||
return true, writeFile(outputPath, newContent)
|
||||
}
|
||||
|
||||
func readFile(path string) (string, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var content strings.Builder
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
content.WriteString(scanner.Text())
|
||||
content.WriteString("\n")
|
||||
}
|
||||
|
||||
return content.String(), scanner.Err()
|
||||
}
|
||||
|
||||
func writeFile(path, content string) error {
|
||||
dir := filepath.Dir(path)
|
||||
err := os.MkdirAll(dir, 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(path, []byte(content), 0644)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue