Annotate failures for Github Actions so they're easier to spot.

This commit is contained in:
Stefan Ceriu
2026-08-10 19:35:36 +03:00
parent b829c6a06f
commit 42a7b6e8ee
7 changed files with 19 additions and 0 deletions
@@ -24,6 +24,7 @@ struct AccessibilityTests: AsyncParsableCommand {
} catch {
testsFailed = true
logger.error("\n❌ Accessibility tests failed.\n")
CI.annotateError(title: "Accessibility tests failed", "Download the artifacts for the xcresult bundle.")
}
// Zip results (best-effort, useful for CI artifact uploads)
+10
View File
@@ -45,11 +45,21 @@ struct CI: ParsableCommand {
try await run(.name("swiftformat"), ["--lint", "."])
} catch {
logger.error("\n❌ SwiftFormat failed.\n")
annotateError(title: "SwiftFormat lint failed", "Run `swiftformat .` from the project root and commit the changes.")
throw error
}
logger.info("\n✅ SwiftFormat passed.\n")
}
// MARK: - GitHub Actions
/// Logs an error annotation, surfacing the failure on the workflow run's summary page.
///
/// This is printed directly as the workflow command must be at the very start of the line.
static func annotateError(title: String, _ message: String) {
print("::error title=\(title)::\(message)")
}
// MARK: - Test Results
/// Collects coverage from an xcresult bundle using xcresultparser (cobertura format).
@@ -32,6 +32,7 @@ struct IntegrationTests: AsyncParsableCommand {
} catch {
testsFailed = true
logger.error("\n❌ Integration tests failed.\n")
CI.annotateError(title: "Integration tests failed", "One or more tests failed. Download the Results artifact for the xcresult bundle.")
// Preserve the app's logs alongside the results so that they can be uploaded as artifacts.
logger.info("🗂️ Collecting log files…")
@@ -47,6 +48,7 @@ struct IntegrationTests: AsyncParsableCommand {
} catch {
testsFailed = true
logger.error("❌ Logs are not set to the trace level.")
CI.annotateError(title: "Logs are not set to the trace level", "The tests passed, this is a logging failure.")
}
do {
@@ -56,6 +58,7 @@ struct IntegrationTests: AsyncParsableCommand {
} catch {
testsFailed = true
logger.error("❌ Private messages found in logs.")
CI.annotateError(title: "Private data found in the logs", "A message body was logged. The tests passed, this is a logging failure.")
}
}
@@ -39,11 +39,13 @@ struct PreviewTests: AsyncParsableCommand {
.appending(path: "\(CI.testOutputDirectory)/\(Self.scheme).xcresult")
guard FileManager.default.fileExists(atPath: resultBundleURL.path) else {
logger.error("\n❌ Preview tests could not run. Check for compilation or configuration errors.\n")
CI.annotateError(title: "Preview tests could not run", "No result bundle was produced. Check for compilation or configuration errors.")
throw error
}
logger.info("\n📸 Snapshots recorded.\n")
} else {
logger.error("\n❌ Preview tests failed.\n")
CI.annotateError(title: "Preview tests failed", "Snapshots don't match their references. Re-record them if the change is intentional.")
testsFailed = true
}
}
+1
View File
@@ -61,6 +61,7 @@ struct UITests: AsyncParsableCommand {
} catch {
testsFailed = true
print("\n❌ UI tests (\(deviceType.rawValue)) failed.\n")
CI.annotateError(title: "UI tests (\(deviceType.rawValue)) failed", "Download the artifacts for the xcresult bundle.")
}
await CI.zipResults(bundles: ["UITests.xcresult"],
@@ -59,6 +59,7 @@ struct UnitTests: AsyncParsableCommand {
if !failures.isEmpty {
let failedSuites = "[\(failures.joined(separator: ","))]"
logger.error("\n❌ \(failures.count) test suite(s) failed \(failedSuites)\n")
CI.annotateError(title: "\(failures.count) test suite(s) failed", "Failing suites: \(failedSuites). Download the artifacts for the xcresult bundles.")
throw ExitCode.failure
}
@@ -51,6 +51,7 @@ struct UploadDSYMs: AsyncParsableCommand {
}
if let lastError {
CI.annotateError(title: "Failed to upload dSYMs to Sentry", "All \(maxRetries) attempts failed: \(lastError.localizedDescription)")
throw lastError
}
}