From 42a7b6e8eeb44e48ea01611ce8d0bb9e2091d757 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Mon, 10 Aug 2026 19:35:36 +0300 Subject: [PATCH] Annotate failures for Github Actions so they're easier to spot. --- Tools/Sources/Commands/CI/AccessibilityTests.swift | 1 + Tools/Sources/Commands/CI/CI.swift | 10 ++++++++++ Tools/Sources/Commands/CI/IntegrationTests.swift | 3 +++ Tools/Sources/Commands/CI/PreviewTests.swift | 2 ++ Tools/Sources/Commands/CI/UITests.swift | 1 + Tools/Sources/Commands/CI/UnitTests.swift | 1 + Tools/Sources/Commands/CI/UploadDSYMs.swift | 1 + 7 files changed, 19 insertions(+) diff --git a/Tools/Sources/Commands/CI/AccessibilityTests.swift b/Tools/Sources/Commands/CI/AccessibilityTests.swift index db4fc6a6e..3332b688a 100644 --- a/Tools/Sources/Commands/CI/AccessibilityTests.swift +++ b/Tools/Sources/Commands/CI/AccessibilityTests.swift @@ -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) diff --git a/Tools/Sources/Commands/CI/CI.swift b/Tools/Sources/Commands/CI/CI.swift index 95a206d1e..d0678b14b 100644 --- a/Tools/Sources/Commands/CI/CI.swift +++ b/Tools/Sources/Commands/CI/CI.swift @@ -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). diff --git a/Tools/Sources/Commands/CI/IntegrationTests.swift b/Tools/Sources/Commands/CI/IntegrationTests.swift index 852d0eadc..8fa35db11 100644 --- a/Tools/Sources/Commands/CI/IntegrationTests.swift +++ b/Tools/Sources/Commands/CI/IntegrationTests.swift @@ -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.") } } diff --git a/Tools/Sources/Commands/CI/PreviewTests.swift b/Tools/Sources/Commands/CI/PreviewTests.swift index 8b28b4826..d2c53973c 100644 --- a/Tools/Sources/Commands/CI/PreviewTests.swift +++ b/Tools/Sources/Commands/CI/PreviewTests.swift @@ -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 } } diff --git a/Tools/Sources/Commands/CI/UITests.swift b/Tools/Sources/Commands/CI/UITests.swift index ad46c4cf4..947a52569 100644 --- a/Tools/Sources/Commands/CI/UITests.swift +++ b/Tools/Sources/Commands/CI/UITests.swift @@ -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"], diff --git a/Tools/Sources/Commands/CI/UnitTests.swift b/Tools/Sources/Commands/CI/UnitTests.swift index 70aac08ce..983bd15ef 100644 --- a/Tools/Sources/Commands/CI/UnitTests.swift +++ b/Tools/Sources/Commands/CI/UnitTests.swift @@ -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 } diff --git a/Tools/Sources/Commands/CI/UploadDSYMs.swift b/Tools/Sources/Commands/CI/UploadDSYMs.swift index 4a7fded2d..dedbff66f 100644 --- a/Tools/Sources/Commands/CI/UploadDSYMs.swift +++ b/Tools/Sources/Commands/CI/UploadDSYMs.swift @@ -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 } }