diff --git a/platform/jewel/RELEASE NOTES.md b/platform/jewel/RELEASE NOTES.md index 9825eb3326b3..b450a5c4c16a 100644 --- a/platform/jewel/RELEASE NOTES.md +++ b/platform/jewel/RELEASE NOTES.md @@ -1,6 +1,6 @@ # Jewel Release Notes -## 0.29 (2025-07-22) +## v0.29 (2025-07-22) | Supported IJP versions | Compose Multiplatform version | |------------------------|-------------------------------| diff --git a/platform/jewel/scripts/extract-release-notes.main.kts b/platform/jewel/scripts/extract-release-notes.main.kts index b090989a85d9..b03e51d7e958 100755 --- a/platform/jewel/scripts/extract-release-notes.main.kts +++ b/platform/jewel/scripts/extract-release-notes.main.kts @@ -1,97 +1,116 @@ #!/usr/bin/env kotlin -// Coroutine dependency for KTS scripts @file:DependsOn("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1") +@file:DependsOn("org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.9.0") +@file:Import("utils.main.kts") +@file:Suppress("RAW_RUN_BLOCKING") +import java.io.File +import java.util.concurrent.ConcurrentHashMap +import kotlin.system.exitProcess +import kotlin.time.TimeSource.Monotonic.markNow import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import java.io.File -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.TimeUnit -import java.util.concurrent.TimeoutException -import kotlin.system.exitProcess -import kotlin.time.TimeSource.Monotonic.markNow +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive // --- Configuration --- -object Config { +private object Config { const val UPSTREAM_REPO = "JetBrains/intellij-community" const val JEWEL_DIR = "." const val OUTPUT_FILE = "new_release_notes.md" - const val MAX_CONCURRENT_JOBS = 5 + const val MAX_CONCURRENT_JOBS = 7 const val RELEASE_NOTES_FILE = "RELEASE NOTES.md" } -// --- Data Structures --- -data class ReleaseNoteItem(val issueId: String?, val description: String, val prId: String, val prUrl: String) +private val workingDir = File("").absoluteFile -enum class ProcessedPrStatus { +// --- Data Structures --- +private data class ReleaseNoteItem(val issueId: String?, val description: String, val prId: String, val prUrl: String) + +private enum class PrProcessingStatus { Extracted, BlankReleaseNotes, NoReleaseNotes, Error, } -data class CommitInfo(val commitHash: String, val prId: String, val issueId: String?) +private data class CommitInfo(val commitHash: String, val prId: String, val issueId: String?) -data class CommitResult( +private data class PrProcessingResult( val prId: String, - val status: ProcessedPrStatus, + val prTitle: String, + val status: PrProcessingStatus, val notes: Map> = emptyMap(), val logMessages: List = emptyList(), ) // --- Helper Functions --- -fun runCommand(vararg command: String, workDir: File = File(".")): String { - val process = - ProcessBuilder(*command) - .directory(workDir) - .redirectOutput(ProcessBuilder.Redirect.PIPE) - .redirectError(ProcessBuilder.Redirect.PIPE) - .start() +private fun getIndentation(line: String): Int = line.takeWhile { it.isWhitespace() }.length - if (!process.waitFor(60, TimeUnit.SECONDS)) { - process.destroy() - throw TimeoutException("Command timed out: ${command.joinToString(" ")}") - } +private fun formatReleaseNotesLine(note: ReleaseNoteItem): String { + val lines = note.description.lines() + val firstLine = lines.first() + val otherLines = lines.drop(1) - val output = process.inputStream.bufferedReader().readText() - if (process.exitValue() != 0) { - val error = process.errorStream.bufferedReader().readText() - error("Command failed with exit code ${process.exitValue()}: ${command.joinToString(" ")}\n$error") + return buildString { + append(" *") + if (note.issueId != null) { + append(" **") + append(note.issueId) + append("**") + } + append(" ") + append(firstLine.cleanupEntry(note.issueId)) + append(" ([#") + append(note.prId) + append("](") + append(note.prUrl) + append("))") + + if (otherLines.isNotEmpty()) { + val otherLinesText = otherLines.joinToString("\n") + if (otherLinesText.isNotBlank()) { + append("\n") + append(otherLinesText) + } + } } - return output.trim() } -fun formatReleaseNotesLine(note: ReleaseNoteItem): String = buildString { - append(" *") - if (note.issueId != null) { - append(" **") - append(note.issueId) - append("**") - } - append(" ") - append(note.description) - append(" ([#") - append(note.prId) - append("](") - append(note.prUrl) - append("))") +private fun String.cleanupEntry(issueIdText: String?): String { + // 1. Remove trailing dot + val step1 = removeSuffix(".") + // 2. Remove issue ID if present + val step2 = + if (issueIdText != null) { + step1.removePrefix("$issueIdText ").removePrefix("**$issueIdText** ") + } else { + step1 + } + // 3. Trim + return step2.trim() } -// --- Core Logic (now collects logs instead of printing them) --- -fun processPr(commitInfo: CommitInfo, isVerbose: Boolean): CommitResult { +private suspend fun processPr(commitInfo: CommitInfo, isVerbose: Boolean): PrProcessingResult { val (_, prNumber, issueId) = commitInfo val logs = mutableListOf() try { - val prUrl = - runCommand("gh", "pr", "view", prNumber, "--repo", Config.UPSTREAM_REPO, "--json", "url", "-q", ".url") - val prBody = - runCommand("gh", "pr", "view", prNumber, "--repo", Config.UPSTREAM_REPO, "--json", "body", "-q", ".body") + val prInfo = + runCommand("gh pr view $prNumber --repo ${Config.UPSTREAM_REPO} --json url,body,title", workingDir) + .output + .let { Json.parseToJsonElement(it).jsonObject } + + val prUrl = prInfo["url"]?.jsonPrimitive?.content!! + val prBody = prInfo["body"]?.jsonPrimitive?.content!! + val prTitle = prInfo["title"]?.jsonPrimitive?.content!! if (isVerbose) logs.add("ℹ️ PR body fetched:\n${prBody.prependIndent(" ")}\n") val lines = prBody.lines() @@ -99,8 +118,8 @@ fun processPr(commitInfo: CommitInfo, isVerbose: Boolean): CommitResult { lines.indexOfFirst { it.trim().matches("""##+\s+release notes""".toRegex(RegexOption.IGNORE_CASE)) } if (headerIndex == -1) { - logs.add("⚠️ No 'Release Notes' section found.") - return CommitResult(prNumber, ProcessedPrStatus.NoReleaseNotes, logMessages = logs) + logs.add("⚠️ No 'Release Notes' section found.".asWarning()) + return PrProcessingResult(prNumber, prTitle, PrProcessingStatus.NoReleaseNotes, logMessages = logs) } val subsequentLines = lines.drop(headerIndex + 1) @@ -111,57 +130,95 @@ fun processPr(commitInfo: CommitInfo, isVerbose: Boolean): CommitResult { .trim() if (releaseNotesText.isBlank()) { - logs.add("⚠️ 'Release Notes' section found but it was empty.") - return CommitResult(prNumber, ProcessedPrStatus.BlankReleaseNotes, logMessages = logs) + logs.add("⚠️ 'Release Notes' section found but it was empty.".asWarning()) + return PrProcessingResult(prNumber, prTitle, PrProcessingStatus.BlankReleaseNotes, logMessages = logs) } if (isVerbose) logs.add("ℹ️ Extracted release notes text:\n$releaseNotesText\n") val notesInPr = mutableMapOf>() - var currentSection = "Uncategorized" - releaseNotesText.lines().forEach { line -> - val headerMatch = """^#+\s+(.*)""".toRegex().find(line.trim()) - if (headerMatch != null) { - currentSection = - headerMatch.groupValues[1].trim().lowercase().replaceFirstChar { - if (it.isLowerCase()) it.titlecase() else it.toString() - } - } else if (line.isNotBlank()) { - val mainText = line.trim().removePrefix("*").removePrefix("-").trim() - val noteItem = ReleaseNoteItem(issueId, mainText, prNumber, prUrl) - notesInPr.getOrPut(currentSection) { mutableListOf() }.add(noteItem) - } + var currentSection = "Other" + val releaseLines = releaseNotesText.lines() + + var i = 0 + while (i < releaseLines.size) { + val (nextIndex, nextSection) = + processLine(i, releaseLines, currentSection, issueId, prNumber, prUrl, notesInPr) + i = nextIndex + currentSection = nextSection } + logs.add("✅ Parsed notes successfully.") - return CommitResult(prNumber, ProcessedPrStatus.Extracted, notesInPr, logs) + return PrProcessingResult(prNumber, prTitle, PrProcessingStatus.Extracted, notesInPr, logs) } catch (e: Exception) { - logs.add("❌ Error processing PR: ${e.message?.lines()?.firstOrNull()}") - return CommitResult(prNumber, ProcessedPrStatus.Error, logMessages = logs) + logs.add("❌ Error processing PR: ${e.message?.lines()?.firstOrNull()}".asError()) + return PrProcessingResult(prNumber, "[ERROR]", PrProcessingStatus.Error, logMessages = logs) + } +} + +private fun processLine( + index: Int, + releaseLines: List, + currentSectionIn: String, + issueId: String?, + prNumber: String, + prUrl: String, + notesInPr: MutableMap>, +): Pair { + var currentSection = currentSectionIn + val line = releaseLines[index] + + val headerMatch = """^#+\s+(.*)""".toRegex().find(line.trim()) + if (headerMatch != null) { + currentSection = headerMatch.groupValues[1].trim() + return index + 1 to currentSection + } + + if (line.isBlank()) { + return index + 1 to currentSection + } + + val trimmedLine = line.trim() + val isListItem = trimmedLine.startsWith("*") || trimmedLine.startsWith("-") + + if (isListItem) { + val baseIndentation = getIndentation(line) + val mainText = trimmedLine.removePrefix("*").removePrefix("-").trim() + val noteLines = mutableListOf(mainText) + + var j = index + 1 + while (j < releaseLines.size) { + val nextLine = releaseLines[j] + if (nextLine.isNotBlank()) { + if ("""^#+\s+(.*)""".toRegex().find(nextLine.trim()) != null) break // Stop at next header + if (getIndentation(nextLine) <= baseIndentation) break // Stop at new top-level item + } + + noteLines.add(nextLine) + j++ + } + + val fullDescription = noteLines.joinToString("\n") + val noteItem = ReleaseNoteItem(issueId, fullDescription, prNumber, prUrl) + notesInPr.getOrPut(currentSection) { mutableListOf() }.add(noteItem) + return j to currentSection + } else { + // This line is not a list item, so we skip it. + return index + 1 to currentSection } } // --- Main Entry Point --- -val isVerbose = args.contains("--verbose") || args.contains("-v") - -fun getArg(name: String, shortName: String? = null): String? { - val nameFlag = "--$name" - val shortNameFlag = shortName?.let { "-$it" } - - val values = args.asSequence() - .mapIndexedNotNull { index, s -> - if (s == nameFlag || s == shortNameFlag) { - args.getOrNull(index + 1) - } else { - null - } - } - .toList() - return values.firstOrNull() +if (workingDir.name != "jewel" || workingDir.parentFile.name != "platform") { + printlnErr("This script must be run from the 'jewel' directory.") + exitProcess(1) } -fun getLatestReleaseDate(): String? { +private fun getLatestReleaseDate(): String? { val releaseNotesFile = File(Config.RELEASE_NOTES_FILE) if (!releaseNotesFile.exists()) { - println("⚠️ Release notes file not found at '${releaseNotesFile.absolutePath}', can't determine start date.") + printlnWarn( + "⚠️ Release notes file not found at '${releaseNotesFile.absolutePath}', can't determine start date." + ) return null } @@ -174,69 +231,92 @@ fun getLatestReleaseDate(): String? { } } } - println("⚠️ Could not find any release date in ${Config.RELEASE_NOTES_FILE}.") + printlnWarn("⚠️ Could not find any release date in ${Config.RELEASE_NOTES_FILE}.") return null } -fun printUsageAndExit() { +private fun printUsageAndExit() { println("Usage: ./extract-release-notes.main.kts --start-date [--end-date ] [--verbose]") println("If --start-date is omitted, it will be inferred from the latest release in ${Config.RELEASE_NOTES_FILE}.") println("Example: ./extract-release-notes.main.kts --start-date 2025-05-01 --end-date 2025-05-31") exitProcess(1) } -val startDate: String = +private val startDate: String = getArg("start-date", "s") ?: getLatestReleaseDate() ?: run { printUsageAndExit() "" // Should be unreachable } -val endDate = getArg("end-date", "e") +private val endDate = getArg("end-date", "e") // --- Phase 1: Sequentially parse local git history --- -val normalizedJewelPath: String = File(Config.JEWEL_DIR).normalize().absolutePath -val logMessage = buildString { +private val normalizedJewelPath: String = File(Config.JEWEL_DIR).normalize().canonicalPath +private val logMessage = buildString { append("🔍 Enumerating commits in '$normalizedJewelPath' since $startDate") if (endDate != null) { append(" until $endDate") } } -println("$logMessage...") -val mark = markNow() -val gitLogCommand = mutableListOf("git", "log", "--since=$startDate", "--pretty=format:%H") -if (endDate != null) { - gitLogCommand.add("--until=$endDate") +print("$logMessage...") + +private val mark = markNow() +private val gitLogCommand = buildString { + append("git log --since=") + append(startDate) + append(" --pretty=format:%H") + if (endDate != null) { + append(" --until=$endDate") + } + append(" -- ") + append(Config.JEWEL_DIR) } -gitLogCommand.add("--") -gitLogCommand.add(Config.JEWEL_DIR) -val allCommitHashes = - runCommand(*gitLogCommand.toTypedArray()) - .lines() - .filter { it.isNotBlank() } +private val allCommitHashes = runBlocking { + runCommand(gitLogCommand, workingDir).output.lines().filter { it.isNotBlank() } +} -val elapsed = mark.elapsedNow() +private val elapsed = mark.elapsedNow() -println(" Found ${allCommitHashes.size} commits in $elapsed") +println(" DONE") -val prCommits = mutableListOf() -val issueIdRegex = """\[(JEWEL-\d+)]""".toRegex() -val prRegex = """closes https://github.com/JetBrains/intellij-community/pull/(\d+)""".toRegex() +println(" ℹ️ Found ${allCommitHashes.size} commits in $elapsed") + +print("🔍 Filtering relevant commits...") + +private val prCommits = mutableListOf() +private val jewelCommitsWithoutPr = mutableListOf>() +private val issueIdRegex = """\[(JEWEL-\d+.*)+]""".toRegex() +private val prRegex = """closes https://github.com/JetBrains/intellij-community/pull/(\d+)""".toRegex() for (commitHash in allCommitHashes) { - val commitBody = runCommand("git", "show", "-s", "--format=%B", commitHash) - prRegex.find(commitBody)?.groups?.get(1)?.value?.let { prNumber -> + val commitBody = runBlocking { runCommand("git show -s --format=%B $commitHash", workingDir).output } + + val prNumber = prRegex.find(commitBody)?.groups?.get(1)?.value + if (prNumber != null) { + if (isVerbose) { + println(" Commit $commitHash -> PR #$prNumber") + } val issueId = issueIdRegex.find(commitBody)?.groups?.get(1)?.value prCommits.add(CommitInfo(commitHash, prNumber, issueId)) + } else { + if (commitBody.contains("JEWEL", ignoreCase = true)) { + jewelCommitsWithoutPr.add(commitHash to commitBody.lineSequence().first()) + } + if (isVerbose) { + println(" Commit $commitHash -> NO PR") + } } } -val uniquePrCommits = prCommits.distinctBy { it.prId } +private val uniquePrCommits = prCommits.distinctBy { it.prId }.sortedBy { it.issueId } + +println(" DONE") println( - " Found ${uniquePrCommits.size} unique PRs to process. " + + " ℹ️ Found ${uniquePrCommits.size} unique PRs to process. " + "(${allCommitHashes.size - uniquePrCommits.size} commits were skipped or were duplicates)" ) @@ -248,18 +328,20 @@ if (isVerbose) { } // --- Phase 2: Process all PRs in parallel --- -println("\n🔎 Processing ${uniquePrCommits.size} PRs with up to ${Config.MAX_CONCURRENT_JOBS} parallel jobs...") +println("🔎 Processing ${uniquePrCommits.size} PRs with up to ${Config.MAX_CONCURRENT_JOBS} parallel jobs...") -@Suppress("RAW_RUN_BLOCKING") // This is not IJP code -val results = runBlocking { +@OptIn(ExperimentalCoroutinesApi::class) +private val results = runBlocking { val dispatcher = Dispatchers.IO.limitedParallelism(Config.MAX_CONCURRENT_JOBS) val inProgressPrs = ConcurrentHashMap.newKeySet() // Launch a separate logger coroutine to print progress val loggerJob = launch { while (isActive) { - val currentPrs = inProgressPrs.map { "#$it" }.sorted().joinToString(", ") - print("\r Currently processing: [${currentPrs.padEnd(50)}]") + val currentPrs = inProgressPrs.sorted().joinToString(", ") { "#$it" } + val terminalWidth = getTerminalWidth() + val maxLen = terminalWidth - 20 // Hardcoded to include the "chrome" + print("\u001B[2K ⏳ Processing: [${currentPrs.take(maxLen).padEnd(maxLen)}]\r") delay(100) } } @@ -278,39 +360,43 @@ val results = runBlocking { val completedResults = jobs.awaitAll() loggerJob.cancel() - print("\r".padEnd(80) + "\r") // Clear the progress line completely - println("\n✅ All PRs have been processed.") + print("\r\u001B[2K") // Clear the progress line completely + println(" ✅ All PRs have been processed.") completedResults } // 3. Aggregate final results -val allReleaseNotes = mutableMapOf>() -val processedPrs = mutableMapOf() +private val allReleaseNotes = mutableMapOf>() +private val processedPrs = mutableMapOf() results.forEach { result -> - processedPrs[result.prId] = result.status + processedPrs[result.prId] = result result.notes.forEach { (section, items) -> allReleaseNotes.getOrPut(section) { mutableListOf() }.addAll(items) } } -// --- NEW: Print collated logs --- -println("\n--- PROCESSING LOGS ---") +// --- Print collated logs --- +if (isVerbose) { + println("\n--- PROCESSING LOGS ---") -results - .sortedBy { it.prId.toInt() } - .forEach { result -> - println("\n[PR #${result.prId}]") - result.logMessages.forEach { msg -> println(" $msg") } - } + results + .sortedBy { it.prId.toInt() } + .forEach { result -> + println("\n[PR #${result.prId}]") + result.logMessages.forEach { msg -> println(" $msg") } + } -// 4. Write grouped release notes to file -println("\n\n✍️ Writing release notes to ${Config.OUTPUT_FILE}...") + println() +} -val outputFile = File(Config.OUTPUT_FILE) +// 4. Write grouped release notes to the output file +println("✍️ Writing release notes to ${Config.OUTPUT_FILE}...") + +private val outputFile = File(Config.OUTPUT_FILE) outputFile.writeText("") -val sectionOrder = listOf("New Features", "Enhancements", "Bug Fixes", "Deprecations", "Uncategorized") -val sortedSections = +private val sectionOrder = listOf("⚠️ Important Changes", "New features", "Bug fixes", "Deprecated API", "Other") +private val sortedSections = allReleaseNotes.keys.sortedWith( compareBy { sectionKey -> sectionOrder.indexOf(sectionKey).let { if (it == -1) Int.MAX_VALUE else it } } ) @@ -330,13 +416,28 @@ println(" ✅ Done.") // 5. Final Summary Table println("\n--- SUMMARY ---") -val summaryData = processedPrs.entries.groupBy({ it.value }, { it.key }) +private val summaryData: Map> = + processedPrs.entries.groupBy({ it.value.status }, { it.value }) -ProcessedPrStatus.entries.forEach { status -> - val prs = summaryData[status]?.map { it.toInt() }?.sorted() ?: emptyList() - if (prs.isEmpty()) return@forEach +PrProcessingStatus.entries.forEach { status -> + val processingResults = summaryData[status] ?: return@forEach + val prs = processingResults.sortedBy { it.prId } println("\n[${status.name}] - ${prs.size} PRs") - println(prs.joinToString(", ") { "#$it" }) + for (pr in prs) { + print(" * ") + val id = pr.prId + println("#$id — ${pr.prTitle}".asLink("https://github.com/JetBrains/intellij-community/pull/$id")) + } } println("\n\n✅ All tasks complete.") + +if (jewelCommitsWithoutPr.isNotEmpty()) { + println() + printlnWarn("⚠️ Found ${jewelCommitsWithoutPr.size} commits with 'JEWEL' in the message but no PR number:") + + for ((commitHash, headerLine) in jewelCommitsWithoutPr) { + println(" * ${commitHash.take(7)} ${headerLine}") + } + println() +} diff --git a/platform/jewel/scripts/utils.main.kts b/platform/jewel/scripts/utils.main.kts index 9ff4e6fcae62..127412bcf1f2 100755 --- a/platform/jewel/scripts/utils.main.kts +++ b/platform/jewel/scripts/utils.main.kts @@ -21,23 +21,113 @@ fun getPrNumber() = checkNotNull(System.getenv("PR_NUMBER")?.trim()) { "PR numbe fun requireGhTool() { if (checkGhTool()) return - echoErr("ERROR: the GitHub CLI tool must be present on the PATH.") + printlnErr("ERROR: the GitHub CLI tool must be present on the PATH.") exitProcess(1) } fun requirePrNumber() { if (checkPrNumber()) return - echoErr("ERROR: PR_NUMBER environment variable not set.") + printlnErr("ERROR: PR_NUMBER environment variable not set.") exitProcess(2) } -fun echoErr(message: String) { - System.err.println("\u001b[0;31m$message\u001b[0m") +private val CODE_ERR = "\u001b[0;31m" +private val CODE_WARN = "\u001b[0;33m" +private val CODE_CLEAR = "\u001b[0m" + +fun printlnErr(message: String) { + System.err.println(message.asError()) } -fun echoWarn(message: String) { - System.err.println("\u001b[0;33m$message\u001b[0m") +fun printlnWarn(message: String) { + System.err.println(message.asWarning()) +} + +fun String.asWarning() = "$CODE_WARN$this$CODE_CLEAR" + +fun String.asError() = "$CODE_ERR$this$CODE_CLEAR" + +/** + * Detects if the current terminal likely supports OSC 8 hyperlinks by checking for known environment variables. + * + * @return `true` if a compatible terminal is detected, `false` otherwise. + */ +private fun doesTerminalSupportHyperlinks(): Boolean { + // Check for iTerm, VSCode, Hyper, WezTerm, etc. + val termProgram = System.getenv("TERM_PROGRAM") + if (termProgram != null) { + return when (termProgram) { + "iTerm.app", + "vscode", + "WezTerm", + "Hyper" -> true + else -> false + } + } + + // Check for VTE-based terminals (GNOME Terminal, Tilix) + // Support was added in version 0.50 -> 5000 + val vteVersion = System.getenv("VTE_VERSION")?.toIntOrNull() + if (vteVersion != null && vteVersion >= 5000) { + return true + } + + // Check for IntelliJ's built-in terminal + if (System.getenv("TERMINAL_EMULATOR") == "JetBrains-JediTerm") { + return true + } + + // Fallback if no known terminal is detected + return false +} + +fun String.asLink(url: String): String { + if (!doesTerminalSupportHyperlinks()) return this + + val esc = '\u001B' // Escape character + val bell = '\u0007' // Bell character (acts as separator) + return "$esc]8;;$url$bell$this$esc]8;;$bell" +} + +/** + * Gets the current terminal width by executing `tput cols`, `stty size`, or checking COLUMNS, or using a fallback + * value. + * + * @return The terminal width in columns. + */ +fun getTerminalWidth(): Int { + try { + val tputCols = runBlocking { runCommand(command = "tput cols", workingDir = null, exitOnError = false) } + if (tputCols.isSuccess) return tputCols.output.trim().toInt() + + val sttySize = runBlocking { runCommand(command = "stty size", workingDir = null, exitOnError = false) } + if (sttySize.isSuccess) return sttySize.output.trim().split(" ").last().toInt() + + return System.getenv("COLUMNS")?.toIntOrNull() ?: 80 + } catch (_: Exception) { + return 80 + } +} + +val isVerbose = args.contains("--verbose") || args.contains("-v") + +fun getArg(name: String, shortName: String? = null): String? { + val nameFlag = "--$name" + val shortNameFlag = shortName?.let { "-$it" } + + val values = + args + .asSequence() + .mapIndexedNotNull { index, s -> + if (s == nameFlag || s == shortNameFlag) { + args.getOrNull(index + 1) + } else { + null + } + } + .toList() + return values.firstOrNull() } suspend fun runCommand( @@ -61,7 +151,7 @@ suspend fun runCommand( CmdResult.Success(output) } else { if (exitOnError) { - echoErr("Command '$command' failed with exit code ${result.resultCode}:\n$output") + printlnErr("Command '$command' failed with exit code ${result.resultCode}:\n$output") exitProcess(result.resultCode) } CmdResult.Failure(output) diff --git a/platform/jewel/scripts/utils.sh b/platform/jewel/scripts/utils.sh index 4e8d149b857e..c9a44cc147a1 100644 --- a/platform/jewel/scripts/utils.sh +++ b/platform/jewel/scripts/utils.sh @@ -17,11 +17,26 @@ fail_check() { local message=$1 echoerr "$message" - if [[ -n "$PR_NUMBER" && -n "$GITHUB_TOKEN" ]]; then - if ! gh pr comment "$PR_NUMBER" --body "$message"; then - echowarn "Failed to post comment to PR #$PR_NUMBER. Continuing..." - fi - else - echowarn "PR_NUMBER or GITHUB_TOKEN not set, skipping posting PR comment." +# TODO figure out how to reliably post comments to PRs from forks +# if [[ -n "$PR_NUMBER" && -n "$GITHUB_TOKEN" ]]; then +# if ! gh pr comment "$PR_NUMBER" --body "$message"; then +# echowarn "Failed to post comment to PR #$PR_NUMBER. Continuing..." +# fi +# else +# echowarn "PR_NUMBER or GITHUB_TOKEN not set, skipping posting PR comment." +# fi +} + +check_gh_tool() { + if ! command -v gh &>/dev/null; then + echoerr "ERROR: The GitHub CLI (gh) is not installed. Please install it to continue." + exit 1 + fi +} + +check_pr_number() { + if [ -z "$PR_NUMBER" ]; then + echoerr "ERROR: PR_NUMBER environment variable not set." + exit 1 fi } diff --git a/platform/jewel/scripts/validate-api-dump-changes.main.kts b/platform/jewel/scripts/validate-api-dump-changes.main.kts index 7047d823bda2..7a6a163594d2 100755 --- a/platform/jewel/scripts/validate-api-dump-changes.main.kts +++ b/platform/jewel/scripts/validate-api-dump-changes.main.kts @@ -169,7 +169,7 @@ private val baseCommit = runBlocking { requireGhTool() runCommand("gh pr view ${getPrNumber()} --json baseRefOid -q .baseRefOid", baseDir).getOrThrow().trim() } else { - echoWarn("GitHub PR number not found, falling back to checking against HEAD~1 instead") + printlnWarn("GitHub PR number not found, falling back to checking against HEAD~1 instead") runCommand("git rev-parse HEAD~1", baseDir).getOrThrow().trim() } } @@ -210,7 +210,7 @@ runBlocking { summaryFile.writeText(summary) println("Summary written to ${summaryFile.absolutePath}") } else { - echoWarn("GITHUB_STEP_SUMMARY environment variable not set") + printlnWarn("GITHUB_STEP_SUMMARY environment variable not set") } } diff --git a/platform/jewel/scripts/validate-commit-message.sh b/platform/jewel/scripts/validate-commit-message.sh index 898741366a90..40b201c662ce 100644 --- a/platform/jewel/scripts/validate-commit-message.sh +++ b/platform/jewel/scripts/validate-commit-message.sh @@ -5,15 +5,9 @@ set -o pipefail source "$(dirname "$0")/utils.sh" -if ! command -v gh &>/dev/null; then - echoerr "ERROR: The GitHub CLI (gh) is not installed. Please install it to continue." - exit 1 -fi - -if [ -z "$PR_NUMBER" ]; then - echoerr "ERROR: PR_NUMBER environment variable not set." - exit 1 -fi +# Precondition checks: gh tool on path, and PR_NUMBER env +check_gh_tool +check_pr_number echo "Checking commit messages for PR #$PR_NUMBER" diff --git a/platform/jewel/scripts/validate-pr-commits.sh b/platform/jewel/scripts/validate-pr-commits.sh index 89a7647a96fb..6e18d0c8a0bf 100644 --- a/platform/jewel/scripts/validate-pr-commits.sh +++ b/platform/jewel/scripts/validate-pr-commits.sh @@ -4,15 +4,9 @@ set -o pipefail source "$(dirname "$0")/utils.sh" -if ! command -v gh &> /dev/null; then - echo "ERROR: The GitHub CLI (gh) could not be found. Please install it to continue." >&2 - exit 1 -fi - -if [ -z "$PR_NUMBER" ]; then - echo "ERROR: The PR_NUMBER environment variable is not set." >&2 - exit 1 -fi +# Precondition checks: gh tool on path, and PR_NUMBER env +check_gh_tool +check_pr_number echo "Checking commit count for PR #$PR_NUMBER"