diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 628d69b..06a9e9f 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -1,11 +1,11 @@ name: Nightly on: - push: + pull_request: branches: - main - schedule: - - cron: "0 3 * * *" + types: + - closed workflow_dispatch: permissions: @@ -17,6 +17,7 @@ concurrency: jobs: build-macos: + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.merged == true }} name: Build nightly macOS (${{ matrix.arch }}) runs-on: macos-15 strategy: @@ -61,6 +62,7 @@ jobs: path: dist/* publish-nightly: + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.merged == true }} name: Publish Nightly Release runs-on: ubuntu-latest needs: [build-macos] @@ -82,7 +84,9 @@ jobs: run: | set -euo pipefail NIGHTLY_VERSION="$(date -u +'%Y.%m.%d').${GITHUB_RUN_NUMBER}" + NIGHTLY_COMMIT="${{ github.event.pull_request.merge_commit_sha || github.sha }}" echo "NIGHTLY_VERSION=${NIGHTLY_VERSION}" >> "${GITHUB_ENV}" + echo "NIGHTLY_COMMIT=${NIGHTLY_COMMIT}" >> "${GITHUB_ENV}" - name: Publish nightly assets uses: softprops/action-gh-release@v2 @@ -93,7 +97,7 @@ jobs: make_latest: false generate_release_notes: false body: | - Nightly build for commit `${{ github.sha }}`. + Nightly build for commit `${{ env.NIGHTLY_COMMIT }}`. Version: `${{ env.NIGHTLY_VERSION }}` files: | dist/*.tar.gz @@ -101,6 +105,7 @@ jobs: overwrite_files: true update-homebrew-tap-nightly: + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.merged == true }} name: Update Homebrew Tap (nightly) runs-on: ubuntu-latest needs: [publish-nightly] diff --git a/README.md b/README.md index a9f2d9c..062473a 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,10 @@ Release automation included: - Builds `snapshot-report` for macOS `arm64` and `x86_64` on tag push (`v*`) - Uploads artifacts to GitHub Releases - Updates `oscarcv/tap` formula automatically +- `.github/workflows/nightly.yml` + - Runs on PR merges into `main` + - Publishes/updates a moving `nightly` prerelease (`arm64` + `x86_64`) + - Updates `oscarcv/tap` nightly formula (`snapshot-report-nightly`) ## Examples @@ -157,6 +161,18 @@ swift run snapshot-report \ --output .artifacts/report ``` +### Verbose diagnostics + +Use `--verbose` to print detailed processing diagnostics (inputs, phase progress, odiff usage, and timings): + +```bash +swift run snapshot-report \ + --input-dir .artifacts/snapshot-runs \ + --output .artifacts/report \ + --format json,junit,html \ + --verbose +``` + ### odiff pixel diff [odiff](https://github.com/dmtrKovalenko/odiff) is a SIMD-accelerated image diff tool that produces highlighted difference images. Install it first: diff --git a/Sources/snapshot-report/CLI/main.swift b/Sources/snapshot-report/CLI/main.swift index ea3f92a..ac16aaf 100644 --- a/Sources/snapshot-report/CLI/main.swift +++ b/Sources/snapshot-report/CLI/main.swift @@ -5,7 +5,6 @@ import SnapshotReportCLI struct CLI { static func run() throws { - CLIUI.header("snapshot-report") let arguments = Array(CommandLine.arguments.dropFirst()) if arguments.first == "inspect" { @@ -19,10 +18,14 @@ struct CLI { } let options = try parse(arguments: arguments) + CLIUI.setVerbose(options.verbose) + CLIUI.header("snapshot-report") try FileManager.default.createDirectory(at: options.outputDirectory, withIntermediateDirectories: true) + let startedAt = Date() CLIUI.step("Resolving input files") let resolvedInputs = try resolveInputs(options: options) + CLIUI.debug("Resolved \(resolvedInputs.count) JSON inputs and \(options.xcresultInputs.count) xcresult inputs") CLIUI.step("Loading JSON reports (\(resolvedInputs.count))") let reports = try _loadJSONReports(inputs: resolvedInputs, jobs: options.jobs) { completed, total, input in @@ -36,13 +39,16 @@ struct CLI { CLIUI.step("Merging report data") let mergedReport = SnapshotReportAggregator.merge(reports: reports + xcresultReports, name: options.reportName) + CLIUI.debug("Merged report: \(mergedReport.summary.total) tests (\(mergedReport.summary.passed) passed, \(mergedReport.summary.failed) failed, \(mergedReport.summary.skipped) skipped)") let effectiveOdiffPath = options.odiffPath ?? _resolveOnPATH("odiff") let finalReport: SnapshotReport if let odiffPath = effectiveOdiffPath { CLIUI.step("Generating odiff attachments") + CLIUI.debug("Using odiff binary: \(odiffPath)") finalReport = OdiffProcessor(odiffBinaryPath: odiffPath).process(report: mergedReport) } else { + CLIUI.debug("odiff not found; skipping diff enrichment") finalReport = mergedReport } @@ -57,6 +63,8 @@ struct CLI { CLIUI.progress("Output \(completed)/\(total): \(format.rawValue)") } + let elapsed = Date().timeIntervalSince(startedAt) + CLIUI.debug(String(format: "Total processing time: %.3fs", elapsed)) CLIUI.success("Generated report \(options.formats.map(\.rawValue).joined(separator: ", ")) at \(options.outputDirectory.path)") } @@ -70,6 +78,7 @@ struct CLI { var reportName: String? var odiffPath: String? var jobs = max(1, ProcessInfo.processInfo.activeProcessorCount) + var verbose = false var index = 0 while index < arguments.count { @@ -122,6 +131,8 @@ struct CLI { throw SnapshotReportError.invalidInput("--jobs must be a positive integer") } jobs = parsed + case "--verbose", "-v": + verbose = true default: throw SnapshotReportError.invalidInput("Unknown argument: \(argument)") } @@ -142,7 +153,8 @@ struct CLI { htmlTemplate: htmlTemplate, reportName: reportName, odiffPath: odiffPath, - jobs: jobs + jobs: jobs, + verbose: verbose ) } @@ -191,6 +203,7 @@ struct CLI { --name Override merged report name --odiff Path to odiff binary (default: auto-detect on PATH) --jobs Max parallel xcresult reads (default: CPU count) + -v, --verbose Enable diagnostic output --help Show help """ ) @@ -206,6 +219,7 @@ struct CLI { let reportName: String? let odiffPath: String? let jobs: Int + let verbose: Bool } } @@ -421,17 +435,34 @@ private final class ReportWriteState: @unchecked Sendable { private enum CLIUI { private static let lock = NSLock() private static let prefix = "[snapshot-report]" + private static let state = VerboseState() + + static func setVerbose(_ value: Bool) { + state.set(value) + } static func header(_ title: String) { - log("\(prefix) \(title)") + if verboseEnabled() { + log("\(prefix) \(title)") + } } static func step(_ message: String) { - log("\(prefix) step: \(message)") + if verboseEnabled() { + log("\(prefix) step: \(message)") + } } static func progress(_ message: String) { - log("\(prefix) progress: \(message)") + if verboseEnabled() { + log("\(prefix) progress: \(message)") + } + } + + static func debug(_ message: String) { + if verboseEnabled() { + log("\(prefix) debug: \(message)") + } } static func success(_ message: String) { @@ -443,6 +474,27 @@ private enum CLIUI { print(message) lock.unlock() } + + private static func verboseEnabled() -> Bool { + state.get() + } +} + +private final class VerboseState: @unchecked Sendable { + private let lock = NSLock() + private var value = false + + func set(_ newValue: Bool) { + lock.lock() + value = newValue + lock.unlock() + } + + func get() -> Bool { + lock.lock() + defer { lock.unlock() } + return value + } } private func _resolveOnPATH(_ name: String) -> String? {