[JEWEL-956] Clean up scripts

This cleans up a number of small issues with the scripts:
* Release notes script
  * Ensure it's run from the `jewel` folder
  * Add support for nested release note bullet points
  * Add warnings when there are "orphan" Jewel commits have no
    matching PR information attached
  * Fix a number of bugs
* Fix version number in release notes doc to work with script
* Extract common checks from other scripts to utils.sh
* Temporarily remove the ability to post PR comments as it won't work
  across forks anyway

closes https://github.com/JetBrains/intellij-community/pull/3177

GitOrigin-RevId: c7c51779b65c4d3b6ee5f002356be7cb2f56f80a
This commit is contained in:
Sebastiano Poggi
2025-08-15 09:14:23 +00:00
committed by intellij-monorepo-bot
parent 3e352e3297
commit 3db404b9bb
7 changed files with 371 additions and 177 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# Jewel Release Notes
## 0.29 (2025-07-22)
## v0.29 (2025-07-22)
| Supported IJP versions | Compose Multiplatform version |
|------------------------|-------------------------------|
@@ -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<String, List<ReleaseNoteItem>> = emptyMap(),
val logMessages: List<String> = 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<String>()
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<String, MutableList<ReleaseNoteItem>>()
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<String>,
currentSectionIn: String,
issueId: String?,
prNumber: String,
prUrl: String,
notesInPr: MutableMap<String, MutableList<ReleaseNoteItem>>,
): Pair<Int, String> {
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 <yyyy-mm-dd> [--end-date <yyyy-mm-dd>] [--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<CommitInfo>()
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<CommitInfo>()
private val jewelCommitsWithoutPr = mutableListOf<Pair<String, String>>()
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<String>()
// 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<String, MutableList<ReleaseNoteItem>>()
val processedPrs = mutableMapOf<String, ProcessedPrStatus>()
private val allReleaseNotes = mutableMapOf<String, MutableList<ReleaseNoteItem>>()
private val processedPrs = mutableMapOf<String, PrProcessingResult>()
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<PrProcessingStatus, List<PrProcessingResult>> =
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()
}
+97 -7
View File
@@ -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)
+21 -6
View File
@@ -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
}
@@ -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")
}
}
@@ -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"
@@ -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"