mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
This change adds a script to validate that the IJP API dumps are up-to-date, and runs it in the GitHub Actions CI. It also rewrites the validate-api-dump-changes.main.kts script to use clikt as well. closes https://github.com/JetBrains/intellij-community/pull/3245 (cherry picked from commit c11e3fecf8d04d2abc9dbd027bcc7cbcce8cda22) (cherry picked from commit 2af726c6cd283a4b46dcc35bea04c8a53c780141) IJ-MR-179919 GitOrigin-RevId: 7f80e6e540ee0769d5c9228de2c0ce8047d0b90c
242 lines
9.5 KiB
Kotlin
Executable File
242 lines
9.5 KiB
Kotlin
Executable File
#!/usr/bin/env kotlin
|
|
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
|
|
|
@file:Import("utils.main.kts")
|
|
@file:DependsOn("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1")
|
|
@file:DependsOn("com.github.ajalt.clikt:clikt-jvm:5.0.3")
|
|
@file:Suppress("RAW_RUN_BLOCKING")
|
|
|
|
import com.github.ajalt.clikt.command.SuspendingCliktCommand
|
|
import com.github.ajalt.clikt.command.main
|
|
import com.github.ajalt.clikt.core.Context
|
|
import com.github.ajalt.clikt.parameters.options.flag
|
|
import com.github.ajalt.clikt.parameters.options.option
|
|
import java.io.File
|
|
import kotlinx.coroutines.async
|
|
import kotlinx.coroutines.awaitAll
|
|
import kotlinx.coroutines.coroutineScope
|
|
import kotlinx.coroutines.runBlocking
|
|
|
|
private class AnnotateApiDumpChangesCommand : SuspendingCliktCommand(name = "annotate") {
|
|
private val verbose: Boolean by option("--verbose", "-v", help = "Enable verbose output.").flag(default = false)
|
|
|
|
override fun help(context: Context): String = "Annotates API dump files for breaking changes against a base commit."
|
|
|
|
override suspend fun run() {
|
|
print("⏳ Locating Jewel root...")
|
|
val jewelRoot = findJewelRoot() ?: exitWithError("Could not find the Jewel root directory.")
|
|
println(" DONE: ${jewelRoot.canonicalPath}")
|
|
|
|
val baseCommit = determineBaseCommit(jewelRoot)
|
|
println("Checking against base commit: $baseCommit")
|
|
|
|
println("\nValidating stable API dumps...")
|
|
val stableViolations = validateDumps(experimental = false, baseCommit, jewelRoot) { it.name == "api-dump.txt" }
|
|
|
|
println("\nValidating experimental API dumps...")
|
|
val experimentalViolations =
|
|
validateDumps(experimental = true, baseCommit, jewelRoot) { it.name == "api-dump-experimental.txt" }
|
|
|
|
println("\nWriting summary...")
|
|
writeSummary(stableViolations, experimentalViolations)
|
|
|
|
println("\nDone processing API dumps")
|
|
|
|
if (stableViolations) {
|
|
exitWithError("Stable API breakages found.")
|
|
} else {
|
|
printlnSuccess("✅ No stable API breakages found.")
|
|
}
|
|
}
|
|
|
|
private suspend fun determineBaseCommit(jewelRoot: File): String =
|
|
if (checkPrNumber()) {
|
|
requireGhTool()
|
|
runCommand("gh pr view ${getPrNumber()} --json baseRefOid -q .baseRefOid", jewelRoot).getOrThrow().trim()
|
|
} else {
|
|
printlnWarn("GitHub PR number not found, falling back to checking against HEAD~1 instead")
|
|
runCommand("git rev-parse HEAD~1", jewelRoot).getOrThrow().trim()
|
|
}
|
|
|
|
private data class ValidationResult(val file: File, val log: CharSequence, val foundBreakages: Boolean)
|
|
|
|
private suspend fun validateDumps(
|
|
experimental: Boolean,
|
|
baseCommit: String,
|
|
jewelRoot: File,
|
|
dumpsFilter: (File) -> Boolean,
|
|
): Boolean {
|
|
val samplesDir: String = File(jewelRoot, "samples").absolutePath
|
|
val apiDumpFiles =
|
|
jewelRoot.walkTopDown().filter { dumpsFilter(it) && !it.absolutePath.startsWith(samplesDir) }.toList()
|
|
|
|
println()
|
|
println("Detected API dumps:\n${apiDumpFiles.joinToString("\n") { " * ${it.toRelativeString(jewelRoot)}" }}")
|
|
|
|
val results = coroutineScope {
|
|
apiDumpFiles
|
|
.map { file ->
|
|
async {
|
|
var foundBreakages = false
|
|
val log = buildString {
|
|
appendLine("\n Checking ${file.toRelativeString(jewelRoot)}")
|
|
|
|
val isModifiedResult =
|
|
runCommand(
|
|
"git diff --quiet $baseCommit -- ${file.absolutePath}",
|
|
jewelRoot,
|
|
exitOnError = false,
|
|
)
|
|
|
|
if (verbose) {
|
|
appendLine(" First diff result:\n${isModifiedResult.output}")
|
|
}
|
|
|
|
if (isModifiedResult.isFailure) {
|
|
appendLine(" Detected changes, investigating...")
|
|
|
|
val command = "git --no-pager diff --unified=0 $baseCommit -- ${file.absolutePath}"
|
|
if (verbose) {
|
|
appendLine(" Running: $command...")
|
|
}
|
|
|
|
val diffResult = runCommand(command, jewelRoot)
|
|
if (verbose) {
|
|
appendLine(" Second diff result:\n${diffResult.output}")
|
|
}
|
|
|
|
foundBreakages = processDiff(diffResult.output, file, experimental, this, jewelRoot)
|
|
}
|
|
}
|
|
ValidationResult(file, log, foundBreakages)
|
|
}
|
|
}
|
|
.awaitAll()
|
|
}
|
|
|
|
results.forEach { result -> print(result.log) }
|
|
return results.any { it.foundBreakages }
|
|
}
|
|
|
|
private val chunkHeaderRegex = "^@@ \\-([0-9]+)(?:,[0-9]+)? \\+([0-9]+)".toRegex()
|
|
|
|
private fun processDiff(
|
|
diff: String,
|
|
file: File,
|
|
experimental: Boolean,
|
|
log: StringBuilder,
|
|
jewelRoot: File,
|
|
): Boolean {
|
|
var foundBreakages = false
|
|
var oldLineNum = 0
|
|
var newLineNum = 0
|
|
var lastLineWasRemoval = false
|
|
|
|
diff.lines().forEach { line ->
|
|
when {
|
|
line.startsWith("@@") -> {
|
|
val match = chunkHeaderRegex.find(line)
|
|
if (match != null) {
|
|
oldLineNum = match.groupValues[1].toInt()
|
|
newLineNum = match.groupValues[2].toInt()
|
|
}
|
|
lastLineWasRemoval = false
|
|
}
|
|
line.startsWith("-") && !line.startsWith("---") -> {
|
|
reportBreakage(
|
|
file,
|
|
line,
|
|
log,
|
|
oldLineNum,
|
|
newLineNum,
|
|
experimental,
|
|
annotate = !lastLineWasRemoval,
|
|
jewelRoot,
|
|
)
|
|
foundBreakages = true
|
|
oldLineNum++
|
|
lastLineWasRemoval = true
|
|
}
|
|
line.startsWith("+") && !line.startsWith("+++") -> {
|
|
newLineNum++
|
|
lastLineWasRemoval = false
|
|
}
|
|
line.startsWith(" ") -> {
|
|
oldLineNum++
|
|
newLineNum++
|
|
lastLineWasRemoval = false
|
|
}
|
|
}
|
|
}
|
|
|
|
return foundBreakages
|
|
}
|
|
|
|
private fun reportBreakage(
|
|
file: File,
|
|
line: String,
|
|
log: StringBuilder,
|
|
oldLineNum: Int,
|
|
newLineNum: Int,
|
|
experimental: Boolean,
|
|
annotate: Boolean,
|
|
jewelRoot: File,
|
|
) {
|
|
val lineContent =
|
|
line
|
|
.substring(1) // Skip the first character (either + or -)
|
|
.replace("%", "%25") // Escape the % character
|
|
.replace("\r", "%0D") // Escape the \r character
|
|
.replace("\n", "%0A") // Escape the \n character
|
|
|
|
val type = if (experimental) "experimental" else "stable"
|
|
val message = "⚠️ Breaking $type API change:\n line $oldLineNum removed: $lineContent"
|
|
log.appendLine(" " + if (experimental) message.asWarning() else message.asError())
|
|
|
|
if (!annotate) return
|
|
|
|
// Use the magic log format that GitHub workflows accept to annotate code
|
|
// See https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-commands
|
|
val severity = if (experimental) "warning" else "error"
|
|
log.append("::$severity ")
|
|
|
|
val repoRoot = jewelRoot.parentFile.parentFile
|
|
log.append("file=${file.toRelativeString(repoRoot)},")
|
|
|
|
log.append("line=$newLineNum,")
|
|
|
|
val title = if (experimental) "Breaking experimental API change" else "Breaking API change"
|
|
log.appendLine("title=$title::This looks like a breaking API change, make sure it's intended.")
|
|
}
|
|
|
|
private fun writeSummary(stableViolations: Boolean, experimentalViolations: Boolean) {
|
|
val summary = buildString {
|
|
appendLine("## Binary check result")
|
|
if (!stableViolations && !experimentalViolations) {
|
|
appendLine("✅ No API breakages found.")
|
|
} else {
|
|
if (stableViolations) {
|
|
appendLine("❌ Stable API breakages found.")
|
|
}
|
|
if (experimentalViolations) {
|
|
appendLine("⚠️ Experimental API breakages found.")
|
|
}
|
|
}
|
|
}
|
|
|
|
println(summary.prependIndent())
|
|
|
|
// Write the summary to the magical GITHUB_STEP_SUMMARY file
|
|
// See https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-commands
|
|
val summaryFile = System.getenv("GITHUB_STEP_SUMMARY")?.takeIf { !it.isBlank() }?.let { File(it) }
|
|
if (summaryFile != null) {
|
|
summaryFile.writeText(summary)
|
|
println("Summary written to ${summaryFile.absolutePath}")
|
|
} else {
|
|
printlnWarn("GITHUB_STEP_SUMMARY environment variable not set")
|
|
}
|
|
}
|
|
}
|
|
|
|
runBlocking { AnnotateApiDumpChangesCommand().main(args) }
|