Skip to content

Commit d28fd64

Browse files
authored
feat(build): add validation task for CHANGES.md formatting (#36041)
* feat(build): add validation task for CHANGES.md formatting Add a new gradle task 'validateChanges' that checks CHANGES.md follows required formatting rules including: - Language references must use parentheses - Each entry must have a proper issue link - Template and unreleased sections must exist * lint
1 parent cccaa75 commit d28fd64

1 file changed

Lines changed: 121 additions & 0 deletions

File tree

build.gradle.kts

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -637,6 +637,127 @@ tasks.register("formatChanges") {
637637
}
638638
}
639639

640+
tasks.register("validateChanges") {
641+
group = "verification"
642+
description = "Validates CHANGES.md follows required formatting rules"
643+
644+
doLast {
645+
val changesFile = file("CHANGES.md")
646+
if (!changesFile.exists()) {
647+
throw GradleException("CHANGES.md file not found")
648+
}
649+
650+
val content = changesFile.readText()
651+
val lines = content.lines()
652+
val errors = mutableListOf<String>()
653+
654+
// Find template section boundaries
655+
var templateStartIndex = -1
656+
var templateEndIndex = -1
657+
658+
for (i in lines.indices) {
659+
if (lines[i].trim() == "<!-- Template -->") {
660+
templateStartIndex = i
661+
println("Found template start at line ${i+1}")
662+
} else if (templateStartIndex != -1 && lines[i].trim() == "-->") {
663+
templateEndIndex = i
664+
println("Found template end at line ${i+1}")
665+
break
666+
}
667+
}
668+
669+
if (templateStartIndex == -1 || templateEndIndex == -1) {
670+
throw GradleException("Template section not found in CHANGES.md")
671+
}
672+
673+
println("Template section: lines ${templateStartIndex+1} to ${templateEndIndex+1}")
674+
675+
// Find unreleased section after the template section
676+
var unreleasedSectionStart = -1
677+
for (i in (templateEndIndex + 1) until lines.size) {
678+
if (lines[i].startsWith("# [") && lines[i].contains("Unreleased")) {
679+
unreleasedSectionStart = i
680+
println("Found unreleased section at line ${i+1}: ${lines[i]}")
681+
break
682+
}
683+
}
684+
685+
if (unreleasedSectionStart == -1) {
686+
throw GradleException("Unreleased section not found in CHANGES.md")
687+
}
688+
689+
// Check entries in the unreleased section
690+
var i = unreleasedSectionStart + 1
691+
println("Starting validation from line ${i+1}")
692+
693+
while (i < lines.size && !lines[i].startsWith("# [")) {
694+
val line = lines[i].trim()
695+
696+
if (line.startsWith("* ") && line.isNotEmpty()) {
697+
println("Checking line ${i+1}: $line")
698+
699+
// Skip comment lines
700+
if (line.startsWith("* [comment]:")) {
701+
println(" Skipping comment line")
702+
} else {
703+
// Rule 1: Check if language references use parentheses instead of brackets
704+
val languagePattern = "\\[(Java|Python|Go|Kotlin|TypeScript|YAML)(?:/(?:Java|Python|Go|Kotlin|TypeScript|YAML))*\\]"
705+
val languageRegex = Regex(languagePattern)
706+
707+
// Check if there's a language reference in brackets
708+
val matches = languageRegex.findAll(line).toList()
709+
if (matches.isNotEmpty()) {
710+
for (match in matches) {
711+
val matchText = match.value
712+
val matchPosition = match.range.first
713+
println(" Found language reference: $matchText at position $matchPosition")
714+
715+
// Check if this is part of an issue link or URL
716+
val beforeMatch = if (matchPosition > 0) line.substring(0, matchPosition) else ""
717+
val isPartOfLink = beforeMatch.contains("[#") ||
718+
beforeMatch.contains("http") ||
719+
line.contains("CVE-")
720+
721+
println(" Is part of link: $isPartOfLink")
722+
723+
if (!isPartOfLink) {
724+
val error = "Line ${i+1}: Language references should use parentheses () instead of brackets []: $line"
725+
println(" Adding error: $error")
726+
errors.add(error)
727+
}
728+
}
729+
} else {
730+
println(" No bracketed language reference found")
731+
}
732+
733+
// Rule 2: Check if each entry has an issue link
734+
val issueLinkPattern = "\\(\\[#[0-9a-zA-Z]+\\]\\(https://github\\.com/apache/beam/issues/[0-9a-zA-Z]+\\)\\)"
735+
val issueLinkRegex = Regex(issueLinkPattern)
736+
737+
val hasIssueLink = issueLinkRegex.containsMatchIn(line)
738+
println(" Has issue link: $hasIssueLink")
739+
740+
if (!hasIssueLink) {
741+
val error = "Line ${i+1}: Missing or malformed issue link. Each entry should end with ([#X](https://github.com/apache/beam/issues/X)): $line"
742+
println(" Adding error: $error")
743+
errors.add(error)
744+
}
745+
}
746+
}
747+
748+
i++
749+
}
750+
751+
println("Found ${errors.size} errors")
752+
753+
if (errors.isNotEmpty()) {
754+
throw GradleException("CHANGES.md validation failed with the following errors:\n${errors.joinToString("\n")}\n\nYou can run ./gradlew formatChanges to correct some issues.")
755+
}
756+
757+
println("CHANGES.md validation successful")
758+
}
759+
}
760+
640761
tasks.register("python39PostCommit") {
641762
dependsOn(":sdks:python:test-suites:dataflow:py39:postCommitIT")
642763
dependsOn(":sdks:python:test-suites:direct:py39:postCommitIT")

0 commit comments

Comments
 (0)