Skip to content
This repository was archived by the owner on Oct 14, 2024. It is now read-only.

Commit 0c1010a

Browse files
authored
Fix stderror not being captured in processed exec and max retries (#26)
* Verbose echo if bugsnag isn't enabled * Log issue count * Spotless * Add a debug flag * Print the full path to logs * undo Not necessary * Add no-exit for debugging in tests * Add a stderr test * Fix message not being optional * Undo * Cap attempts * Spotless * Opportunistic fix toStrings * Detekt * Upload failure reports * What about... this? * Spotless * Silence slf4j * Restore * Test stderr on CI * Remove * Try macos * Explanatory comment
1 parent 0fb81cd commit 0c1010a

File tree

7 files changed

+89
-11
lines changed

7 files changed

+89
-11
lines changed

.github/workflows/ci.yml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ on:
1313

1414
jobs:
1515
build:
16-
runs-on: 'ubuntu-latest'
16+
# Necessary for ResultProcessorTest.testExecuteCommandWithStderr
17+
runs-on: 'macos-latest'
1718
steps:
1819
- name: Checkout
1920
uses: actions/checkout@v3
@@ -32,6 +33,14 @@ jobs:
3233
with:
3334
arguments: check
3435

36+
- name: (Fail-only) Upload build reports
37+
if: failure()
38+
uses: actions/upload-artifact@v3
39+
with:
40+
name: reports
41+
path: |
42+
**/build/reports/**
43+
3544
- name: Upload snapshot (main only)
3645
env:
3746
ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SonatypeUsername }}

build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ dependencies {
9696
implementation(libs.bugsnag)
9797
implementation(libs.moshi)
9898
implementation(libs.kotlin.reflect)
99+
// To silence this stupid log https://www.slf4j.org/codes.html#StaticLoggerBinder
100+
implementation(libs.slf4jNop)
99101
testImplementation(libs.junit)
100102
testImplementation(libs.truth)
101103
}

gradle/libs.versions.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,6 @@ kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref =
2323
moshi = { module = "com.squareup.moshi:moshi", version.ref = "moshi" }
2424
okhttp = "com.squareup.okhttp3:okhttp:4.11.0"
2525
okio = "com.squareup.okio:okio:3.3.0"
26+
slf4jNop = "org.slf4j:slf4j-nop:2.0.7"
2627
junit = "junit:junit:4.13.2"
2728
truth = "com.google.truth:truth:1.1.5"

src/main/kotlin/slack/cli/exec/ProcessedExecCli.kt

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,20 @@ public class ProcessedExecCli :
5252
option("--config", envvar = "PE_CONFIGURATION_FILE")
5353
.path(mustExist = true, canBeFile = true, canBeDir = false)
5454

55+
private val debug by option("--debug", "-d").flag()
56+
57+
@get:TestOnly
58+
private val noExit by
59+
option(
60+
"--no-exit",
61+
help = "Instructs this CLI to not exit the process with the status code. Test only!"
62+
)
63+
.flag()
5564
@get:TestOnly internal val parseOnly by option("--parse-only").flag(default = false)
5665

5766
internal val args by argument().multiple()
5867

68+
@Suppress("CyclomaticComplexMethod")
5969
@OptIn(ExperimentalStdlibApi::class, ExperimentalPathApi::class)
6070
override fun run() {
6171
if (parseOnly) return
@@ -76,8 +86,12 @@ public class ProcessedExecCli :
7686

7787
// Initial command execution
7888
val (exitCode, logFile) = executeCommand(cmd, tmpDir)
79-
while (exitCode != 0) {
80-
echo("Command failed with exit code $exitCode. Running processor script...")
89+
var attempts = 0
90+
while (exitCode != 0 && attempts < 1) {
91+
attempts++
92+
echo(
93+
"Command failed with exit code $exitCode. Running processor script (attempt $attempts)..."
94+
)
8195

8296
echo("Processing CI failure")
8397
val resultProcessor = ResultProcessor(verbose, bugsnagKey, config, ::echo)
@@ -114,8 +128,14 @@ public class ProcessedExecCli :
114128

115129
// If we got here, all is well
116130
// Delete the tmp files
117-
tmpDir.deleteRecursively()
118-
exitProcess(exitCode)
131+
if (!debug) {
132+
tmpDir.deleteRecursively()
133+
}
134+
135+
echo("Exiting with code $exitCode")
136+
if (!noExit) {
137+
exitProcess(exitCode)
138+
}
119139
}
120140

121141
// Function to execute command and capture output. Shorthand to the testable top-level function.
@@ -148,7 +168,7 @@ internal fun executeCommand(
148168
// Pass the line through unmodified
149169
line to ""
150170
}
151-
val process = command.process()
171+
val process = command.process() forkErr { it pipe echoHandler pipe tmpFile.toFile() }
152172
pipeline { process pipe echoHandler pipe tmpFile.toFile() }.join()
153173
exitCode = process.process.pcb.exitCode
154174
}

src/main/kotlin/slack/cli/exec/ResultProcessor.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,15 @@ internal class ResultProcessor(
5454
val bugsnag: Bugsnag? by lazy { bugsnagKey?.let { key -> createBugsnag(key) } }
5555

5656
val logLinesReversed = logFile.readLines().asReversed()
57+
echo("Checking ${config.knownIssues.size} known issues")
5758
for (issue in config.knownIssues) {
5859
val retrySignal = issue.check(logLinesReversed, echo)
5960

6061
if (retrySignal != RetrySignal.Unknown) {
6162
// Report to bugsnag. Shared common Throwable but with different messages.
62-
bugsnag?.apply {
63+
bugsnag?.let {
6364
verboseEcho("Reporting to bugsnag: $retrySignal")
64-
notify(IssueThrowable(issue), Severity.ERROR) { report ->
65+
it.notify(IssueThrowable(issue), Severity.ERROR) { report ->
6566
// Group by the throwable message
6667
report.setGroupingHash(issue.groupingHash)
6768
report.addToTab("Run Info", "After-Retry", isAfterRetry)
@@ -70,6 +71,7 @@ internal class ResultProcessor(
7071
}
7172
}
7273
}
74+
?: run { verboseEcho("Skipping bugsnag reporting: $retrySignal") }
7375

7476
if (retrySignal is RetrySignal.Ack) {
7577
echo("Recognized known issue but cannot retry: ${issue.message}")

src/main/kotlin/slack/cli/exec/RetrySignal.kt

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,25 @@ import kotlin.time.Duration
2323
internal sealed interface RetrySignal {
2424

2525
/** Unknown issue. */
26-
@TypeLabel("unknown") object Unknown : RetrySignal
26+
@TypeLabel("unknown")
27+
object Unknown : RetrySignal {
28+
// TODO remove when we have data objects in Kotlin 1.9
29+
override fun toString() = this::class.simpleName!!
30+
}
2731

2832
/** Indicates an issue that is recognized but cannot be retried. */
29-
@TypeLabel("ack") object Ack : RetrySignal
33+
@TypeLabel("ack")
34+
object Ack : RetrySignal {
35+
// TODO remove when we have data objects in Kotlin 1.9
36+
override fun toString() = this::class.simpleName!!
37+
}
3038

3139
/** Indicates this issue should be retried immediately. */
32-
@TypeLabel("immediate") object RetryImmediately : RetrySignal
40+
@TypeLabel("immediate")
41+
object RetryImmediately : RetrySignal {
42+
// TODO remove when we have data objects in Kotlin 1.9
43+
override fun toString() = this::class.simpleName!!
44+
}
3345

3446
/** Indicates this issue should be retried after a [delay]. */
3547
@TypeLabel("delayed")

src/test/kotlin/slack/cli/exec/ResultProcessorTest.kt

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,38 @@ class ResultProcessorTest {
4848
assertThat(logs.joinToString("\n").trim()).contains(expectedOutput)
4949
}
5050

51+
@Test
52+
fun testExecuteCommandWithStderr() {
53+
val script =
54+
"""
55+
#!/bin/bash
56+
57+
>&2 echo "Error text"
58+
"""
59+
.trimIndent()
60+
val scriptFile =
61+
tmpFolder.newFile("script.sh").apply {
62+
writeText(script)
63+
setExecutable(true)
64+
}
65+
tmpFolder.newFile("test.txt")
66+
val tmpDir = tmpFolder.newFolder("tmp/processed_exec")
67+
val (exitCode, outputFile) =
68+
executeCommand(tmpFolder.root.toPath(), scriptFile.absolutePath, tmpDir.toPath(), logs::add)
69+
assertThat(exitCode).isEqualTo(0)
70+
71+
val expectedOutput =
72+
"""
73+
Error text
74+
"""
75+
.trimIndent()
76+
77+
assertThat(outputFile.readText().trim()).isEqualTo(expectedOutput)
78+
79+
// Note we use "contains" here because our script may output additional logs
80+
assertThat(logs.joinToString("\n").trim()).contains(expectedOutput)
81+
}
82+
5183
@Test
5284
fun unknownIssue() {
5385
val outputFile = tmpFolder.newFile("logs.txt")

0 commit comments

Comments
 (0)